已添加2个文件
已修改7个文件
1333 ■■■■■ 文件已修改
src/layout/components/AppMain.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/index.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/warningSystem/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockWarning/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitorManagement/areaControl/index.vue 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitorManagement/videoMonitor/index.vue 990 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/reportManagement/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppMain.vue
@@ -2,9 +2,12 @@
  <section class="app-main">
    <router-view v-slot="{ Component, route }">
      <transition name="fade-transform" mode="out-in">
        <keep-alive :include="tagsViewStore.cachedViews">
          <component v-if="!route.meta.link" :is="Component" :key="route.path"/>
        </keep-alive>
        <div v-if="!route.meta.link" class="route-view-wrapper">
          <keep-alive :include="tagsViewStore.cachedViews">
            <component :is="Component" :key="route.path"/>
          </keep-alive>
        </div>
        <div v-else class="route-view-wrapper"></div>
      </transition>
    </router-view>
    <iframe-toggle />
@@ -43,6 +46,11 @@
  background: #F5F7FB;
}
.route-view-wrapper {
  width: 100%;
  height: 100%;
}
.fixed-header + .app-main {
  padding-top: 50px;
}
src/views/basicData/customerFile/index.vue
@@ -209,13 +209,13 @@
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
<!--            <el-link-->
<!--              type="primary"-->
<!--              :underline="false"-->
<!--              style="font-size: 12px; vertical-align: baseline"-->
<!--              @click="importTemplate"-->
<!--              >下载模板</el-link-->
<!--            >-->
            <el-link
              type="primary"
              :underline="false"
              style="font-size: 12px; vertical-align: baseline"
              @click="importTemplate"
              >下载模板</el-link
            >
          </div>
        </template>
      </el-upload>
@@ -399,8 +399,12 @@
  // æ–‡ä»¶ä¸Šä¼ æˆåŠŸæ—¶çš„å›žè°ƒ
  onSuccess: (response, file, fileList) => {
    console.log('上传成功', response, file, fileList);
    upload.isUploading = false;
    if(response.code === 200){
      proxy.$modal.msgSuccess("文件上传成功");
      upload.open = false;
      proxy.$refs["uploadRef"].clearFiles();
      getList();
    }else if(response.code === 500){
      proxy.$modal.msgError(response.msg);
    }else{
@@ -410,6 +414,7 @@
  // æ–‡ä»¶ä¸Šä¼ å¤±è´¥æ—¶çš„回调
  onError: (error, file, fileList) => {
    console.error('上传失败', error, file, fileList);
    upload.isUploading = false;
    proxy.$modal.msgError("文件上传失败");
  },
  // æ–‡ä»¶ä¸Šä¼ è¿›åº¦å›žè°ƒ
@@ -455,6 +460,7 @@
};
/** æäº¤ä¸Šä¼ æ–‡ä»¶ */
function submitFileForm() {
  upload.isUploading = true;
  proxy.$refs["uploadRef"].submit();
}
/** å¯¼å…¥æŒ‰é’®æ“ä½œ */
@@ -462,6 +468,10 @@
  upload.title = "客户导入";
  upload.open = true;
}
/** ä¸‹è½½æ¨¡æ¿ */
function importTemplate() {
  proxy.download("/basic/customer/downloadTemplate", {}, "客户导入模板.xlsx");
}
// æ‰“开弹框
const openForm = (type, row) => {
  operationType.value = type;
src/views/basicData/supplierManage/index.vue
@@ -192,6 +192,7 @@
        :disabled="upload.isUploading"
        :on-progress="handleFileUploadProgress"
        :on-success="handleFileSuccess"
        :on-error="handleFileError"
        :auto-upload="false"
        drag
      >
@@ -200,13 +201,13 @@
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
            <!-- <el-link
            <el-link
              type="primary"
              :underline="false"
              style="font-size: 12px; vertical-align: baseline"
              @click="importTemplate"
              >下载模板</el-link
            > -->
            >
          </div>
        </template>
      </el-upload>
@@ -365,7 +366,7 @@
};
/** æäº¤ä¸Šä¼ æ–‡ä»¶ */
function submitFileForm() {
  console.log(upload.url + '?updateSupport=' + upload.updateSupport)
  upload.isUploading = true;
  proxy.$refs["uploadRef"].submit();
}
const getList = () => {
@@ -395,6 +396,10 @@
  upload.title = "供应商导入";
  upload.open = true;
}
/** ä¸‹è½½æ¨¡æ¿ */
function importTemplate() {
  proxy.download("/system/supplier/downloadTemplate", {}, "供应商导入模板.xlsx");
}
/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
@@ -403,10 +408,23 @@
/** æ–‡ä»¶ä¸Šä¼ æˆåŠŸå¤„ç† */
const handleFileSuccess = (response, file, fileList) => {
  upload.open = false;
  upload.isUploading = false;
  proxy.$refs["uploadRef"].handleRemove(file);
  getList();
  if(response.code === 200){
    proxy.$modal.msgSuccess("文件上传成功");
    upload.open = false;
    proxy.$refs["uploadRef"].clearFiles();
    getList();
  }else if(response.code === 500){
    proxy.$modal.msgError(response.msg);
  }else{
    proxy.$modal.msgWarning(response.msg);
  }
};
/** æ–‡ä»¶ä¸Šä¼ å¤±è´¥å¤„理 */
const handleFileError = (error, file, fileList) => {
  upload.isUploading = false;
  proxy.$modal.msgError("文件上传失败");
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
src/views/collaborativeApproval/warningSystem/index.vue
@@ -33,7 +33,6 @@
            <th>类型</th>
            <th>等级</th>
            <th>状态</th>
            <th>责任人</th>
            <th>操作</th>
          </tr>
        </thead>
@@ -52,7 +51,6 @@
                {{ warning.statusText }}
              </span>
            </td>
            <td>{{ warning.responsible }}</td>
            <td>
              <button @click="viewDetail(warning)">查看详情</button>
            </td>
src/views/inventoryManagement/stockWarning/index.vue
@@ -120,7 +120,7 @@
        <el-table-column fixed="right" label="操作" width="200" align="center">
          <template #default="scope">
            <el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
            <el-button link type="success" size="small" @click="handleProcess(scope.row)">处理@</el-button>
<!--            <el-button link type="success" size="small" @click="handleProcess(scope.row)">处理@</el-button>-->
            <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
src/views/monitorManagement/areaControl/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
<template>
  <div class="app-container">
    <el-row :gutter="16">
      <el-col :span="16">
        <el-card shadow="never" class="section-card">
          <template #header>
            <div class="card-header">
              <span>区域管理(双重门禁)</span>
              <div class="header-actions">
                <el-select v-model="selectedPlant" placeholder="选择厂区" size="small" style="width: 160px" @change="filterZones">
                  <el-option v-for="plant in plants" :key="plant.id" :label="plant.name" :value="plant.id" />
                </el-select>
                <el-switch v-model="onlyCritical" inline-prompt :active-text="'仅关键区'" :inactive-text="'全部'" @change="filterZones" />
              </div>
            </div>
          </template>
          <el-table :data="filteredZones" border style="width: 100%" height="320">
            <el-table-column type="index" width="60" label="序号" align="center" />
            <el-table-column prop="name" label="区域名称" min-width="160" show-overflow-tooltip />
            <el-table-column prop="zoneType" label="类型" width="120" />
            <el-table-column label="双门联动" width="120" align="center">
              <template #default="{ row }">
                <el-tag v-if="row.dualAccess" type="success">已启用</el-tag>
                <el-tag v-else type="info">未启用</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="在线人数" width="100" align="center">
              <template #default="{ row }">{{ row.currentPersons }}</template>
            </el-table-column>
            <el-table-column label="安全状态" width="140" align="center">
              <template #default="{ row }">
                <el-tag :type="row.status === '正常' ? 'success' : row.status === '预警' ? 'warning' : 'danger'">
                  {{ row.status }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="180" align="center" fixed="right">
              <template #default="{ row }">
                <el-button link type="primary" size="small" @click="toggleDual(row)">
                  {{ row.dualAccess ? '停用双门' : '启用双门' }}
                </el-button>
                <el-button link type="success" size="small" @click="openAccessSim(row)">模拟开门</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
        <el-card shadow="never" class="section-card">
          <template #header>
            <div class="card-header">
              <span>培训联动(未完成/过期禁止进入)</span>
              <div class="header-actions">
                <el-input v-model="accessSim.personId" placeholder="人员工号" size="small" style="width: 140px" />
                <el-select v-model="accessSim.targetZoneId" placeholder="选择目标区域" size="small" style="width: 180px">
                  <el-option v-for="z in zones" :key="z.id" :label="z.name" :value="z.id" />
                </el-select>
                <el-button type="primary" size="small" @click="simulateAccess">检验准入</el-button>
              </div>
            </div>
          </template>
          <el-descriptions :column="3" border size="small" v-if="accessResult">
            <el-descriptions-item label="工号">{{ accessResult.person.id }}({{ accessResult.person.dept }})</el-descriptions-item>
            <el-descriptions-item label="培训状态">
              <el-tag :type="accessResult.person.training.valid ? 'success' : 'danger'">
                {{ accessResult.person.training.valid ? '有效' : '失效/未完成' }}
              </el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="目标区域">{{ accessResult.zone.name }}</el-descriptions-item>
            <el-descriptions-item label="最近培训">{{ accessResult.person.training.lastDate }}</el-descriptions-item>
            <el-descriptions-item label="适岗证有效期">{{ accessResult.person.training.expireDate }}</el-descriptions-item>
            <el-descriptions-item label="准入结果">
              <el-tag :type="accessResult.allowed ? 'success' : 'danger'">{{ accessResult.allowed ? '允许进入' : '禁止进入' }}</el-tag>
            </el-descriptions-item>
          </el-descriptions>
          <el-empty v-else description="请输入人员与区域进行检验" />
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="never" class="section-card">
          <template #header>
            <div class="card-header">
              <span>佩戴设备滞留告警(危险区超时)</span>
              <div class="header-actions">
                <el-select v-model="stayThreshold" size="small" style="width: 140px">
                  <el-option :value="10" label="阈值 10 åˆ†é’Ÿ" />
                  <el-option :value="20" label="阈值 20 åˆ†é’Ÿ" />
                  <el-option :value="30" label="阈值 30 åˆ†é’Ÿ" />
                </el-select>
                <el-switch v-model="alarmOn" inline-prompt :active-text="'告警开'" :inactive-text="'告警关'" />
              </div>
            </div>
          </template>
          <el-timeline style="max-height: 520px; overflow: auto">
            <el-timeline-item v-for="(item, idx) in alarms" :key="idx" :type="item.level" :timestamp="item.time">
              <div class="alarm-item">
                <div class="title">
                  {{ item.personId }} Â· {{ item.zoneName }} Â· æ»žç•™ {{ item.stayMins }} åˆ†é’Ÿ
                </div>
                <div class="desc">设备:{{ item.deviceId }}(信号强度 {{ item.rssi }} dBm)</div>
              </div>
            </el-timeline-item>
          </el-timeline>
        </el-card>
      </el-col>
    </el-row>
  </div>
  <el-dialog v-model="doorSimVisible" title="门禁开门模拟" width="420px">
    <el-form :model="doorSim" label-width="90px">
      <el-form-item label="区域">
        <el-input v-model="doorSim.zoneName" disabled />
      </el-form-item>
      <el-form-item label="门禁1">
        <el-switch v-model="doorSim.door1" />
      </el-form-item>
      <el-form-item label="门禁2">
        <el-switch v-model="doorSim.door2" />
      </el-form-item>
      <el-alert type="info" show-icon :closable="false" title="双门均为开启方可通行" />
    </el-form>
    <template #footer>
      <el-button @click="doorSimVisible = false">关闭</el-button>
      <el-button type="primary" :disabled="!(doorSim.door1 && doorSim.door2)" @click="confirmPass">通行</el-button>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
// åŽ‚åŒºä¸ŽåŒºåŸŸï¼ˆç…¤ç‚­è¡Œä¸šè¯­ä¹‰ã€å°½é‡è´´è¿‘çœŸå®žï¼‰
const plants = ref([
  { id: "P01", name: "一号选煤厂" },
  { id: "P02", name: "二号洗煤分厂" },
]);
const zones = ref([
  { id: "Z01", plantId: "P01", name: "中控室", zoneType: "控制室", dualAccess: true, currentPersons: 4, status: "正常" },
  { id: "Z02", plantId: "P01", name: "煤场A区", zoneType: "堆存区", dualAccess: true, currentPersons: 12, status: "预警" },
  { id: "Z03", plantId: "P01", name: "危险品库", zoneType: "危化品", dualAccess: true, currentPersons: 1, status: "正常" },
  { id: "Z04", plantId: "P01", name: "高压配电室", zoneType: "电气间", dualAccess: true, currentPersons: 2, status: "正常" },
  { id: "Z05", plantId: "P02", name: "皮带廊北段", zoneType: "输送廊道", dualAccess: false, currentPersons: 5, status: "正常" },
  { id: "Z06", plantId: "P02", name: "筛分车间", zoneType: "作业区", dualAccess: false, currentPersons: 9, status: "预警" },
]);
const selectedPlant = ref(plants.value[0].id);
const onlyCritical = ref(true);
const filteredZones = ref([]);
function filterZones() {
  const data = zones.value.filter((z) => z.plantId === selectedPlant.value);
  filteredZones.value = onlyCritical.value ? data.filter((z) => z.dualAccess) : data;
}
function toggleDual(row) {
  row.dualAccess = !row.dualAccess;
  filterZones();
}
// é—¨ç¦å¼€é—¨æ¨¡æ‹Ÿ
const doorSimVisible = ref(false);
const doorSim = reactive({ zoneId: "", zoneName: "", door1: false, door2: false });
function openAccessSim(row) {
  doorSim.zoneId = row.id;
  doorSim.zoneName = row.name;
  doorSim.door1 = false;
  doorSim.door2 = false;
  doorSimVisible.value = true;
}
function confirmPass() {
  doorSimVisible.value = false;
}
// åŸ¹è®­è”动模拟
const persons = ref([
  { id: "EMP1001", dept: "生产一队", training: { valid: true, lastDate: "2025-09-12", expireDate: "2026-09-12" } },
  { id: "EMP1018", dept: "机电班", training: { valid: false, lastDate: "2024-07-03", expireDate: "2025-07-03" } },
  { id: "EMP1022", dept: "安监科", training: { valid: true, lastDate: "2025-08-01", expireDate: "2026-08-01" } },
]);
const accessSim = reactive({ personId: "", targetZoneId: "" });
const accessResult = ref(null);
function simulateAccess() {
  const person = persons.value.find((p) => p.id === accessSim.personId);
  const zone = zones.value.find((z) => z.id === accessSim.targetZoneId);
  if (!person || !zone) {
    accessResult.value = null;
    return;
  }
  const allowed = person.training.valid && (zone.zoneType !== "危化品" || person.dept === "安监科");
  accessResult.value = { allowed, person, zone };
}
// ä½©æˆ´è®¾å¤‡æ»žç•™å‘Šè­¦ï¼ˆå‡æ•°æ®å®šæ—¶æŽ¨é€ï¼‰
const stayThreshold = ref(20);
const alarmOn = ref(true);
const alarms = ref([
  { time: "09:35", level: "warning", personId: "EMP1001", zoneName: "煤场A区", stayMins: 18, deviceId: "TAG-7A12", rssi: -67 },
]);
let timer = null;
function pushMockAlarm() {
  if (!alarmOn.value) return;
  const candidates = [
    { personId: "EMP1018", zoneName: "筛分车间", base: 12 },
    { personId: "EMP1022", zoneName: "高压配电室", base: 9 },
    { personId: "EMP1001", zoneName: "煤场A区", base: 16 },
  ];
  const pick = candidates[Math.floor(Math.random() * candidates.length)];
  const stay = pick.base + Math.floor(Math.random() * 10);
  if (stay >= stayThreshold.value) {
    const now = new Date();
    const hh = String(now.getHours()).padStart(2, "0");
    const mm = String(now.getMinutes()).padStart(2, "0");
    alarms.value.unshift({
      time: `${hh}:${mm}`,
      level: stay >= stayThreshold.value + 10 ? "danger" : "warning",
      personId: pick.personId,
      zoneName: pick.zoneName,
      stayMins: stay,
      deviceId: `TAG-${Math.random().toString(16).slice(2, 6).toUpperCase()}`,
      rssi: -60 - Math.floor(Math.random() * 15),
    });
    if (alarms.value.length > 30) alarms.value.pop();
  }
}
onMounted(() => {
  filterZones();
  timer = setInterval(pushMockAlarm, 4500);
});
// ç¦»å¼€æ—¶æ¸…理
if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    if (timer) clearInterval(timer);
  });
}
</script>
<style scoped lang="scss">
.section-card {
  margin-bottom: 16px;
}
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.header-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}
.alarm-item .title {
  font-weight: 600;
  margin-bottom: 4px;
}
.alarm-item .desc {
  color: #666;
  font-size: 12px;
}
</style>
src/views/monitorManagement/videoMonitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,990 @@
<template>
  <div class="app-container">
    <el-row :gutter="16">
      <!-- å·¦ä¾§ï¼šè§†é¢‘监控列表与抓拍记录 -->
      <el-col :span="16">
        <!-- è§†é¢‘监控列表 -->
        <el-card shadow="never" class="section-card">
          <template #header>
            <div class="card-header">
              <span>视频监控点位管理</span>
              <div class="header-actions">
                <el-select v-model="selectedArea" placeholder="选择区域" size="small" style="width: 160px" @change="filterCameras">
                  <el-option label="全部区域" value="all" />
                  <el-option v-for="area in areas" :key="area.id" :label="area.name" :value="area.id" />
                </el-select>
                <el-select v-model="cameraStatus" placeholder="设备状态" size="small" style="width: 120px" @change="filterCameras">
                  <el-option label="全部状态" value="all" />
                  <el-option label="在线" value="online" />
                  <el-option label="离线" value="offline" />
                </el-select>
              </div>
            </div>
          </template>
          <el-table :data="filteredCameras" border style="width: 100%" max-height="320">
            <el-table-column type="index" width="50" label="序号" align="center" />
            <el-table-column prop="name" label="监控点位" min-width="140" show-overflow-tooltip />
            <el-table-column prop="areaName" label="所属区域" width="120" />
            <el-table-column label="设备状态" width="100" align="center">
              <template #default="{ row }">
                <el-tag :type="row.status === 'online' ? 'success' : 'danger'" size="small">
                  {{ row.status === 'online' ? '在线' : '离线' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="AI识别" width="100" align="center">
              <template #default="{ row }">
                <el-tag v-if="row.aiEnabled" type="success" size="small">已启用</el-tag>
                <el-tag v-else type="info" size="small">未启用</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="门禁联动" width="100" align="center">
              <template #default="{ row }">
                <el-tag v-if="row.doorLinked" type="primary" size="small">已绑定</el-tag>
                <el-tag v-else type="info" size="small">未绑定</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="180" align="center" fixed="right">
              <template #default="{ row }">
                <el-button link type="primary" size="small" @click="viewRealtime(row)">实时画面</el-button>
                <el-button link type="success" size="small" @click="viewCaptures(row)">抓拍记录</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
        <!-- é—¨ç¦æŠ“拍记录 -->
        <el-card shadow="never" class="section-card">
          <template #header>
            <div class="card-header">
              <span>门禁抓拍记录</span>
              <div class="header-actions">
                <el-date-picker
                  v-model="captureDate"
                  type="daterange"
                  range-separator="至"
                  start-placeholder="开始日期"
                  end-placeholder="结束日期"
                  size="small"
                  style="width: 260px"
                  @change="loadCaptures"
                />
                <el-select v-model="captureEventType" placeholder="事件类型" size="small" style="width: 100px" clearable>
                  <el-option label="全部" value="" />
                  <el-option label="进入" value="entry" />
                  <el-option label="离开" value="exit" />
                </el-select>
                <el-input v-model="captureSearch" placeholder="搜索工号/区域" size="small" style="width: 140px" clearable>
                  <template #prefix>
                    <el-icon><Search /></el-icon>
                  </template>
                </el-input>
                <el-button type="primary" size="small" @click="exportCaptures">
                  <el-icon><Download /></el-icon>
                  å¯¼å‡º
                </el-button>
              </div>
            </div>
          </template>
          <!-- ç»Ÿè®¡ä¿¡æ¯ -->
          <div class="capture-stats">
            <el-row :gutter="16">
              <el-col :span="6">
                <div class="stat-item">
                  <div class="stat-label">今日抓拍</div>
                  <div class="stat-value">{{ captureStats.today }}</div>
                </div>
              </el-col>
              <el-col :span="6">
                <div class="stat-item">
                  <div class="stat-label">进入记录</div>
                  <div class="stat-value" style="color: #67c23a">{{ captureStats.entry }}</div>
                </div>
              </el-col>
              <el-col :span="6">
                <div class="stat-item">
                  <div class="stat-label">离开记录</div>
                  <div class="stat-value" style="color: #e6a23c">{{ captureStats.exit }}</div>
                </div>
              </el-col>
              <el-col :span="6">
                <div class="stat-item">
                  <div class="stat-label">低匹配度</div>
                  <div class="stat-value" style="color: #f56c6c">{{ captureStats.lowMatch }}</div>
                </div>
              </el-col>
            </el-row>
          </div>
          <el-table
            :data="paginatedCaptures"
            border
            style="width: 100%"
            max-height="350"
            @selection-change="handleCaptureSelection"
          >
            <el-table-column type="selection" width="45" align="center" />
            <el-table-column type="index" width="50" label="序号" align="center" :index="indexMethod" />
            <el-table-column prop="time" label="抓拍时间" width="155" sortable />
            <el-table-column prop="personId" label="工号" width="110" show-overflow-tooltip />
            <el-table-column prop="department" label="部门" width="100" show-overflow-tooltip />
            <el-table-column prop="areaName" label="区域" width="110" show-overflow-tooltip />
            <el-table-column label="门禁事件" width="90" align="center">
              <template #default="{ row }">
                <el-tag :type="row.eventType === 'entry' ? 'success' : 'warning'" size="small">
                  {{ row.eventType === 'entry' ? '进入' : '离开' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="人脸匹配" width="95" align="center" sortable :sort-method="(a, b) => a.faceMatch - b.faceMatch">
              <template #default="{ row }">
                <el-tag :type="getFaceMatchType(row.faceMatch)" size="small">
                  {{ row.faceMatch }}%
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="cameraName" label="摄像头" width="120" show-overflow-tooltip />
            <el-table-column label="操作" width="150" align="center" fixed="right">
              <template #default="{ row }">
                <el-button link type="primary" size="small" @click="viewSnapshot(row)">
                  <el-icon><View /></el-icon>
                  æŸ¥çœ‹
                </el-button>
                <el-button link type="success" size="small" @click="downloadSnapshot(row)">
                  <el-icon><Download /></el-icon>
                  ä¸‹è½½
                </el-button>
              </template>
            </el-table-column>
          </el-table>
          <!-- åˆ†é¡µ -->
          <div class="pagination-container">
            <el-pagination
              v-model:current-page="capturePage"
              v-model:page-size="capturePageSize"
              :page-sizes="[10, 20, 50, 100]"
              :total="filteredCaptures.length"
              layout="total, sizes, prev, pager, next, jumper"
              @size-change="handleSizeChange"
              @current-change="handleCurrentChange"
            />
          </div>
        </el-card>
      </el-col>
      <!-- å³ä¾§ï¼šè¿è§„告警 -->
      <el-col :span="8">
        <el-card shadow="never" class="section-card">
          <template #header>
            <div class="card-header">
              <span>违规行为告警</span>
              <div class="header-actions">
                <el-badge :value="unreadAlarmCount" :max="99" type="danger">
                  <el-switch v-model="alarmEnabled" inline-prompt active-text="告警开" inactive-text="告警关" />
                </el-badge>
              </div>
            </div>
          </template>
          <el-tabs v-model="alarmTab" type="border-card" style="height: 650px">
            <el-tab-pane label="全部告警" name="all">
              <div>
              <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px">
                <el-timeline-item
                  v-for="(alarm, idx) in displayAlarms"
                  :key="idx"
                  :type="getAlarmType(alarm.type)"
                  :timestamp="alarm.time"
                  :hollow="alarm.handled"
                >
                  <div class="alarm-item" :class="{ handled: alarm.handled }">
                    <div class="alarm-header">
                      <el-tag :type="getAlarmTagType(alarm.type)" size="small">{{ alarm.typeText }}</el-tag>
                      <span class="alarm-area">{{ alarm.areaName }}</span>
                    </div>
                    <div class="alarm-content">
                      {{ alarm.description }}
                    </div>
                    <div class="alarm-footer">
                      <span class="alarm-camera">摄像头: {{ alarm.cameraName }}</span>
                      <el-button
                        v-if="!alarm.handled"
                        link
                        type="primary"
                        size="small"
                        @click="handleAlarm(alarm)"
                      >
                        å¤„理
                      </el-button>
                      <span v-else class="handled-text">已处理</span>
                    </div>
                  </div>
                </el-timeline-item>
              </el-timeline>
              </div>
            </el-tab-pane>
            <el-tab-pane label="强闯告警" name="intrusion">
              <div>
              <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px">
                <el-timeline-item
                  v-for="(alarm, idx) in intrusionAlarms"
                  :key="idx"
                  type="danger"
                  :timestamp="alarm.time"
                >
                  <div class="alarm-item">
                    <div class="alarm-header">
                      <el-tag type="danger" size="small">强闯告警</el-tag>
                      <span class="alarm-area">{{ alarm.areaName }}</span>
                    </div>
                    <div class="alarm-content">{{ alarm.description }}</div>
                  </div>
                </el-timeline-item>
              </el-timeline>
              </div>
            </el-tab-pane>
            <el-tab-pane label="尾随告警" name="tailgating">
              <div>
              <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px">
                <el-timeline-item
                  v-for="(alarm, idx) in tailgatingAlarms"
                  :key="idx"
                  type="warning"
                  :timestamp="alarm.time"
                >
                  <div class="alarm-item">
                    <div class="alarm-header">
                      <el-tag type="warning" size="small">尾随告警</el-tag>
                      <span class="alarm-area">{{ alarm.areaName }}</span>
                    </div>
                    <div class="alarm-content">{{ alarm.description }}</div>
                  </div>
                </el-timeline-item>
              </el-timeline>
              </div>
            </el-tab-pane>
            <el-tab-pane label="多人通行" name="multiple">
              <div>
              <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px">
                <el-timeline-item
                  v-for="(alarm, idx) in multipleAlarms"
                  :key="idx"
                  type="warning"
                  :timestamp="alarm.time"
                >
                  <div class="alarm-item">
                    <div class="alarm-header">
                      <el-tag type="warning" size="small">多人通行</el-tag>
                      <span class="alarm-area">{{ alarm.areaName }}</span>
                    </div>
                    <div class="alarm-content">{{ alarm.description }}</div>
                  </div>
                </el-timeline-item>
              </el-timeline>
              </div>
            </el-tab-pane>
          </el-tabs>
        </el-card>
      </el-col>
    </el-row>
    <!-- å®žæ—¶ç”»é¢å¯¹è¯æ¡† -->
    <el-dialog v-model="realtimeVisible" :title="`实时监控 - ${currentCamera.name}`" width="800px">
      <div class="video-container">
        <div class="video-placeholder">
          <el-icon :size="80" color="#909399"><VideoCameraFilled /></el-icon>
          <div class="video-info">
            <p>摄像头: {{ currentCamera.name }}</p>
            <p>位置: {{ currentCamera.areaName }}</p>
            <p>状态: <el-tag :type="currentCamera.status === 'online' ? 'success' : 'danger'" size="small">{{ currentCamera.status === 'online' ? '在线' : '离线' }}</el-tag></p>
            <p class="tip">(实际环境将显示实时视频流)</p>
          </div>
        </div>
      </div>
      <template #footer>
        <el-button @click="realtimeVisible = false">关闭</el-button>
        <el-button type="primary" @click="captureSnapshot">手动抓拍</el-button>
      </template>
    </el-dialog>
    <!-- å¿«ç…§æŸ¥çœ‹å¯¹è¯æ¡† -->
    <el-dialog v-model="snapshotVisible" title="抓拍快照详情" width="800px" :close-on-click-modal="false">
      <div class="snapshot-dialog-body">
        <el-row :gutter="20">
        <el-col :span="14">
          <div class="snapshot-preview">
            <div class="snapshot-placeholder">
              <el-icon :size="80" color="#909399"><Picture /></el-icon>
              <p>抓拍图片预览</p>
              <p class="tip">(实际环境将显示高清抓拍图片)</p>
              <div class="snapshot-tools">
                <el-button-group>
                  <el-button size="small">
                    <el-icon><ZoomIn /></el-icon>
                    æ”¾å¤§
                  </el-button>
                  <el-button size="small">
                    <el-icon><ZoomOut /></el-icon>
                    ç¼©å°
                  </el-button>
                  <el-button size="small">
                    <el-icon><RefreshRight /></el-icon>
                    æ—‹è½¬
                  </el-button>
                </el-button-group>
              </div>
            </div>
          </div>
        </el-col>
        <el-col :span="10">
          <el-descriptions :column="1" border size="small">
            <el-descriptions-item label="抓拍时间">
              <el-icon><Clock /></el-icon>
              {{ currentSnapshot.time }}
            </el-descriptions-item>
            <el-descriptions-item label="工号">
              <el-icon><User /></el-icon>
              {{ currentSnapshot.personId }}
            </el-descriptions-item>
            <el-descriptions-item label="部门">
              {{ currentSnapshot.department }}
            </el-descriptions-item>
            <el-descriptions-item label="所属区域">
              <el-icon><Location /></el-icon>
              {{ currentSnapshot.areaName }}
            </el-descriptions-item>
            <el-descriptions-item label="摄像头">
              <el-icon><VideoCameraFilled /></el-icon>
              {{ currentSnapshot.cameraName }}
            </el-descriptions-item>
            <el-descriptions-item label="事件类型">
              <el-tag :type="currentSnapshot.eventType === 'entry' ? 'success' : 'warning'" size="small">
                {{ currentSnapshot.eventType === 'entry' ? '进入' : '离开' }}
              </el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="人脸匹配度">
              <el-progress
                :percentage="currentSnapshot.faceMatch"
                :color="currentSnapshot.faceMatch >= 90 ? '#67c23a' : '#e6a23c'"
                :stroke-width="12"
              >
                <span style="font-size: 12px">{{ currentSnapshot.faceMatch }}%</span>
              </el-progress>
            </el-descriptions-item>
            <el-descriptions-item label="体温检测">
              <span :style="{ color: currentSnapshot.temperature > 37.3 ? '#f56c6c' : '#67c23a' }">
                {{ currentSnapshot.temperature }}°C
              </span>
              <el-tag v-if="currentSnapshot.temperature > 37.3" type="danger" size="small" style="margin-left: 8px">
                å¼‚常
              </el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="口罩佩戴">
              <el-tag :type="currentSnapshot.maskWearing ? 'success' : 'warning'" size="small">
                {{ currentSnapshot.maskWearing ? '已佩戴' : '未佩戴' }}
              </el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="安全帽">
              <el-tag :type="currentSnapshot.helmetWearing ? 'success' : 'danger'" size="small">
                {{ currentSnapshot.helmetWearing ? '已佩戴' : '未佩戴' }}
              </el-tag>
            </el-descriptions-item>
          </el-descriptions>
        </el-col>
        </el-row>
        <div class="snapshot-notes">
        <el-divider content-position="left">备注信息</el-divider>
        <el-input
          v-model="currentSnapshot.notes"
          type="textarea"
          :rows="3"
          placeholder="添加备注信息..."
        />
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer-custom">
          <div>
            <el-button size="small" @click="printSnapshot">
              <el-icon><Printer /></el-icon>
              æ‰“印
            </el-button>
          </div>
          <div>
            <el-button @click="snapshotVisible = false">关闭</el-button>
            <el-button type="success" @click="downloadSnapshot(currentSnapshot)">
              <el-icon><Download /></el-icon>
              ä¸‹è½½å›¾ç‰‡
            </el-button>
            <el-button type="primary" @click="saveNotes">保存备注</el-button>
          </div>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
import {
  VideoCameraFilled, Picture, Search, Download, View,
  Clock, User, Location, ZoomIn, ZoomOut, RefreshRight, Printer
} from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
// åŒºåŸŸæ•°æ®
const areas = ref([
  { id: "A01", name: "煤场入口" },
  { id: "A02", name: "洗煤车间" },
  { id: "A03", name: "危险品库" },
  { id: "A04", name: "中控室" },
  { id: "A05", name: "配电室" },
]);
// æ‘„像头数据
const cameras = ref([
  { id: "C001", name: "煤场入口东门", areaId: "A01", areaName: "煤场入口", status: "online", aiEnabled: true, doorLinked: true },
  { id: "C002", name: "煤场入口西门", areaId: "A01", areaName: "煤场入口", status: "online", aiEnabled: true, doorLinked: true },
  { id: "C003", name: "洗煤车间主通道", areaId: "A02", areaName: "洗煤车间", status: "online", aiEnabled: true, doorLinked: true },
  { id: "C004", name: "洗煤车间东侧", areaId: "A02", areaName: "洗煤车间", status: "online", aiEnabled: false, doorLinked: false },
  { id: "C005", name: "危险品库大门", areaId: "A03", areaName: "危险品库", status: "online", aiEnabled: true, doorLinked: true },
  { id: "C006", name: "危险品库内部", areaId: "A03", areaName: "危险品库", status: "offline", aiEnabled: true, doorLinked: false },
  { id: "C007", name: "中控室入口", areaId: "A04", areaName: "中控室", status: "online", aiEnabled: true, doorLinked: true },
  { id: "C008", name: "配电室门禁", areaId: "A05", areaName: "配电室", status: "online", aiEnabled: true, doorLinked: true },
]);
const selectedArea = ref("all");
const cameraStatus = ref("all");
const filteredCameras = ref([]);
function filterCameras() {
  let result = cameras.value;
  if (selectedArea.value !== "all") {
    result = result.filter(c => c.areaId === selectedArea.value);
  }
  if (cameraStatus.value !== "all") {
    result = result.filter(c => c.status === cameraStatus.value);
  }
  filteredCameras.value = result;
}
// é—¨ç¦æŠ“拍记录
const captureDate = ref([new Date(), new Date()]);
const captureSearch = ref("");
const captureEventType = ref("");
const capturePage = ref(1);
const capturePageSize = ref(10);
const selectedCaptures = ref([]);
// ç”Ÿæˆæ›´å¤šæ¨¡æ‹Ÿæ•°æ®
const generateCaptureData = () => {
  const departments = ["生产一队", "机电班", "安监科", "调度室", "化验室", "运输队", "维修班", "仓储科"];
  const areaList = ["煤场入口", "洗煤车间", "危险品库", "中控室", "配电室"];
  const cameraList = [
    "煤场入口东门", "煤场入口西门", "洗煤车间主通道", "洗煤车间东侧",
    "危险品库大门", "危险品库内部", "中控室入口", "配电室门禁"
  ];
  const data = [];
  const now = new Date();
  for (let i = 0; i < 86; i++) {
    const randomMinutes = Math.floor(Math.random() * 1440); // 24小时内
    const captureTime = new Date(now.getTime() - randomMinutes * 60 * 1000);
    const hh = String(captureTime.getHours()).padStart(2, "0");
    const mm = String(captureTime.getMinutes()).padStart(2, "0");
    const ss = String(captureTime.getSeconds()).padStart(2, "0");
    const area = areaList[Math.floor(Math.random() * areaList.length)];
    const eventType = Math.random() > 0.5 ? "entry" : "exit";
    const faceMatch = Math.floor(Math.random() * 15) + 85; // 85-100
    const temperature = (36.0 + Math.random() * 1.5).toFixed(1);
    data.push({
      id: i + 1,
      time: `2025-10-28 ${hh}:${mm}:${ss}`,
      personId: `EMP${String(1001 + i).padStart(4, '0')}`,
      department: departments[Math.floor(Math.random() * departments.length)],
      areaName: area,
      cameraName: cameraList[Math.floor(Math.random() * cameraList.length)],
      eventType: eventType,
      faceMatch: faceMatch,
      temperature: parseFloat(temperature),
      maskWearing: Math.random() > 0.1,
      helmetWearing: Math.random() > 0.15,
      notes: ""
    });
  }
  return data.sort((a, b) => b.time.localeCompare(a.time));
};
const captures = ref(generateCaptureData());
// ç»Ÿè®¡ä¿¡æ¯
const captureStats = computed(() => {
  const today = captures.value.length;
  const entry = captures.value.filter(c => c.eventType === 'entry').length;
  const exit = captures.value.filter(c => c.eventType === 'exit').length;
  const lowMatch = captures.value.filter(c => c.faceMatch < 90).length;
  return { today, entry, exit, lowMatch };
});
// è¿‡æ»¤æŠ“拍记录
const filteredCaptures = computed(() => {
  let result = captures.value;
  // æŒ‰å…³é”®è¯æœç´¢
  if (captureSearch.value) {
    const keyword = captureSearch.value.toLowerCase();
    result = result.filter(c =>
      c.personId.toLowerCase().includes(keyword) ||
      c.areaName.toLowerCase().includes(keyword)
    );
  }
  // æŒ‰äº‹ä»¶ç±»åž‹è¿‡æ»¤
  if (captureEventType.value) {
    result = result.filter(c => c.eventType === captureEventType.value);
  }
  return result;
});
// åˆ†é¡µæ•°æ®
const paginatedCaptures = computed(() => {
  const start = (capturePage.value - 1) * capturePageSize.value;
  const end = start + capturePageSize.value;
  return filteredCaptures.value.slice(start, end);
});
// åˆ†é¡µç´¢å¼•方法
const indexMethod = (index) => {
  return (capturePage.value - 1) * capturePageSize.value + index + 1;
};
function loadCaptures() {
  ElMessage.success("已加载选定日期范围的抓拍记录");
}
function handleSizeChange(val) {
  capturePageSize.value = val;
  capturePage.value = 1;
}
function handleCurrentChange(val) {
  capturePage.value = val;
}
function handleCaptureSelection(selection) {
  selectedCaptures.value = selection;
}
function getFaceMatchType(faceMatch) {
  if (faceMatch >= 95) return 'success';
  if (faceMatch >= 90) return 'info';
  if (faceMatch >= 85) return 'warning';
  return 'danger';
}
// å¯¼å‡ºæŠ“拍记录
function exportCaptures() {
  if (filteredCaptures.value.length === 0) {
    ElMessage.warning("没有可导出的记录");
    return;
  }
  ElMessage.success(`正在导出 ${filteredCaptures.value.length} æ¡æŠ“拍记录...`);
  // å®žé™…环境中这里会调用导出接口
}
// ä¸‹è½½å¿«ç…§
function downloadSnapshot(capture) {
  ElMessage.success(`正在下载 ${capture.personId} çš„æŠ“拍图片...`);
  // å®žé™…环境中这里会下载图片文件
}
// æ‰“印快照
function printSnapshot() {
  ElMessage.info("正在准备打印...");
  // å®žé™…环境中这里会调用打印功能
}
// ä¿å­˜å¤‡æ³¨
function saveNotes() {
  ElMessage.success("备注信息已保存");
  snapshotVisible.value = false;
}
// å‘Šè­¦æ•°æ®
const alarmEnabled = ref(true);
const alarmTab = ref("all");
const alarms = ref([
  {
    id: 1,
    time: "09:25:15",
    type: "intrusion",
    typeText: "强闯告警",
    areaName: "危险品库",
    cameraName: "危险品库大门",
    description: "检测到未授权人员强行闯入,未刷卡直接开门",
    handled: false
  },
  {
    id: 2,
    time: "09:18:42",
    type: "tailgating",
    typeText: "尾随告警",
    areaName: "中控室",
    cameraName: "中控室入口",
    description: "检测到尾随行为,一人刷卡后两人进入",
    handled: false
  },
  {
    id: 3,
    time: "09:10:28",
    type: "multiple",
    typeText: "多人通行",
    areaName: "煤场入口",
    cameraName: "煤场入口东门",
    description: "检测到3人同时通过单人门禁通道",
    handled: true
  },
]);
const unreadAlarmCount = computed(() => alarms.value.filter(a => !a.handled).length);
const displayAlarms = computed(() => {
  if (alarmTab.value === "all") return alarms.value;
  return alarms.value.filter(a => a.type === alarmTab.value);
});
const intrusionAlarms = computed(() => alarms.value.filter(a => a.type === "intrusion"));
const tailgatingAlarms = computed(() => alarms.value.filter(a => a.type === "tailgating"));
const multipleAlarms = computed(() => alarms.value.filter(a => a.type === "multiple"));
function getAlarmType(type) {
  const typeMap = { intrusion: "danger", tailgating: "warning", multiple: "warning" };
  return typeMap[type] || "info";
}
function getAlarmTagType(type) {
  const typeMap = { intrusion: "danger", tailgating: "warning", multiple: "warning" };
  return typeMap[type] || "info";
}
function handleAlarm(alarm) {
  alarm.handled = true;
  ElMessage.success("告警已标记为已处理");
}
// å®žæ—¶ç”»é¢
const realtimeVisible = ref(false);
const currentCamera = ref({});
function viewRealtime(camera) {
  currentCamera.value = camera;
  realtimeVisible.value = true;
}
function captureSnapshot() {
  ElMessage.success("抓拍成功,已保存至抓拍记录");
}
// æŸ¥çœ‹æŠ“拍记录详情
const snapshotVisible = ref(false);
const currentSnapshot = ref({});
function viewSnapshot(capture) {
  currentSnapshot.value = capture;
  snapshotVisible.value = true;
}
function viewCaptures(camera) {
  ElMessage.info(`查看 ${camera.name} çš„历史抓拍记录`);
}
// æ¨¡æ‹Ÿå‘Šè­¦æŽ¨é€
let alarmTimer = null;
const alarmTemplates = [
  { type: "intrusion", typeText: "强闯告警", areas: ["危险品库", "配电室", "中控室"] },
  { type: "tailgating", typeText: "尾随告警", areas: ["中控室", "配电室", "危险品库"] },
  { type: "multiple", typeText: "多人通行", areas: ["煤场入口", "洗煤车间"] },
];
function pushMockAlarm() {
  if (!alarmEnabled.value) return;
  const template = alarmTemplates[Math.floor(Math.random() * alarmTemplates.length)];
  const area = template.areas[Math.floor(Math.random() * template.areas.length)];
  const camera = cameras.value.find(c => c.areaName === area);
  const now = new Date();
  const hh = String(now.getHours()).padStart(2, "0");
  const mm = String(now.getMinutes()).padStart(2, "0");
  const ss = String(now.getSeconds()).padStart(2, "0");
  let description = "";
  if (template.type === "intrusion") {
    description = "检测到未授权人员强行闯入,未刷卡直接开门";
  } else if (template.type === "tailgating") {
    description = "检测到尾随行为,一人刷卡后两人进入";
  } else if (template.type === "multiple") {
    const count = Math.floor(Math.random() * 3) + 2;
    description = `检测到${count}人同时通过单人门禁通道`;
  }
  alarms.value.unshift({
    id: Date.now(),
    time: `${hh}:${mm}:${ss}`,
    type: template.type,
    typeText: template.typeText,
    areaName: area,
    cameraName: camera ? camera.name : area + "监控",
    description: description,
    handled: false
  });
  // é™åˆ¶å‘Šè­¦åˆ—表长度
  if (alarms.value.length > 50) {
    alarms.value = alarms.value.slice(0, 50);
  }
}
onMounted(() => {
  filterCameras();
  // æ¯éš”8秒模拟一次告警
  alarmTimer = setInterval(pushMockAlarm, 8000);
});
onBeforeUnmount(() => {
  if (alarmTimer) {
    clearInterval(alarmTimer);
  }
});
</script>
<style scoped lang="scss">
.section-card {
  margin-bottom: 16px;
}
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.header-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}
.alarm-item {
  padding: 8px;
  background: #f5f7fa;
  border-radius: 4px;
  &.handled {
    opacity: 0.6;
  }
}
.alarm-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.alarm-area {
  font-weight: 600;
  color: #303133;
}
.alarm-content {
  color: #606266;
  font-size: 14px;
  margin-bottom: 6px;
  line-height: 1.5;
}
.alarm-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 12px;
  color: #909399;
}
.alarm-camera {
  flex: 1;
}
.handled-text {
  color: #67c23a;
  font-size: 12px;
}
.video-container {
  width: 100%;
  height: 450px;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
}
.video-placeholder {
  text-align: center;
  color: #909399;
}
.video-info {
  margin-top: 20px;
  p {
    margin: 8px 0;
    font-size: 14px;
  }
  .tip {
    color: #c0c4cc;
    font-size: 12px;
    margin-top: 16px;
  }
}
.snapshot-placeholder {
  margin-top: 20px;
  height: 300px;
  background: #f5f7fa;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  color: #909399;
  p {
    margin: 8px 0;
  }
  .tip {
    color: #c0c4cc;
    font-size: 12px;
  }
}
:deep(.el-tabs--border-card) {
  border: none;
  box-shadow: none;
}
:deep(.el-tabs__content) {
  padding: 10px;
}
// æŠ“拍记录统计
.capture-stats {
  margin-bottom: 16px;
  padding: 16px;
  background: #f5f7fa;
  border-radius: 4px;
}
.stat-item {
  text-align: center;
  padding: 8px;
  background: #fff;
  border-radius: 4px;
}
.stat-label {
  font-size: 13px;
  color: #909399;
  margin-bottom: 8px;
}
.stat-value {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
}
// åˆ†é¡µå®¹å™¨
.pagination-container {
  margin-top: 16px;
  display: flex;
  justify-content: flex-end;
}
// å¿«ç…§é¢„览
.snapshot-preview {
  width: 100%;
}
.snapshot-placeholder {
  width: 100%;
  height: 420px;
  background: #f5f7fa;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  border: 1px dashed #dcdfe6;
  color: #909399;
  p {
    margin: 8px 0;
    font-size: 14px;
  }
  .tip {
    color: #c0c4cc;
    font-size: 12px;
  }
}
.snapshot-tools {
  margin-top: 20px;
}
.snapshot-notes {
  margin-top: 20px;
  :deep(.el-divider__text) {
    font-weight: 600;
    color: #606266;
  }
}
.dialog-footer-custom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  > div {
    display: flex;
    gap: 8px;
  }
}
// æè¿°åˆ—表样式优化
:deep(.el-descriptions__label) {
  width: 100px;
}
:deep(.el-descriptions__content) {
  display: flex;
  align-items: center;
  gap: 4px;
}
</style>
src/views/reportAnalysis/reportManagement/index.vue
@@ -170,15 +170,14 @@
            >
                <el-table-column prop="id" label="编号" width="80" />
                <el-table-column prop="name" label="名称" />
                <el-table-column prop="type" label="类型" width="120" />
                <el-table-column prop="status" label="状态" width="100">
                <el-table-column prop="status" label="状态">
                    <template #default="scope">
                        <el-tag :type="getStatusType(scope.row.status)">
                            {{ scope.row.status }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="progress" label="进度" width="120">
                <el-table-column prop="progress" label="进度">
                    <template #default="scope">
                        <el-progress :percentage="scope.row.progress" :status="getProgressStatus(scope.row.progress)" />
                    </template>
vite.config.js
@@ -8,7 +8,7 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
      env.VITE_APP_ENV === "development"
          ? "http://192.168.1.147:9036"
          ? "http://114.132.189.42:9036"
          : env.VITE_BASE_API;
  const javaUrl =
      env.VITE_APP_ENV === "development"