已添加2个文件
已修改8个文件
1147 ■■■■ 文件已修改
src/api/login.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/attendance.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/cooperativeOffice/collaborativeApproval/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/humanResources/attendance/checkin.vue 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/humanResources/attendance/report.vue 204 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/login.vue 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/message.vue 423 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/safeProduction/emergencyPlanReview/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login.js
@@ -57,4 +57,23 @@
    method: 'post',
    data: data
  })
}
// æŸ¥è¯¢å…¬å‘Šåˆ—表
export function listNotice(query) {
  return request({
    url: '/system/notice/list',
    method: 'get',
    params: query
  })
}
// èŽ·å–æœªè¯»æ¶ˆæ¯æ•°é‡
export function getNoticeCount(consigneeId) {
  return request({
    url: '/system/notice/getCount',
    method: 'get',
    params: { consigneeId }
  })
}
src/api/personnelManagement/attendance.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
import request from '@/utils/request'
export function createPersonalAttendanceRecord(params) {
    return request({
        url: "/personalAttendanceRecords",
        method: "post",
        data: params,
    });
}
export function findPersonalAttendanceRecords(query) {
    return request({
        url: "/personalAttendanceRecords/listPage",
        method: "get",
        params: query,
    });
}
export function findTodayPersonalAttendanceRecord(query) {
    return request({
        url: "/personalAttendanceRecords/today",
        method: "get",
        params: query,
    });
}
src/pages.json
@@ -671,7 +671,7 @@
    {
      "path": "pages/safeProduction/safeQualifications/index",
      "style": {
        "navigationBarTitleText": "规程资质",
        "navigationBarTitleText": "规程与资质",
        "navigationStyle": "custom"
      }
    },
@@ -783,7 +783,7 @@
    {
      "path": "pages/safeProduction/hazardousMaterialsControl/index",
      "style": {
        "navigationBarTitleText": "危险物料",
        "navigationBarTitleText": "危险物料管控",
        "navigationStyle": "custom"
      }
    },
@@ -883,6 +883,12 @@
      "style": {
        "navigationBarTitleText": "薪资台账详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/message",
      "style": {
        "navigationBarTitleText": "消息中心"
      }
    }
  ],
@@ -1096,6 +1102,12 @@
        "text": "首页"
      },
      {
        "pagePath": "pages/message",
        "iconPath": "static/images/tabbar/work.png",
        "selectedIconPath": "static/images/tabbar/work_.png",
        "text": "消息"
      },
      {
        "pagePath": "pages/mine",
        "iconPath": "static/images/tabbar/mine.png",
        "selectedIconPath": "static/images/tabbar/mine_.png",
src/pages/cooperativeOffice/collaborativeApproval/index.vue
@@ -158,7 +158,7 @@
      5: "采购管理",
      6: "报价管理",
      7: "发货审批",
      8: "危险作业管理",
      8: "危险作业审批",
    };
    return titleMap[type] || "审批管理";
  };
src/pages/humanResources/attendance/checkin.vue
@@ -14,14 +14,14 @@
          <u-icon name="calendar"
                  color="#348fe2"
                  size="16"></u-icon>
          <text class="shift-text">白班: 09:00-18:00</text>
          <text class="shift-text">白班: {{ workTimeDict.startAt }}-{{ workTimeDict.endAt }}</text>
        </view>
      </view>
      <!-- æ‰“卡按钮 -->
      <view class="checkin-button-container">
        <view class="checkin-button-wrapper">
          <view class="checkin-button"
                :class="{ 'disabled': checkInOutText === '已打卡' }"
                :class="{ 'disabled': todayRecord.workEndAt }"
                @click="handleCheckInOut">
            <text class="checkin-button-text">{{ checkInOutText }}</text>
            <text class="checkin-time">{{ nowTime.split(' ')[1] }}</text>
@@ -41,53 +41,53 @@
      <view class="employee-info">
        <view class="info-item">
          <text class="info-label">部门</text>
          <text class="info-value">{{ currentUser.dept }}</text>
          <text class="info-value">{{ todayRecord.deptName || '-' }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">姓名</text>
          <text class="info-value">{{ currentUser.name }}</text>
          <text class="info-value">{{ todayRecord.staffName || '-' }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">工号</text>
          <text class="info-value">{{ currentUser.no }}</text>
          <text class="info-value">{{ todayRecord.staffNo || '-' }}</text>
        </view>
      </view>
      <!-- ä»Šæ—¥è€ƒå‹¤çŠ¶æ€ -->
      <view class="today-status">
        <u-icon :name="todayRecord ? 'checkmark-circle' : 'close-circle'"
                :color="todayRecord ? '#4cd964' : '#ff3b30'"
        <u-icon :name="todayRecord.id ? 'checkmark-circle' : 'close-circle'"
                :color="todayRecord.id ? '#4cd964' : '#ff3b30'"
                size="16"></u-icon>
        <text class="status-text">
          {{ todayRecord ? `今日考勤: ä¸Šç­ ${todayRecord.checkInTime}` : '今日未打卡' }}
          {{ todayRecord.id ? `今日考勤: ä¸Šç­ ${todayRecord.workStartAt || '-'}` : '今日未打卡' }}
        </text>
      </view>
      <!-- ä¸‹ç­è€ƒå‹¤çŠ¶æ€ -->
      <view v-if="todayRecord && todayRecord.checkOutTime"
      <view v-if="todayRecord.id && todayRecord.workEndAt"
            class="today-status">
        <u-icon :name="todayRecord ? 'checkmark-circle' : 'close-circle'"
                :color="todayRecord ? '#4cd964' : '#ff3b30'"
        <u-icon name="checkmark-circle"
                color="#4cd964"
                size="16"></u-icon>
        <text class="status-text">
          {{ `今日考勤: ä¸‹ç­ ${todayRecord.checkOutTime}` }}
          {{ `今日考勤: ä¸‹ç­ ${todayRecord.workEndAt}` }}
        </text>
      </view>
      <!-- æ‰“卡状态 -->
      <view v-if="todayRecord"
            class="today-status">
        <u-icon :name="todayRecord.status === 'normal' ? 'checkmark-circle' : 'clock'"
                :color="todayRecord.status === 'normal' ? '#4cd964' : '#ff3b30'"
      <view class="today-status">
        <u-icon :name="todayRecord.id ? (todayRecord.status === 0 ? 'checkmark-circle' : 'clock') : 'clock'"
                :color="todayRecord.id ? (todayRecord.status === 0 ? '#4cd964' : '#ff3b30') : '#ff3b30'"
                size="16"></u-icon>
        <text class="status-text">
          {{ `打卡状态: ${todayRecord.statusText}` }}
          {{ `打卡状态: ${todayStatusText}` }}
        </text>
      </view>
      <view v-else
      <!-- å·¥æ—¶ä¿¡æ¯ -->
      <view v-if="todayRecord.id && todayRecord.workHours"
            class="today-status">
        <u-icon name="clock"
                color="#ff3b30"
                color="#348fe2"
                size="16"></u-icon>
        <text class="status-text">
          æ‰“卡状态: ç¼ºå¡
          {{ `工时(小时): ${todayRecord.workHours}` }}
        </text>
      </view>
    </view>
@@ -97,45 +97,19 @@
<script setup>
  import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  // æ¨¡æ‹Ÿå½“前登录员工
  const currentUser = reactive({
    id: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
  });
  import { getDicts } from "@/api/system/dict/data";
  import {
    createPersonalAttendanceRecord,
    findTodayPersonalAttendanceRecord,
  } from "@/api/personnelManagement/attendance.js";
  // ä»Šæ—¥æ‰“卡记录
  const todayRecord = ref({});
  // æ¨¡æ‹Ÿè€ƒå‹¤åŽŸå§‹æ•°æ®
  const rawAttendance = ref([
    {
      id: 2,
      date: "2026-02-08",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "09:15",
      checkOutTime: "18:05",
      workHours: 8.8,
      status: "late",
      statusText: "迟到",
      remark: "因交通拥堵迟到",
    },
    {
      id: 3,
      date: "2026-02-07",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "08:45",
      checkOutTime: "18:30",
      workHours: 9.7,
      status: "normal",
      statusText: "正常",
      remark: "加班0.5小时",
    },
  ]);
  // ç­æ¬¡ä¿¡æ¯
  const workTimeDict = ref({
    startAt: "09:00",
    endAt: "18:00",
  });
  // å½“前时间展示
  const nowTime = ref("");
@@ -143,6 +117,12 @@
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æŸ¥è¯¢ä»Šæ—¥æ‰“卡信息
  const fetchTodayData = () => {
    findTodayPersonalAttendanceRecord({}).then(res => {
      todayRecord.value = res.data;
    });
  };
  // æ‰“卡范围状态
@@ -165,22 +145,39 @@
  // ä»Šæ—¥æ—¥æœŸ
  const todayStr = computed(() => nowTime.value.slice(0, 10));
  // å½“日当前员工考勤记录
  const todayRecord = computed(() =>
    rawAttendance.value.find(
      item => item.userId === currentUser.id && item.date === todayStr.value
    )
  );
  // æ‰“卡按钮文本
  const checkInOutText = computed(() => {
    if (!todayRecord.value || !todayRecord.value.checkInTime) {
    if (!todayRecord.value || !todayRecord.value.workStartAt) {
      return "上班打卡";
    }
    if (!todayRecord.value.checkOutTime) {
    if (!todayRecord.value.workEndAt) {
      return "下班打卡";
    }
    return "已打卡";
  });
  // ä»Šæ—¥çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const todayStatusTag = computed(() => {
    if (!todayRecord.value.id) return "info";
    if (todayRecord.value.status === 0) return "success";
    return "danger";
  });
  // ä»Šæ—¥çŠ¶æ€æ–‡æœ¬
  const todayStatusText = computed(() => {
    if (!todayRecord.value.id) return "未打卡";
    switch (todayRecord.value.status) {
      case 0:
        return "正常";
      case 1:
        return "迟到";
      case 2:
        return "早退";
      case 3:
        return "迟到、早退";
      case 4:
        return "缺勤";
    }
  });
  // å¯¼èˆªåˆ°è¯¦ç»†æŠ¥å‘Šé¡µé¢
@@ -243,8 +240,32 @@
    });
  };
  // æ‰“卡逻辑(仅前端模拟)
  // èŽ·å–ç­æ¬¡å­—å…¸æ•°æ®
  const getWorkTimeDict = () => {
    getDicts("sys_work_time")
      .then(res => {
        if (res.data && res.data.length > 0) {
          const dictData = res.data;
          workTimeDict.value = {
            startAt: dictData[0].dictValue || "-",
            endAt: dictData[1].dictValue || "-",
          };
        }
      })
      .catch(error => {
        console.error("获取班次字典失败:", error);
      });
  };
  // æ‰“卡逻辑
  const handleCheckInOut = async () => {
    if (todayRecord.value.workEndAt) {
      uni.showToast({
        title: "您已经打过卡了,无需重复打卡!!!",
        icon: "none",
      });
      return;
    }
    // æ£€æŸ¥æ˜¯å¦åœ¨æ‰“卡范围内
    if (!inCheckRange.value) {
      uni.showToast({
@@ -254,75 +275,30 @@
      return;
    }
    const [dateStr, timeStr] = nowTime.value.split(" ");
    if (!dateStr || !timeStr) return;
    // ä¸Šç­æ‰“卡
    if (!todayRecord.value) {
      const newId = rawAttendance.value.length
        ? Math.max(...rawAttendance.value.map(i => i.id)) + 1
        : 1;
      const status = timeStr > "09:00:00" ? "late" : "normal";
      const statusText = status === "late" ? "迟到" : "正常";
      rawAttendance.value.push({
        id: newId,
        date: dateStr,
        userId: currentUser.id,
        name: currentUser.name,
        no: currentUser.no,
        dept: currentUser.dept,
        checkInTime: timeStr.slice(0, 5),
        checkOutTime: "",
        workHours: null,
        status,
        statusText,
        remark: "",
    // è°ƒç”¨æ‰“卡API
    createPersonalAttendanceRecord({})
      .then(res => {
        uni.showToast({
          title: "打卡成功!",
          icon: "success",
        });
        // é‡æ–°èŽ·å–ä»Šæ—¥æ‰“å¡ä¿¡æ¯
        fetchTodayData();
      })
      .catch(error => {
        console.error("打卡失败:", error);
        uni.showToast({
          title: error.msg || "打卡失败,请重试",
          icon: "none",
        });
      });
      uni.showToast({
        title: "上班打卡成功",
        icon: "success",
      });
    } else if (!todayRecord.value.checkOutTime) {
      // ä¸‹ç­æ‰“卡
      todayRecord.value.checkOutTime = timeStr.slice(0, 5);
      // ç®€å•按 9:00-18:00 è®¡ç®—工时
      const start = todayRecord.value.checkInTime || "09:00";
      const [sh, sm] = start.split(":").map(v => parseInt(v, 10));
      const [eh, em] = todayRecord.value.checkOutTime
        .split(":")
        .map(v => parseInt(v, 10));
      const diff = (eh * 60 + em - (sh * 60 + sm)) / 60;
      todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1));
      // æ—©é€€åˆ¤æ–­ï¼š18:00 å‰ç¦»å¼€è§†ä¸ºæ—©é€€ï¼ˆåªç¤ºæ„ï¼‰
      if (timeStr < "18:00:00") {
        if (todayRecord.value.status === "late") {
          // æ—¢è¿Ÿåˆ°åˆæ—©é€€
          todayRecord.value.status = "late-early";
          todayRecord.value.statusText = "迟到 + æ—©é€€";
        } else {
          // ä»…早退
          todayRecord.value.status = "early";
          todayRecord.value.statusText = "早退";
        }
      } else if (todayRecord.value.status === "normal") {
        todayRecord.value.statusText = "正常";
      }
      uni.showToast({
        title: "下班打卡成功",
        icon: "success",
      });
    } else {
      uni.showToast({
        title: "今日已完成上下班打卡",
        icon: "none",
      });
    }
  };
  onMounted(async () => {
    fetchTodayData();
    updateNowTime();
    timer = setInterval(updateNowTime, 1000);
    getWorkTimeDict();
    // èŽ·å–ä½ç½®æƒé™å¹¶æ£€æŸ¥ä½ç½®
    try {
src/pages/humanResources/attendance/report.vue
@@ -26,33 +26,43 @@
                        @cancel="showDatePicker = false"
                        title="搜索日期" />
    <view class="record-list">
      <view v-for="(item) in tableData"
      <!-- åŠ è½½çŠ¶æ€ -->
      <view v-if="loading"
            class="loading-state">
        <u-icon name="loading"
                size="40"
                color="#348fe2"></u-icon>
        <text class="loading-text">加载中...</text>
      </view>
      <view v-else
            v-for="(item) in tableData"
            :key="item.id"
            class="record-item-card"
            :class="{ 'abnormal': item.status !== 'normal' }">
            :class="{ 'abnormal': item.status !== 0 }">
        <view class="record-item-header">
          <text class="record-date">{{ item.date }}</text>
          <u-tag :type="item.status === 'normal' ? 'success' : 'error'"
          <u-tag :type="item.status === 0 ? 'success' : 'error'"
                 size="small">
            {{ item.statusText }}
            <!-- {{ item.status === 0 ? '正常' : (item.status === 1 ? '迟到' : (item.status === 2 ? '早退' : '迟到、早退')) }} -->
            {{ getStatusText(item.status) }}
          </u-tag>
        </view>
        <view class="record-item-body">
          <view class="record-detail">
            <text class="detail-label">员工</text>
            <text class="detail-value">{{ item.name }} ({{ item.no }})</text>
            <text class="detail-value">{{ item.staffName }} ({{ item.staffNo }})</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">部门</text>
            <text class="detail-value">{{ item.dept }}</text>
            <text class="detail-value">{{ item.deptName }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">上班时间</text>
            <text class="detail-value">{{ item.checkInTime ? item.checkInTime : '缺卡' }}</text>
            <text class="detail-value">{{ item.workStartAt || '缺卡' }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">下班时间</text>
            <text class="detail-value">{{ item.checkOutTime? item.checkOutTime : '缺卡' }}</text>
            <text class="detail-value">{{ item.workEndAt || '缺卡' }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">工时</text>
@@ -66,11 +76,11 @@
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-if="tableData.length === 0"
      <view v-if="tableData.length === 0 && !loading"
            class="empty-state">
        <u-icon name="clock-o"
        <!-- <u-icon name="clock-o"
                size="60"
                color="#999"></u-icon>
                color="#999"></u-icon> -->
        <text class="empty-text">暂无考勤记录</text>
      </view>
    </view>
@@ -89,96 +99,25 @@
  import { ref, reactive, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  // æ¨¡æ‹Ÿå½“前登录员工
  const currentUser = reactive({
    id: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
  });
  // æ¨¡æ‹Ÿè€ƒå‹¤åŽŸå§‹æ•°æ®
  const rawAttendance = ref([
    {
      id: 1,
      date: "2026-02-09",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "08:58",
      checkOutTime: "",
      workHours: null,
      status: "normal",
      statusText: "正常",
      remark: "",
    },
    {
      id: 2,
      date: "2026-02-08",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "09:15",
      checkOutTime: "18:05",
      workHours: 8.8,
      status: "late",
      statusText: "迟到",
      remark: "因交通拥堵迟到",
    },
    {
      id: 3,
      date: "2026-02-07",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "08:45",
      checkOutTime: "18:30",
      workHours: 9.7,
      status: "normal",
      statusText: "正常",
      remark: "加班0.5小时",
    },
    {
      id: 4,
      date: "2026-02-06",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "08:50",
      checkOutTime: "17:45",
      workHours: 8.9,
      status: "early",
      statusText: "早退",
      remark: "家中有事提前离开",
    },
    {
      id: 5,
      date: "2026-02-05",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "08:40",
      checkOutTime: "18:20",
      workHours: 9.7,
      status: "normal",
      statusText: "正常",
      remark: "加班0.5小时",
    },
  ]);
  import { findPersonalAttendanceRecords } from "@/api/personnelManagement/attendance.js";
  // æŸ¥è¯¢è¡¨å•
  const searchForm = reactive({
    date: "",
  });
  // åˆ†é¡µå‚æ•°
  const page = reactive({
    current: -1,
    size: -1,
    total: 0,
  });
  // è¡¨æ ¼æ•°æ®
  const tableData = ref([]);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // è¿”回上一页
  const goBack = () => {
@@ -196,6 +135,20 @@
    showDatePicker.value = false;
    handleQuery();
  };
  const getStatusText = status => {
    switch (status) {
      case 0:
        return "正常";
      case 1:
        return "迟到";
      case 2:
        return "早退";
      case 3:
        return "迟到、早退";
      case 4:
        return "缺勤";
    }
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const selectDate = () => {
@@ -204,27 +157,42 @@
  // æ¸…除日期选择
  const clearDate = () => {
    resetQuery();
  };
  // æŸ¥è¯¢
  const handleQuery = () => {
    loading.value = true;
    console.log(searchForm, "searchForm");
    findPersonalAttendanceRecords({
      ...page,
      ...searchForm,
    })
      .then(res => {
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(error => {
        console.error("查询失败:", error);
        uni.showToast({
          title: "查询失败,请重试",
          icon: "none",
        });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  // é‡ç½®æŸ¥è¯¢
  const resetQuery = () => {
    searchForm.date = "";
    handleQuery();
  };
  // æŸ¥è¯¢
  const recomputeTable = () => {
    const list = rawAttendance.value.filter(item => {
      if (searchForm.date && item.date !== searchForm.date) {
        return false;
      }
      return true;
    });
    tableData.value = list;
  };
  const handleQuery = () => {
    recomputeTable();
  };
  onMounted(() => {
    recomputeTable();
    handleQuery();
  });
</script>
@@ -307,6 +275,24 @@
    margin: 0 20rpx 24rpx;
  }
  /* åŠ è½½çŠ¶æ€ */
  .loading-state {
    background-color: $card-bg;
    border-radius: 16rpx;
    box-shadow: $shadow-md;
    text-align: center;
    padding: 120rpx 0;
    margin: 0 20rpx;
    transition: all 0.3s ease;
  }
  .loading-text {
    font-size: 14px;
    color: $text-tertiary;
    margin-top: 24rpx;
    font-weight: 500;
  }
  .record-item-card {
    background-color: $card-bg;
    border-radius: 16rpx;
src/pages/index.vue
@@ -38,7 +38,8 @@
    <!--            </view>-->
    <!--        </view>-->
    <!-- è¥é”€ç®¡ç†æ¨¡å— -->
    <view class="common-module marketing-module" v-if="hasMarketingItems">
    <view class="common-module marketing-module"
          v-if="hasMarketingItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">营销管理</text>
@@ -62,7 +63,8 @@
      </view>
    </view>
    <!--    &lt;!&ndash; é‡‡è´­ç®¡ç†æ¨¡å— &ndash;&gt;-->
    <view class="common-module purchase-module" v-if="hasPurchaseItems">
    <view class="common-module purchase-module"
          v-if="hasPurchaseItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">采购管理</text>
@@ -86,7 +88,8 @@
      </view>
    </view>
    <!-- &lt;!&ndash; ååŒåŠžå…¬æ¨¡å— &ndash;&gt; -->
    <view class="common-module collaboration-module" v-if="hasCollaborationItems">
    <view class="common-module collaboration-module"
          v-if="hasCollaborationItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">协同办公</text>
@@ -110,7 +113,8 @@
      </view>
    </view>
    <!-- å®‰å…¨ç”Ÿäº§æ¨¡å— -->
    <view class="common-module collaboration-module" v-if="hasSafetyItems">
    <view class="common-module collaboration-module"
          v-if="hasSafetyItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">安全生产</text>
@@ -134,7 +138,8 @@
      </view>
    </view>
    <!-- äººåŠ›èµ„æºæ¨¡å— -->
    <view class="common-module collaboration-module" v-if="hasHumanResourcesItems">
    <view class="common-module collaboration-module"
          v-if="hasHumanResourcesItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">人力资源</text>
@@ -158,7 +163,8 @@
      </view>
    </view>
    <!-- ç”Ÿäº§ç®¡æŽ§æ¨¡å— -->
    <view class="common-module equipment-module" v-if="hasProductionItems">
    <view class="common-module equipment-module"
          v-if="hasProductionItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">生产管控</text>
@@ -182,7 +188,8 @@
      </view>
    </view>
    <!-- è®¾å¤‡ç®¡ç†æ¨¡å— -->
    <view class="common-module equipment-module" v-if="hasEquipmentItems">
    <view class="common-module equipment-module"
          v-if="hasEquipmentItems">
      <view class="module-header">
        <view class="module-title-container">
          <text class="module-title">设备管理</text>
@@ -306,7 +313,7 @@
  const safetyItems = reactive([
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "规程资质",
      label: "规程与资质",
    },
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
@@ -314,27 +321,27 @@
    },
    {
      icon: "/static/images/icon/caigoutaizhang@2x.png",
      label: "危险作业",
      label: "危险作业审批",
    },
    {
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "隐患排查",
      label: "隐患排查上报",
    },
    {
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "危险物料",
      label: "危险物料管控",
    },
    {
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "应急预案",
      label: "应急预案查阅",
    },
    {
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "事故上报",
      label: "事故上报记录",
    },
    {
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "安全培训",
      label: "安全培训考核",
    },
  ]);
  // ååŒåŠžå…¬åŠŸèƒ½æ•°æ®
@@ -659,7 +666,7 @@
          url: "/pages/equipmentManagement/verification/index",
        });
        break;
      case "规程资质":
      case "规程与资质":
        uni.navigateTo({
          url: "/pages/safeProduction/safeQualifications/index",
        });
@@ -669,32 +676,32 @@
          url: "/pages/safeProduction/hazardSourceLedger/index",
        });
        break;
      case "危险作业":
      case "危险作业审批":
        uni.navigateTo({
          url: "/pages/cooperativeOffice/collaborativeApproval/index8",
        });
        break;
      case "隐患排查":
      case "隐患排查上报":
        uni.navigateTo({
          url: "/pages/safeProduction/dangerInvestigation/index",
        });
        break;
      case "危险物料":
      case "危险物料管控":
        uni.navigateTo({
          url: "/pages/safeProduction/hazardousMaterialsControl/index",
        });
        break;
      case "应急预案":
      case "应急预案查阅":
        uni.navigateTo({
          url: "/pages/safeProduction/emergencyPlanReview/index",
        });
        break;
      case "事故上报":
      case "事故上报记录":
        uni.navigateTo({
          url: "/pages/safeProduction/accidentReportingRecord/index",
        });
        break;
      case "安全培训":
      case "安全培训考核":
        uni.navigateTo({
          url: "/pages/safeProduction/safetyTrainingAssessment/index",
        });
@@ -811,8 +818,6 @@
            return;
          }
        }
        console.log(orderRow, "orderRow======@@@@@@@@");
        // æ‰«ç æˆåŠŸåŽè·³è½¬åˆ°ç”Ÿäº§æŠ¥å·¥é¡µé¢ï¼Œå¹¶ä¼ é€’orderRow参数
        uni.navigateTo({
          url: `/pages/productionManagement/productionReport/index?orderRow=${orderRow}`,
@@ -848,13 +853,16 @@
  };
  function loginSuccess(result) {
    // èŽ·å–è·¯ç”±æƒé™
    userStore.getRouters().then(() => {
      console.log("路由权限获取成功");
      // è¿‡æ»¤èœå•项
      filterMenuItemsByRoutes();
    }).catch(error => {
      console.error("获取路由权限失败:", error);
    });
    userStore
      .getRouters()
      .then(() => {
        console.log("路由权限获取成功");
        // è¿‡æ»¤èœå•项
        filterMenuItemsByRoutes();
      })
      .catch(error => {
        console.error("获取路由权限失败:", error);
      });
    uni.reLaunch({
      url: "/pages/index",
    });
@@ -870,16 +878,16 @@
  // æ ¹æ®è·¯ç”±æƒé™è¿‡æ»¤èœå•项
  const filterMenuItemsByRoutes = () => {
    const routers = userStore.routers || [];
    // å¦‚果没有路由权限数据,不进行过滤(显示所有菜单)
    if (!routers || routers.length === 0) {
      console.log("暂无路由权限数据,显示所有菜单");
      return;
    }
    // æ”¶é›†æ‰€æœ‰æœ‰æƒé™çš„菜单标题(根据 meta.title)
    const allowedMenuTitles = new Set();
    const collectMenuTitles = (routes) => {
    const collectMenuTitles = routes => {
      if (!Array.isArray(routes)) return;
      routes.forEach(route => {
        // æ”¶é›†å½“前路由的标题
@@ -893,10 +901,6 @@
      });
    };
    collectMenuTitles(routers);
    console.log("允许的菜单标题:", Array.from(allowedMenuTitles));
    console.log("过滤前 - è¥é”€ç®¡ç†:", marketingItems.length, "采购管理:", purchaseItems.length, "协同办公:", collaborationItems.length);
    console.log("过滤前 - å®‰å…¨ç”Ÿäº§:", safetyItems.length, "人力资源:", humanResourcesItems.length, "生产管控:", productionItems.length, "设备管理:", equipmentItems.length);
    // è¿‡æ»¤è¥é”€ç®¡ç†èœå•
    const originalMarketing = [
@@ -931,7 +935,11 @@
      }
      return matched;
    });
    collaborationItems.splice(0, collaborationItems.length, ...filteredCollaboration);
    collaborationItems.splice(
      0,
      collaborationItems.length,
      ...filteredCollaboration
    );
    // è¿‡æ»¤é‡‡è´­ç®¡ç†èœå•
    const originalPurchase = [
@@ -940,7 +948,10 @@
      { icon: "/static/images/icon/laipiaotaizhang@2x.png", label: "来票台账" },
      { icon: "/static/images/icon/fukuanjingji@2x.png", label: "付款登记" },
      { icon: "/static/images/icon/fukuanliushui@2x.png", label: "付款流水" },
      { icon: "/static/images/icon/gongyingshangwanglai@2x.png", label: "供应商往来" },
      {
        icon: "/static/images/icon/gongyingshangwanglai@2x.png",
        label: "供应商往来",
      },
    ];
    const filteredPurchase = originalPurchase.filter(item => {
      return allowedMenuTitles.has(item.label);
@@ -949,14 +960,17 @@
    // è¿‡æ»¤å®‰å…¨ç”Ÿäº§èœå•
    const originalSafety = [
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "规程资质" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "规程与资质" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "危险源台账" },
      { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "危险作业" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "隐患排查" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "危险物料" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "应急预案" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "事故上报" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "安全培训" },
      {
        icon: "/static/images/icon/caigoutaizhang@2x.png",
        label: "危险作业审批",
      },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "隐患排查上报" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "危险物料管控" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "应急预案查阅" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "事故上报记录" },
      { icon: "/static/images/icon/guzhangfenxi@2x.png", label: "安全培训考核" },
    ];
    const filteredSafety = originalSafety.filter(item => {
      return allowedMenuTitles.has(item.label);
@@ -971,11 +985,19 @@
    const filteredHumanResources = originalHumanResources.filter(item => {
      return allowedMenuTitles.has(item.label);
    });
    humanResourcesItems.splice(0, humanResourcesItems.length, ...filteredHumanResources);
    humanResourcesItems.splice(
      0,
      humanResourcesItems.length,
      ...filteredHumanResources
    );
    // è¿‡æ»¤ç”Ÿäº§ç®¡æŽ§èœå•
    const originalProduction = [
      { icon: "/static/images/icon/shengchanbaogong@2x.png", label: "生产报工", bgColor: "#673AB7" },
      {
        icon: "/static/images/icon/shengchanbaogong@2x.png",
        label: "生产报工",
        bgColor: "#673AB7",
      },
    ];
    const filteredProduction = originalProduction.filter(item => {
      return allowedMenuTitles.has(item.label);
@@ -993,10 +1015,6 @@
      return allowedMenuTitles.has(item.label);
    });
    equipmentItems.splice(0, equipmentItems.length, ...filteredEquipment);
    console.log("过滤后 - è¥é”€ç®¡ç†:", marketingItems.length, "采购管理:", purchaseItems.length, "协同办公:", collaborationItems.length);
    console.log("过滤后 - å®‰å…¨ç”Ÿäº§:", safetyItems.length, "人力资源:", humanResourcesItems.length, "生产管控:", productionItems.length, "设备管理:", equipmentItems.length);
    console.log("生产管控菜单项:", productionItems.map(item => item.label));
  };
  // æ£€æŸ¥æ¨¡å—是否有菜单项需要显示
@@ -1011,11 +1029,14 @@
  onMounted(() => {
    // æ¯æ¬¡è¿›å…¥é¦–页都强制刷新用户信息和路由权限,不做本地缓存判断
    userStore.getInfo().then(() => {
      userStore.getRouters().then(() => {
        filterMenuItemsByRoutes();
      }).catch(error => {
        console.error("获取路由权限失败:", error);
      });
      userStore
        .getRouters()
        .then(() => {
          filterMenuItemsByRoutes();
        })
        .catch(error => {
          console.error("获取路由权限失败:", error);
        });
    });
    getUserLoginFacotryList();
    // å¯åŠ¨é€šçŸ¥çŠ¶æ€å®šæ—¶å™¨
@@ -1028,7 +1049,7 @@
  .content {
    background: #f6f7fb;
    min-height: 100vh;
    padding: 12px;
    // padding: 12px;
    /* ä¸ºæ‰€æœ‰è®¾å¤‡è®¾ç½®åŸºç¡€padding-top(包含安全区) */
    padding-top: calc(env(safe-area-inset-top) + 30px);
    position: relative;
@@ -1102,6 +1123,7 @@
  }
  .hero-section {
    margin: 0 12px;
    margin-bottom: 12px;
    animation: fadeInUp 0.6s ease-out 0.1s both;
  }
@@ -1154,7 +1176,9 @@
  .hero-content {
    position: relative;
    z-index: 1;
    padding: 14px 14px 18px 14px;
    margin: 0 12px;
    height: 100%;
  }
  .hero-wave {
@@ -1330,7 +1354,7 @@
  .common-module {
    margin-bottom: 12px;
    background: linear-gradient(135deg, #ffffff 0%, #f9fbff 100%);
    border-radius: 14px;
    // border-radius: 14px;
    padding: 12px;
    box-shadow: 0 8px 22px rgba(17, 24, 39, 0.06);
    border: none;
@@ -1608,8 +1632,9 @@
  .common-module {
    margin-bottom: 12px;
    background: #ffffff;
    border-radius: 16px;
    padding: 12px;
    // border-radius: 16px;
    // padding: 0px;
    padding: 12px 0;
    box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06);
    border: 1px solid rgba(148, 163, 184, 0.18);
    position: relative;
@@ -1658,7 +1683,8 @@
  }
  .module-header {
    margin-bottom: 10px;
    margin-bottom: 18px;
    padding-left: 18px;
    display: flex;
    align-items: center;
    justify-content: space-between;
@@ -1683,8 +1709,8 @@
  }
  .icon-container {
    width: 52px;
    height: 52px;
    width: 48px;
    height: 48px;
    border-radius: 14px;
    display: flex;
    align-items: center;
@@ -1731,9 +1757,9 @@
  }
  .item-label {
    font-size: 12px;
    font-size: 22rpx;
    margin-top: 4px;
    margin-bottom: 6px;
    margin-bottom: 20px;
  }
  .grid-text {
    font-size: 0.875rem;
src/pages/login.vue
@@ -48,7 +48,11 @@
      icon: "none",
    });
  };
  import { userLoginFacotryList, updateClientId } from "@/api/login";
  import {
    userLoginFacotryList,
    updateClientId,
    getNoticeCount,
  } from "@/api/login";
  import { ref, onMounted } from "vue";
  import useUserStore from "@/store/modules/user";
  import { getWxCode } from "@/utils/geek";
@@ -160,20 +164,57 @@
  function loginSuccess(result) {
    // è®¾ç½®ç”¨æˆ·ä¿¡æ¯
    userStore.getInfo().then(res => {
      const userId = res.user.userId;
      // èŽ·å–è·¯ç”±æƒé™
      userStore.getRouters().then(() => {
        console.log("路由权限获取成功");
      }).catch(error => {
        console.error("获取路由权限失败:", error);
      });
      userStore
        .getRouters()
        .then(() => {
          console.log("路由权限获取成功");
        })
        .catch(error => {
          console.error("获取路由权限失败:", error);
        });
      // ç™»å½•成功后,将客户端推送标识发送到服务器
      sendClientIdToServer();
      uni.switchTab({
        url: "/pages/index",
      });
      // å¯åŠ¨å®šæ—¶èŽ·å–æœªè¯»æ¶ˆæ¯æ•°é‡çš„å®šæ—¶å™¨
      startNoticeCountTimer(userId);
      uni.switchTab({ url: "/pages/index" });
    });
  }
  // å¯åŠ¨å®šæ—¶èŽ·å–æœªè¯»æ¶ˆæ¯æ•°é‡çš„å®šæ—¶å™¨
  function startNoticeCountTimer(userId) {
    // ç«‹å³èŽ·å–ä¸€æ¬¡æœªè¯»æ¶ˆæ¯æ•°é‡
    updateNoticeCount(userId);
    // è®¾ç½®å®šæ—¶å™¨ï¼Œæ¯30秒获取一次
    setInterval(() => {
      updateNoticeCount(userId);
    }, 30000);
  }
  // æ›´æ–°æœªè¯»æ¶ˆæ¯æ•°é‡
  function updateNoticeCount(userId) {
    getNoticeCount(userId)
      .then(res => {
        const count = res.data || 0;
        console.log("未读消息数量:", count);
        // æ›´æ–°tabbar的角标
        if (count > 0) {
          uni.setTabBarBadge({
            index: 1, // æ¶ˆæ¯æ ‡ç­¾é¡µçš„索引
            text: count.toString(),
          });
        } else {
          uni.removeTabBarBadge({
            index: 1,
          });
        }
      })
      .catch(error => {
        console.error("获取未读消息数量失败:", error);
      });
  }
  // å°†å®¢æˆ·ç«¯æŽ¨é€æ ‡è¯†å‘送到服务器
  function sendClientIdToServer() {
    // èŽ·å–æœ¬åœ°å­˜å‚¨çš„å®¢æˆ·ç«¯æ ‡è¯†
src/pages/message.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,423 @@
<template>
  <view class="message-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <!-- <PageHeader title="消息中心"
                :showBack="false" /> -->
    <!-- ç­›é€‰æ ‡ç­¾ -->
    <view class="tabs-container">
      <up-tabs v-model="activeTab"
               @change="handleTabChange"
               :list="tabList"
               :current="activeTab"
               itemStyle="width: 50%;height: 80rpx;"></up-tabs>
    </view>
    <!-- æ¶ˆæ¯åˆ—表 -->
    <scroll-view class="message-list"
                 scroll-y="true"
                 refresher-enabled="true"
                 :refresher-triggered="triggered"
                 :refresher-threshold="100"
                 refresher-background="#f5f7fa"
                 @refresherrefresh="onRefresh"
                 @scrolltolower="loadMore">
      <!-- åŠ è½½çŠ¶æ€ -->
      <view v-if="loading"
            class="loading-state">
        <text class="loading-text">加载中...</text>
      </view>
      <!-- æ¶ˆæ¯åˆ—表 -->
      <view v-else
            v-for="(item) in messageList"
            :key="item.id"
            class="message-item"
            :class="{ 'unread': !item.read }">
        <view class="message-content">
          <view class="message-header">
            <text class="message-title">{{ item.noticeTitle }}</text>
            <text class="message-time">{{ formatTime(item.createTime) }}</text>
          </view>
          <text class="message-desc">{{ item.noticeContent }}</text>
          <view class="message-footer">
            <text class="message-view"
                  @click="goToDetail(item)">
              åŽ»æŸ¥çœ‹ >
            </text>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-if="!loading && messageList.length === 0"
            class="empty-state">
        <text class="empty-text">暂无消息</text>
      </view>
      <!-- åŠ è½½æ›´å¤šçŠ¶æ€ -->
      <view v-if="loadingMore"
            class="loading-more-state">
        <text class="loading-more-text">加载更多...</text>
      </view>
    </scroll-view>
  </view>
</template>
<script setup>
  import { ref, reactive, onMounted } from "vue";
  import { listNotice } from "@/api/login.js";
  import useUserStore from "@/store/modules/user";
  // æ ‡ç­¾é¡µæ•°æ®
  const tabList = [
    { name: "未读", id: 0 },
    { name: "已读", id: 1 },
  ];
  // å½“前激活的标签
  const activeTab = ref(0);
  // æ¶ˆæ¯åˆ—表数据
  const messageList = ref([]);
  const loading = ref(false);
  const loadingMore = ref(false);
  const total = ref(0);
  const triggered = ref(false);
  // åˆ†é¡µå‚æ•°
  const page = reactive({
    current: 1,
    size: 10,
  });
  // æ ¼å¼åŒ–æ—¶é—´
  const formatTime = time => {
    if (!time) return "";
    const date = new Date(time);
    const Y = date.getFullYear();
    const M = String(date.getMonth() + 1).padStart(2, "0");
    const D = String(date.getDate()).padStart(2, "0");
    const h = String(date.getHours()).padStart(2, "0");
    const m = String(date.getMinutes()).padStart(2, "0");
    return `${Y}-${M}-${D} ${h}:${m}`;
  };
  // è·³è½¬åˆ°è¯¦æƒ…页
  const goToDetail = item => {
    if (item.appJumpPath.indexOf("/") === 0) {
      uni.navigateTo({
        url: item.appJumpPath,
      });
    } else {
      uni.navigateTo({
        url: "/" + item.appJumpPath,
      });
    }
  };
  const userStore = useUserStore();
  const userId = ref("");
  const getUserId = () => {
    return userStore.getInfo().then(res => {
      console.log(res.user.userId, "res@@@@@@@@@@@2");
      userId.value = res.user.userId;
    });
  };
  // å¤„理标签页切换
  const handleTabChange = val => {
    console.log(val);
    activeTab.value = val.id;
    page.current = 1;
    loadMessages(false);
  };
  // åŠ è½½æ¶ˆæ¯åˆ—è¡¨
  const loadMessages = (append = false) => {
    if (append) {
      loadingMore.value = true;
    } else {
      loading.value = true;
    }
    // æž„建查询参数
    const params = {
      consigneeId: userId.value,
      current: page.current,
      size: page.size,
      status: activeTab.value,
    };
    console.log(params, "===========");
    return listNotice(params)
      .then(res => {
        const records = res?.data?.records || [];
        total.value = res?.data?.total || 0;
        if (append) {
          messageList.value = [...messageList.value, ...records];
        } else {
          messageList.value = records;
        }
      })
      .catch(error => {
        console.error("获取消息失败:", error);
        uni.showToast({ title: "获取消息失败,请重试", icon: "none" });
      })
      .finally(() => {
        loading.value = false;
        loadingMore.value = false;
      });
  };
  // åŠ è½½æ›´å¤š
  const loadMore = () => {
    console.log("===========");
    if (loading.value || loadingMore.value) return;
    if (messageList.value.length >= total.value) return;
    page.current++;
    loadMessages(true);
  };
  // ä¸‹æ‹‰åˆ·æ–°
  const onRefresh = () => {
    triggered.value = true;
    // é‡ç½®é¡µç 
    page.current = 1;
    // é‡æ–°åŠ è½½æ¶ˆæ¯
    loadMessages(false).finally(() => {
      // å…³é—­åˆ·æ–°çŠ¶æ€
      setTimeout(() => {
        triggered.value = false;
      }, 500);
    });
  };
  // é¡µé¢åŠ è½½æ—¶èŽ·å–æ¶ˆæ¯åˆ—è¡¨
  onMounted(() => {
    getUserId().then(() => {
      loadMessages();
    });
  });
</script>
<style scoped lang="scss">
  // å…¨å±€å˜é‡
  $primary-color: #2c7be5;
  $primary-light: #4a90e2;
  $success-color: #4cd964;
  $warning-color: #ff9500;
  $danger-color: #ff3b30;
  $text-primary: #333333;
  $text-secondary: #666666;
  $text-tertiary: #999999;
  $bg-color: #f5f7fa;
  $card-bg: #ffffff;
  $border-color: #e8e8e8;
  $shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
  .message-page {
    min-height: 100vh;
    background-color: $bg-color;
    padding-bottom: 30rpx;
  }
  /* æ ‡ç­¾é¡µå®¹å™¨ */
  .tabs-container {
    background-color: #ffffff;
    margin-bottom: 20rpx;
    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
  }
  /* æ¶ˆæ¯åˆ—表 */
  .message-list {
    margin: 0 20rpx 20rpx;
    min-height: 600rpx;
    height: calc(100vh - 200rpx);
  }
  /* åŠ è½½çŠ¶æ€ */
  .loading-state {
    background-color: $card-bg;
    border-radius: 16rpx;
    box-shadow: $shadow-sm;
    text-align: center;
    padding: 120rpx 0;
    margin-bottom: 20rpx;
  }
  .loading-text {
    font-size: 14px;
    color: $text-tertiary;
    margin-top: 24rpx;
    font-weight: 500;
  }
  /* æ¶ˆæ¯é¡¹ */
  .message-item {
    display: flex;
    align-items: flex-start;
    background-color: $card-bg;
    border-radius: 16rpx;
    box-shadow: $shadow-sm;
    padding: 24rpx;
    margin-bottom: 20rpx;
    margin-right: 40rpx;
    transition: all 0.3s ease;
  }
  .message-item:hover {
    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
    transform: translateY(-2rpx);
  }
  .message-item.unread {
    border-left: 4rpx solid $primary-color;
  }
  /* æ¶ˆæ¯å›¾æ ‡ */
  .message-icon {
    margin-right: 20rpx;
    margin-top: 4rpx;
  }
  /* æ¶ˆæ¯å†…容 */
  .message-content {
    flex: 1;
  }
  /* æ¶ˆæ¯å¤´éƒ¨ */
  .message-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12rpx;
  }
  .message-title {
    font-size: 16px;
    font-weight: 600;
    color: $text-primary;
    flex: 1;
    margin-right: 20rpx;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .message-time {
    font-size: 12px;
    color: $text-tertiary;
  }
  /* æ¶ˆæ¯æè¿° */
  .message-desc {
    font-size: 14px;
    color: $text-secondary;
    line-height: 1.4;
    margin-bottom: 16rpx;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* æ¶ˆæ¯åº•部 */
  .message-footer {
    display: flex;
    justify-content: flex-end;
    align-items: center;
  }
  .message-creator {
    font-size: 12px;
    color: $text-tertiary;
  }
  /* ç©ºçŠ¶æ€ */
  .empty-state {
    background-color: $card-bg;
    border-radius: 16rpx;
    box-shadow: $shadow-sm;
    text-align: center;
    padding: 160rpx 0;
    margin: 40rpx 0;
  }
  .empty-text {
    font-size: 14px;
    color: $text-tertiary;
    margin-top: 24rpx;
    font-weight: 500;
  }
  /* åŠ è½½æ›´å¤šçŠ¶æ€ */
  .loading-more-state {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 30rpx 0;
    margin-top: 10rpx;
  }
  .loading-more-text {
    font-size: 14px;
    color: $text-tertiary;
    margin-left: 10rpx;
  }
  /* åŠ è½½æ›´å¤š */
  .load-more {
    text-align: center;
    padding: 30rpx 0;
    font-size: 14px;
    color: $primary-color;
    font-weight: 500;
    margin-top: 20rpx;
  }
  .load-more-text {
    display: inline-block;
    padding: 10rpx 30rpx;
    background-color: rgba($primary-color, 0.1);
    border-radius: 20rpx;
    transition: all 0.3s ease;
  }
  .load-more-text:hover {
    background-color: rgba($primary-color, 0.2);
    transform: translateY(-2rpx);
  }
  /* åŠ¨ç”»æ•ˆæžœ */
  @keyframes fadeInUp {
    from {
      opacity: 0;
      transform: translateY(20rpx);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
  .message-item {
    animation: fadeInUp 0.3s ease-out;
  }
  .message-item:nth-child(2) {
    animation-delay: 0.1s;
  }
  .message-item:nth-child(3) {
    animation-delay: 0.2s;
  }
  .message-item:nth-child(4) {
    animation-delay: 0.3s;
  }
  .message-item:nth-child(5) {
    animation-delay: 0.4s;
  }
  .message-view {
    float: right;
    color: $primary-color;
  }
</style>
src/pages/safeProduction/emergencyPlanReview/index.vue
@@ -1,7 +1,7 @@
<template>
  <view class="emergency-plan-review">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="应急预案审核"
    <PageHeader title="应急预案查阅"
                @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">