zhangwencui
4 天以前 67461f55748c32d885db965bdff6c2cc63eb327b
推送cid传值,以及打卡签到模块开发
已添加2个文件
已修改6个文件
1815 ■■■■■ 文件已修改
src/App.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/attendance/checkin.vue 855 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/attendance/report.vue 505 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/login.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages_template/pages/login/index2.vue 269 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue
@@ -28,6 +28,8 @@
      console.log("使用 plus.push.getClientInfo èŽ·å–å®¢æˆ·ç«¯æ ‡è¯†");
      plus.push.getClientInfoAsync(info => {
        console.log("客户端推送标识:", info);
        uni.setStorageSync("clientid", info.clientid);
        // è¿™é‡Œå¯ä»¥å°†å®¢æˆ·ç«¯æ ‡è¯†å‘送到服务器
      });
      setTimeout(() => {
src/api/login.js
@@ -49,3 +49,12 @@
    params: params
  })
}
// å‘送客户端推送标识到服务器
export function updateClientId(data) {
  return request({
    url: '/system/client/addOrUpdateClientId',
    method: 'post',
    data: data
  })
}
src/pages.json
@@ -856,6 +856,20 @@
        "navigationBarTitleText": "培训记录",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/attendance/checkin",
      "style": {
        "navigationBarTitleText": "打卡签到",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/attendance/report",
      "style": {
        "navigationBarTitleText": "考勤日报",
        "navigationStyle": "custom"
      }
    }
  ],
  "subPackages": [
src/pages/attendance/checkin.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,855 @@
<template>
  <view class="attendance-checkin">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="打卡签到"
                @back="goBack" />
    <!-- ä»Šæ—¥è€ƒå‹¤åŒºåŸŸ -->
    <view class="today-attendance">
      <view class="attendance-header">
        <text class="attendance-title">今日考勤</text>
      </view>
      <!-- ç­æ¬¡ä¿¡æ¯ -->
      <view class="shift-info">
        <view class="shift-item">
          <u-icon name="calendar"
                  color="#348fe2"
                  size="16"></u-icon>
          <text class="shift-text">白班: 09:00-18:00</text>
        </view>
        <!-- <view v-if="todayRecord?.checkInTime"
              class="shift-item">
          <u-icon name="checkmark-circle"
                  color="#4cd964"
                  size="16"></u-icon>
          <text class="shift-text">{{ todayRecord.checkInTime }}已打卡</text>
        </view> -->
      </view>
      <!-- æ‰“卡按钮 -->
      <view class="checkin-button-container">
        <view class="checkin-button-wrapper">
          <view class="checkin-button"
                :class="{ 'disabled': checkInOutText === '已打卡' }"
                @click="handleCheckInOut">
            <text class="checkin-button-text">{{ checkInOutText }}</text>
            <text class="checkin-time">{{ nowTime.split(' ')[1] }}</text>
          </view>
        </view>
        <!-- æ‰“卡范围状态 -->
      </view>
    </view>
    <!-- æˆ‘的考勤记录 -->
    <view class="attendance-records">
      <view class="records-header">
        <text class="records-title">今日考勤</text>
        <view @click="navigateToReport"
              class="detail-button">查看详情</view>
      </view>
      <!-- å‘˜å·¥ä¿¡æ¯ -->
      <view class="employee-info">
        <view class="info-item">
          <text class="info-label">部门</text>
          <text class="info-value">{{ currentUser.dept }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">姓名</text>
          <text class="info-value">{{ currentUser.name }}</text>
        </view>
        <view class="info-item">
          <text class="info-label">工号</text>
          <text class="info-value">{{ currentUser.no }}</text>
        </view>
      </view>
      <!-- ä»Šæ—¥è€ƒå‹¤çŠ¶æ€ -->
      <view class="today-status">
        <u-icon :name="todayRecord ? 'checkmark-circle' : 'close-circle'"
                :color="todayRecord ? '#4cd964' : '#ff3b30'"
                size="16"></u-icon>
        <text class="status-text">
          {{ todayRecord ? `今日考勤: ä¸Šç­ ${todayRecord.checkInTime}` : '今日未打卡' }}
        </text>
      </view>
      <!-- ä¸‹ç­è€ƒå‹¤çŠ¶æ€ -->
      <view v-if="todayRecord && todayRecord.checkOutTime"
            class="today-status">
        <u-icon :name="todayRecord ? 'checkmark-circle' : 'close-circle'"
                :color="todayRecord ? '#4cd964' : '#ff3b30'"
                size="16"></u-icon>
        <text class="status-text">
          {{ `今日考勤: ä¸‹ç­ ${todayRecord.checkOutTime}` }}
        </text>
      </view>
      <!-- æ‰“卡状态 -->
      <view v-if="todayRecord"
            class="today-status">
        <u-icon :name="todayRecord.status === 'normal' ? 'checkmark-circle' : 'clock'"
                :color="todayRecord.status === 'normal' ? '#4cd964' : '#ff3b30'"
                size="16"></u-icon>
        <text class="status-text">
          {{ `打卡状态: ${todayRecord.statusText}` }}
        </text>
      </view>
      <view v-else
            class="today-status">
        <u-icon name="clock"
                color="#ff3b30"
                size="16"></u-icon>
        <text class="status-text">
          æ‰“卡状态: ç¼ºå¡
        </text>
      </view>
    </view>
  </view>
</template>
<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: "生产一部",
  });
  // æ¨¡æ‹Ÿè€ƒå‹¤åŽŸå§‹æ•°æ®
  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 nowTime = ref("");
  let timer = null;
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ‰“卡范围状态
  const inCheckRange = ref(true);
  // å½“前位置
  const currentLocation = ref(null);
  const updateNowTime = () => {
    const now = new Date();
    const Y = now.getFullYear();
    const M = String(now.getMonth() + 1).padStart(2, "0");
    const D = String(now.getDate()).padStart(2, "0");
    const h = String(now.getHours()).padStart(2, "0");
    const m = String(now.getMinutes()).padStart(2, "0");
    const s = String(now.getSeconds()).padStart(2, "0");
    nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
  };
  // ä»Šæ—¥æ—¥æœŸ
  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) {
      return "上班打卡";
    }
    if (!todayRecord.value.checkOutTime) {
      return "下班打卡";
    }
    return "已打卡";
  });
  // ç”Ÿæˆæœ€è¿‘日期
  const recentDates = computed(() => {
    const dates = [];
    const today = new Date();
    // èŽ·å–æœ€è¿‘7天的日期
    for (let i = 6; i >= 0; i--) {
      const date = new Date(today);
      date.setDate(today.getDate() - i);
      const dateStr = `${date.getFullYear()}-${String(
        date.getMonth() + 1
      ).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
      const hasRecord = rawAttendance.value.some(item => item.date === dateStr);
      dates.push({
        date: dateStr,
        day: date.getDate(),
        isToday: i === 0,
        hasRecord,
      });
    }
    return dates;
  });
  // å¯¼èˆªåˆ°è¯¦ç»†æŠ¥å‘Šé¡µé¢
  const navigateToReport = () => {
    uni.navigateTo({
      url: "/pages/attendance/report",
    });
  };
  // èŽ·å–ä½ç½®æƒé™
  const getLocationPermission = () => {
    return new Promise((resolve, reject) => {
      // #ifdef APP-PLUS
      uni.getAppAuthorizeSetting({
        success: res => {
          if (res.authSetting["scope.userLocation"]) {
            resolve(true);
          } else {
            uni.requestAppAuthorize({
              scope: "scope.userLocation",
              success: res => {
                resolve(res.authSetting["scope.userLocation"]);
              },
              fail: err => {
                reject(err);
              },
            });
          }
        },
        fail: err => {
          reject(err);
        },
      });
      // #else
      // éžAPP环境直接返回成功
      resolve(true);
      // #endif
    });
  };
  // èŽ·å–å½“å‰ä½ç½®
  const getCurrentLocation = () => {
    return new Promise((resolve, reject) => {
      uni.getLocation({
        type: "wgs84",
        success: res => {
          currentLocation.value = res;
          // æ¨¡æ‹Ÿæ£€æŸ¥æ˜¯å¦åœ¨æ‰“卡范围内(实际项目中应根据公司位置和允许的半径进行计算)
          // è¿™é‡Œç®€å•模拟为始终在范围内
          inCheckRange.value = true;
          resolve(res);
        },
        fail: err => {
          console.error("获取位置失败:", err);
          // å¤±è´¥æ—¶é»˜è®¤å…è®¸æ‰“卡(实际项目中应根据业务需求处理)
          inCheckRange.value = true;
          reject(err);
        },
      });
    });
  };
  // æ‰“卡逻辑(仅前端模拟)
  const handleCheckInOut = async () => {
    // æ£€æŸ¥æ˜¯å¦åœ¨æ‰“卡范围内
    if (!inCheckRange.value) {
      uni.showToast({
        title: "不在打卡范围内,无法打卡",
        icon: "none",
      });
      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: "",
      });
      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 () => {
    updateNowTime();
    timer = setInterval(updateNowTime, 1000);
    // èŽ·å–ä½ç½®æƒé™å¹¶æ£€æŸ¥ä½ç½®
    try {
      await getLocationPermission();
      await getCurrentLocation();
    } catch (error) {
      console.error("位置权限获取失败:", error);
    }
  });
  onBeforeUnmount(() => {
    if (timer) {
      clearInterval(timer);
    }
  });
</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);
  $shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
  $shadow-lg: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
  .attendance-checkin {
    min-height: 100vh;
    background-color: $bg-color;
    padding-bottom: 30rpx;
    background-image: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
    display: flex;
    flex-direction: column;
  }
  /* ä»Šæ—¥è€ƒå‹¤åŒºåŸŸ */
  .today-attendance {
    background-color: $card-bg;
    margin: 20rpx;
    border-radius: 16rpx;
    box-shadow: $shadow-md;
    padding: 32rpx;
    transition: all 0.3s ease;
    flex: 1;
    display: flex;
    flex-direction: column;
  }
  .today-attendance:hover {
    box-shadow: $shadow-lg;
    transform: translateY(-2rpx);
  }
  .attendance-header {
    margin-bottom: 24rpx;
    display: flex;
    align-items: center;
  }
  .attendance-title {
    font-size: 18px;
    font-weight: 600;
    color: $text-primary;
    position: relative;
    padding-left: 16rpx;
  }
  .attendance-title::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 4rpx;
    height: 20rpx;
    background-color: $primary-color;
    border-radius: 2rpx;
  }
  /* ç­æ¬¡ä¿¡æ¯ */
  .shift-info {
    margin-bottom: 36rpx;
    padding: 20rpx;
    background-color: rgba($primary-color, 0.05);
    border-radius: 12rpx;
    border-left: 4rpx solid $primary-color;
  }
  .shift-item {
    display: flex;
    align-items: center;
    margin-bottom: 16rpx;
  }
  .shift-item:last-child {
    margin-bottom: 0;
  }
  .shift-item u-icon {
    margin-right: 14rpx;
    transition: all 0.3s ease;
  }
  .shift-item:hover u-icon {
    transform: scale(1.1);
  }
  .shift-text {
    font-size: 14px;
    color: $text-secondary;
    font-weight: 500;
  }
  /* æ‰“卡按钮容器 */
  .checkin-button-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-bottom: 20rpx;
    flex: 1;
    justify-content: center;
  }
  /* æ‰“卡按钮 */
  .checkin-button-wrapper {
    position: relative;
    margin-bottom: 36rpx;
    margin-top: 40rpx;
  }
  .checkin-button {
    width: 260rpx;
    height: 260rpx;
    border-radius: 50%;
    background: linear-gradient(135deg, $primary-color, $primary-light);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    box-shadow: 0 8rpx 32rpx rgba($primary-color, 0.4);
    transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }
  .checkin-button::before {
    content: "";
    position: absolute;
    top: -50%;
    left: -50%;
    width: 200%;
    height: 200%;
    background: linear-gradient(
      45deg,
      transparent,
      rgba(255, 255, 255, 0.1),
      transparent
    );
    transform: rotate(45deg);
    animation: shine 3s infinite;
    opacity: 0;
  }
  @keyframes shine {
    0% {
      transform: translateX(-100%) rotate(45deg);
      opacity: 0;
    }
    50% {
      opacity: 0.3;
    }
    100% {
      transform: translateX(100%) rotate(45deg);
      opacity: 0;
    }
  }
  .checkin-button:hover:not(.disabled) {
    transform: scale(1.08);
    box-shadow: 0 12rpx 40rpx rgba($primary-color, 0.5);
  }
  .checkin-button:active:not(.disabled) {
    transform: scale(0.98);
  }
  .checkin-button.disabled {
    background: linear-gradient(135deg, #d1d1d6, #e5e5ea);
    box-shadow: $shadow-sm;
    cursor: not-allowed;
  }
  .checkin-button-text {
    font-size: 22px;
    font-weight: 700;
    color: #ffffff;
    margin-bottom: 12rpx;
    text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
    z-index: 1;
  }
  .checkin-time {
    font-size: 16px;
    color: rgba(255, 255, 255, 0.95);
    font-weight: 500;
    z-index: 1;
  }
  /* æ‰“卡范围状态 */
  .location-status {
    display: flex;
    align-items: center;
    margin-top: 16rpx;
    padding: 16rpx 24rpx;
    background-color: rgba($success-color, 0.05);
    border-radius: 8rpx;
    border-left: 4rpx solid $success-color;
    transition: all 0.3s ease;
  }
  .location-status.warning {
    background-color: rgba($danger-color, 0.05);
    border-left-color: $danger-color;
  }
  .location-status u-icon {
    margin-right: 10rpx;
    animation: pulse 2s infinite;
  }
  @keyframes pulse {
    0% {
      transform: scale(1);
    }
    50% {
      transform: scale(1.1);
    }
    100% {
      transform: scale(1);
    }
  }
  .location-text {
    font-size: 14px;
    color: $text-secondary;
    font-weight: 500;
  }
  .location-text.warning {
    color: $danger-color;
  }
  /* è€ƒå‹¤è®°å½•区域 */
  .attendance-records {
    background-color: $card-bg;
    margin: 0 20rpx 20rpx;
    border-radius: 16rpx;
    box-shadow: $shadow-md;
    padding: 32rpx;
    transition: all 0.3s ease;
  }
  .attendance-records:hover {
    box-shadow: $shadow-lg;
    transform: translateY(-2rpx);
  }
  .records-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24rpx;
    padding-bottom: 16rpx;
    border-bottom: 1rpx solid $border-color;
  }
  .records-title {
    font-size: 16px;
    font-weight: 600;
    color: $text-primary;
    position: relative;
    padding-left: 12rpx;
    width: 300rpx;
  }
  .records-title::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 3rpx;
    height: 16rpx;
    background-color: $primary-color;
    border-radius: 1.5rpx;
  }
  .detail-button {
    font-size: 14px;
    color: $primary-color;
    font-weight: 500;
    transition: all 0.3s ease;
    padding: 8rpx 16rpx;
    border-radius: 6rpx;
    float: right;
  }
  .detail-button:hover {
    background-color: rgba($primary-color, 0.1);
    transform: translateX(4rpx);
  }
  /* å‘˜å·¥ä¿¡æ¯ */
  .employee-info {
    background-color: rgba($primary-color, 0.05);
    border-radius: 12rpx;
    padding: 20rpx;
    margin-bottom: 24rpx;
  }
  .info-item {
    display: flex;
    align-items: center;
    margin-bottom: 12rpx;
  }
  .info-item:last-child {
    margin-bottom: 0;
  }
  .info-label {
    font-size: 13px;
    color: $text-secondary;
    font-weight: 500;
    width: 80rpx;
  }
  .info-value {
    font-size: 13px;
    color: $text-primary;
    font-weight: 500;
    flex: 1;
  }
  /* æ—¥æœŸé€‰æ‹©å™¨ */
  .date-picker {
    margin-bottom: 24rpx;
  }
  .weekday {
    display: inline-block;
    width: 44rpx;
    text-align: center;
    font-size: 12px;
    color: $text-tertiary;
    margin-bottom: 12rpx;
    font-weight: 500;
  }
  .date-items {
    display: flex;
    justify-content: space-between;
  }
  .date-item {
    width: 44rpx;
    height: 44rpx;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: $text-secondary;
    background-color: rgba($primary-color, 0.05);
    transition: all 0.3s ease;
    cursor: pointer;
  }
  .date-item:hover {
    background-color: rgba($primary-color, 0.1);
    transform: scale(1.1);
  }
  .date-item.active {
    background-color: $primary-color;
    color: #ffffff;
    font-weight: 600;
    box-shadow: 0 2rpx 8rpx rgba($primary-color, 0.3);
  }
  .date-item.has-record {
    position: relative;
  }
  .date-item.has-record::after {
    content: "";
    position: absolute;
    bottom: 4rpx;
    width: 8rpx;
    height: 8rpx;
    border-radius: 50%;
    background-color: $success-color;
    box-shadow: 0 1rpx 2rpx rgba($success-color, 0.3);
  }
  /* ä»Šæ—¥è€ƒå‹¤çŠ¶æ€ */
  .today-status {
    display: flex;
    align-items: center;
    margin-top: 24rpx;
    padding: 20rpx;
    background-color: rgba($primary-color, 0.05);
    border-radius: 12rpx;
    transition: all 0.3s ease;
  }
  .today-status:hover {
    background-color: rgba($primary-color, 0.08);
  }
  .today-status u-icon {
    margin-right: 14rpx;
    transition: all 0.3s ease;
  }
  .today-status:hover u-icon {
    transform: scale(1.1);
  }
  .status-text {
    font-size: 14px;
    color: $text-secondary;
    font-weight: 500;
    margin-left: 10rpx;
  }
  /* å“åº”式调整 */
  @media (max-width: 375px) {
    .today-attendance,
    .attendance-records {
      margin: 12rpx;
      padding: 24rpx;
    }
    .checkin-button {
      width: 220rpx;
      height: 220rpx;
    }
    .checkin-button-text {
      font-size: 20px;
    }
    .checkin-time {
      font-size: 14px;
    }
    .weekday {
      width: 38rpx;
    }
    .date-item {
      width: 38rpx;
      height: 38rpx;
    }
  }
  /* åŠ¨ç”»æ•ˆæžœ */
  @keyframes fadeInUp {
    from {
      opacity: 0;
      transform: translateY(20rpx);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
  .today-attendance,
  .attendance-records {
    animation: fadeInUp 0.5s ease-out;
  }
  .attendance-records {
    animation-delay: 0.2s;
  }
</style>
src/pages/attendance/report.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,505 @@
<template>
  <view class="attendance-report">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="考勤日报"
                @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view @click="selectDate"
              class="search-input">
          <view class="search-text">{{ searchForm.date? searchForm.date : '请选择日期' }}</view>
        </view>
        <view class="filter-button"
              @click="clearDate">
          <u-icon name="close-circle"
                  size="24"
                  color="#999"></u-icon>
        </view>
      </view>
    </view>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <up-datetime-picker :show="showDatePicker"
                        mode="date"
                        v-model="currentDate"
                        @confirm="handleDateConfirm"
                        @cancel="showDatePicker = false"
                        title="搜索日期" />
    <view class="record-list">
      <view v-for="(item) in tableData"
            :key="item.id"
            class="record-item-card"
            :class="{ 'abnormal': item.status !== 'normal' }">
        <view class="record-item-header">
          <text class="record-date">{{ item.date }}</text>
          <u-tag :type="item.status === 'normal' ? 'success' : 'error'"
                 size="small">
            {{ item.statusText }}
          </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>
          </view>
          <view class="record-detail">
            <text class="detail-label">部门</text>
            <text class="detail-value">{{ item.dept }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">上班时间</text>
            <text class="detail-value">{{ item.checkInTime ? item.checkInTime : '缺卡' }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">下班时间</text>
            <text class="detail-value">{{ item.checkOutTime? item.checkOutTime : '缺卡' }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">工时</text>
            <text class="detail-value">{{ item.workHours ? item.workHours + '小时' : '-' }}</text>
          </view>
          <view v-if="item.remark"
                class="record-detail">
            <text class="detail-label">备注</text>
            <text class="detail-value">{{ item.remark }}</text>
          </view>
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-if="tableData.length === 0"
            class="empty-state">
        <u-icon name="clock-o"
                size="60"
                color="#999"></u-icon>
        <text class="empty-text">暂无考勤记录</text>
      </view>
    </view>
    <!-- å¯¼å‡ºæŒ‰é’® -->
    <!-- <view class="export-section">
      <u-button type="default"
                size="medium"
                text="导出考勤日报"
                @click="handleExport"
                class="export-btn"></u-button>
    </view> -->
  </view>
</template>
<script setup>
  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 deptOptions = [
    { label: "生产一部", value: "生产一部" },
    { label: "生产二部", value: "生产二部" },
    { label: "设备维护部", value: "设备维护部" },
    { label: "质检部", value: "质检部" },
  ];
  // æ¨¡æ‹Ÿè€ƒå‹¤åŽŸå§‹æ•°æ®
  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小时",
    },
  ]);
  // æŸ¥è¯¢è¡¨å•
  const searchForm = reactive({
    dept: "",
    date: "",
  });
  // è¡¨æ ¼æ•°æ®
  const tableData = ref([]);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æ—¥æœŸé€‰æ‹©å™¨
  const showDatePicker = ref(false);
  const currentDate = ref(new Date());
  // å¤„理日期选择
  const handleDateConfirm = e => {
    currentDate.value = e.value;
    searchForm.date = dayjs(e.value).format("YYYY-MM-DD");
    showDatePicker.value = false;
    handleQuery();
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const selectDate = () => {
    showDatePicker.value = true;
  };
  // æ¸…除日期选择
  const clearDate = () => {
    searchForm.date = "";
    handleQuery();
  };
  // æŸ¥è¯¢
  const recomputeTable = () => {
    const list = rawAttendance.value.filter(item => {
      if (searchForm.dept && item.dept !== searchForm.dept) {
        return false;
      }
      if (searchForm.date && item.date !== searchForm.date) {
        return false;
      }
      return true;
    });
    tableData.value = list;
  };
  const handleQuery = () => {
    recomputeTable();
  };
  const resetSearch = () => {
    searchForm.dept = "";
    searchForm.date = "";
    recomputeTable();
  };
  // å¯¼å‡ºï¼ˆæ¼”示)
  const handleExport = () => {
    uni.showToast({
      title: "当前为演示页面,导出功能未对接实际接口",
      icon: "none",
    });
  };
  onMounted(() => {
    // é»˜è®¤å±•示当天数据
    // const today = new Date();
    // const Y = today.getFullYear();
    // const M = String(today.getMonth() + 1).padStart(2, "0");
    // const D = String(today.getDate()).padStart(2, "0");
    // searchForm.date = `${Y}-${M}-${D}`;
    recomputeTable();
  });
</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);
  $shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
  $shadow-lg: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
  .attendance-report {
    min-height: 100vh;
    background-color: $bg-color;
    padding-bottom: 30rpx;
    background-image: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
  }
  /* æœç´¢å’Œç­›é€‰åŒºåŸŸ */
  .search-section {
    background-color: $card-bg;
    margin: 20rpx;
    border-radius: 16rpx;
    box-shadow: $shadow-md;
    padding: 20rpx;
    margin-bottom: 24rpx;
    transition: all 0.3s ease;
  }
  .search-section:hover {
    box-shadow: $shadow-lg;
    transform: translateY(-2rpx);
  }
  .search-bar {
    display: flex;
    align-items: center;
    background-color: rgba($primary-color, 0.05);
    border-radius: 8rpx;
    padding: 0 16rpx;
    height: 70rpx;
  }
  .search-input {
    flex: 1;
    height: 100%;
    display: flex;
    align-items: center;
  }
  .search-text {
    font-size: 14px;
    color: $text-tertiary;
    height: 70rpx;
    line-height: 70rpx;
    margin-left: 8rpx;
  }
  .filter-button {
    padding: 8rpx;
    transition: all 0.3s ease;
  }
  .filter-button:hover {
    background-color: rgba($primary-color, 0.1);
    border-radius: 4rpx;
  }
  /* è®°å½•列表 */
  .record-list {
    margin: 0 20rpx 24rpx;
  }
  .record-item-card {
    background-color: $card-bg;
    border-radius: 16rpx;
    box-shadow: $shadow-md;
    margin-bottom: 24rpx;
    overflow: hidden;
    transition: all 0.3s ease;
  }
  .record-item-card:hover {
    box-shadow: $shadow-lg;
    transform: translateY(-2rpx);
  }
  .record-item-card.abnormal {
    background-color: rgba($danger-color, 0.05);
    border-left: 4rpx solid $danger-color;
  }
  .record-item-header {
    background-color: rgba($primary-color, 0.05);
    padding: 20rpx;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1rpx solid $border-color;
  }
  .record-date {
    font-size: 14px;
    font-weight: 600;
    color: $text-primary;
  }
  .record-item-body {
    padding: 24rpx;
  }
  .record-detail {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16rpx;
    padding: 8rpx 0;
    border-bottom: 1rpx solid rgba($border-color, 0.5);
  }
  .record-detail:last-child {
    margin-bottom: 0;
    border-bottom: none;
  }
  .detail-label {
    font-size: 13px;
    color: $text-secondary;
    font-weight: 500;
  }
  .detail-value {
    font-size: 13px;
    color: $text-primary;
    font-weight: 500;
  }
  /* ç©ºçŠ¶æ€ */
  .empty-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;
  }
  .empty-state:hover {
    box-shadow: $shadow-lg;
  }
  .empty-text {
    font-size: 14px;
    color: $text-tertiary;
    margin-top: 24rpx;
    font-weight: 500;
  }
  /* å¯¼å‡ºæŒ‰é’® */
  .export-section {
    text-align: center;
    margin: 0 20rpx 30rpx;
  }
  .export-btn {
    width: 100%;
    border-radius: 8rpx;
    border: 1rpx solid $primary-color;
    color: $primary-color;
    transition: all 0.3s ease;
  }
  .export-btn:hover {
    background-color: $primary-color;
    color: #ffffff;
    box-shadow: 0 4rpx 12rpx rgba($primary-color, 0.3);
    transform: translateY(-2rpx);
  }
  /* å“åº”式调整 */
  @media (max-width: 375px) {
    .search-section,
    .record-list,
    .empty-state,
    .export-section {
      margin: 12rpx;
    }
    .search-section {
      padding: 16rpx;
    }
    .record-item-body {
      padding: 20rpx;
    }
  }
  /* åŠ¨ç”»æ•ˆæžœ */
  @keyframes fadeInUp {
    from {
      opacity: 0;
      transform: translateY(20rpx);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
  .search-section,
  .record-item-card,
  .empty-state,
  .export-section {
    animation: fadeInUp 0.5s ease-out;
  }
  .record-item-card {
    animation-delay: 0.1s;
  }
  .record-item-card:nth-child(2) {
    animation-delay: 0.2s;
  }
  .record-item-card:nth-child(3) {
    animation-delay: 0.3s;
  }
  .empty-state {
    animation-delay: 0.2s;
  }
  .export-section {
    animation-delay: 0.3s;
  }
</style>
src/pages/index.vue
@@ -374,6 +374,10 @@
      icon: "/static/images/icon/kehubaifang@2x.png",
      label: "客户拜访",
    },
    {
      icon: "/static/images/icon/guzhangfenxi@2x.png",
      label: "打卡签到",
    },
  ]);
  // ç”Ÿäº§ç®¡æŽ§åŠŸèƒ½æ•°æ®
@@ -733,7 +737,11 @@
          url: "/pages/safeProduction/safetyTrainingAssessment/index",
        });
        break;
      case "打卡签到":
        uni.navigateTo({
          url: "/pages/attendance/checkin",
        });
        break;
      default:
        uni.showToast({
          title: `点击了${item.label}`,
src/pages/login.vue
@@ -5,26 +5,35 @@
        </view>
        <view class="login-form-content">
            <view class="input-item flex align-center">
                <up-input prefixIcon="account" placeholder="请输入账号" border="bottom"
        <up-input prefixIcon="account"
                  placeholder="请输入账号"
                  border="bottom"
                                    @blur="getUserLoginFacotryList"
                                    maxlength="30" v-model="loginForm.userName" clearable></up-input>
                  maxlength="30"
                  v-model="loginForm.userName"
                  clearable></up-input>
            </view>
            <view class="input-item flex align-center">
                <up-input prefixIcon="lock" placeholder="请输入密码" border="bottom" maxlength="20" v-model="loginForm.password" clearable type="password"></up-input>
        <up-input prefixIcon="lock"
                  placeholder="请输入密码"
                  border="bottom"
                  maxlength="20"
                  v-model="loginForm.password"
                  clearable
                  type="password"></up-input>
            </view>
            <view>
                <button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button>
        <button @click="handleLogin"
                class="login-btn cu-btn block bg-blue lg round">登录</button>
            </view>
        </view>
        <!-- è®°ä½å¯†ç é€‰é¡¹ -->
        <view class="remember-password">
            <up-checkbox
                :customStyle="{marginBottom: '8px'}"
      <up-checkbox :customStyle="{marginBottom: '8px'}"
                label="记住密码"
                name="agree"
                usedAlone
                v-model:checked="rememberPassword"
            >
                   v-model:checked="rememberPassword">
            </up-checkbox>
        </view>
    </view>
@@ -33,20 +42,20 @@
<script setup>
import {modal} from "@/plugins";
const showToast = (message) => {
  const showToast = message => {
    uni.showToast({
        title: message,
        icon: 'none'
    })
}
import { userLoginFacotryList} from '@/api/login'
      icon: "none",
    });
  };
  import { userLoginFacotryList, updateClientId } from "@/api/login";
import { ref, onMounted } from "vue";
import useUserStore from '@/store/modules/user'
import { getWxCode } from '@/utils/geek';
import { wxLogin } from '@/api/oauth';
import { setToken } from '@/utils/auth';
  import useUserStore from "@/store/modules/user";
  import { getWxCode } from "@/utils/geek";
  import { wxLogin } from "@/api/oauth";
  import { setToken } from "@/utils/auth";
import View from "@/pages/procurementManagement/procurementLedger/view.vue";
const userStore = useUserStore()
  const userStore = useUserStore();
const useWxLogin = ref(false); // æ˜¯å¦ä½¿ç”¨å¾®ä¿¡ç™»å½•
const rememberPassword = ref(false); // è®°ä½å¯†ç 
const loginForm = ref({
@@ -54,28 +63,28 @@
    password: "",
    currentFatoryName: "",
});
const factoryList = ref([]) // å…¬å¸åˆ—表
  const factoryList = ref([]); // å…¬å¸åˆ—表
// ä¿å­˜å¯†ç åˆ°æœ¬åœ°å­˜å‚¨
function savePassword() {
    if (rememberPassword.value) {
        uni.setStorageSync('remembered_username', loginForm.value.userName);
        uni.setStorageSync('remembered_password', loginForm.value.password);
        uni.setStorageSync('remember_password', true);
      uni.setStorageSync("remembered_username", loginForm.value.userName);
      uni.setStorageSync("remembered_password", loginForm.value.password);
      uni.setStorageSync("remember_password", true);
    } else {
        uni.removeStorageSync('remembered_username');
        uni.removeStorageSync('remembered_password');
        uni.setStorageSync('remember_password', false);
      uni.removeStorageSync("remembered_username");
      uni.removeStorageSync("remembered_password");
      uni.setStorageSync("remember_password", false);
    }
}
// ä»Žæœ¬åœ°å­˜å‚¨åŠ è½½å¯†ç 
function loadPassword() {
    const remembered = uni.getStorageSync('remember_password');
    const remembered = uni.getStorageSync("remember_password");
    if (remembered) {
        rememberPassword.value = true;
        const savedUsername = uni.getStorageSync('remembered_username');
        const savedPassword = uni.getStorageSync('remembered_password');
      const savedUsername = uni.getStorageSync("remembered_username");
      const savedPassword = uni.getStorageSync("remembered_password");
        if (savedUsername) {
            loginForm.value.userName = savedUsername;
        }
@@ -88,73 +97,107 @@
if (useWxLogin.value) {
    getWxCode().then(res => {
        console.log(res);
        wxLogin('miniapp',res).then(res => {
      wxLogin("miniapp", res).then(res => {
            if(res.token != null){
                setToken(res.token);
                loginSuccess()
          loginSuccess();
            }
        });
    })
    });
}
function getUserLoginFacotryList() {
    if(loginForm.value.userName){
        userLoginFacotryList({userName:loginForm.value.userName}).then(res => {
            console.log('res',res)
      userLoginFacotryList({ userName: loginForm.value.userName })
        .then(res => {
          console.log("res", res);
            // æ£€æŸ¥res.data是否为数组
            if (res.data && Array.isArray(res.data)) {
                // é‡æ–°ç»„装数据格式:deptId变成id,deptName变成name
                factoryList.value = res.data.map(item => ({
                    id: item.deptId,
                    name: item.deptName
                }))
              name: item.deptName,
            }));
            } else {
                // å¦‚æžœres.data不是数组,设置为空数组
                factoryList.value = []
            factoryList.value = [];
            }
        }).catch(error => {
            showToast('获取公司列表失败:', error)
            factoryList.value = []
        })
        .catch(error => {
          showToast("获取公司列表失败:", error);
          factoryList.value = [];
        });
    }else {
        factoryList.value = []
      factoryList.value = [];
    }
}
async function handleLogin() {
    if (loginForm.value.userName === "") {
        showToast("请输入您的账号")
      showToast("请输入您的账号");
    } else if (loginForm.value.password === "") {
        showToast("请输入您的密码")
      showToast("请输入您的密码");
    } else {
        showToast("登录中,请耐心等待...")
        pwdLogin()
      showToast("登录中,请耐心等待...");
      pwdLogin();
    }
};
  }
// å¯†ç ç™»å½•
async function pwdLogin() {
    userStore.loginCheckFactory(loginForm.value).then(() => {
        modal.closeLoading()
    userStore
      .loginCheckFactory(loginForm.value)
      .then(() => {
        modal.closeLoading();
        // ç™»å½•成功后保存密码
        savePassword();
        loginSuccess()
    }).catch(() => {
        modal.closeLoading()
        loginSuccess();
    })
};
      .catch(() => {
        modal.closeLoading();
      });
  }
function loginSuccess(result) {
    // è®¾ç½®ç”¨æˆ·ä¿¡æ¯
    userStore.getInfo().then(res => {
      // ç™»å½•成功后,将客户端推送标识发送到服务器
      sendClientIdToServer();
        uni.switchTab({
            url: '/pages/index'
        url: "/pages/index",
        });
    });
  }
  // å°†å®¢æˆ·ç«¯æŽ¨é€æ ‡è¯†å‘送到服务器
  function sendClientIdToServer() {
    // èŽ·å–æœ¬åœ°å­˜å‚¨çš„å®¢æˆ·ç«¯æ ‡è¯†
    const clientId = uni.getStorageSync("clientid");
    if (clientId) {
      console.log("登录成功,准备发送客户端标识到服务器:", clientId);
      // è¿™é‡Œè°ƒç”¨åŽç«¯æŽ¥å£å‘送客户端标识
      updateClientId({ cid: clientId })
        .then(res => {
          console.log("服务器响应:", res);
          if (res.code === 200) {
            console.log("客户端标识已成功发送到服务器");
          } else {
            console.log("服务器返回错误:", res.msg);
          }
    })
        .catch(error => {
          console.log("发送客户端标识到服务器失败:", error);
        });
      // ç¤ºä¾‹ï¼šapi.updateClientId({ clientId: clientId });
      // ç”±äºŽæ²¡æœ‰å…·ä½“的接口,这里只打印日志
      console.log("客户端标识已发送到服务器");
    } else {
      console.log("未获取到客户端推送标识");
    }
}
// é¡µé¢åŠ è½½æ—¶æ£€æŸ¥æ˜¯å¦æœ‰ä¿å­˜çš„å¯†ç 
onMounted(() => {
    loadPassword();
    getUserLoginFacotryList()
    getUserLoginFacotryList();
});
</script>
@@ -233,7 +276,7 @@
        .login-btn {
            margin-top: 60px;
            height: 50px;
            background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
        background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
            box-shadow: 0px 4px 10px 0px rgba(3,88,185,0.2);
            border-radius: 40px 40px 40px 40px;
        }
src/pages_template/pages/login/index2.vue
@@ -1,8 +1,12 @@
<template>
    <view>
        <view class="normal-login-container " v-if="page == 'login'">
            <view class="left" @click="back">
                <image src="../../../static/uview/demo/backTop.png" mode="" style="height: 30rpx;"></image>
    <view class="normal-login-container "
          v-if="page == 'login'">
      <view class="left"
            @click="back">
        <image src="../../../static/uview/demo/backTop.png"
               mode=""
               style="height: 30rpx;"></image>
            </view>
            <view class="scale-in-center">
                <view class="logo-content align-center justify-center flex">
@@ -11,54 +15,72 @@
                <view class="login-form-content">
                    <view class="input-item flex align-center">
                        <view class="iconfont icon-user icon"></view>
                        <input v-model="loginForm.username" class="input" type="text" placeholder="请输入账号"
            <input v-model="loginForm.username"
                   class="input"
                   type="text"
                   placeholder="请输入账号"
                            maxlength="30" />
                    </view>
                    <view class="input-item flex align-center">
                        <view class="iconfont icon-password icon"></view>
                        <input v-model="loginForm.password" type="password" class="input" placeholder="请输入密码"
            <input v-model="loginForm.password"
                   type="password"
                   class="input"
                   placeholder="请输入密码"
                            maxlength="20" />
                    </view>
                    <view class="input-item flex align-center" style="width: 60%;margin: 0px;" v-if="captchaEnabled">
          <view class="input-item flex align-center"
                style="width: 60%;margin: 0px;"
                v-if="captchaEnabled">
                        <view class="iconfont icon-code icon"></view>
                        <input v-model="loginForm.code" type="number" class="input" placeholder="请输入验证码"
            <input v-model="loginForm.code"
                   type="number"
                   class="input"
                   placeholder="请输入验证码"
                            maxlength="4" />
                        <view class="login-code">
                            <image :src="codeUrl" @click="getCode" class="login-code-img"></image>
              <image :src="codeUrl"
                     @click="getCode"
                     class="login-code-img"></image>
                        </view>
                    </view>
                    <view class="action-btn">
                        <button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button>
            <button @click="handleLogin"
                    class="login-btn cu-btn block bg-blue lg round">登录</button>
                    </view>
                </view>
            </view>
        </view>
        <!-- ç™»å½• -->
        <view v-else>
            <view class="container">
                <view class="cover slide-top1" :style="'animation-play-state:' + play[2]">
                    <view class="masking slide-top" :class="[collapsedClass, { animating: isAnimating }]"
                        ref="fixedViewRef" :style="'animation-play-state:' + play[0]">
        <view class="cover slide-top1"
              :style="'animation-play-state:' + play[2]">
          <view class="masking slide-top"
                :class="[collapsedClass, { animating: isAnimating }]"
                ref="fixedViewRef"
                :style="'animation-play-state:' + play[0]">
                        <uni-row>
                            <text class="text-first">欢迎使用</text>
                            <text class="text-second">校园访客</text>
                            <text class="text-third">预约系统</text>
                            <view :class="{ active: isActive === true, button: isActive !== true }" @click="startplay"
              <view :class="{ active: isActive === true, button: isActive !== true }"
                    @click="startplay"
                                :style="'animation-play-state:' + play[1]">
                                <uni-row>
                                    <text class="text-fifth">访客登录</text>
                                </uni-row>
                            </view>
                            <view class="shadow1" :style="'animation-play-state:' + play[1]" />
                            <view class="shadow2" :style="'animation-play-state:' + play[1]" />
                            <view class="shadow3" :style="'animation-play-state:' + play[1]" />
                            <image
                                style="width: 100%;height: 1050rpx;opacity: 0.05;border-radius: 0 0 400rpx 400rpx;position: absolute;"
              <view class="shadow1"
                    :style="'animation-play-state:' + play[1]" />
              <view class="shadow2"
                    :style="'animation-play-state:' + play[1]" />
              <view class="shadow3"
                    :style="'animation-play-state:' + play[1]" />
              <image style="width: 100%;height: 1050rpx;opacity: 0.05;border-radius: 0 0 400rpx 400rpx;position: absolute;"
                                src="../../../static/uview/common/gray-logo.png"></image>
                            <text class="text-forth" @click="login()">账号密码登录</text>
              <text class="text-forth"
                    @click="login()">账号密码登录</text>
                        </uni-row>
                    </view>
                </view>
@@ -73,101 +95,145 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import useUserStore from '@/store/modules/user'
import tab from '@/plugins/tab'
import modal from '@/plugins/modal'
  import { ref, reactive, computed, onMounted } from "vue";
  import { updateClientId } from "@/api/login";
  import useUserStore from "@/store/modules/user";
  import tab from "@/plugins/tab";
  import modal from "@/plugins/modal";
const src = ref("staticimagessoutheast.jpg")
const isActive = ref(false)
const isFixedViewVisible = ref(true)
const animationType = ref("up") // å¯é€‰å€¼ï¼šright æˆ– up
const isAnimating = ref(false) // æŽ§åˆ¶åŠ¨ç”»æ‰§è¡ŒçŠ¶æ€
const play = ref(["paused", "paused", "paused"])
const page = ref("index")
  const src = ref("staticimagessoutheast.jpg");
  const isActive = ref(false);
  const isFixedViewVisible = ref(true);
  const animationType = ref("up"); // å¯é€‰å€¼ï¼šright æˆ– up
  const isAnimating = ref(false); // æŽ§åˆ¶åŠ¨ç”»æ‰§è¡ŒçŠ¶æ€
  const play = ref(["paused", "paused", "paused"]);
  const page = ref("index");
const codeUrl = ref("")
const captchaEnabled = ref(true)
  const codeUrl = ref("");
  const captchaEnabled = ref(true);
const loginForm = reactive({
    username: "admin",
    password: "admin123",
    code: "",
    uuid: ''
})
    uuid: "",
  });
const collapsedClass = computed(() => {
    return isFixedViewVisible.value ? "" : `collapsed-${animationType.value}`;
})
  });
onMounted(() => {
    getCode()
})
    getCode();
  });
const login = () => {
    play.value[2] = "running"
    setTimeout(() => { page.value = 'login' }, 1000)
}
    play.value[2] = "running";
    setTimeout(() => {
      page.value = "login";
    }, 1000);
  };
const back = () => {
    page.value = 'index'
    play.value[0] = "paused"
    play.value[1] = "paused"
    play.value[2] = "paused"
}
    page.value = "index";
    play.value[0] = "paused";
    play.value[1] = "paused";
    play.value[2] = "paused";
  };
const startplay = () => {
    play.value[1] = "running"
    play.value[1] = "running";
    isActive.value = true;
    setTimeout(() => { isActive.value = false; }, 300);
    if (isAnimating.value) { return; }
    isAnimating.value = false; // å¼€å§‹åŠ¨ç”»æ‰§è¡Œ
    play.value[0] = "running"
    isFixedViewVisible.value = !isFixedViewVisible.value;
    setTimeout(() => { uni.navigateBack({ delta: 1 }); }, 1000)
    setTimeout(() => {
      isActive.value = false;
    }, 300);
    if (isAnimating.value) {
      return;
}
    isAnimating.value = false; // å¼€å§‹åŠ¨ç”»æ‰§è¡Œ
    play.value[0] = "running";
    isFixedViewVisible.value = !isFixedViewVisible.value;
    setTimeout(() => {
      uni.navigateBack({ delta: 1 });
    }, 1000);
  };
// èŽ·å–å›¾å½¢éªŒè¯ç 
const getCode = () => {
    let res = {}
    captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
    let res = {};
    captchaEnabled.value =
      res.captchaEnabled === undefined ? true : res.captchaEnabled;
    if (captchaEnabled.value) {
        codeUrl.value = 'data:image/gif;base64,' + res.img
        loginForm.uuid = res.uuid
      codeUrl.value = "data:image/gif;base64," + res.img;
      loginForm.uuid = res.uuid;
    }
}
  };
// ç™»å½•方法
const handleLogin = async () => {
    if (loginForm.username === "") {
        modal.msgError("请输入您的账号")
      modal.msgError("请输入您的账号");
    } else if (loginForm.password === "") {
        modal.msgError("请输入您的密码")
      modal.msgError("请输入您的密码");
    } else if (loginForm.code === "" && captchaEnabled.value) {
        modal.msgError("请输入验证码")
      modal.msgError("请输入验证码");
    } else {
        modal.loading("登录中,请耐心等待...")
        pwdLogin()
      modal.loading("登录中,请耐心等待...");
      pwdLogin();
    }
}
  };
// å¯†ç ç™»å½•
const pwdLogin = async () => {
    useUserStore().login(loginForm).then(() => {
        modal.closeLoading()
        loginSuccess()
    }).catch(() => {
        if (captchaEnabled.value) {
            getCode()
        }
    useUserStore()
      .login(loginForm)
      .then(() => {
        modal.closeLoading();
        loginSuccess();
    })
      .catch(() => {
        if (captchaEnabled.value) {
          getCode();
}
      });
  };
// ç™»å½•成功后,处理函数
const loginSuccess = (result) => {
  const loginSuccess = result => {
    // è®¾ç½®ç”¨æˆ·ä¿¡æ¯
    useUserStore().getInfo().then(res => {
        tab.reLaunch('/pages/index')
    })
    useUserStore()
      .getInfo()
      .then(res => {
        // ç™»å½•成功后,将客户端推送标识发送到服务器
        sendClientIdToServer();
        tab.reLaunch("/pages/index");
      });
  };
  // å°†å®¢æˆ·ç«¯æŽ¨é€æ ‡è¯†å‘送到服务器
  const sendClientIdToServer = () => {
    // èŽ·å–æœ¬åœ°å­˜å‚¨çš„å®¢æˆ·ç«¯æ ‡è¯†
    const clientId = uni.getStorageSync("clientid");
    if (clientId) {
      console.log("登录成功,准备发送客户端标识到服务器:", clientId);
      // è¿™é‡Œè°ƒç”¨åŽç«¯æŽ¥å£å‘送客户端标识
      // ç¤ºä¾‹ï¼šapi.updateClientId({ clientId: clientId });
      updateClientId({ cid: clientId })
        .then(res => {
          console.log("服务器响应:", res);
          if (res.code === 200) {
            console.log("客户端标识已成功发送到服务器");
          } else {
            console.log("服务器返回错误:", res.msg);
}
        })
        .catch(error => {
          console.log("发送客户端标识到服务器失败:", error);
        });
      // ç”±äºŽæ²¡æœ‰å…·ä½“的接口,这里只打印日志
      console.log("客户端标识已发送到服务器");
    } else {
      console.log("未获取到客户端推送标识");
    }
  };
</script>
<style lang="scss">
page {
@@ -234,7 +300,6 @@
                text-align: left;
                padding-left: 15px;
            }
        }
        .login-btn {
@@ -261,14 +326,6 @@
    }
}
.container {
    position: relative;
}
@@ -284,10 +341,12 @@
    border-radius: 50%;
    z-index: 9999;
    background: linear-gradient(180deg,
    background: linear-gradient(
      180deg,
            rgba(110, 109, 122, 0.595),
            rgba(46, 87, 190, 0.714) 70.792%,
            rgb(17, 120, 222) 100%);
      rgb(17, 120, 222) 100%
    );
    box-shadow: inset 0 0 10px 5px rgba(101, 97, 97, 0.5);
}
@@ -320,14 +379,15 @@
    transition: background-color 0.3s;
    z-index: 9999;
    background: linear-gradient(180deg,
    background: linear-gradient(
      180deg,
            rgba(60, 53, 239, 0.595),
            rgba(63, 117, 255, 0.714) 70.792%,
            rgb(70, 161, 253) 100%);
      rgb(70, 161, 253) 100%
    );
    // box-shadow: 0px 0px 62rpx rgba(1, 7, 22, 0.468);
    box-shadow: 0 0 10px rgba(0, 0, 0, .3) inset;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) inset;
}
.shadow1 {
@@ -347,8 +407,8 @@
    // box-shadow: 0px 0px 62rpx rgba(1, 7, 22, 0.468);
    box-shadow: 0 0 10px rgba(0, 0, 0, .3) inset;
    -webkit-animation-name: 'ripple1';
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) inset;
    -webkit-animation-name: "ripple1";
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.3s;
    /*动画持续时间*/
@@ -377,8 +437,8 @@
    // box-shadow: 0px 0px 62rpx rgba(1, 7, 22, 0.468);
    box-shadow: 0 0 10px rgba(0, 0, 0, .3) inset;
    -webkit-animation-name: 'ripple2';
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) inset;
    -webkit-animation-name: "ripple2";
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.4s;
    /*动画持续时间*/
@@ -407,8 +467,8 @@
    // box-shadow: 0px 0px 62rpx rgba(1, 7, 22, 0.468);
    box-shadow: 0 0 10px rgba(0, 0, 0, .3) inset;
    -webkit-animation-name: 'ripple3';
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) inset;
    -webkit-animation-name: "ripple3";
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.5s;
    /*动画持续时间*/
@@ -551,17 +611,17 @@
}
.slide-top {
    -webkit-animation: slide-top 1s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
    animation: slide-top 1s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
    -webkit-animation: slide-top 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) both;
    animation: slide-top 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) both;
}
.slide-top1 {
    -webkit-animation: slide-top1 1s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
    animation: slide-top1 1s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
    -webkit-animation: slide-top1 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) both;
    animation: slide-top1 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) both;
}
.scale-in-center {
    animation: scale-in-center 0.4s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
    animation: scale-in-center 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
@-webkit-keyframes slide-top {
@@ -596,7 +656,6 @@
    100% {
        transform: scale(1.4);
        opacity: 0;
    }
}
@@ -608,7 +667,6 @@
    100% {
        transform: scale(1.5);
        opacity: 0;
    }
}
@@ -620,7 +678,6 @@
    100% {
        transform: scale(1.6);
        opacity: 0;
    }
}