已修改2个文件
已添加7个文件
4484 ■■■■■ 文件已修改
src/api/collaborativeApproval/rpaManagement.js 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/attendanceManagement/index.vue 714 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 848 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/index.vue 1187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rpaManagement/index.vue 400 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/meterCollection/index.vue 556 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/iotMonitor/indexWD.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 349 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/index.vue 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/rpaManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
import request from "@/utils/request";
// æŸ¥è¯¢RPA列表
export function listRpa(query) {
  return request({
    url: "/collaborativeApproval/rpa/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢RPA详细
export function getRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/" + rpaId,
    method: "get",
  });
}
// æ–°å¢žRPA
export function addRpa(data) {
  return request({
    url: "/collaborativeApproval/rpa",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹RPA
export function updateRpa(data) {
  return request({
    url: "/collaborativeApproval/rpa",
    method: "put",
    data: data,
  });
}
// åˆ é™¤RPA
export function delRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/" + rpaId,
    method: "delete",
  });
}
// æ‰¹é‡åˆ é™¤RPA
export function delRpaBatch(rpaIds) {
  return request({
    url: "/collaborativeApproval/rpa/batch",
    method: "delete",
    data: rpaIds,
  });
}
// å¯åЍRPA
export function startRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/start/" + rpaId,
    method: "post",
  });
}
// åœæ­¢RPA
export function stopRpa(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/stop/" + rpaId,
    method: "post",
  });
}
// èŽ·å–RPA状态
export function getRpaStatus(rpaId) {
  return request({
    url: "/collaborativeApproval/rpa/status/" + rpaId,
    method: "get",
  });
}
src/views/collaborativeApproval/attendanceManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,714 @@
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab" type="border-card">
      <!-- å‡æœŸè®¾ç½® -->
      <el-tab-pane label="假期设置" name="holiday">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('holiday', 'add')">新增假期</el-button>
          <el-table :data="holidayData" border style="width: 100%; margin-top: 20px;">
            <el-table-column prop="name" label="假期名称" />
            <el-table-column prop="type" label="假期类型">
              <template #default="scope">
                <el-tag :type="getTagType(scope.row.type)">{{ getTypeLabel(scope.row.type) }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="startDate" label="开始日期"  />
            <el-table-column prop="endDate" label="结束日期"  />
            <el-table-column prop="days" label="天数"  align="center" />
            <el-table-column prop="status" label="状态" >
              <template #default="scope">
                <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                  {{ scope.row.status === 'active' ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right">
              <template #default="scope">
                <el-button type="primary" size="small" @click="openDialog('holiday', 'edit', scope.row)">编辑</el-button>
                <el-button type="danger" size="small" @click="deleteItem('holiday', scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>
      <!-- å¹´å‡è®¾ç½® -->
      <el-tab-pane label="年假设置" name="annual">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('annual', 'add')">新增年假规则</el-button>
          <el-table :data="annualData" border style="width: 100%; margin-top: 20px;">
            <el-table-column prop="employeeType" label="员工类型"/>
            <el-table-column prop="workYears" label="工作年限" />
            <el-table-column prop="annualDays" label="年假天数" align="center" />
            <el-table-column prop="maxCarryOver" label="最大结转天数" align="center" />
            <el-table-column prop="status" label="状态">
              <template #default="scope">
                <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                  {{ scope.row.status === 'active' ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right">
              <template #default="scope">
                <el-button type="primary" size="small" @click="openDialog('annual', 'edit', scope.row)">编辑</el-button>
                <el-button type="danger" size="small" @click="deleteItem('annual', scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>
      <!-- åŠ ç­è®¾ç½® -->
      <el-tab-pane label="加班设置" name="overtime">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('overtime', 'add')">新增加班规则</el-button>
          <el-table :data="overtimeData" border style="width: 100%; margin-top: 20px;">
            <el-table-column prop="name" label="规则名称" />
            <el-table-column prop="type" label="加班类型" >
              <template #default="scope">
                <el-tag :type="getTagType(scope.row.type)">{{ getTypeLabel(scope.row.type) }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="startTime" label="开始时间"  />
            <el-table-column prop="endTime" label="结束时间"  />
            <el-table-column prop="rate" label="倍率" align="center" />
            <el-table-column prop="status" label="状态" >
              <template #default="scope">
                <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                  {{ scope.row.status === 'active' ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right">
              <template #default="scope">
                <el-button type="primary" size="small" @click="openDialog('overtime', 'edit', scope.row)">编辑</el-button>
                <el-button type="danger" size="small" @click="deleteItem('overtime', scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>
      <!-- ä¸Šç­æ—¶é—´è®¾ç½® -->
      <el-tab-pane label="上班时间设置" name="worktime">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('worktime', 'add')">新增时间段</el-button>
          <el-table :data="worktimeData" border style="width: 100%; margin-top: 20px;">
            <el-table-column prop="name" label="时间段名称"  />
            <el-table-column prop="startTime" label="上班时间"/>
            <el-table-column prop="endTime" label="下班时间" />
            <el-table-column prop="flexibleStart" label="弹性上班">
              <template #default="scope">
                <el-tag :type="scope.row.flexibleStart ? 'success' : 'info'">
                  {{ scope.row.flexibleStart ? '是' : '否' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="flexibleMinutes" label="弹性时间(分钟)" width="120" align="center" />
            <el-table-column prop="status" label="状态" >
              <template #default="scope">
                <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                  {{ scope.row.status === 'active' ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" fixed="right">
              <template #default="scope">
                <el-button type="primary" size="small" @click="openDialog('worktime', 'edit', scope.row)">编辑</el-button>
                <el-button type="danger" size="small" @click="deleteItem('worktime', scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-tab-pane>
    </el-tabs>
    <!-- é€šç”¨å¼¹çª— -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="名称" prop="name" v-if="currentType !== 'annual'">
          <el-input v-model="form.name" placeholder="请输入名称" />
        </el-form-item>
        <el-form-item label="类型" prop="type" v-if="currentType === 'holiday' || currentType === 'overtime'">
          <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
            <el-option
              v-for="option in getTypeOptions()"
              :key="option.value"
              :label="option.label"
              :value="option.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="员工类型" prop="employeeType" v-if="currentType === 'annual'">
          <el-select v-model="form.employeeType" placeholder="请选择员工类型" style="width: 100%">
            <el-option label="正式员工" value="regular" />
            <el-option label="试用期员工" value="probation" />
            <el-option label="实习生" value="intern" />
          </el-select>
        </el-form-item>
        <el-form-item label="工作年限" prop="workYears" v-if="currentType === 'annual'">
          <el-input v-model="form.workYears" placeholder="如:1-3年、3-5年等" />
        </el-form-item>
        <el-form-item label="年假天数" prop="annualDays" v-if="currentType === 'annual'">
          <el-input-number v-model="form.annualDays" :min="0" :max="365" style="width: 100%" />
        </el-form-item>
        <el-form-item label="最大结转天数" prop="maxCarryOver" v-if="currentType === 'annual'">
          <el-input-number v-model="form.maxCarryOver" :min="0" :max="30" style="width: 100%" />
        </el-form-item>
                          <el-form-item label="日期范围" prop="dateRange" v-if="currentType === 'holiday'">
           <el-date-picker
             v-model="form.dateRange"
             type="daterange"
             range-separator="至"
             start-placeholder="开始日期"
             end-placeholder="结束日期"
             style="width: 100%"
             @change="calculateDays"
           />
         </el-form-item>
        <el-form-item label="天数" prop="days" v-if="currentType === 'holiday'">
          <el-input-number v-model="form.days" :min="0" style="width: 100%" />
        </el-form-item>
                 <el-form-item label="开始时间" prop="startTime" v-if="currentType === 'overtime'">
           <el-time-picker
             v-model="form.startTime"
             placeholder="开始时间"
             format="HH:mm"
             value-format="HH:mm"
             style="width: 100%"
             @change="validateTimeField('startTime')"
           />
         </el-form-item>
         <el-form-item label="结束时间" prop="endTime" v-if="currentType === 'overtime'">
           <el-time-picker
             v-model="form.endTime"
             placeholder="结束时间"
             format="HH:mm"
             value-format="HH:mm"
             style="width: 100%"
             @change="validateTimeField('endTime')"
           />
         </el-form-item>
        <el-form-item label="倍率" prop="rate" v-if="currentType === 'overtime'">
          <el-input-number v-model="form.rate" :min="1" :max="3" :step="0.5" style="width: 100%" />
        </el-form-item>
                 <el-form-item label="上班时间" prop="workStartTime" v-if="currentType === 'worktime'">
           <el-time-picker
             v-model="form.workStartTime"
             placeholder="上班时间"
             format="HH:mm"
             value-format="HH:mm"
             style="width: 100%"
             @change="validateTimeField('workStartTime')"
           />
         </el-form-item>
         <el-form-item label="下班时间" prop="workEndTime" v-if="currentType === 'worktime'">
           <el-time-picker
             v-model="form.workEndTime"
             placeholder="下班时间"
             format="HH:mm"
             value-format="HH:mm"
             style="width: 100%"
             @change="validateTimeField('workEndTime')"
           />
         </el-form-item>
        <el-form-item label="弹性上班" prop="flexibleStart" v-if="currentType === 'worktime'">
          <el-switch v-model="form.flexibleStart" />
        </el-form-item>
        <el-form-item label="弹性时间(分钟)" prop="flexibleMinutes" v-if="currentType === 'worktime' && form.flexibleStart">
          <el-input-number v-model="form.flexibleMinutes" :min="0" :max="120" style="width: 100%" />
        </el-form-item>
                 <el-form-item label="状态" prop="status">
           <el-radio-group v-model="form.status">
             <el-radio value="active">启用</el-radio>
             <el-radio value="inactive">停用</el-radio>
           </el-radio-group>
         </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// å½“前激活的标签页
const activeTab = ref('holiday')
// å¼¹çª—相关
const dialogVisible = ref(false)
const dialogTitle = ref('')
const currentType = ref('')
const currentAction = ref('')
const currentEditId = ref('')
const formRef = ref()
// è¡¨å•数据
const form = reactive({
  name: '',
  type: '',
  dateRange: [],
  days: 0,
  employeeType: '',
  workYears: '',
  annualDays: 0,
  maxCarryOver: 0,
  startTime: '', // åŠ ç­å¼€å§‹æ—¶é—´
  endTime: '',   // åŠ ç­ç»“æŸæ—¶é—´
  workStartTime: '', // ä¸Šç­æ—¶é—´
  workEndTime: '',   // ä¸‹ç­æ—¶é—´
  rate: 1.5,
  flexibleStart: false,
  flexibleMinutes: 30,
  status: 'active'
})
// è¡¨å•验证规则
const rules = {
  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
  type: [{ required: true, message: '请选择类型', trigger: 'change' }],
  dateRange: [{ required: true, message: '请选择日期范围', trigger: 'change' }],
  days: [{ required: true, message: '请输入天数', trigger: 'blur' }],
  employeeType: [{ required: true, message: '请选择员工类型', trigger: 'change' }],
  workYears: [{ required: true, message: '请输入工作年限', trigger: 'blur' }],
  annualDays: [{ required: true, message: '请输入年假天数', trigger: 'blur' }],
  maxCarryOver: [{ required: true, message: '请输入最大结转天数', trigger: 'blur' }],
  startTime: [{
    required: true,
    message: '请选择开始时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择开始时间'))
      } else {
        callback()
      }
    }
  }],
  endTime: [{
    required: true,
    message: '请选择结束时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择结束时间'))
      } else {
        callback()
      }
    }
  }],
  workStartTime: [{
    required: true,
    message: '请选择上班时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择上班时间'))
      } else {
        callback()
      }
    }
  }],
  workEndTime: [{
    required: true,
    message: '请选择下班时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择下班时间'))
      } else {
        callback()
      }
    }
  }],
  rate: [{ required: true, message: '请输入倍率', trigger: 'blur' }]
}
// æ¨¡æ‹Ÿæ•°æ®
const holidayData = ref([
  { id: '1', name: '春节', type: 'legal', startDate: '2024-02-10', endDate: '2024-02-17', days: 8, status: 'active' },
  { id: '2', name: '清明节', type: 'legal', startDate: '2024-04-05', endDate: '2024-04-05', days: 1, status: 'active' },
  { id: '3', name: '劳动节', type: 'legal', startDate: '2024-05-01', endDate: '2024-05-05', days: 5, status: 'active' }
])
const annualData = ref([
  { id: '1', employeeType: 'regular', workYears: '1-3å¹´', annualDays: 5, maxCarryOver: 2, status: 'active' },
  { id: '2', employeeType: 'regular', workYears: '3-5å¹´', annualDays: 10, maxCarryOver: 5, status: 'active' },
  { id: '3', employeeType: 'regular', workYears: '5年以上', annualDays: 15, maxCarryOver: 10, status: 'active' }
])
const overtimeData = ref([
  { id: '1', name: '工作日加班', type: 'weekday', startTime: '18:00', endTime: '22:00', rate: 1.5, status: 'active' },
  { id: '2', name: '周末加班', type: 'weekend', startTime: '09:00', endTime: '18:00', rate: 2.0, status: 'active' },
  { id: '3', name: '深夜加班', type: 'night', startTime: '22:00', endTime: '06:00', rate: 2.5, status: 'active' }
])
const worktimeData = ref([
  { id: '1', name: '标准工作时间', startTime: '09:00', endTime: '18:00', flexibleStart: true, flexibleMinutes: 30, status: 'active' },
  { id: '2', name: '早班时间', startTime: '08:00', endTime: '17:00', flexibleStart: false, flexibleMinutes: 0, status: 'active' },
  { id: '3', name: '晚班时间', startTime: '14:00', endTime: '23:00', flexibleStart: false, flexibleMinutes: 0, status: 'active' }
])
// å·¥å…·å‡½æ•°
const getTagType = (type) => {
  const tagMap = {
    legal: 'success', adjustment: 'warning', special: 'info', company: 'primary',
    weekday: 'primary', weekend: 'warning', holiday: 'danger', night: 'info'
  }
  return tagMap[type] || 'info'
}
const getTypeLabel = (type) => {
  const labelMap = {
    legal: '法定节假日', adjustment: '调休日', special: '特殊假期', company: '公司假期',
    weekday: '工作日加班', weekend: '周末加班', holiday: '节假日加班', night: '深夜加班'
  }
  return labelMap[type] || type
}
const getTypeOptions = () => {
  if (currentType.value === 'holiday') {
    return [
      { label: '法定节假日', value: 'legal' },
      { label: '调休日', value: 'adjustment' },
      { label: '特殊假期', value: 'special' },
      { label: '公司假期', value: 'company' }
    ]
  } else if (currentType.value === 'overtime') {
    return [
      { label: '工作日加班', value: 'weekday' },
      { label: '周末加班', value: 'weekend' },
      { label: '节假日加班', value: 'holiday' },
      { label: '深夜加班', value: 'night' }
    ]
  }
  return []
}
// è®¡ç®—假期天数
const calculateDays = () => {
  try {
    if (form.dateRange && form.dateRange.length === 2 && form.dateRange[0] && form.dateRange[1]) {
      const start = new Date(form.dateRange[0])
      const end = new Date(form.dateRange[1])
      if (isNaN(start.getTime()) || isNaN(end.getTime())) {
        console.warn('无效的日期格式')
        return
      }
      const diffTime = Math.abs(end - start)
      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
      form.days = diffDays
    }
  } catch (error) {
    console.error('计算天数失败:', error)
  }
}
// éªŒè¯æ—¶é—´æ ¼å¼
const validateTime = (time) => {
  if (!time) return ''
  if (typeof time === 'string') return time
  if (time instanceof Date) {
    return time.toTimeString().slice(0, 5)
  }
  return ''
}
// éªŒè¯æ—¶é—´å­—段
const validateTimeField = (fieldName) => {
  try {
    const value = form[fieldName]
    if (value && typeof value === 'object' && value.hour !== undefined) {
      // å¦‚果是时间对象,转换为字符串格式
      const hours = value.hour.toString().padStart(2, '0')
      const minutes = value.minute.toString().padStart(2, '0')
      form[fieldName] = `${hours}:${minutes}`
    }
  } catch (error) {
    console.error(`验证时间字段 ${fieldName} å¤±è´¥:`, error)
    form[fieldName] = ''
  }
}
// æ‰“开弹窗
const openDialog = (type, action, row = null) => {
  try {
    currentType.value = type
    currentAction.value = action
    if (action === 'add') {
      dialogTitle.value = `新增${getTypeName(type)}`
      currentEditId.value = ''
      resetForm()
    } else if (action === 'edit' && row) {
      dialogTitle.value = `编辑${getTypeName(type)}`
      currentEditId.value = row.id
      fillForm(row)
    }
    dialogVisible.value = true
  } catch (error) {
    console.error('打开弹窗失败:', error)
    ElMessage.error('打开弹窗失败,请重试')
  }
}
const getTypeName = (type) => {
  const nameMap = {
    holiday: '假期',
    annual: '年假规则',
    overtime: '加班规则',
    worktime: '时间段'
  }
  return nameMap[type] || ''
}
const resetForm = () => {
  Object.assign(form, {
    name: '',
    type: '',
    dateRange: [],
    days: 0,
    employeeType: '',
    workYears: '',
    annualDays: 0,
    maxCarryOver: 0,
    startTime: '',
    endTime: '',
    workStartTime: '',
    workEndTime: '',
    rate: 1.5,
    flexibleStart: false,
    flexibleMinutes: 30,
    status: 'active'
  })
}
const fillForm = (row) => {
  if (currentType.value === 'holiday') {
    Object.assign(form, {
      name: row.name,
      type: row.type,
      dateRange: [new Date(row.startDate), new Date(row.endDate)],
      days: row.days,
      status: row.status
    })
  } else if (currentType.value === 'annual') {
    Object.assign(form, {
      employeeType: row.employeeType,
      workYears: row.workYears,
      annualDays: row.annualDays,
      maxCarryOver: row.maxCarryOver,
      status: row.status
    })
  } else if (currentType.value === 'overtime') {
    Object.assign(form, {
      name: row.name,
      type: row.type,
      startTime: row.startTime || '',
      endTime: row.endTime || '',
      rate: row.rate,
      status: row.status
    })
  } else if (currentType.value === 'worktime') {
    Object.assign(form, {
      name: row.name,
      workStartTime: row.startTime || '',
      workEndTime: row.endTime || '',
      flexibleStart: row.flexibleStart,
      flexibleMinutes: row.flexibleMinutes,
      status: row.status
    })
  }
}
// æäº¤è¡¨å•
const submitForm = async () => {
  try {
    if (!formRef.value) {
      ElMessage.error('表单引用不存在')
      return
    }
    await formRef.value.validate()
    if (currentAction.value === 'add') {
      addItem()
    } else if (currentAction.value === 'edit') {
      editItem()
    }
    dialogVisible.value = false
    ElMessage.success('操作成功')
  } catch (error) {
    console.error('表单验证失败:', error)
    ElMessage.error('表单验证失败,请检查输入')
  }
}
const addItem = () => {
  const newItem = { ...form, id: Date.now().toString() }
  if (currentType.value === 'holiday') {
    newItem.startDate = form.dateRange[0].toISOString().split('T')[0]
    newItem.endDate = form.dateRange[1].toISOString().split('T')[0]
    holidayData.value.push(newItem)
  } else if (currentType.value === 'annual') {
    annualData.value.push(newItem)
  } else if (currentType.value === 'overtime') {
    newItem.startTime = form.startTime || ''
    newItem.endTime = form.endTime || ''
    overtimeData.value.push(newItem)
  } else if (currentType.value === 'worktime') {
    newItem.startTime = form.workStartTime || ''
    newItem.endTime = form.workEndTime || ''
    worktimeData.value.push(newItem)
  }
}
const editItem = () => {
  let dataArray
  let index
  if (currentType.value === 'holiday') {
    dataArray = holidayData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        type: form.type,
        startDate: form.dateRange[0].toISOString().split('T')[0],
        endDate: form.dateRange[1].toISOString().split('T')[0],
        days: form.days,
        status: form.status
      }
    }
  } else if (currentType.value === 'annual') {
    dataArray = annualData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        employeeType: form.employeeType,
        workYears: form.workYears,
        annualDays: form.annualDays,
        maxCarryOver: form.maxCarryOver,
        status: form.status
      }
    }
  } else if (currentType.value === 'overtime') {
    dataArray = overtimeData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        type: form.type,
        startTime: form.startTime || '',
        endTime: form.endTime || '',
        rate: form.rate,
        status: form.status
      }
    }
  } else if (currentType.value === 'worktime') {
    dataArray = worktimeData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        startTime: form.workStartTime || '',
        endTime: form.workEndTime || '',
        flexibleStart: form.flexibleStart,
        flexibleMinutes: form.flexibleMinutes,
        status: form.status
      }
    }
  }
}
// åˆ é™¤é¡¹ç›®
const deleteItem = (type, row) => {
  ElMessageBox.confirm('确定要删除这个项目吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    let dataArray
    if (type === 'holiday') dataArray = holidayData.value
    else if (type === 'annual') dataArray = annualData.value
    else if (type === 'overtime') dataArray = overtimeData.value
    else if (type === 'worktime') dataArray = worktimeData.value
    const index = dataArray.findIndex(item => item.id === row.id)
    if (index > -1) {
      dataArray.splice(index, 1)
      ElMessage.success('删除成功')
    }
  })
}
onMounted(() => {
  console.log('考勤管理页面加载完成')
})
onUnmounted(() => {
  // æ¸…理工作
  dialogVisible.value = false
  currentType.value = ''
  currentAction.value = ''
  currentEditId.value = ''
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.tab-content {
  padding: 20px 0;
}
.dialog-footer {
  text-align: right;
}
:deep(.el-tabs__content) {
  padding: 20px;
}
:deep(.el-form-item) {
  margin-bottom: 20px;
}
</style>
src/views/collaborativeApproval/knowledgeBase/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,848 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">知识标题:</span>
        <el-input
          v-model="searchForm.title"
          style="width: 240px"
          placeholder="请输入知识标题搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">知识类型:</span>
        <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px">
          <el-option label="合同特批" :value="'contract'" />
          <el-option label="审批案例" :value="'approval'" />
          <el-option label="解决方案" :value="'solution'" />
          <el-option label="经验总结" :value="'experience'" />
          <el-option label="操作指南" :value="'guide'" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增知识</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      ></PIMTable>
    </div>
    <!-- æ–°å¢ž/编辑知识弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="800px"
      :close-on-click-modal="false"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="知识标题" prop="title">
              <el-input v-model="form.title" placeholder="请输入知识标题" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="知识类型" prop="type">
              <el-select v-model="form.type" placeholder="请选择知识类型" style="width: 100%">
                <el-option label="合同特批" value="contract" />
                <el-option label="审批案例" value="approval" />
                <el-option label="解决方案" value="solution" />
                <el-option label="经验总结" value="experience" />
                <el-option label="操作指南" value="guide" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="适用场景" prop="scenario">
              <el-input v-model="form.scenario" placeholder="请输入适用场景" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="解决效率" prop="efficiency">
              <el-select v-model="form.efficiency" placeholder="请选择解决效率" style="width: 100%">
                <el-option label="显著提升" value="high" />
                <el-option label="一般提升" value="medium" />
                <el-option label="轻微提升" value="low" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="问题描述" prop="problem">
          <el-input
            v-model="form.problem"
            type="textarea"
            :rows="3"
            placeholder="请描述遇到的问题"
          />
        </el-form-item>
        <el-form-item label="解决方案" prop="solution">
          <el-input
            v-model="form.solution"
            type="textarea"
            :rows="4"
            placeholder="请详细描述解决方案"
          />
        </el-form-item>
        <el-form-item label="关键要点" prop="keyPoints">
          <el-input
            v-model="form.keyPoints"
            type="textarea"
            :rows="3"
            placeholder="请输入关键要点,用逗号分隔"
          />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="创建人" prop="creator">
              <el-input v-model="form.creator" placeholder="请输入创建人" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="使用次数" prop="usageCount">
              <el-input-number v-model="form.usageCount" :min="0" style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æŸ¥çœ‹çŸ¥è¯†è¯¦æƒ…弹窗 -->
    <el-dialog
      v-model="viewDialogVisible"
      title="知识详情"
      width="900px"
      :close-on-click-modal="false"
    >
      <div class="knowledge-detail">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="知识标题" :span="2">
            <span class="detail-title">{{ currentKnowledge.title }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="知识类型">
            <el-tag :type="getTypeTagType(currentKnowledge.type)">
              {{ getTypeLabel(currentKnowledge.type) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="适用场景">
            {{ currentKnowledge.scenario }}
          </el-descriptions-item>
          <el-descriptions-item label="解决效率">
            <el-tag :type="getEfficiencyTagType(currentKnowledge.efficiency)">
              {{ getEfficiencyLabel(currentKnowledge.efficiency) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="使用次数">
            <el-tag type="info">{{ currentKnowledge.usageCount }} æ¬¡</el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="创建人">
            {{ currentKnowledge.creator }}
          </el-descriptions-item>
          <el-descriptions-item label="创建时间">
            {{ currentKnowledge.createTime }}
          </el-descriptions-item>
        </el-descriptions>
        <div class="detail-section">
          <h4>问题描述</h4>
          <div class="detail-content">{{ currentKnowledge.problem }}</div>
        </div>
        <div class="detail-section">
          <h4>解决方案</h4>
          <div class="detail-content">{{ currentKnowledge.solution }}</div>
        </div>
        <div class="detail-section">
          <h4>关键要点</h4>
          <div class="key-points">
            <el-tag
              v-for="(point, index) in currentKnowledge.keyPoints.split(',')"
              :key="index"
              type="success"
              style="margin-right: 8px; margin-bottom: 8px;"
            >
              {{ point.trim() }}
            </el-tag>
          </div>
        </div>
        <div class="detail-section">
          <h4>使用统计</h4>
          <div class="usage-stats">
            <el-row :gutter="20">
              <el-col :span="8">
                <div class="stat-item">
                  <div class="stat-number">{{ currentKnowledge.usageCount }}</div>
                  <div class="stat-label">使用次数</div>
                </div>
              </el-col>
              <el-col :span="8">
                <div class="stat-item">
                  <div class="stat-number">{{ getEfficiencyScore(currentKnowledge.efficiency) }}%</div>
                  <div class="stat-label">效率提升</div>
                </div>
              </el-col>
              <el-col :span="8">
                <div class="stat-item">
                  <div class="stat-number">{{ getTimeSaved(currentKnowledge.efficiency) }}</div>
                  <div class="stat-label">平均节省时间</div>
                </div>
              </el-col>
            </el-row>
          </div>
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="viewDialogVisible = false">关闭</el-button>
          <el-button type="primary" @click="copyKnowledge">复制知识</el-button>
          <el-button type="success" @click="markAsFavorite">收藏</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
// è¡¨å•验证规则
const rules = {
  title: [
    { required: true, message: "请输入知识标题", trigger: "blur" }
  ],
  type: [
    { required: true, message: "请选择知识类型", trigger: "change" }
  ],
  problem: [
    { required: true, message: "请描述遇到的问题", trigger: "blur" }
  ],
  solution: [
    { required: true, message: "请详细描述解决方案", trigger: "blur" }
  ]
};
// å“åº”式数据
const data = reactive({
  searchForm: {
    title: "",
    type: "",
  },
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    total: 0,
  },
  tableData: [],
  selectedIds: [],
  form: {
    title: "",
    type: "",
    scenario: "",
    efficiency: "medium",
    problem: "",
    solution: "",
    keyPoints: "",
    creator: "",
    usageCount: 0
  },
  dialogVisible: false,
  dialogTitle: "",
  dialogType: "add",
  viewDialogVisible: false,
  currentKnowledge: {}
});
const {
  searchForm,
  tableLoading,
  page,
  tableData,
  selectedIds,
  form,
  dialogVisible,
  dialogTitle,
  dialogType,
  viewDialogVisible,
  currentKnowledge
} = toRefs(data);
// è¡¨å•引用
const formRef = ref();
// è¡¨æ ¼åˆ—配置
const tableColumn = ref([
  {
    label: "知识标题",
    prop: "title",
    showOverflowTooltip: true,
  },
  {
    label: "知识类型",
    prop: "type",
    dataType: "tag",
    formatData: (params) => {
      const typeMap = {
        contract: "合同特批",
        approval: "审批案例",
        solution: "解决方案",
        experience: "经验总结",
        guide: "操作指南"
      };
      return typeMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        contract: "success",
        approval: "warning",
        solution: "primary",
        experience: "info",
        guide: "danger"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "适用场景",
    prop: "scenario",
    width: 150,
    showOverflowTooltip: true,
  },
  {
    label: "解决效率",
    prop: "efficiency",
    dataType: "tag",
    formatData: (params) => {
      const efficiencyMap = {
        high: "显著提升",
        medium: "一般提升",
        low: "轻微提升"
      };
      return efficiencyMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        high: "success",
        medium: "warning",
        low: "info"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "使用次数",
    prop: "usageCount",
    width: 100,
    align: "center"
  },
  {
    label: "创建人",
    prop: "creator",
    width: 120,
  },
  {
    label: "创建时间",
    prop: "createTime",
    width: 180,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 200,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          viewKnowledge(row);
        }
      }
    ]
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
let mockData = [
  {
    id: "1",
    title: "特殊合同审批流程优化方案",
    type: "contract",
    scenario: "大额合同快速审批",
    efficiency: "high",
    problem: "大额合同审批流程复杂,审批时间长,影响业务进展",
    solution: "建立绿色通道,对符合条件的合同采用简化审批流程,由部门负责人直接审批,平均审批时间从3天缩短至1天",
    keyPoints: "绿色通道条件,简化流程,审批权限,时间控制",
    creator: "张经理",
    usageCount: 15,
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    title: "跨部门协作审批经验总结",
    type: "experience",
    scenario: "多部门协作项目",
    efficiency: "medium",
    problem: "跨部门项目审批时,各部门意见不统一,审批进度缓慢",
    solution: "建立项目协调机制,指定项目负责人,定期召开协调会议,统一各方意见后再进行审批",
    keyPoints: "项目协调,定期会议,统一意见,负责人制度",
    creator: "李主管",
    usageCount: 8,
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    title: "紧急采购审批操作指南",
    type: "guide",
    scenario: "紧急采购需求",
    efficiency: "high",
    problem: "紧急采购时审批流程复杂,无法满足紧急需求",
    solution: "制定紧急采购审批标准,明确紧急程度分级,不同级别采用不同审批流程,确保紧急需求得到及时处理",
    keyPoints: "紧急分级,标准制定,流程简化,及时处理",
    creator: "王专员",
    usageCount: 12,
    createTime: "2024-01-13 09:15:00"
  }
];
// çŸ¥è¯†æ ‡é¢˜æ¨¡æ¿
const titleTemplates = [
  "{type}审批流程优化方案",
  "{scenario}处理经验总结",
  "{type}特殊情况处理指南",
  "{scenario}快速审批方案",
  "{type}标准化操作流程",
  "{scenario}问题解决方案",
  "{type}最佳实践总结",
  "{scenario}效率提升方案"
];
// çŸ¥è¯†ç±»åž‹é…ç½®
const knowledgeTypes = [
  { type: "contract", label: "合同特批", efficiency: "high" },
  { type: "approval", label: "审批案例", efficiency: "medium" },
  { type: "solution", label: "解决方案", efficiency: "high" },
  { type: "experience", label: "经验总结", efficiency: "medium" },
  { type: "guide", label: "操作指南", efficiency: "low" }
];
// åœºæ™¯åˆ—表
const scenarios = ["大额合同审批", "跨部门协作", "紧急采购", "特殊申请", "流程优化", "问题处理", "标准化建设", "效率提升"];
// è‡ªåŠ¨ç”Ÿæˆæ–°æ•°æ®
const generateNewData = () => {
  const newId = (mockData.length + 1).toString();
  const now = new Date();
  const randomType = knowledgeTypes[Math.floor(Math.random() * knowledgeTypes.length)];
  const randomScenario = scenarios[Math.floor(Math.random() * scenarios.length)];
  // ç”Ÿæˆéšæœºæ ‡é¢˜
  let title = titleTemplates[Math.floor(Math.random() * titleTemplates.length)];
  title = title
    .replace('{type}', randomType.label)
    .replace('{scenario}', randomScenario);
  const newKnowledge = {
    id: newId,
    title: title,
    type: randomType.type,
    scenario: randomScenario,
    efficiency: randomType.efficiency,
    problem: `在${randomScenario}过程中遇到的问题描述...`,
    solution: `针对${randomScenario}的解决方案和操作步骤...`,
    keyPoints: "关键要点1,关键要点2,关键要点3,关键要点4",
    creator: ["张经理", "李主管", "王专员", "刘总监"][Math.floor(Math.random() * 4)],
    usageCount: Math.floor(Math.random() * 20) + 1,
    createTime: now.toLocaleString()
  };
  // æ·»åŠ åˆ°æ•°æ®å¼€å¤´
  mockData.unshift(newKnowledge);
  // ä¿æŒæ•°æ®é‡åœ¨åˆç†èŒƒå›´å†…(最多保留30条)
  if (mockData.length > 30) {
    mockData = mockData.slice(0, 30);
  }
  console.log(`[${new Date().toLocaleString()}] è‡ªåŠ¨ç”Ÿæˆæ–°çŸ¥è¯†: ${title}`);
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  getList();
  startAutoRefresh();
});
// å¼€å§‹è‡ªåŠ¨åˆ·æ–°
const startAutoRefresh = () => {
  setInterval(() => {
    generateNewData();
    getList();
  }, 600000); // 10分钟刷新一次 (10 * 60 * 1000 = 600000ms)
};
// æŸ¥è¯¢æ•°æ®
const handleQuery = () => {
  page.value.current = 1;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  setTimeout(() => {
    let filteredData = [...mockData];
    if (searchForm.value.title) {
      filteredData = filteredData.filter(item =>
        item.title.toLowerCase().includes(searchForm.value.title.toLowerCase())
      );
    }
    if (searchForm.value.type) {
      filteredData = filteredData.filter(item => item.type === searchForm.value.type);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
    tableLoading.value = false;
  }, 500);
};
// åˆ†é¡µå¤„理
const pagination = (obj) => {
  page.value.current = obj.page;
  page.value.size = obj.limit;
  handleQuery();
};
// é€‰æ‹©å˜åŒ–处理
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
// æ‰“开表单
const openForm = (type, row = null) => {
  dialogType.value = type;
  if (type === "add") {
    dialogTitle.value = "新增知识";
    // é‡ç½®è¡¨å•
    Object.assign(form.value, {
      title: "",
      type: "",
      scenario: "",
      efficiency: "medium",
      problem: "",
      solution: "",
      keyPoints: "",
      creator: "",
      usageCount: 0
    });
  } else if (type === "edit" && row) {
    dialogTitle.value = "编辑知识";
    Object.assign(form.value, {
      title: row.title,
      type: row.type,
      scenario: row.scenario,
      efficiency: row.efficiency,
      problem: row.problem,
      solution: row.solution,
      keyPoints: row.keyPoints,
      creator: row.creator,
      usageCount: row.usageCount
    });
  }
  dialogVisible.value = true;
};
// æŸ¥çœ‹çŸ¥è¯†è¯¦æƒ…
const viewKnowledge = (row) => {
  currentKnowledge.value = { ...row };
  viewDialogVisible.value = true;
};
// èŽ·å–ç±»åž‹æ ‡ç­¾ç±»åž‹
const getTypeTagType = (type) => {
  const typeMap = {
    contract: "success",
    approval: "warning",
    solution: "primary",
    experience: "info",
    guide: "danger"
  };
  return typeMap[type] || "info";
};
// èŽ·å–ç±»åž‹æ ‡ç­¾æ–‡æœ¬
const getTypeLabel = (type) => {
  const typeMap = {
    contract: "合同特批",
    approval: "审批案例",
    solution: "解决方案",
    experience: "经验总结",
    guide: "操作指南"
  };
  return typeMap[type] || type;
};
// èŽ·å–æ•ˆçŽ‡æ ‡ç­¾ç±»åž‹
const getEfficiencyTagType = (efficiency) => {
  const typeMap = {
    high: "success",
    medium: "warning",
    low: "info"
  };
  return typeMap[efficiency] || "info";
};
// èŽ·å–æ•ˆçŽ‡æ ‡ç­¾æ–‡æœ¬
const getEfficiencyLabel = (efficiency) => {
  const efficiencyMap = {
    high: "显著提升",
    medium: "一般提升",
    low: "轻微提升"
  };
  return efficiencyMap[efficiency] || efficiency;
};
// èŽ·å–æ•ˆçŽ‡æå‡ç™¾åˆ†æ¯”
const getEfficiencyScore = (efficiency) => {
  const scoreMap = {
    high: 40,
    medium: 25,
    low: 15
  };
  return scoreMap[efficiency] || 0;
};
// èŽ·å–å¹³å‡èŠ‚çœæ—¶é—´
const getTimeSaved = (efficiency) => {
  const timeMap = {
    high: "2-3天",
    medium: "1-2天",
    low: "0.5-1天"
  };
  return timeMap[efficiency] || "未知";
};
// å¤åˆ¶çŸ¥è¯†
const copyKnowledge = () => {
  const knowledgeText = `
知识标题:${currentKnowledge.value.title}
知识类型:${getTypeLabel(currentKnowledge.value.type)}
适用场景:${currentKnowledge.value.scenario}
问题描述:${currentKnowledge.value.problem}
解决方案:${currentKnowledge.value.solution}
关键要点:${currentKnowledge.value.keyPoints}
创建人:${currentKnowledge.value.creator}
  `.trim();
  // å¤åˆ¶åˆ°å‰ªè´´æ¿
  navigator.clipboard.writeText(knowledgeText).then(() => {
    ElMessage.success("知识内容已复制到剪贴板");
  }).catch(() => {
    ElMessage.error("复制失败,请手动复制");
  });
};
// æ”¶è—çŸ¥è¯†
const markAsFavorite = () => {
  // å¢žåŠ ä½¿ç”¨æ¬¡æ•°
  const index = mockData.findIndex(item => item.id === currentKnowledge.value.id);
  if (index !== -1) {
    mockData[index].usageCount += 1;
    currentKnowledge.value.usageCount += 1;
  }
  ElMessage.success("已收藏,使用次数+1");
};
// æäº¤çŸ¥è¯†è¡¨å•
const submitForm = async () => {
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ–°å¢žçŸ¥è¯†
      const newKnowledge = {
        id: (mockData.length + 1).toString(),
        title: form.value.title,
        type: form.value.type,
        scenario: form.value.scenario,
        efficiency: form.value.efficiency,
        problem: form.value.problem,
        solution: form.value.solution,
        keyPoints: form.value.keyPoints,
        creator: form.value.creator,
        usageCount: form.value.usageCount,
        createTime: new Date().toLocaleString()
      };
      mockData.unshift(newKnowledge);
      ElMessage.success("知识创建成功");
    } else {
      // ç¼–辑知识
      const index = mockData.findIndex(item => item.id === selectedIds.value[0]);
      if (index !== -1) {
        Object.assign(mockData[index], {
          title: form.value.title,
          type: form.value.type,
          scenario: form.value.scenario,
          efficiency: form.value.efficiency,
          problem: form.value.problem,
          solution: form.value.solution,
          keyPoints: form.value.keyPoints,
          creator: form.value.creator,
          usageCount: form.value.usageCount
        });
        ElMessage.success("知识更新成功");
      }
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
};
// åˆ é™¤çŸ¥è¯†
const handleDelete = () => {
  if (selectedIds.value.length === 0) {
    ElMessage.warning("请选择要删除的知识");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    // ä»ŽmockData中删除选中的项
    selectedIds.value.forEach(id => {
      const index = mockData.findIndex(item => item.id === id);
      if (index !== -1) {
        mockData.splice(index, 1);
      }
    });
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
</script>
<style scoped>
.auto-refresh-info {
  margin-bottom: 15px;
}
.auto-refresh-info .el-alert {
  border-radius: 8px;
}
.dialog-footer {
  text-align: right;
}
.knowledge-detail {
  padding: 20px 0;
}
.detail-title {
  font-size: 18px;
  font-weight: bold;
  color: #303133;
}
.detail-section {
  margin-top: 24px;
}
.detail-section h4 {
  margin: 0 0 12px 0;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  border-left: 4px solid #409eff;
  padding-left: 12px;
}
.detail-content {
  background: #f8f9fa;
  padding: 16px;
  border-radius: 6px;
  line-height: 1.6;
  color: #606266;
  white-space: pre-wrap;
}
.key-points {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.usage-stats {
  margin-top: 16px;
}
.stat-item {
  text-align: center;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 8px;
}
.stat-number {
  font-size: 24px;
  font-weight: bold;
  color: #409eff;
  margin-bottom: 8px;
}
.stat-label {
  font-size: 14px;
  color: #909399;
}
</style>
src/views/collaborativeApproval/notificationManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1187 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">通知标题:</span>
        <el-input
          v-model="searchForm.title"
          style="width: 240px"
          placeholder="请输入通知标题搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">通知类型:</span>
        <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px">
          <el-option label="放假通知" :value="'holiday'" />
          <el-option label="处罚通知" :value="'penalty'" />
          <el-option label="开会通知" :value="'meeting'" />
          <el-option label="临时通知" :value="'temporary'" />
          <el-option label="正式通知" :value="'formal'" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增通知</el-button>
        <el-button type="success" @click="openMeetingDialog">在线会议</el-button>
        <el-button type="warning" @click="openFileShareDialog">文件共享</el-button>
        <!-- <el-button type="info" @click="refreshEmployees">刷新员工</el-button> -->
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      ></PIMTable>
    </div>
    <!-- æ–°å¢ž/编辑通知弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="800px"
      :close-on-click-modal="false"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="通知标题" prop="title">
              <el-input v-model="form.title" placeholder="请输入通知标题" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="通知类型" prop="type">
              <el-select v-model="form.type" placeholder="请选择通知类型" style="width: 100%">
                <el-option label="放假通知" value="holiday" />
                <el-option label="处罚通知" value="penalty" />
                <el-option label="开会通知" value="meeting" />
                <el-option label="临时通知" value="temporary" />
                <el-option label="正式通知" value="formal" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="优先级" prop="priority">
              <el-select v-model="form.priority" placeholder="请选择优先级" style="width: 100%">
                <el-option label="普通" value="low" />
                <el-option label="重要" value="medium" />
                <el-option label="紧急" value="high" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="有效期至" prop="expireDate">
              <el-date-picker
                v-model="form.expireDate"
                type="date"
                placeholder="请选择有效期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="接收部门" prop="departments">
          <el-select
            v-model="form.departments"
            multiple
            placeholder="请选择接收部门"
            style="width: 100%"
          >
            <el-option
              v-for="dept in departments"
              :key="dept"
              :label="dept"
              :value="dept"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="同步方式" prop="syncMethods">
          <el-checkbox-group v-model="form.syncMethods">
            <el-checkbox
              v-for="method in syncMethods"
              :key="method.value"
              :label="method.value"
            >
              {{ method.label }}
            </el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        <el-form-item label="通知内容" prop="content">
          <el-input
            v-model="form.content"
            type="textarea"
            :rows="4"
            placeholder="请输入通知内容"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- åœ¨çº¿ä¼šè®®å¼¹çª— -->
    <el-dialog
      v-model="meetingDialogVisible"
      title="创建在线会议"
      width="700px"
      :close-on-click-modal="false"
    >
      <el-form ref="meetingFormRef" :model="meetingForm" :rules="meetingRules" label-width="120px">
        <el-form-item label="会议标题" prop="title">
          <el-input v-model="meetingForm.title" placeholder="请输入会议标题" />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="开始时间" prop="startTime">
              <el-date-picker
                v-model="meetingForm.startTime"
                type="datetime"
                placeholder="请选择开始时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="会议时长" prop="duration">
              <el-input-number
                v-model="meetingForm.duration"
                :min="15"
                :max="480"
                :step="15"
                style="width: 100%"
              />
              <span style="margin-left: 10px">分钟</span>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="会议平台" prop="platform">
          <el-select v-model="meetingForm.platform" placeholder="请选择会议平台" style="width: 100%">
            <el-option
              v-for="platform in meetingPlatforms"
              :key="platform.value"
              :label="platform.label"
              :value="platform.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="参会人员" prop="participants">
          <el-select
            v-model="meetingForm.participants"
            multiple
            filterable
            remote
            :remote-method="filterEmployees"
            :loading="employeesLoading"
            placeholder="请选择参会人员"
            style="width: 100%"
          >
            <el-option-group
              v-for="group in employeeGroups"
              :key="group.label"
              :label="group.label"
            >
                             <el-option
                 v-for="employee in group.options"
                 :key="employee.value"
                 :label="`${employee.label} (${employee.dept})`"
                 :value="employee.value"
               >
                 <div style="display: flex; justify-content: space-between; align-items: center;">
                   <div>
                     <div style="font-weight: 500;">{{ employee.label }}</div>
                     <div style="color: #909399; font-size: 12px;">{{ employee.dept }}</div>
                   </div>
                   <div style="text-align: right; font-size: 12px; color: #909399;">
                     <div v-if="employee.phone">{{ employee.phone }}</div>
                     <div v-if="employee.email">{{ employee.email }}</div>
                   </div>
                 </div>
               </el-option>
            </el-option-group>
          </el-select>
          <div style="margin-top: 8px; color: #909399; font-size: 12px;">
            å·²é€‰æ‹© {{ meetingForm.participants.length }} äºº
          </div>
          <!-- å·²é€‰æ‹©äººå‘˜è¯¦æƒ… -->
          <div v-if="meetingForm.participants.length > 0" style="margin-top: 10px;">
            <el-tag
              v-for="participantId in meetingForm.participants"
              :key="participantId"
              closable
              @close="removeParticipant(participantId)"
              style="margin-right: 8px; margin-bottom: 8px;"
            >
              {{ getEmployeeName(participantId) }}
            </el-tag>
          </div>
        </el-form-item>
        <el-form-item label="会议描述" prop="description">
          <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="meetingDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="createMeeting">创建会议</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æ–‡ä»¶å…±äº«å¼¹çª— -->
    <el-dialog
      v-model="fileShareDialogVisible"
      title="文件共享"
      width="700px"
      :close-on-click-modal="false"
    >
      <el-form ref="fileShareFormRef" :model="fileShareForm" :rules="fileShareRules" label-width="120px">
        <el-form-item label="共享标题" prop="title">
          <el-input v-model="fileShareForm.title" placeholder="请输入共享标题" />
        </el-form-item>
        <el-form-item label="共享描述" prop="description">
          <el-input
            v-model="fileShareForm.description"
            type="textarea"
            :rows="3"
            placeholder="请输入共享描述"
          />
        </el-form-item>
        <el-form-item label="接收部门" prop="departments">
          <el-select
            v-model="fileShareForm.departments"
            multiple
            placeholder="请选择接收部门"
            style="width: 100%"
          >
            <el-option
              v-for="dept in departments"
              :key="dept"
              :label="dept"
              :value="dept"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="上传文件" prop="files">
          <el-upload
            ref="uploadRef"
            :auto-upload="false"
            :on-change="handleFileChange"
            :on-remove="removeFile"
            :file-list="fileList"
            multiple
            :limit="10"
            accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.gif"
          >
            <el-button type="primary">选择文件</el-button>
            <template #tip>
              <div class="el-upload__tip">
                æ”¯æŒä¸Šä¼ æ–‡æ¡£ã€å›¾ç‰‡ç­‰æ ¼å¼ï¼Œå•个文件不超过10MB,最多10个文件
              </div>
            </template>
          </el-upload>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="fileShareDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="shareFiles">共享文件</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { staffOnJobListPage } from "@/api/personnelManagement/employeeRecord.js";
// è¡¨å•验证规则
const rules = {
  title: [
    { required: true, message: "请输入通知标题", trigger: "blur" }
  ],
  type: [
    { required: true, message: "请选择通知类型", trigger: "change" }
  ],
  content: [
    { required: true, message: "请输入通知内容", trigger: "blur" }
  ]
};
const meetingRules = {
  title: [
    { required: true, message: "请输入会议标题", trigger: "blur" }
  ],
  startTime: [
    { required: true, message: "请选择会议开始时间", trigger: "change" }
  ],
  participants: [
    { required: true, message: "请选择参会人员", trigger: "change" }
  ]
};
const fileShareRules = {
  title: [
    { required: true, message: "请输入共享标题", trigger: "blur" }
  ],
  description: [
    { required: true, message: "请输入共享描述", trigger: "blur" }
  ]
};
// å“åº”式数据
const data = reactive({
  searchForm: {
    title: "",
    type: "",
    status: "",
  },
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    total: 0,
  },
  tableData: [],
  selectedIds: [],
  // æ–°å¢žé€šçŸ¥ç›¸å…³
  form: {
    title: "",
    type: "",
    priority: "medium",
    content: "",
    departments: [],
    expireDate: "",
    syncMethods: []
  },
  dialogVisible: false,
  dialogTitle: "",
  dialogType: "add",
  // åœ¨çº¿ä¼šè®®ç›¸å…³
  meetingDialogVisible: false,
  meetingForm: {
    title: "",
    startTime: "",
    duration: 60,
    participants: [],
    description: "",
    platform: "wechat"
  },
  // æ–‡ä»¶å…±äº«ç›¸å…³
  fileShareDialogVisible: false,
  fileShareForm: {
    title: "",
    description: "",
    departments: [],
    files: []
  },
  fileList: []
});
const {
  searchForm,
  tableLoading,
  page,
  tableData,
  selectedIds,
  form,
  dialogVisible,
  dialogTitle,
  dialogType,
  meetingDialogVisible,
  meetingForm,
  fileShareDialogVisible,
  fileShareForm,
  fileList
} = toRefs(data);
// è¡¨å•引用
const formRef = ref();
const meetingFormRef = ref();
const fileShareFormRef = ref();
// è¡¨æ ¼åˆ—配置
const tableColumn = ref([
  {
    label: "通知标题",
    prop: "title",
    showOverflowTooltip: true,
  },
  {
    label: "通知类型",
    prop: "type",
    dataType: "tag",
    formatData: (params) => {
      const typeMap = {
        holiday: "放假通知",
        penalty: "处罚通知",
        meeting: "开会通知",
        temporary: "临时通知",
        formal: "正式通知"
      };
      return typeMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        holiday: "success",
        penalty: "danger",
        meeting: "warning",
        temporary: "info",
        formal: "primary"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "优先级",
    prop: "priority",
    dataType: "tag",
    formatData: (params) => {
      const priorityMap = {
        low: "普通",
        medium: "重要",
        high: "紧急"
      };
      return priorityMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        low: "info",
        medium: "warning",
        high: "danger"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "状态",
    prop: "status",
    dataType: "tag",
    formatData: (params) => {
      const statusMap = {
        draft: "草稿",
        published: "已发布",
        expired: "已过期"
      };
      return statusMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        draft: "info",
        published: "success",
        expired: "danger"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "接收部门",
    prop: "departments",
    width: 150,
    showOverflowTooltip: true,
    formatData: (params) => {
      if (!params || params.length === 0) return "全部部门";
      return params.join(", ");
    }
  },
  {
    label: "有效期至",
    prop: "expireDate",
    width: 150,
    formatData: (params) => {
      if (!params) return "永久有效";
      return params;
    }
  },
  {
    label: "创建时间",
    prop: "createTime",
    width: 180,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 280,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        }
      },
      {
        name: "发布",
        type: "text",
        clickFun: (row) => {
          publishNotification(row);
        },
        // disabled: (row) => row.status === "published"
      },
      {
        name: "撤回",
        type: "text",
        clickFun: (row) => {
          revokeNotification(row);
        },
        // disabled: (row) => row.status !== "published"
      }
    ]
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
let mockData = [
  {
    id: "1",
    title: "2024年春节放假通知",
    type: "holiday",
    priority: "high",
    status: "published",
    content: "根据国家规定,结合公司实际情况,现将2024年春节放假安排通知如下...",
    departments: ["技术部", "销售部", "人事部", "财务部", "运营部", "市场部", "客服部"],
    expireDate: "2024-02-15",
    syncMethods: ["wechat", "dingtalk", "email"],
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    title: "技术部周例会通知",
    type: "meeting",
    priority: "medium",
    status: "published",
    content: "技术部定于每周五下午2点召开周例会,请各位同事准时参加...",
    departments: ["技术部"],
    expireDate: "2024-01-20",
    syncMethods: ["wechat", "dingtalk"],
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    title: "员工行为规范处罚通知",
    type: "penalty",
    priority: "high",
    status: "draft",
    content: "为维护公司正常秩序,规范员工行为,现对违反公司规定的行为进行处罚...",
    departments: ["人事部", "技术部", "销售部"],
    expireDate: "2024-02-13",
    syncMethods: ["wechat", "email"],
    createTime: "2024-01-13 09:15:00"
  }
];
// é€šçŸ¥æ ‡é¢˜æ¨¡æ¿
const titleTemplates = [
  "关于{year}å¹´{holiday}放假安排的通知",
  "{dept}部门{meeting}会议通知",
  "员工{behavior}行为规范提醒",
  "{company}重要事项通知",
  "{dept}部门工作安排通知",
  "关于{project}项目进度的通知",
  "{dept}部门人员调整通知",
  "公司{policy}政策更新通知"
];
// é€šçŸ¥ç±»åž‹é…ç½®
const notificationTypes = [
  { type: "holiday", label: "放假通知", priority: "high" },
  { type: "meeting", label: "开会通知", priority: "medium" },
  { type: "penalty", label: "处罚通知", priority: "high" },
  { type: "temporary", label: "临时通知", priority: "low" },
  { type: "formal", label: "正式通知", priority: "medium" }
];
// éƒ¨é—¨åˆ—表
const departments = ["技术部", "销售部", "人事部", "财务部", "运营部", "市场部", "客服部"];
// äººå‘˜åˆ—表
const employees = ref([]);
const employeesLoading = ref(false);
// èŽ·å–åœ¨èŒå‘˜å·¥åˆ—è¡¨
const getEmployeesList = async () => {
  try {
    employeesLoading.value = true;
    // ä¼˜å…ˆä½¿ç”¨ç³»ç»Ÿç”¨æˆ·æŽ¥å£ï¼ˆæŒ‰ç§Ÿæˆ·èŽ·å–ï¼‰
    const userResponse = await userListNoPageByTenantId();
    if (userResponse.data) {
      employees.value = userResponse.data.map(user => ({
        label: user.nickName || user.userName || '未知姓名',
        value: user.userId || user.id,
        dept: user.dept?.deptName || '未知部门',
        phone: user.phonenumber || '',
        email: user.email || '',
        status: user.status || '0'
      })).filter(user => user.status === '0'); // åªæ˜¾ç¤ºæ­£å¸¸çŠ¶æ€çš„ç”¨æˆ·
    } else {
      // å¦‚果系统用户接口失败,使用员工台账接口
      const response = await staffOnJobListPage({
        pageNum: 1,
        pageSize: 1000,
        staffState: 1 // åœ¨èŒçŠ¶æ€
      });
      if (response.data && response.data.records) {
        employees.value = response.data.records.map(employee => ({
          label: employee.staffName || employee.name || '未知姓名',
          value: employee.staffNo || employee.id || employee.staffId,
          dept: employee.deptName || employee.department || '未知部门',
          phone: employee.phone || employee.mobile || '',
          email: employee.email || '',
          status: '0'
        }));
      }
    }
  } catch (error) {
    console.error('获取员工列表失败:', error);
    // å¦‚果接口都失败,使用默认数据
    employees.value = [
      { label: "张三", value: "001", dept: "技术部", phone: "13800138001", email: "zhangsan@company.com", status: "0" },
      { label: "李四", value: "002", dept: "销售部", phone: "13800138002", email: "lisi@company.com", status: "0" },
      { label: "王五", value: "003", dept: "人事部", phone: "13800138003", email: "wangwu@company.com", status: "0" }
    ];
  } finally {
    employeesLoading.value = false;
  }
};
// å‘˜å·¥åˆ†ç»„
const employeeGroups = computed(() => {
  const groups = {};
  employees.value.forEach(employee => {
    const dept = employee.dept || '其他部门';
    if (!groups[dept]) {
      groups[dept] = [];
    }
    groups[dept].push(employee);
  });
  // æŒ‰éƒ¨é—¨åç§°æŽ’序,确保显示顺序一致
  return Object.keys(groups)
    .sort()
    .map(dept => ({
      label: dept,
      options: groups[dept].sort((a, b) => a.label.localeCompare(b.label, 'zh-CN'))
    }));
});
// è¿‡æ»¤å‘˜å·¥ï¼ˆè¿œç¨‹æœç´¢ï¼‰
const filterEmployees = (query) => {
  if (query !== '') {
    const lowerQuery = query.toLowerCase();
    return employees.value.filter(employee =>
      employee.label.toLowerCase().includes(lowerQuery) ||
      employee.dept.toLowerCase().includes(lowerQuery) ||
      (employee.phone && employee.phone.includes(query)) ||
      (employee.email && employee.email.toLowerCase().includes(lowerQuery))
    );
  } else {
    return employees.value;
  }
};
// åˆ·æ–°å‘˜å·¥åˆ—表
const refreshEmployees = async () => {
  ElMessage.info("正在刷新员工列表...");
  await getEmployeesList();
  // ç»Ÿè®¡å„部门人数
  const deptStats = {};
  employees.value.forEach(emp => {
    const dept = emp.dept || '其他部门';
    deptStats[dept] = (deptStats[dept] || 0) + 1;
  });
  const deptInfo = Object.entries(deptStats)
    .map(([dept, count]) => `${dept}: ${count}人`)
    .join(', ');
  ElMessage.success(`员工列表刷新完成,共 ${employees.value.length} äºº (${deptInfo})`);
};
// èŽ·å–å‘˜å·¥å§“å
const getEmployeeName = (employeeId) => {
  const employee = employees.value.find(emp => emp.value === employeeId);
  return employee ? employee.label : '未知人员';
};
// èŽ·å–å‘˜å·¥è¯¦ç»†ä¿¡æ¯
const getEmployeeInfo = (employeeId) => {
  const employee = employees.value.find(emp => emp.value === employeeId);
  if (!employee) return null;
  return {
    name: employee.label,
    dept: employee.dept,
    phone: employee.phone,
    email: employee.email
  };
};
// ç§»é™¤å‚会人员
const removeParticipant = (participantId) => {
  const index = meetingForm.value.participants.indexOf(participantId);
  if (index > -1) {
    meetingForm.value.participants.splice(index, 1);
  }
};
// åŒæ­¥æ–¹å¼é€‰é¡¹
const syncMethods = [
  { label: "企业微信", value: "wechat" },
  { label: "钉钉", value: "dingtalk" },
  { label: "邮件", value: "email" },
  { label: "短信", value: "sms" }
];
// ä¼šè®®å¹³å°é€‰é¡¹
const meetingPlatforms = [
  { label: "企业微信会议", value: "wechat" },
  { label: "钉钉会议", value: "dingtalk" },
  { label: "腾讯会议", value: "tencent" },
  { label: "Zoom", value: "zoom" }
];
// è‡ªåŠ¨ç”Ÿæˆæ–°æ•°æ®
const generateNewData = () => {
  const newId = (mockData.length + 1).toString();
  const now = new Date();
  const randomType = notificationTypes[Math.floor(Math.random() * notificationTypes.length)];
  const randomDept = departments[Math.floor(Math.random() * departments.length)];
  // ç”Ÿæˆéšæœºæ ‡é¢˜
  let title = titleTemplates[Math.floor(Math.random() * titleTemplates.length)];
  title = title
    .replace('{year}', now.getFullYear())
    .replace('{holiday}', ['春节', '国庆', '中秋', '元旦'][Math.floor(Math.random() * 4)])
    .replace('{dept}', randomDept)
    .replace('{meeting}', ['周例会', '月度总结', '项目评审', '培训会议'][Math.floor(Math.random() * 4)])
    .replace('{behavior}', ['考勤', '着装', '工作态度', '团队协作'][Math.floor(Math.random() * 4)])
    .replace('{company}', ['公司', '集团', '总部'][Math.floor(Math.random() * 4)])
    .replace('{project}', ['数字化转型', '产品升级', '市场拓展', '人才培养'][Math.floor(Math.random() * 4)])
    .replace('{policy}', ['考勤', '薪酬', '福利', '晋升'][Math.floor(Math.random() * 4)]);
  // éšæœºçŠ¶æ€
  const statuses = ['draft', 'published'];
  const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
  // éšæœºä¼˜å…ˆçº§
  const priorities = ['low', 'medium', 'high'];
  const randomPriority = priorities[Math.floor(Math.random() * priorities.length)];
  const newNotification = {
    id: newId,
    title: title,
    type: randomType.type,
    priority: randomPriority,
    status: randomStatus,
    content: `这是${title}的详细内容,请相关人员注意查看...`,
    departments: [randomDept],
    expireDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 30天后过期
    syncMethods: ["wechat", "dingtalk"],
    createTime: now.toLocaleString()
  };
  // æ·»åŠ åˆ°æ•°æ®å¼€å¤´
  mockData.unshift(newNotification);
  // ä¿æŒæ•°æ®é‡åœ¨åˆç†èŒƒå›´å†…(最多保留20条)
  if (mockData.length > 20) {
    mockData = mockData.slice(0, 20);
  }
  console.log(`[${new Date().toLocaleString()}] è‡ªåŠ¨ç”Ÿæˆæ–°é€šçŸ¥: ${title}`);
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  getList();
  getEmployeesList(); // èŽ·å–å‘˜å·¥åˆ—è¡¨
  startAutoRefresh();
});
// å¼€å§‹è‡ªåŠ¨åˆ·æ–°
const startAutoRefresh = () => {
  setInterval(() => {
    generateNewData();
    getList();
  }, 600000); // 10分钟刷新一次 (10 * 60 * 1000 = 600000ms)
};
// æŸ¥è¯¢æ•°æ®
const handleQuery = () => {
  page.value.current = 1;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  setTimeout(() => {
    let filteredData = [...mockData];
    if (searchForm.value.title) {
      filteredData = filteredData.filter(item =>
        item.title.toLowerCase().includes(searchForm.value.title.toLowerCase())
      );
    }
    if (searchForm.value.type) {
      filteredData = filteredData.filter(item => item.type === searchForm.value.type);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
    tableLoading.value = false;
  }, 500);
};
// åˆ†é¡µå¤„理
const pagination = (obj) => {
  page.value.current = obj.page;
  page.value.size = obj.limit;
  handleQuery();
};
// é€‰æ‹©å˜åŒ–处理
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
// æ‰“开表单
const openForm = (type, row = null) => {
  dialogType.value = type;
  if (type === "add") {
    dialogTitle.value = "新增通知";
    // é‡ç½®è¡¨å•
    Object.assign(form.value, {
      title: "",
      type: "",
      priority: "medium",
      content: "",
      departments: [],
      expireDate: "",
      syncMethods: []
    });
  } else if (type === "edit" && row) {
    dialogTitle.value = "编辑通知";
    Object.assign(form.value, {
      title: row.title,
      type: row.type,
      priority: row.priority,
      content: row.content || "",
      departments: row.departments || [],
      expireDate: row.expireDate || "",
      syncMethods: row.syncMethods || []
    });
  }
  dialogVisible.value = true;
};
// æ‰“开在线会议弹窗
const openMeetingDialog = () => {
  // é‡ç½®è¡¨å•
  Object.assign(meetingForm.value, {
    title: "",
    startTime: "",
    duration: 60,
    participants: [],
    description: "",
    platform: "wechat"
  });
  meetingDialogVisible.value = true;
};
// æ‰“开文件共享弹窗
const openFileShareDialog = () => {
  // é‡ç½®è¡¨å•
  Object.assign(fileShareForm.value, {
    title: "",
    description: "",
    departments: [],
    files: []
  });
  fileList.value = [];
  fileShareDialogVisible.value = true;
};
// æ‰‹åŠ¨åˆ·æ–°æ•°æ®
const manualRefresh = () => {
  generateNewData();
  getList();
  ElMessage.success("手动刷新完成,已生成新通知");
};
// æäº¤é€šçŸ¥è¡¨å•
const submitForm = async () => {
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ–°å¢žé€šçŸ¥
      const newNotification = {
        id: (mockData.length + 1).toString(),
        title: form.value.title,
        type: form.value.type,
        priority: form.value.priority,
        status: "draft",
        content: form.value.content,
        departments: form.value.departments,
        expireDate: form.value.expireDate,
        syncMethods: form.value.syncMethods,
        createTime: new Date().toLocaleString()
      };
      mockData.unshift(newNotification);
      ElMessage.success("通知创建成功");
    } else {
      // ç¼–辑通知
      const index = mockData.findIndex(item => item.id === selectedIds.value[0]);
      if (index !== -1) {
        Object.assign(mockData[index], {
          title: form.value.title,
          type: form.value.type,
          priority: form.value.priority,
          content: form.value.content,
          departments: form.value.departments,
          expireDate: form.value.expireDate,
          syncMethods: form.value.syncMethods
        });
        ElMessage.success("通知更新成功");
      }
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
};
// åˆ›å»ºä¼šè®®
const createMeeting = async () => {
  try {
    await meetingFormRef.value.validate();
    // æ¨¡æ‹Ÿåˆ›å»ºä¼šè®®
    const meetingInfo = {
      title: meetingForm.value.title,
      startTime: meetingForm.value.startTime,
      duration: meetingForm.value.duration,
      participants: meetingForm.value.participants,
      description: meetingForm.value.description,
      platform: meetingForm.value.platform,
      meetingId: `MTG${Date.now()}`
    };
    // æ¨¡æ‹Ÿå‘送到企业微信/钉钉
    const platformName = meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "未知平台";
    ElMessage.success(`会议创建成功!会议ID: ${meetingInfo.meetingId},将通过${platformName}发送通知`);
    meetingDialogVisible.value = false;
         // èŽ·å–å‚ä¼šäººå‘˜ä¿¡æ¯
     const participantNames = meetingForm.value.participants.map(participantId => {
       const employee = employees.value.find(emp => emp.value === participantId);
       return employee ? employee.label : '未知人员';
     }).join('、');
     // èŽ·å–å‚ä¼šäººå‘˜è¯¦ç»†ä¿¡æ¯
     const participantDetails = meetingForm.value.participants.map(participantId => {
       const employee = employees.value.find(emp => emp.value === participantId);
       return employee ? {
         name: employee.label,
         dept: employee.dept,
         phone: employee.phone,
         email: employee.email
       } : null;
     }).filter(Boolean);
    // å°†ä¼šè®®ä¿¡æ¯æ·»åŠ åˆ°é€šçŸ¥åˆ—è¡¨
    const meetingNotification = {
      id: (mockData.length + 1).toString(),
      title: `[会议通知] ${meetingInfo.title}`,
      type: "meeting",
      priority: "high",
      status: "published",
             content: `会议时间: ${meetingInfo.startTime},时长: ${meetingInfo.duration}分钟,平台: ${meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "未知平台"},参会人员: ${participantNames},共${participantDetails.length}人`,
      departments: [],
      expireDate: "",
      syncMethods: [meetingForm.value.platform],
      createTime: new Date().toLocaleString()
    };
    mockData.unshift(meetingNotification);
    getList();
  } catch (error) {
    console.error("会议表单验证失败:", error);
  }
};
// æ–‡ä»¶ä¸Šä¼ å¤„理
const handleFileChange = (file) => {
  const isLt10M = file.size / 1024 / 1024 < 10;
  if (!isLt10M) {
    ElMessage.error("上传文件大小不能超过 10MB!");
    return false;
  }
  const fileInfo = {
    name: file.name,
    size: file.size,
    type: file.type,
    uid: file.uid
  };
  fileList.value.push(fileInfo);
  fileShareForm.value.files.push(fileInfo);
  return false; // é˜»æ­¢è‡ªåŠ¨ä¸Šä¼ 
};
// ç§»é™¤æ–‡ä»¶
const removeFile = (file) => {
  const index = fileList.value.findIndex(item => item.uid === file.uid);
  if (index !== -1) {
    const index2 = fileShareForm.value.files.findIndex(item => item.uid === file.uid);
    if (index2 !== -1) {
      fileShareForm.value.files.splice(index2, 1);
    }
    fileList.value.splice(index, 1);
  }
};
// å…±äº«æ–‡ä»¶
const shareFiles = async () => {
  try {
    await fileShareFormRef.value.validate();
    if (fileShareForm.value.files.length === 0) {
      ElMessage.warning("请至少选择一个文件");
      return;
    }
    // æ¨¡æ‹Ÿæ–‡ä»¶å…±äº«
    const shareInfo = {
      title: fileShareForm.value.title,
      description: fileShareForm.value.description,
      departments: fileShareForm.value.departments,
      files: fileShareForm.value.files,
      shareId: `FILE${Date.now()}`
    };
    ElMessage.success(`文件共享成功!共享ID: ${shareInfo.shareId},已通知相关部门`);
    fileShareDialogVisible.value = false;
    // å°†æ–‡ä»¶å…±äº«ä¿¡æ¯æ·»åŠ åˆ°é€šçŸ¥åˆ—è¡¨
    const fileShareNotification = {
      id: (mockData.length + 1).toString(),
      title: `[文件共享] ${shareInfo.title}`,
      type: "temporary",
      priority: "medium",
      status: "published",
      content: `共享描述: ${shareInfo.description},文件数量: ${shareInfo.files.length}个`,
      departments: shareInfo.departments,
      expireDate: "",
      syncMethods: ["wechat", "dingtalk"],
      createTime: new Date().toLocaleString()
    };
    mockData.unshift(fileShareNotification);
    getList();
  } catch (error) {
    console.error("文件共享表单验证失败:", error);
  }
};
// å‘布通知
const publishNotification = (row) => {
  row.status = "published";
  ElMessage.success("通知发布成功");
  getList();
};
// æ’¤å›žé€šçŸ¥
const revokeNotification = (row) => {
  row.status = "draft";
  ElMessage.success("通知已撤回");
  getList();
};
// åˆ é™¤é€šçŸ¥
const handleDelete = () => {
  if (selectedIds.value.length === 0) {
    ElMessage.warning("请选择要删除的通知");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
</script>
<style scoped>
.auto-refresh-info {
  margin-bottom: 15px;
}
.auto-refresh-info .el-alert {
  border-radius: 8px;
}
.dialog-footer {
  text-align: right;
}
.el-upload__tip {
  color: #909399;
  font-size: 12px;
  margin-top: 8px;
}
.el-checkbox-group {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.el-checkbox {
  margin-right: 0;
}
</style>
src/views/collaborativeApproval/rpaManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,400 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">程序名:</span>
        <el-input
          v-model="searchForm.programName"
          style="width: 240px"
          placeholder="请输入程序名搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">执行状态:</span>
        <el-select v-model="searchForm.status" clearable @change="handleQuery" style="width: 240px">
          <el-option label="运行中" :value="'running'" />
          <el-option label="已停止" :value="'stopped'" />
          <el-option label="异常" :value="'error'" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      ></PIMTable>
    </div>
    <!-- RPA表单弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="500px"
      :close-on-click-modal="false"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="程序名" prop="programName">
          <el-input
            v-model="form.programName"
            placeholder="请输入程序名"
            clearable
          />
        </el-form-item>
        <el-form-item label="执行状态" prop="status">
          <el-select v-model="form.status" placeholder="请选择执行状态" style="width: 100%">
            <el-option label="运行中" value="running" />
            <el-option label="已停止" value="stopped" />
            <el-option label="异常" value="error" />
          </el-select>
        </el-form-item>
        <el-form-item label="描述" prop="description">
          <el-input
            v-model="form.description"
            type="textarea"
            :rows="3"
            placeholder="请输入RPA程序描述"
            clearable
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
// å“åº”式数据
const data = reactive({
  searchForm: {
    programName: "",
    status: "",
  },
  form: {
    id: "",
    programName: "",
    status: "stopped",
    description: "",
    createTime: "",
  },
  dialogVisible: false,
  dialogTitle: "",
  dialogType: "add",
  selectedIds: [],
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    total: 0,
  },
  tableData: [],
});
const { searchForm, form, dialogVisible, dialogTitle, dialogType, selectedIds, tableLoading, page, tableData } = toRefs(data);
// è¡¨å•引用
const formRef = ref();
// è¡¨å•验证规则
const rules = {
  programName: [
    { required: true, message: "请输入程序名", trigger: "blur" }
  ],
  status: [
    { required: true, message: "请选择执行状态", trigger: "change" }
  ]
};
// è¡¨æ ¼åˆ—配置
const tableColumn = ref([
  {
    label: "程序名",
    prop: "programName",
    // width: 200,
  },
  {
    label: "执行状态",
    prop: "status",
    dataType: "tag",
    // width: 120,
    formatData: (params) => {
      const statusMap = {
        running: "运行中",
        stopped: "已停止",
        error: "异常"
      };
      return statusMap[params] || params;
    },
    formatType: (params) => {
      const typeMap = {
        running: "success",
        stopped: "info",
        error: "danger"
      };
      return typeMap[params] || "info";
    }
  },
  {
    label: "描述",
    prop: "description",
    // width: 300,
    showOverflowTooltip: true,
  },
  {
    label: "创建时间",
    prop: "createTime",
    // width: 180,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 230,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        }
      },
      {
        name: "开始",
        type: "text",
        clickFun: (row) => {
          handleStart(row);
        },
        disabled: (row) => row.status !== 'stopped'
      },
      {
        name: "停止",
        type: "text",
        clickFun: (row) => {
          handleStop(row);
        },
        disabled: (row) => row.status === 'stopped'
      }
    ]
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
const mockData = [
  {
    id: "1",
    programName: "订单处理RPA",
    status: "running",
    description: "自动处理客户订单,包括验证、分配和确认",
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    programName: "库存同步RPA",
    status: "stopped",
    description: "同步多个仓库的库存数据,确保数据一致性",
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    programName: "报表生成RPA",
    status: "error",
    description: "自动生成每日销售报表和库存报表",
    createTime: "2024-01-13 09:15:00"
  }
];
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  getList();
});
// æŸ¥è¯¢æ•°æ®
const handleQuery = () => {
  page.value.current = 1;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  // æ¨¡æ‹ŸAPI调用延迟
  setTimeout(() => {
    let filteredData = [...mockData];
    // æ ¹æ®æœç´¢æ¡ä»¶è¿‡æ»¤æ•°æ®
    if (searchForm.value.programName) {
      filteredData = filteredData.filter(item =>
        item.programName.toLowerCase().includes(searchForm.value.programName.toLowerCase())
      );
    }
    if (searchForm.value.status) {
      filteredData = filteredData.filter(item => item.status === searchForm.value.status);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
    tableLoading.value = false;
  }, 500);
};
// åˆ†é¡µå¤„理
const pagination = (obj) => {
  page.value.current = obj.page;
  page.value.size = obj.limit;
  handleQuery();
};
// é€‰æ‹©å˜åŒ–处理
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
// æ‰“开表单
const openForm = (type, row) => {
  dialogType.value = type;
  dialogVisible.value = true;
  if (type === "add") {
    dialogTitle.value = "添加RPA";
    form.value = {
      id: "",
      programName: "",
      status: "stopped",
      description: "",
      createTime: "",
    };
  } else {
    dialogTitle.value = "编辑RPA";
    form.value = { ...row };
  }
};
// æäº¤è¡¨å•
const submitForm = async () => {
  if (!formRef.value) return;
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ·»åŠ æ–°RPA
      const newRPA = {
        id: Date.now().toString(),
        programName: form.value.programName,
        status: form.value.status,
        description: form.value.description,
        createTime: new Date().toLocaleString(),
      };
      mockData.unshift(newRPA);
      ElMessage.success("RPA添加成功");
    } else {
      // ç¼–辑RPA
      const index = mockData.findIndex(item => item.id === form.value.id);
      if (index !== -1) {
        mockData[index] = { ...form.value };
        ElMessage.success("RPA更新成功");
      }
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
};
// å¼€å§‹RPA
const handleStart = (row) => {
  ElMessageBox.confirm(`确定要启动RPA程序"${row.programName}"吗?`, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    row.status = "running";
    ElMessage.success("RPA启动成功");
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
// åœæ­¢RPA
const handleStop = (row) => {
  ElMessageBox.confirm(`确定要停止RPA程序"${row.programName}"吗?`, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    row.status = "stopped";
    ElMessage.success("RPA停止成功");
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
// åˆ é™¤RPA
const handleDelete = () => {
  let ids = [];
  if (selectedIds.value.length > 0) {
    ids = selectedIds.value.map((item) => item.id);
  } else {
    ElMessage.warning("请选择要删除的RPA");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    // ä»Žæ¨¡æ‹Ÿæ•°æ®ä¸­åˆ é™¤é€‰ä¸­çš„项
    ids.forEach(id => {
      const index = mockData.findIndex(item => item.id === id);
      if (index !== -1) {
        mockData.splice(index, 1);
      }
    });
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
</script>
<style scoped></style>
src/views/energyManagement/meterCollection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,556 @@
<template>
  <div class="app-container">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>电表采集管理</span>
        <el-button style="float: right; padding: 3px 0" link @click="refreshData">
          <i class="el-icon-refresh"></i> åˆ·æ–°
        </el-button>
      </div>
             <!-- æµ‹è¯•按钮 -->
       <el-row :gutter="20" style="margin-bottom: 15px;">
         <el-col :span="24">
           <el-button @click="addTestData" type="primary" size="small">添加测试数据</el-button>
           <el-button @click="clearData" type="danger" size="small">清空数据</el-button>
           <el-button @click="testChart" type="success" size="small">测试图表</el-button>
         </el-col>
       </el-row>
       <!-- æœç´¢åŒºåŸŸ -->
       <el-row :gutter="20" class="search-row">
        <el-col :span="6">
          <el-input
            v-model="searchForm.meterNo"
            placeholder="请输入电表编号"
            clearable
            @keyup.enter.native="handleSearch"
          >
            <i slot="prefix" class="el-input__icon el-icon-search"></i>
          </el-input>
        </el-col>
        <el-col :span="6">
          <el-select v-model="searchForm.location" placeholder="请选择位置" clearable>
            <el-option label="生产车间A" value="车间A"></el-option>
            <el-option label="生产车间B" value="车间B"></el-option>
            <el-option label="办公区域" value="办公区"></el-option>
            <el-option label="配电室" value="配电室"></el-option>
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-date-picker
            v-model="searchForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            format="yyyy-MM-dd"
            value-format="yyyy-MM-dd"
          />
        </el-col>
        <el-col :span="6">
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-col>
      </el-row>
      <!-- ç”µè¡¨åˆ—表 -->
      <el-table
        :data="meterList"
        style="width: 100%"
        v-loading="loading"
        border
        stripe
        height="calc(100vh - 22em)"
      >
        <el-table-column prop="meterNo" label="电表编号" width="120" />
        <el-table-column prop="location" label="安装位置" width="120" />
        <el-table-column prop="meterType" label="电表类型" width="120" />
        <el-table-column prop="voltage" label="电压等级" width="100" />
        <el-table-column prop="currentReading" label="当前读数(kWh)" width="140" />
        <el-table-column prop="lastReading" label="上次读数(kWh)" width="140" />
        <el-table-column prop="consumption" label="用电量(kWh)" width="120" />
        <el-table-column prop="power" label="功率(kW)" width="100" />
        <el-table-column prop="powerFactor" label="功率因数" width="100" />
                          <el-table-column prop="status" label="状态" width="80">
           <template #default="scope">
             <el-tag :type="scope.row.status === '正常' ? 'success' : 'danger'">
               {{ scope.row.status }}
             </el-tag>
           </template>
         </el-table-column>
         <el-table-column prop="lastUpdateTime" label="最后更新时间" width="160" />
         <el-table-column label="操作" width="180" fixed="right" align="center">
           <template #default="scope">
             <el-button link @click="viewDetails(scope.row)">
               æŸ¥çœ‹è¯¦æƒ…
             </el-button>
             <el-button link @click="manualCollection(scope.row)">
               æ‰‹åЍ采集
             </el-button>
           </template>
         </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
            <pagination
                :total="pagination.total"
                layout="total, sizes, prev, pager, next, jumper"
                :page="pagination.currentPage"
                :limit="pagination.pageSize"
                @pagination="handleCurrentChange"
            />
    </el-card>
              <!-- è¯¦æƒ…对话框 -->
      <el-dialog
        title="电表详情"
        v-model="detailDialogVisible"
        width="60%"
        @opened="onDialogOpened"
      >
       <el-row :gutter="20">
         <el-col :span="12">
           <div class="detail-item">
             <label>电表编号:</label>
             <span>{{ currentMeter.meterNo }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>安装位置:</label>
             <span>{{ currentMeter.location }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>电表类型:</label>
             <span>{{ currentMeter.meterType }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>电压等级:</label>
             <span>{{ currentMeter.voltage }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>当前读数:</label>
             <span>{{ currentMeter.currentReading }} kWh</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>上次读数:</label>
             <span>{{ currentMeter.lastReading }} kWh</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>用电量:</label>
             <span>{{ currentMeter.consumption }} kWh</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>功率:</label>
             <span>{{ currentMeter.power }} kW</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>功率因数:</label>
             <span>{{ currentMeter.powerFactor }}</span>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>状态:</label>
             <el-tag :type="currentMeter.status === '正常' ? 'success' : 'danger'">
               {{ currentMeter.status }}
             </el-tag>
           </div>
         </el-col>
         <el-col :span="12">
           <div class="detail-item">
             <label>最后更新时间:</label>
             <span>{{ currentMeter.lastUpdateTime }}</span>
           </div>
         </el-col>
       </el-row>
      <!-- ç”¨ç”µè¶‹åŠ¿å›¾ -->
      <div style="margin-top: 20px;">
        <h4>24小时用电趋势</h4>
        <div ref="chartContainer" style="height: 300px;"></div>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
  name: 'MeterCollection',
  data() {
    return {
      loading: false,
      searchForm: {
        meterNo: '',
        location: '',
        dateRange: []
      },
      meterList: [],
      pagination: {
        currentPage: 1,
        pageSize: 10,
        total: 0
      },
      detailDialogVisible: false,
      currentMeter: {},
      chart: null
    }
  },
  created() {
    // ç«‹å³ç”Ÿæˆä¸€äº›æµ‹è¯•数据
    this.meterList = [
      {
        id: 1,
        meterNo: 'M001',
        location: '车间A',
        meterType: '智能电表',
        voltage: '380V',
        currentReading: 8500,
        lastReading: 8400,
        consumption: 100,
        power: '75.5',
        powerFactor: '0.85',
        status: '正常',
        lastUpdateTime: '2024-01-15 10:30:00'
      },
      {
        id: 2,
        meterNo: 'M002',
        location: '车间B',
        meterType: '多功能电表',
        voltage: '220V',
        currentReading: 6200,
        lastReading: 6100,
        consumption: 100,
        power: '45.2',
        powerFactor: '0.92',
        status: '正常',
        lastUpdateTime: '2024-01-15 10:25:00'
      }
    ]
    this.pagination.total = this.meterList.length
  },
  mounted() {
    // å»¶è¿Ÿä¸€ç‚¹æ—¶é—´å†è°ƒç”¨ï¼Œç¡®ä¿DOM已经渲染
    this.$nextTick(() => {
      this.getMeterList()
    })
  },
  watch: {
    meterList: {
      handler(newVal) {
        console.log('meterList数据变化:', newVal)
      },
      deep: true,
      immediate: true
    }
  },
  methods: {
    // èŽ·å–ç”µè¡¨åˆ—è¡¨
    getMeterList() {
      this.loading = true
      // æ¨¡æ‹ŸAPI调用
      setTimeout(() => {
        const mockData = this.generateMockData()
        this.meterList = mockData
        this.pagination.total = this.meterList.length
        this.loading = false
      }, 500)
    },
    // ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®
    generateMockData() {
      const locations = ['车间A', '车间B', '办公区', '配电室']
      const meterTypes = ['智能电表', '多功能电表', '普通电表']
      const voltages = ['220V', '380V', '10kV']
      const statuses = ['正常', '异常']
      const data = []
      for (let i = 1; i <= 25; i++) {
        const currentReading = Math.floor(Math.random() * 10000) + 5000
        const lastReading = currentReading - Math.floor(Math.random() * 100) - 10
        const consumption = currentReading - lastReading
        const power = Math.random() * 100 + 20
        const powerFactor = (Math.random() * 0.3 + 0.7).toFixed(2)
        data.push({
          id: i,
          meterNo: `M${String(i).padStart(3, '0')}`,
          location: locations[Math.floor(Math.random() * locations.length)],
          meterType: meterTypes[Math.floor(Math.random() * meterTypes.length)],
          voltage: voltages[Math.floor(Math.random() * voltages.length)],
          currentReading: currentReading,
          lastReading: lastReading,
          consumption: consumption,
          power: power.toFixed(2),
          powerFactor: powerFactor,
          status: statuses[Math.floor(Math.random() * statuses.length)],
          lastUpdateTime: this.formatDate(new Date(Date.now() - Math.random() * 86400000))
        })
      }
      return data
    },
    // æ ¼å¼åŒ–日期
    formatDate(date) {
      const year = date.getFullYear()
      const month = String(date.getMonth() + 1).padStart(2, '0')
      const day = String(date.getDate()).padStart(2, '0')
      const hours = String(date.getHours()).padStart(2, '0')
      const minutes = String(date.getMinutes()).padStart(2, '0')
      return `${year}-${month}-${day} ${hours}:${minutes}`
    },
    // æœç´¢
    handleSearch() {
      this.pagination.currentPage = 1
      this.getMeterList()
    },
    // é‡ç½®æœç´¢
    resetSearch() {
      this.searchForm = {
        meterNo: '',
        location: '',
        dateRange: []
      }
      this.handleSearch()
    },
    // æŸ¥çœ‹è¯¦æƒ…
    viewDetails(row) {
      this.currentMeter = row
      this.detailDialogVisible = true
    },
    // å¯¹è¯æ¡†æ‰“开后初始化图表
    onDialogOpened() {
      this.$nextTick(() => {
        setTimeout(() => {
          this.initChart()
        }, 100)
      })
    },
    // æ‰‹åЍ采集
    manualCollection(row) {
      this.$message.success(`正在采集电表 ${row.meterNo} çš„æ•°æ®...`)
      // æ¨¡æ‹Ÿé‡‡é›†è¿‡ç¨‹
      setTimeout(() => {
        row.currentReading = Math.floor(Math.random() * 100) + row.currentReading
        row.lastUpdateTime = this.formatDate(new Date())
        this.$message.success('数据采集完成')
      }, 1000)
    },
    // åˆ·æ–°æ•°æ®
    refreshData() {
      this.getMeterList()
      this.$message.success('数据已刷新')
    },
    // æ·»åŠ æµ‹è¯•æ•°æ®
    addTestData() {
      const testData = {
        id: Date.now(),
        meterNo: `M${String(this.meterList.length + 1).padStart(3, '0')}`,
        location: '测试位置',
        meterType: '测试电表',
        voltage: '220V',
        currentReading: Math.floor(Math.random() * 10000) + 1000,
        lastReading: Math.floor(Math.random() * 5000) + 500,
        consumption: Math.floor(Math.random() * 100) + 10,
        power: (Math.random() * 100 + 10).toFixed(2),
        powerFactor: (Math.random() * 0.3 + 0.7).toFixed(2),
        status: '正常',
        lastUpdateTime: this.formatDate(new Date())
      }
      this.meterList.push(testData)
      this.pagination.total = this.meterList.length
      this.$message.success('测试数据已添加')
    },
    // æ¸…空数据
    clearData() {
      this.meterList = []
      this.pagination.total = 0
      this.$message.success('数据已清空')
    },
    // æµ‹è¯•图表
    testChart() {
      this.$message.info('图表测试功能')
      // åˆ›å»ºä¸€ä¸ªæµ‹è¯•对话框来测试图表
      this.currentMeter = {
        meterNo: 'TEST001',
        location: '测试位置',
        meterType: '测试电表',
        voltage: '220V',
        currentReading: 1000,
        lastReading: 900,
        consumption: 100,
        power: '50.0',
        powerFactor: '0.85',
        status: '正常',
        lastUpdateTime: '2024-01-15 12:00:00'
      }
      this.detailDialogVisible = true
    },
    // åˆ†é¡µå¤§å°æ”¹å˜
    handleSizeChange(val) {
      this.pagination.pageSize = val
      this.getMeterList()
    },
    // å½“前页改变
    handleCurrentChange(val) {
      this.pagination.pageSize = val.limit
      this.pagination.currentPage = val.page
      this.getMeterList()
    },
    // åˆå§‹åŒ–图表
    initChart() {
      try {
        if (this.chart) {
          this.chart.dispose()
          this.chart = null
        }
        // ç¡®ä¿DOM元素存在
        if (!this.$refs.chartContainer) {
          console.error('图表容器不存在,等待DOM更新...')
          // å¦‚果容器不存在,等待一下再试
          setTimeout(() => {
            this.initChart()
          }, 100)
          return
        }
        // æ£€æŸ¥å®¹å™¨å°ºå¯¸
        const container = this.$refs.chartContainer
        if (container.offsetWidth === 0 || container.offsetHeight === 0) {
          setTimeout(() => {
            this.initChart()
          }, 100)
          return
        }
        this.chart = echarts.init(container)
        // ç”Ÿæˆ24小时模拟数据
        const hours = []
        const consumption = []
        for (let i = 0; i < 24; i++) {
          hours.push(`${i}:00`)
          consumption.push(Math.floor(Math.random() * 50) + 20)
        }
        const option = {
          title: {
            text: '24小时用电量趋势',
            left: 'center'
          },
          tooltip: {
            trigger: 'axis',
            formatter: '{b}<br/>用电量: {c} kWh'
          },
          xAxis: {
            type: 'category',
            data: hours,
            axisLabel: {
              rotate: 45
            }
          },
          yAxis: {
            type: 'value',
            name: '用电量 (kWh)'
          },
          series: [{
            data: consumption,
            type: 'line',
            smooth: true,
            areaStyle: {
              opacity: 0.3
            },
            itemStyle: {
              color: '#409EFF'
            }
          }]
        }
        this.chart.setOption(option)
      } catch (error) {
        console.error('图表初始化失败:', error)
        this.$message.error('图表初始化失败: ' + error.message)
      }
    }
  },
  beforeUnmount() {
    if (this.chart) {
      try {
        this.chart.dispose()
        this.chart = null
      } catch (error) {
        console.error('清理图表失败:', error)
      }
    }
  }
}
</script>
<style scoped>
.search-row {
  margin-bottom: 20px;
}
.pagination {
  margin-top: 20px;
  text-align: right;
}
.el-table {
  margin-top: 20px;
}
.detail-item {
  margin-bottom: 15px;
  padding: 10px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background-color: #fafafa;
}
.detail-item label {
  font-weight: bold;
  color: #606266;
  margin-right: 10px;
  min-width: 100px;
  display: inline-block;
}
.detail-item span {
  color: #303133;
}
.detail-item .el-tag {
  margin-left: 0;
}
</style>
src/views/equipmentManagement/iotMonitor/indexWD.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,317 @@
<template>
  <div class="app-container iot-monitor">
    <div class="header">
      <div class="title">实时工况监控(IoT)</div>
      <div class="actions">
        <el-button type="primary" @click="toggleCollecting">{{ collecting ? '暂停采集' : '启动采集' }}</el-button>
        <el-button @click="resetAll">重置</el-button>
        <span class="ts">上次更新时间:{{ lastUpdatedDisplay }}</span>
      </div>
    </div>
<!--    <el-alert-->
<!--      title="边缘预警规则:轴承磨损-振动值偏离基线±5%触发告警;温度/压力越界触发提醒"-->
<!--      type="info"-->
<!--      :closable="false"-->
<!--      show-icon-->
<!--      class="rule-alert"-->
<!--    />-->
    <el-row :gutter="16">
      <el-col v-for="dev in devices" :key="dev.id" :span="12">
        <el-card :class="['device-card', dev.hasAlert ? 'is-alert' : '']">
          <template #header>
            <div class="card-header">
              <div class="card-title">
                <span class="device-name">{{ dev.name }}</span>
                <el-tag :type="dev.hasAlert ? 'danger' : 'success'" size="small">{{ dev.hasAlert ? '告警' : '正常' }}</el-tag>
              </div>
              <div class="meta">类型:{{ dev.type }}|基线振动:{{ dev.baseline.vibration.toFixed(2) }} mm/s</div>
            </div>
          </template>
          <div class="metrics">
            <div class="metric" :class="{ 'metric-alert': dev.alerts.vibration }">
              <div class="metric-head">
                <span>振动(mm/s)</span>
                <el-tag :type="dev.alerts.vibration ? 'danger' : 'info'" size="small">{{ dev.alerts.vibration ? '±5%越界' : '基线±5%' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.vibration).toFixed(2) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: 'mm/s' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.vibration }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#409EFF']"
              />
            </div>
            <div class="metric" :class="{ 'metric-alert': dev.alerts.temperature }">
              <div class="metric-head">
                <span>温度(°C)</span>
                <el-tag :type="dev.alerts.temperature ? 'warning' : 'info'" size="small">{{ dev.alerts.temperature ? '越界' : '20~80' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.temperature).toFixed(1) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: '°C' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.temperature }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#E6A23C']"
              />
            </div>
            <div class="metric" :class="{ 'metric-alert': dev.alerts.pressure }">
              <div class="metric-head">
                <span>压力(MPa)</span>
                <el-tag :type="dev.alerts.pressure ? 'warning' : 'info'" size="small">{{ dev.alerts.pressure ? '越界' : '0.2~1.5' }}</el-tag>
              </div>
              <div class="metric-value">{{ currentValue(dev.series.pressure).toFixed(2) }}</div>
              <Echarts
                :xAxis="[{ type: 'category', data: xAxisLabels }]"
                :yAxis="[{ type: 'value', name: 'MPa' }]"
                :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.pressure }]"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
                :chartStyle="{ height: '160px', width: '100%' }"
                :lineColors="['#67C23A']"
              />
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'
import { ElNotification } from 'element-plus'
import Echarts from '@/components/Echarts/echarts.vue'
defineOptions({ name: 'IoTMonitor' })
const windowSize = 30
const collecting = ref(true)
const lastUpdated = ref(Date.now())
const lastUpdatedDisplay = computed(() => new Date(lastUpdated.value).toLocaleTimeString())
const xAxisLabels = ref(Array.from({ length: windowSize }, (_, i) => i - (windowSize - 1)).map(n => `${n}s`))
function makeSeries(fill, decimals = 2) {
  return Array.from({ length: windowSize }, () => Number(fill.toFixed(decimals)))
}
const devices = reactive([
  {
    id: 'hydrocyclone-desander',
    name: '旋流除砂器',
    type: '分离设备',
    baseline: { vibration: 8 },
    initial: { temperature: 35, pressure: 0.85 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(8),
      temperature: makeSeries(35, 1),
      pressure: makeSeries(0.85, 2),
    },
  },
  {
    id: 'high-pressure-separator',
    name: '高压分离器撬',
    type: '分离设备',
    baseline: { vibration: 6 },
    initial: { temperature: 45, pressure: 1.20 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(6),
      temperature: makeSeries(45, 1),
      pressure: makeSeries(1.2, 2),
    },
  },
  {
    id: 'heating-throttle-pressure',
    name: '组合式加热节流调压',
    type: '调压设备',
    baseline: { vibration: 10 },
    initial: { temperature: 75, pressure: 1.80 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(10),
      temperature: makeSeries(75, 1),
      pressure: makeSeries(1.8, 2),
    },
  },
  {
    id: 'three-phase-separator',
    name: '三相分离器',
    type: '分离设备',
    baseline: { vibration: 7 },
    initial: { temperature: 38, pressure: 0.95 },
    alerts: { vibration: false, temperature: false, pressure: false },
    hasAlert: false,
    series: {
      vibration: makeSeries(7),
      temperature: makeSeries(38, 1),
      pressure: makeSeries(0.95, 2),
    },
  },
])
function currentValue(arr) {
  return arr[arr.length - 1] ?? 0
}
function pushWindow(arr, val) {
  if (arr.length >= windowSize) arr.shift()
  arr.push(val)
}
function clamp(val, min, max) { return Math.max(min, Math.min(max, val)) }
function tickDevice(dev) {
  const vibBase = dev.baseline.vibration
  // æŒ¯åŠ¨ï¼šåŸºçº¿Â±2%随机波动;5%概率触发8%~12%尖峰模拟告警
  const spike = Math.random() < 0.05
  const vibNoise = vibBase * (spike ? (1 + (Math.random() * 0.08 + 0.04) * (Math.random() < 0.5 ? -1 : 1)) : (1 + (Math.random() - 0.5) * 0.04))
  const vibVal = Number(vibNoise.toFixed(2))
  pushWindow(dev.series.vibration, vibVal)
  // æ¸©åº¦ï¼šç¼“慢随机游走,并添加偶发高温偏移
  const tPrev = currentValue(dev.series.temperature)
  const tDrift = tPrev + (Math.random() - 0.5) * 0.8 + (Math.random() < 0.02 ? 6 : 0)
  const tVal = Number(clamp(tDrift, 15, 95).toFixed(1))
  pushWindow(dev.series.temperature, tVal)
  // åŽ‹åŠ›ï¼šå°å¹…æ³¢åŠ¨ï¼Œå¶å‘ä½ŽåŽ‹/高压
  const pPrev = currentValue(dev.series.pressure)
  const pDrift = pPrev + (Math.random() - 0.5) * 0.05 + (Math.random() < 0.02 ? (Math.random() < 0.5 ? -0.3 : 0.3) : 0)
  const pVal = Number(clamp(pDrift, 0.05, 2.0).toFixed(2))
  pushWindow(dev.series.pressure, pVal)
  // è¾¹ç¼˜è®¡ç®—阈值判断
  const vibDelta = Math.abs(vibVal - vibBase) / vibBase
  const vibAlert = vibDelta > 0.05
  const tAlert = tVal < 20 || tVal > 80
  const pAlert = pVal < 0.2 || pVal > 1.5
  const prevHasAlert = dev.hasAlert
  dev.alerts.vibration = vibAlert
  dev.alerts.temperature = tAlert
  dev.alerts.pressure = pAlert
  dev.hasAlert = vibAlert || tAlert || pAlert
  if (dev.hasAlert && !prevHasAlert) {
    const reasons = []
    if (vibAlert) reasons.push(`振动偏离±5% (当前 ${vibVal} / åŸºçº¿ ${vibBase})`)
    if (tAlert) reasons.push(`温度越界 (当前 ${tVal}°C, æœŸæœ› 20~80°C) `)
    if (pAlert) reasons.push(`压力越界 (当前 ${pVal}MPa, æœŸæœ› 0.2~1.5MPa) `)
    ElNotification({
      title: `${dev.name} å‘Šè­¦`,
      message: reasons.join(';'),
      type: vibAlert ? 'error' : 'warning',
      duration: 5000,
    })
  }
}
let timer = null
function start() {
  if (timer) return
  timer = setInterval(() => {
    if (!collecting.value) return
    devices.forEach(tickDevice)
    lastUpdated.value = Date.now()
  }, 10000)
}
function stop() {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
}
function toggleCollecting() { collecting.value = !collecting.value }
function resetAll() {
  devices.forEach(dev => {
    dev.series.vibration = makeSeries(dev.baseline.vibration)
    const t0 = dev.initial?.temperature ?? 45
    const p0 = dev.initial?.pressure ?? 0.8
    dev.series.temperature = makeSeries(t0, 1)
    dev.series.pressure = makeSeries(p0, 2)
    dev.alerts.vibration = false
    dev.alerts.temperature = false
    dev.alerts.pressure = false
    dev.hasAlert = false
  })
  lastUpdated.value = Date.now()
}
onMounted(() => {
  start()
})
onBeforeUnmount(() => {
  stop()
})
</script>
<style lang="scss" scoped>
.iot-monitor {
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
    .title { font-size: 18px; font-weight: 600; }
    .actions { display: flex; align-items: center; gap: 8px; }
    .ts { color: #909399; font-size: 12px; }
  }
  .rule-alert { margin-bottom: 12px; }
}
.device-card {
  margin-bottom: 16px;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
  &.is-alert { border-color: #F56C6C; box-shadow: 0 0 0 2px rgba(245,108,108,0.2) inset; }
  .card-header {
    display: flex; flex-direction: column; gap: 4px;
    .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
    .meta { color: #909399; font-size: 12px; }
  }
  .metrics {
    display: grid;
    grid-template-columns: 1fr;
    gap: 12px;
  }
}
.metric {
  border: 1px solid #ebeef5;
  border-radius: 6px;
  padding: 8px 8px 0 8px;
  &-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; font-size: 13px; color: #606266; }
  &-value { font-size: 20px; font-weight: 600; margin: 2px 0 6px 0; }
}
.metric-alert {
  border-color: #F56C6C;
  background: #FFF6F6;
}
@media (min-width: 1200px) {
  .device-card .metrics { grid-template-columns: 1fr 1fr 1fr; }
}
</style>
src/views/procurementManagement/procurementLedger/index.vue
@@ -39,6 +39,7 @@
    <div class="table_list">
      <div style="display: flex;justify-content: flex-end;margin-bottom: 20px;">
        <el-button type="primary" @click="openForm('add')">新增台账</el-button>
        <el-button type="success" @click="openScanAddDialog">扫码新增</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
@@ -164,6 +165,7 @@
              @click="showQRCode(scope.row)"
              >生成二维码</el-button
            >
          </template>
        </el-table-column>
      </el-table>
@@ -560,6 +562,206 @@
          <el-button type="primary" @click="downloadQRCode">下载二维码图片</el-button>
        </div>
      </div>
    </el-dialog>
    <!-- æ‰«ç æ–°å¢žå¯¹è¯æ¡† -->
    <el-dialog
      v-model="scanAddDialogVisible"
      title="扫码新增采购台账"
      width="70%"
      @close="closeScanAddDialog"
    >
      <el-form
        :model="scanAddForm"
        label-width="140px"
        label-position="top"
        :rules="scanAddRules"
        ref="scanAddFormRef"
      >
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="扫码内容:">
              <el-input
                v-model="scanAddForm.scanContent"
                type="textarea"
                :rows="3"
                placeholder="请扫描二维码或手动输入采购合同信息"
                @input="parseScanContent"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="采购合同号:" prop="purchaseContractNumber">
              <el-input
                v-model="scanAddForm.purchaseContractNumber"
                placeholder="请输入"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="供应商名称:" prop="supplierName">
              <el-input
                v-model="scanAddForm.supplierName"
                placeholder="请输入"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="项目名称:" prop="projectName">
              <el-input
                v-model="scanAddForm.projectName"
                placeholder="请输入"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="合同金额(元):" prop="contractAmount">
              <el-input-number
                v-model="scanAddForm.contractAmount"
                :precision="2"
                :step="0.1"
                clearable
                style="width: 100%"
                placeholder="请输入"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="付款方式:">
              <el-input
                v-model="scanAddForm.paymentMethod"
                placeholder="请输入"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="录入人:">
              <el-input v-model="scanAddForm.recorderName" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="备注:">
              <el-input
                v-model="scanAddForm.remark"
                type="textarea"
                :rows="2"
                placeholder="请输入备注信息"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitScanAdd">确认新增</el-button>
          <el-button @click="closeScanAddDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ‰«ç ç™»è®°å¯¹è¯æ¡† -->
    <el-dialog
      v-model="scanDialogVisible"
      title="扫码登记"
      width="60%"
      @close="closeScanDialog"
    >
      <el-form
        :model="scanForm"
        label-width="120px"
        label-position="left"
        :rules="scanRules"
        ref="scanFormRef"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="采购合同号:">
              <el-input v-model="scanForm.purchaseContractNumber" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="供应商名称:">
              <el-input v-model="scanForm.supplierName" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="项目名称:">
              <el-input v-model="scanForm.projectName" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="扫码时间:">
              <el-input v-model="scanForm.scanTime" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="扫码人:">
              <el-input v-model="scanForm.scannerName" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="扫码状态:">
              <el-tag :type="scanForm.scanStatus === '已扫码' ? 'success' : 'warning'">
                {{ scanForm.scanStatus }}
              </el-tag>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="扫码备注:">
              <el-input
                v-model="scanForm.scanRemark"
                type="textarea"
                :rows="3"
                placeholder="请输入扫码备注信息"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="扫码记录:">
              <el-table :data="scanRecords" border style="width: 100%">
                <el-table-column label="序号" type="index" width="60" align="center" />
                <el-table-column label="扫码时间" prop="scanTime" width="180" />
                <el-table-column label="扫码人" prop="scannerName" width="120" />
                <el-table-column label="扫码状态" prop="scanStatus" width="100">
                  <template #default="scope">
                    <el-tag :type="scope.row.scanStatus === '已扫码' ? 'success' : 'warning'">
                      {{ scope.row.scanStatus }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column label="备注" prop="scanRemark" />
              </el-table>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitScan">确认扫码</el-button>
          <el-button @click="closeScanDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
@@ -1220,6 +1422,153 @@
  proxy.$modal.msgSuccess("下载成功");
};
// æ‰«ç æ–°å¢žå¯¹è¯æ¡†ç›¸å…³å˜é‡
const scanAddDialogVisible = ref(false);
const scanAddForm = reactive({
  scanContent: "",
  purchaseContractNumber: "",
  supplierName: "",
  projectName: "",
  contractAmount: "",
  paymentMethod: "",
  recorderName: "",
  scanRemark: "",
});
const scanAddRules = {
  purchaseContractNumber: [{ required: true, message: "请输入采购合同号", trigger: "blur" }],
  supplierName: [{ required: true, message: "请输入供应商名称", trigger: "blur" }],
  projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
};
// æ‰«ç ç™»è®°å¯¹è¯æ¡†ç›¸å…³å˜é‡
const scanDialogVisible = ref(false);
const scanForm = reactive({
  purchaseContractNumber: "",
  supplierName: "",
  projectName: "",
  scanTime: "",
  scannerName: "",
  scanStatus: "未扫码",
  scanRemark: "",
});
const scanRules = {
  scanRemark: [{ required: true, message: "请输入扫码备注", trigger: "blur" }],
};
const scanRecords = ref([]);
// æ‰“开扫码新增对话框
const openScanAddDialog = () => {
  scanAddForm.scanContent = "";
  scanAddForm.purchaseContractNumber = "";
  scanAddForm.supplierName = "";
  scanAddForm.projectName = "";
  scanAddForm.contractAmount = "";
  scanAddForm.paymentMethod = "";
  scanAddForm.recorderName = userStore.nickName;
  scanAddForm.scanRemark = "";
  scanAddDialogVisible.value = true;
};
// è§£æžæ‰«ç å†…容(模拟解析二维码数据)
const parseScanContent = (content) => {
  if (!content) return;
  // æ¨¡æ‹Ÿè§£æžäºŒç»´ç å†…容,这里可以根据实际需求调整解析逻辑
  // å‡è®¾æ‰«ç å†…容格式为:合同号|供应商|项目|金额|付款方式
  const parts = content.split('|');
  if (parts.length >= 3) {
    scanAddForm.purchaseContractNumber = parts[0] || "";
    scanAddForm.supplierName = parts[1] || "";
    scanAddForm.projectName = parts[2] || "";
    scanAddForm.contractAmount = parts[3] || "";
    scanAddForm.paymentMethod = parts[4] || "";
  }
};
// å…³é—­æ‰«ç æ–°å¢žå¯¹è¯æ¡†
const closeScanAddDialog = () => {
  scanAddDialogVisible.value = false;
  proxy.resetForm("scanAddFormRef");
};
// æäº¤æ‰«ç æ–°å¢ž
const submitScanAdd = () => {
  proxy.$refs["scanAddFormRef"].validate((valid) => {
    if (valid) {
      // æž„建新增数据
      const newData = {
        purchaseContractNumber: scanAddForm.purchaseContractNumber,
        supplierName: scanAddForm.supplierName,
        projectName: scanAddForm.projectName,
        contractAmount: scanAddForm.contractAmount,
        paymentMethod: scanAddForm.paymentMethod,
        recorderName: scanAddForm.recorderName,
        entryDate: getCurrentDate(),
        remark: scanAddForm.scanRemark,
        type: 2
      };
      // æ¨¡æ‹Ÿæ–°å¢žæˆåŠŸ
      proxy.$modal.msgSuccess("扫码新增成功!");
      closeScanAddDialog();
      // å¯ä»¥é€‰æ‹©æ˜¯å¦åˆ·æ–°åˆ—表
      // getList();
    }
  });
};
// æ‰“开扫码登记对话框
const openScanDialog = (row) => {
  scanForm.purchaseContractNumber = row.purchaseContractNumber;
  scanForm.supplierName = row.supplierName;
  scanForm.projectName = row.projectName;
  scanForm.scanTime = getCurrentDateTime();
  scanForm.scannerName = userStore.nickName;
  scanForm.scanStatus = "未扫码";
  scanForm.scanRemark = "";
  scanRecords.value = [];
  scanDialogVisible.value = true;
};
// å…³é—­æ‰«ç ç™»è®°å¯¹è¯æ¡†
const closeScanDialog = () => {
  scanDialogVisible.value = false;
  proxy.resetForm("scanFormRef");
};
// æäº¤æ‰«ç ç™»è®°
const submitScan = () => {
  proxy.$refs["scanFormRef"].validate((valid) => {
    if (valid) {
      // æ·»åŠ æ‰«ç è®°å½•
      scanRecords.value.push({
        ...scanForm,
        id: Date.now(), // æ¨¡æ‹ŸID
        scanTime: getCurrentDateTime(),
      });
      scanForm.scanStatus = "已扫码";
      scanForm.scanRemark = scanForm.scanRemark || "无";
      proxy.$modal.msgSuccess("扫码登记成功!");
      closeScanDialog();
    }
  });
};
// èŽ·å–å½“å‰æ—¥æœŸæ—¶é—´
function getCurrentDateTime() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  const seconds = String(now.getSeconds()).padStart(2, "0");
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
onMounted(() => {
  getList();
});
src/views/system/dept/index.vue
@@ -1,17 +1,17 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
         <el-form-item label="部门名称" prop="deptName">
         <el-form-item label="公司名称" prop="deptName">
            <el-input
               v-model="queryParams.deptName"
               placeholder="请输入部门名称"
               placeholder="请输入公司名称"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="状态" prop="status">
            <el-select v-model="queryParams.status" placeholder="部门状态" clearable style="width: 200px">
            <el-select v-model="queryParams.status" placeholder="公司状态" clearable style="width: 200px">
               <el-option
                  v-for="dict in sys_normal_disable"
                  :key="dict.value"
@@ -55,7 +55,7 @@
         :default-expand-all="isExpandAll"
         :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
      >
         <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
         <el-table-column prop="deptName" label="公司名称" width="260"></el-table-column>
         <el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
         <el-table-column prop="status" label="状态" width="100">
            <template #default="scope">
@@ -76,25 +76,25 @@
         </el-table-column>
      </el-table>
      <!-- æ·»åŠ æˆ–ä¿®æ”¹éƒ¨é—¨å¯¹è¯æ¡† -->
      <!-- æ·»åŠ æˆ–ä¿®æ”¹å…¬å¸å¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="600px" append-to-body>
         <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
            <el-row>
               <el-col :span="24" v-if="form.parentId !== 0">
                  <el-form-item label="上级部门" prop="parentId">
                  <el-form-item label="上级公司" prop="parentId">
                     <el-tree-select
                        v-model="form.parentId"
                        :data="deptOptions"
                        :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
                        value-key="deptId"
                        placeholder="选择上级部门"
                        placeholder="选择上级公司"
                        check-strictly
                     />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="部门名称" prop="deptName">
                     <el-input v-model="form.deptName" placeholder="请输入部门名称" />
                  <el-form-item label="公司名称" prop="deptName">
                     <el-input v-model="form.deptName" placeholder="请输入公司名称" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
@@ -118,7 +118,7 @@
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="部门状态">
                  <el-form-item label="公司状态">
                     <el-radio-group v-model="form.status">
                        <el-radio
                           v-for="dict in sys_normal_disable"
@@ -129,8 +129,8 @@
                  </el-form-item>
               </el-col>
              <el-col :span="12">
                <el-form-item label="部门编号" prop="deptNick">
                  <el-input v-model="form.deptNick" placeholder="请输入部门编号" maxlength="50" />
                <el-form-item label="公司编号" prop="deptNick">
                  <el-input v-model="form.deptNick" placeholder="请输入公司编号" maxlength="50" />
                </el-form-item>
              </el-col>
            </el-row>
@@ -167,18 +167,18 @@
    status: undefined
  },
  rules: {
    parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
    deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
    parentId: [{ required: true, message: "上级公司不能为空", trigger: "blur" }],
    deptName: [{ required: true, message: "公司名称不能为空", trigger: "blur" }],
    orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
    email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
    phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
    deptNick: [{ required: true, message: "部门编号不能为空", trigger: "blur" }],
    deptNick: [{ required: true, message: "公司编号不能为空", trigger: "blur" }],
  },
})
const { queryParams, form, rules } = toRefs(data)
/** æŸ¥è¯¢éƒ¨é—¨åˆ—表 */
/** æŸ¥è¯¢å…¬å¸åˆ—表 */
function getList() {
  loading.value = true
  listDept(queryParams.value).then(response => {
@@ -230,7 +230,7 @@
    form.value.parentId = row.deptId
  }
  open.value = true
  title.value = "添加部门"
  title.value = "添加公司"
}
/** å±•å¼€/折叠操作 */
@@ -251,7 +251,7 @@
  getDept(row.deptId).then(response => {
    form.value = response.data
    open.value = true
    title.value = "修改部门"
    title.value = "修改公司"
  })
}