zhangwencui
4 天以前 67461f55748c32d885db965bdff6c2cc63eb327b
推送cid传值,以及打卡签到模块开发
已添加2个文件
已修改6个文件
3143 ■■■■ 文件已修改
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 529 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages_template/pages/login/index2.vue 1219 ●●●● 补丁 | 查看 | 原始文档 | 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
@@ -48,4 +48,13 @@
    method: 'get',
    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
@@ -1,259 +1,302 @@
<template>
    <view class="normal-login-container">
        <view class="logo-content">
            <text>账号密码登录</text>
        </view>
        <view class="login-form-content">
            <view class="input-item flex align-center">
                <up-input prefixIcon="account" placeholder="请输入账号" border="bottom"
                                    @blur="getUserLoginFacotryList"
                                    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>
            </view>
            <view>
                <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'}"
                label="记住密码"
                name="agree"
                usedAlone
                v-model:checked="rememberPassword"
            >
            </up-checkbox>
        </view>
    </view>
  <view class="normal-login-container">
    <view class="logo-content">
      <text>账号密码登录</text>
    </view>
    <view class="login-form-content">
      <view class="input-item flex align-center">
        <up-input prefixIcon="account"
                  placeholder="请输入账号"
                  border="bottom"
                  @blur="getUserLoginFacotryList"
                  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>
      </view>
      <view>
        <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'}"
                   label="记住密码"
                   name="agree"
                   usedAlone
                   v-model:checked="rememberPassword">
      </up-checkbox>
    </view>
  </view>
</template>
<script setup>
import {modal} from "@/plugins";
  import { modal } from "@/plugins";
const showToast = (message) => {
    uni.showToast({
        title: message,
        icon: 'none'
    })
}
import { userLoginFacotryList} 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 View from "@/pages/procurementManagement/procurementLedger/view.vue";
const userStore = useUserStore()
const useWxLogin = ref(false); // æ˜¯å¦ä½¿ç”¨å¾®ä¿¡ç™»å½•
const rememberPassword = ref(false); // è®°ä½å¯†ç 
const loginForm = ref({
    userName: "",
    password: "",
    currentFatoryName: "",
});
const factoryList = ref([]) // å…¬å¸åˆ—表
  const showToast = message => {
    uni.showToast({
      title: message,
      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 View from "@/pages/procurementManagement/procurementLedger/view.vue";
  const userStore = useUserStore();
  const useWxLogin = ref(false); // æ˜¯å¦ä½¿ç”¨å¾®ä¿¡ç™»å½•
  const rememberPassword = ref(false); // è®°ä½å¯†ç 
  const loginForm = ref({
    userName: "",
    password: "",
    currentFatoryName: "",
  });
  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);
    } else {
        uni.removeStorageSync('remembered_username');
        uni.removeStorageSync('remembered_password');
        uni.setStorageSync('remember_password', false);
    }
}
  // ä¿å­˜å¯†ç åˆ°æœ¬åœ°å­˜å‚¨
  function savePassword() {
    if (rememberPassword.value) {
      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);
    }
  }
// ä»Žæœ¬åœ°å­˜å‚¨åŠ è½½å¯†ç 
function loadPassword() {
    const remembered = uni.getStorageSync('remember_password');
    if (remembered) {
        rememberPassword.value = true;
        const savedUsername = uni.getStorageSync('remembered_username');
        const savedPassword = uni.getStorageSync('remembered_password');
        if (savedUsername) {
            loginForm.value.userName = savedUsername;
        }
        if (savedPassword) {
            loginForm.value.password = savedPassword;
        }
    }
}
  // ä»Žæœ¬åœ°å­˜å‚¨åŠ è½½å¯†ç 
  function loadPassword() {
    const remembered = uni.getStorageSync("remember_password");
    if (remembered) {
      rememberPassword.value = true;
      const savedUsername = uni.getStorageSync("remembered_username");
      const savedPassword = uni.getStorageSync("remembered_password");
      if (savedUsername) {
        loginForm.value.userName = savedUsername;
      }
      if (savedPassword) {
        loginForm.value.password = savedPassword;
      }
    }
  }
if (useWxLogin.value) {
    getWxCode().then(res => {
        console.log(res);
        wxLogin('miniapp',res).then(res => {
            if(res.token != null){
                setToken(res.token);
                loginSuccess()
            }
        });
    })
}
  if (useWxLogin.value) {
    getWxCode().then(res => {
      console.log(res);
      wxLogin("miniapp", res).then(res => {
        if (res.token != null) {
          setToken(res.token);
          loginSuccess();
        }
      });
    });
  }
function getUserLoginFacotryList() {
    if(loginForm.value.userName){
        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
                }))
            } else {
                // å¦‚æžœres.data不是数组,设置为空数组
                factoryList.value = []
            }
        }).catch(error => {
            showToast('获取公司列表失败:', error)
            factoryList.value = []
        })
    }else {
        factoryList.value = []
    }
}
  function getUserLoginFacotryList() {
    if (loginForm.value.userName) {
      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,
            }));
          } else {
            // å¦‚æžœres.data不是数组,设置为空数组
            factoryList.value = [];
          }
        })
        .catch(error => {
          showToast("获取公司列表失败:", error);
          factoryList.value = [];
        });
    } else {
      factoryList.value = [];
    }
  }
async function handleLogin() {
    if (loginForm.value.userName === "") {
        showToast("请输入您的账号")
    } else if (loginForm.value.password === "") {
        showToast("请输入您的密码")
    } else {
        showToast("登录中,请耐心等待...")
        pwdLogin()
    }
};
// å¯†ç ç™»å½•
async function pwdLogin() {
    userStore.loginCheckFactory(loginForm.value).then(() => {
        modal.closeLoading()
        // ç™»å½•成功后保存密码
        savePassword();
        loginSuccess()
    }).catch(() => {
        modal.closeLoading()
    })
};
  async function handleLogin() {
    if (loginForm.value.userName === "") {
      showToast("请输入您的账号");
    } else if (loginForm.value.password === "") {
      showToast("请输入您的密码");
    } else {
      showToast("登录中,请耐心等待...");
      pwdLogin();
    }
  }
  // å¯†ç ç™»å½•
  async function pwdLogin() {
    userStore
      .loginCheckFactory(loginForm.value)
      .then(() => {
        modal.closeLoading();
        // ç™»å½•成功后保存密码
        savePassword();
        loginSuccess();
      })
      .catch(() => {
        modal.closeLoading();
      });
  }
function loginSuccess(result) {
    // è®¾ç½®ç”¨æˆ·ä¿¡æ¯
    userStore.getInfo().then(res => {
        uni.switchTab({
            url: '/pages/index'
        });
    })
}
// é¡µé¢åŠ è½½æ—¶æ£€æŸ¥æ˜¯å¦æœ‰ä¿å­˜çš„å¯†ç 
onMounted(() => {
    loadPassword();
    getUserLoginFacotryList()
});
  function loginSuccess(result) {
    // è®¾ç½®ç”¨æˆ·ä¿¡æ¯
    userStore.getInfo().then(res => {
      // ç™»å½•成功后,将客户端推送标识发送到服务器
      sendClientIdToServer();
      uni.switchTab({
        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();
  });
</script>
<style lang="scss">
page {
    background-color: #ffffff;
}
  page {
    background-color: #ffffff;
  }
.normal-login-container {
    width: 100%;
    height: 100vh;
    .logo-content {
        width: 90%;
        font-weight: 400;
        font-size: 30px;
        color: #333333;
        margin: 80px 0 0 30px;
        image {
            border-radius: 4px;
        }
        .title {
            margin-left: 10px;
        }
    }
    .u-checkbox {
        margin-left: 34px;
    }
    .login-form-content {
        text-align: center;
        margin: 58px auto;
        padding: 0 30px;
        .input-item {
            margin: 30px auto;
            height: 45px;
            .icon {
                font-size: 38rpx;
                margin-left: 10px;
                color: #999;
            }
            .input {
                width: 100%;
                font-size: 14px;
                line-height: 20px;
                text-align: left;
                padding-left: 15px;
            }
        }
        .select-container {
            flex: 1;
            border-bottom: 1px solid #e5e5e5;
            padding: 6px 9px;
            :deep(.up-select) {
                border: none;
                background: transparent;
                .up-select__label {
                    font-size: 14px;
                    color: #333;
                }
                .up-select__value {
                    font-size: 14px;
                    color: #333;
                }
            }
        }
        .login-btn {
            margin-top: 60px;
            height: 50px;
            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;
        }
        .xieyi {
            color: #333;
            margin-top: 20px;
        }
        .login-code {
            height: 38px;
            float: right;
            .login-code-img {
                height: 38px;
                position: absolute;
                margin-left: 10px;
                width: 200rpx;
            }
        }
    }
}
  .normal-login-container {
    width: 100%;
    height: 100vh;
    .logo-content {
      width: 90%;
      font-weight: 400;
      font-size: 30px;
      color: #333333;
      margin: 80px 0 0 30px;
      image {
        border-radius: 4px;
      }
      .title {
        margin-left: 10px;
      }
    }
    .u-checkbox {
      margin-left: 34px;
    }
    .login-form-content {
      text-align: center;
      margin: 58px auto;
      padding: 0 30px;
      .input-item {
        margin: 30px auto;
        height: 45px;
        .icon {
          font-size: 38rpx;
          margin-left: 10px;
          color: #999;
        }
        .input {
          width: 100%;
          font-size: 14px;
          line-height: 20px;
          text-align: left;
          padding-left: 15px;
        }
      }
      .select-container {
        flex: 1;
        border-bottom: 1px solid #e5e5e5;
        padding: 6px 9px;
        :deep(.up-select) {
          border: none;
          background: transparent;
          .up-select__label {
            font-size: 14px;
            color: #333;
          }
          .up-select__value {
            font-size: 14px;
            color: #333;
          }
        }
      }
      .login-btn {
        margin-top: 60px;
        height: 50px;
        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;
      }
      .xieyi {
        color: #333;
        margin-top: 20px;
      }
      .login-code {
        height: 38px;
        float: right;
        .login-code-img {
          height: 38px;
          position: absolute;
          margin-left: 10px;
          width: 200rpx;
        }
      }
    }
  }
</style>
src/pages_template/pages/login/index2.vue
@@ -1,662 +1,719 @@
<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>
            <view class="scale-in-center">
                <view class="logo-content align-center justify-center flex">
                    <text class="title">工作人员入口</text>
                </view>
                <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="请输入账号"
                            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="请输入密码"
                            maxlength="20" />
                    </view>
                    <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="请输入验证码"
                            maxlength="4" />
                        <view class="login-code">
                            <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>
                    </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]">
                        <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"
                                :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;"
                                src="../../../static/uview/common/gray-logo.png"></image>
                            <text class="text-forth" @click="login()">账号密码登录</text>
                        </uni-row>
                    </view>
                </view>
                <view class="sec-masking">
                    <uni-row>
                        <text class="text-sixth">版本号: v1.0</text>
                    </uni-row>
                </view>
            </view>
        </view>
    </view>
  <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>
      <view class="scale-in-center">
        <view class="logo-content align-center justify-center flex">
          <text class="title">工作人员入口</text>
        </view>
        <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="请输入账号"
                   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="请输入密码"
                   maxlength="20" />
          </view>
          <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="请输入验证码"
                   maxlength="4" />
            <view class="login-code">
              <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>
          </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]">
            <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"
                    :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;"
                     src="../../../static/uview/common/gray-logo.png"></image>
              <text class="text-forth"
                    @click="login()">账号密码登录</text>
            </uni-row>
          </view>
        </view>
        <view class="sec-masking">
          <uni-row>
            <text class="text-sixth">版本号: v1.0</text>
          </uni-row>
        </view>
      </view>
    </view>
  </view>
</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 loginForm = reactive({
    username: "admin",
    password: "admin123",
    code: "",
    uuid: ''
})
  const codeUrl = ref("");
  const captchaEnabled = ref(true);
  const loginForm = reactive({
    username: "admin",
    password: "admin123",
    code: "",
    uuid: "",
  });
const collapsedClass = computed(() => {
    return isFixedViewVisible.value ? "" : `collapsed-${animationType.value}`;
})
  const collapsedClass = computed(() => {
    return isFixedViewVisible.value ? "" : `collapsed-${animationType.value}`;
  });
onMounted(() => {
    getCode()
})
  onMounted(() => {
    getCode();
  });
const login = () => {
    play.value[2] = "running"
    setTimeout(() => { page.value = 'login' }, 1000)
}
  const login = () => {
    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"
}
const startplay = () => {
    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)
}
  const back = () => {
    page.value = "index";
    play.value[0] = "paused";
    play.value[1] = "paused";
    play.value[2] = "paused";
  };
  const startplay = () => {
    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);
  };
// èŽ·å–å›¾å½¢éªŒè¯ç 
const getCode = () => {
    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
    }
}
  // èŽ·å–å›¾å½¢éªŒè¯ç 
  const getCode = () => {
    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;
    }
  };
// ç™»å½•方法
const handleLogin = async () => {
    if (loginForm.username === "") {
        modal.msgError("请输入您的账号")
    } else if (loginForm.password === "") {
        modal.msgError("请输入您的密码")
    } else if (loginForm.code === "" && captchaEnabled.value) {
        modal.msgError("请输入验证码")
    } else {
        modal.loading("登录中,请耐心等待...")
        pwdLogin()
    }
}
  // ç™»å½•方法
  const handleLogin = async () => {
    if (loginForm.username === "") {
      modal.msgError("请输入您的账号");
    } else if (loginForm.password === "") {
      modal.msgError("请输入您的密码");
    } else if (loginForm.code === "" && captchaEnabled.value) {
      modal.msgError("请输入验证码");
    } else {
      modal.loading("登录中,请耐心等待...");
      pwdLogin();
    }
  };
// å¯†ç ç™»å½•
const pwdLogin = async () => {
    useUserStore().login(loginForm).then(() => {
        modal.closeLoading()
        loginSuccess()
    }).catch(() => {
        if (captchaEnabled.value) {
            getCode()
        }
    })
}
  // å¯†ç ç™»å½•
  const pwdLogin = async () => {
    useUserStore()
      .login(loginForm)
      .then(() => {
        modal.closeLoading();
        loginSuccess();
      })
      .catch(() => {
        if (captchaEnabled.value) {
          getCode();
        }
      });
  };
// ç™»å½•成功后,处理函数
const loginSuccess = (result) => {
    // è®¾ç½®ç”¨æˆ·ä¿¡æ¯
    useUserStore().getInfo().then(res => {
        tab.reLaunch('/pages/index')
    })
}
  // ç™»å½•成功后,处理函数
  const loginSuccess = result => {
    // è®¾ç½®ç”¨æˆ·ä¿¡æ¯
    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 {
    background-color: #ffffff;
}
  page {
    background-color: #ffffff;
  }
.left {
    display: flex;
    width: 50rpx;
    height: 50rpx;
    position: fixed;
    top: 0px;
    padding: 10rpx;
}
  .left {
    display: flex;
    width: 50rpx;
    height: 50rpx;
    position: fixed;
    top: 0px;
    padding: 10rpx;
  }
.cover {
    position: absolute;
    width: 100%;
    height: 1250rpx;
    z-index: 9997;
}
  .cover {
    position: absolute;
    width: 100%;
    height: 1250rpx;
    z-index: 9997;
  }
.normal-login-container {
    width: 100%;
    margin-top: 200rpx;
  .normal-login-container {
    width: 100%;
    margin-top: 200rpx;
    .logo-content {
        width: 100%;
        font-size: 21px;
        text-align: center;
        padding-top: 15%;
    .logo-content {
      width: 100%;
      font-size: 21px;
      text-align: center;
      padding-top: 15%;
        image {
            border-radius: 4px;
        }
      image {
        border-radius: 4px;
      }
        .title {
            margin-left: 10px;
        }
    }
      .title {
        margin-left: 10px;
      }
    }
    .login-form-content {
        text-align: center;
        margin: 20px auto;
        margin-top: 15%;
        width: 80%;
    .login-form-content {
      text-align: center;
      margin: 20px auto;
      margin-top: 15%;
      width: 80%;
        .input-item {
            margin: 20px auto;
            background-color: #f5f6f7;
            height: 45px;
            border-radius: 20px;
      .input-item {
        margin: 20px auto;
        background-color: #f5f6f7;
        height: 45px;
        border-radius: 20px;
            .icon {
                font-size: 38rpx;
                margin-left: 10px;
                color: #999;
            }
        .icon {
          font-size: 38rpx;
          margin-left: 10px;
          color: #999;
        }
            .input {
                width: 100%;
                font-size: 14px;
                line-height: 20px;
                text-align: left;
                padding-left: 15px;
            }
        .input {
          width: 100%;
          font-size: 14px;
          line-height: 20px;
          text-align: left;
          padding-left: 15px;
        }
      }
        }
      .login-btn {
        margin-top: 40px;
        height: 45px;
      }
        .login-btn {
            margin-top: 40px;
            height: 45px;
        }
      .xieyi {
        color: #333;
        margin-top: 20px;
      }
        .xieyi {
            color: #333;
            margin-top: 20px;
        }
      .login-code {
        height: 38px;
        float: right;
        .login-code {
            height: 38px;
            float: right;
        .login-code-img {
          height: 38px;
          position: absolute;
          margin-left: 10px;
          width: 200rpx;
        }
      }
    }
  }
            .login-code-img {
                height: 38px;
                position: absolute;
                margin-left: 10px;
                width: 200rpx;
            }
        }
    }
}
  .container {
    position: relative;
  }
  .active {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    z-index: 9999;
    background: linear-gradient(
      180deg,
      rgba(110, 109, 122, 0.595),
      rgba(46, 87, 190, 0.714) 70.792%,
      rgb(17, 120, 222) 100%
    );
    box-shadow: inset 0 0 10px 5px rgba(101, 97, 97, 0.5);
  }
  .masking {
    position: absolute;
    top: -200rpx;
    width: 100%;
    height: 1250rpx;
    background-color: #9acafc;
    border-radius: 0 0 400rpx 400rpx;
    box-shadow: 0px 5px 8px rgba(0, 0, 0, 0.27);
    transition: transform 0.3s ease-out;
    z-index: 9998;
    padding: 200rpx 0 0 0;
  }
  .collapsed-up {
    transform: translateY(-100%);
  }
  .button {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    transition: background-color 0.3s;
    z-index: 9999;
    background: linear-gradient(
      180deg,
      rgba(60, 53, 239, 0.595),
      rgba(63, 117, 255, 0.714) 70.792%,
      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, 0.3) inset;
  }
  .shadow1 {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    border-width: 4rpx;
    border-color: rgb(70, 161, 253);
    transition: background-color 0.3s;
    z-index: 9998;
    background-color: rgba(70, 161, 253, 0);
.container {
    position: relative;
}
    // box-shadow: 0px 0px 62rpx rgba(1, 7, 22, 0.468);
.active {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    z-index: 9999;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) inset;
    -webkit-animation-name: "ripple1";
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.3s;
    /*动画持续时间*/
    -webkit-animation-timing-function: ease;
    /*动画频率,和transition-timing-function是一样的*/
    -webkit-animation-delay: 0s;
    /*动画延迟时间*/
    -webkit-animation-direction: normal;
    /*定义动画方式*/
  }
    background: linear-gradient(180deg,
            rgba(110, 109, 122, 0.595),
            rgba(46, 87, 190, 0.714) 70.792%,
            rgb(17, 120, 222) 100%);
    box-shadow: inset 0 0 10px 5px rgba(101, 97, 97, 0.5);
}
  .shadow2 {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    border-width: 4rpx;
    border-color: rgb(70, 161, 253);
    transition: background-color 0.3s;
    z-index: 9998;
    background-color: rgba(70, 161, 253, 0);
.masking {
    position: absolute;
    top: -200rpx;
    width: 100%;
    height: 1250rpx;
    background-color: #9acafc;
    border-radius: 0 0 400rpx 400rpx;
    box-shadow: 0px 5px 8px rgba(0, 0, 0, 0.27);
    transition: transform 0.3s ease-out;
    z-index: 9998;
    padding: 200rpx 0 0 0;
}
    // box-shadow: 0px 0px 62rpx rgba(1, 7, 22, 0.468);
.collapsed-up {
    transform: translateY(-100%);
}
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) inset;
    -webkit-animation-name: "ripple2";
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.4s;
    /*动画持续时间*/
    -webkit-animation-timing-function: ease;
    /*动画频率,和transition-timing-function是一样的*/
    -webkit-animation-delay: 0.1s;
    /*动画延迟时间*/
    -webkit-animation-direction: normal;
    /*定义动画方式*/
  }
.button {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    transition: background-color 0.3s;
    z-index: 9999;
  .shadow3 {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    border-width: 4rpx;
    border-color: rgb(70, 161, 253);
    transition: background-color 0.3s;
    z-index: 9998;
    background-color: rgba(70, 161, 253, 0);
    background: linear-gradient(180deg,
            rgba(60, 53, 239, 0.595),
            rgba(63, 117, 255, 0.714) 70.792%,
            rgb(70, 161, 253) 100%);
    // box-shadow: 0px 0px 62rpx rgba(1, 7, 22, 0.468);
    // 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;
    -webkit-animation-name: "ripple3";
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.5s;
    /*动画持续时间*/
    -webkit-animation-timing-function: ease;
    /*动画频率,和transition-timing-function是一样的*/
    -webkit-animation-delay: 0.2s;
    /*动画延迟时间*/
    -webkit-animation-direction: normal;
    /*定义动画方式*/
  }
}
  .button.disabled {
    pointer-events: none;
    /* ç¦ç”¨æŒ‰é’®çš„点击事件 */
    opacity: 0.5;
    /* åŠé€æ˜Žæ•ˆæžœ */
  }
.shadow1 {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    border-width: 4rpx;
    border-color: rgb(70, 161, 253);
    transition: background-color 0.3s;
    z-index: 9998;
    background-color: rgba(70, 161, 253, 0);
  .text-first {
    position: absolute;
    width: 256rpx;
    height: 84rpx;
    left: 50%;
    transform: translateX(-50%);
    top: 250rpx;
    bottom: 986.67rpx;
    // box-shadow: 0px 0px 62rpx rgba(1, 7, 22, 0.468);
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 64rpx;
    font-weight: 400;
    line-height: 84rpx;
    letter-spacing: 0rpx;
    text-align: center;
  }
    box-shadow: 0 0 10px rgba(0, 0, 0, .3) inset;
    -webkit-animation-name: 'ripple1';
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.3s;
    /*动画持续时间*/
    -webkit-animation-timing-function: ease;
    /*动画频率,和transition-timing-function是一样的*/
    -webkit-animation-delay: 0s;
    /*动画延迟时间*/
    -webkit-animation-direction: normal;
    /*定义动画方式*/
}
  .text-second {
    position: absolute;
    width: 256rpx;
    height: 84rpx;
    left: 50%;
    transform: translateX(-50%);
    top: 350rpx;
    bottom: 986.67rpx;
.shadow2 {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    border-width: 4rpx;
    border-color: rgb(70, 161, 253);
    transition: background-color 0.3s;
    z-index: 9998;
    background-color: rgba(70, 161, 253, 0);
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 64rpx;
    font-weight: 400;
    line-height: 84rpx;
    letter-spacing: 0rpx;
    text-align: center;
  }
    // 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';
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.4s;
    /*动画持续时间*/
    -webkit-animation-timing-function: ease;
    /*动画频率,和transition-timing-function是一样的*/
    -webkit-animation-delay: 0.1s;
    /*动画延迟时间*/
    -webkit-animation-direction: normal;
    /*定义动画方式*/
}
.shadow3 {
    position: absolute;
    width: 280rpx;
    height: 280rpx;
    left: 50%;
    margin-left: -140rpx;
    top: 533rpx;
    bottom: 533rpx;
    border-radius: 50%;
    border-width: 4rpx;
    border-color: rgb(70, 161, 253);
    transition: background-color 0.3s;
    z-index: 9998;
    background-color: rgba(70, 161, 253, 0);
    // 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';
    /*动画属性名,也就是我们前面keyframes定义的动画名*/
    -webkit-animation-duration: 0.5s;
    /*动画持续时间*/
    -webkit-animation-timing-function: ease;
    /*动画频率,和transition-timing-function是一样的*/
    -webkit-animation-delay: 0.2s;
    /*动画延迟时间*/
    -webkit-animation-direction: normal;
    /*定义动画方式*/
}
.button.disabled {
    pointer-events: none;
    /* ç¦ç”¨æŒ‰é’®çš„点击事件 */
    opacity: 0.5;
    /* åŠé€æ˜Žæ•ˆæžœ */
}
.text-first {
    position: absolute;
    width: 256rpx;
    height: 84rpx;
    left: 50%;
    transform: translateX(-50%);
    top: 250rpx;
    bottom: 986.67rpx;
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 64rpx;
    font-weight: 400;
    line-height: 84rpx;
    letter-spacing: 0rpx;
    text-align: center;
}
.text-second {
    position: absolute;
    width: 256rpx;
    height: 84rpx;
    left: 50%;
    transform: translateX(-50%);
    top: 350rpx;
    bottom: 986.67rpx;
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 64rpx;
    font-weight: 400;
    line-height: 84rpx;
    letter-spacing: 0rpx;
    text-align: center;
}
.text-third {
    position: absolute;
    width: 200rpx;
    height: 60rpx;
    left: 50%;
    transform: translateX(-50%);
    top: 450rpx;
    bottom: 615rpx;
    margin: 0 auto;
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 50rpx;
    font-weight: 400;
    line-height: 60rpx;
    letter-spacing: 0rpx;
    text-align: center;
}
.text-forth {
    position: absolute;
    width: 180rpx;
    height: 36rpx;
    left: 0;
    right: 0;
    top: 948rpx;
    bottom: 330rpx;
    margin: 0 auto;
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 27rpx;
    font-weight: 400;
    line-height: 36rpx;
    letter-spacing: 0rpx;
    text-align: center;
    text-decoration-line: underline;
}
  .text-third {
    position: absolute;
    width: 200rpx;
    height: 60rpx;
    left: 50%;
    transform: translateX(-50%);
    top: 450rpx;
    bottom: 615rpx;
    margin: 0 auto;
.text-fifth {
    position: absolute;
    width: 120rpx;
    height: 150rpx;
    left: 0;
    right: 0;
    top: 60rpx;
    bottom: 418rpx;
    margin: 0 auto;
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 50rpx;
    font-weight: 400;
    line-height: 60rpx;
    letter-spacing: 0rpx;
    text-align: center;
  }
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 60rpx;
    font-weight: 400;
    line-height: 80rpx;
    letter-spacing: 0rpx;
    text-align: center;
}
  .text-forth {
    position: absolute;
    width: 180rpx;
    height: 36rpx;
    left: 0;
    right: 0;
    top: 948rpx;
    bottom: 330rpx;
    margin: 0 auto;
.text-sixth {
    position: absolute;
    width: 100rpx;
    height: 17rpx;
    margin: 0 auto;
    top: 710rpx;
    left: 50%;
    transform: translateX(-50%);
    padding: 0 179rpx 7rpx 180rpx;
    z-index: 9997;
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 27rpx;
    font-weight: 400;
    line-height: 36rpx;
    letter-spacing: 0rpx;
    text-align: center;
    text-decoration-line: underline;
  }
    color: rgb(15, 15, 15);
    font-family: "Microsoft YaHei";
    font-size: 17rpx;
    font-weight: 400;
    line-height: 17rpx;
    letter-spacing: 0;
    text-align: left;
}
  .text-fifth {
    position: absolute;
    width: 120rpx;
    height: 150rpx;
    left: 0;
    right: 0;
    top: 60rpx;
    bottom: 418rpx;
    margin: 0 auto;
.sec-masking {
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 750rpx;
    background-color: #ffffff;
    z-index: 9996;
}
    color: rgb(255, 255, 255);
    font-family: "Microsoft YaHei";
    font-size: 60rpx;
    font-weight: 400;
    line-height: 80rpx;
    letter-spacing: 0rpx;
    text-align: center;
  }
.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;
}
  .text-sixth {
    position: absolute;
    width: 100rpx;
    height: 17rpx;
    margin: 0 auto;
    top: 710rpx;
    left: 50%;
    transform: translateX(-50%);
    padding: 0 179rpx 7rpx 180rpx;
    z-index: 9997;
.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;
}
    color: rgb(15, 15, 15);
    font-family: "Microsoft YaHei";
    font-size: 17rpx;
    font-weight: 400;
    line-height: 17rpx;
    letter-spacing: 0;
    text-align: left;
  }
.scale-in-center {
    animation: scale-in-center 0.4s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
  .sec-masking {
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 750rpx;
    background-color: #ffffff;
    z-index: 9996;
  }
@-webkit-keyframes slide-top {
    0% {
        -webkit-transform: translateY(0);
        transform: translateY(0);
    }
  .slide-top {
    -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;
  }
    100% {
        -webkit-transform: translateY(-1050px);
        transform: translateY(-1050px);
    }
}
  .slide-top1 {
    -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;
  }
@-webkit-keyframes slide-top1 {
    0% {
        -webkit-transform: translateY(0);
        transform: translateY(0);
    }
  .scale-in-center {
    animation: scale-in-center 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
  }
    100% {
        -webkit-transform: translateY(-1050px);
        transform: translateY(-1050px);
    }
}
  @-webkit-keyframes slide-top {
    0% {
      -webkit-transform: translateY(0);
      transform: translateY(0);
    }
@keyframes ripple1 {
    0% {
        transform: scale(1);
    }
    100% {
      -webkit-transform: translateY(-1050px);
      transform: translateY(-1050px);
    }
  }
    100% {
        transform: scale(1.4);
        opacity: 0;
  @-webkit-keyframes slide-top1 {
    0% {
      -webkit-transform: translateY(0);
      transform: translateY(0);
    }
    }
}
    100% {
      -webkit-transform: translateY(-1050px);
      transform: translateY(-1050px);
    }
  }
@keyframes ripple2 {
    0% {
        transform: scale(1);
    }
  @keyframes ripple1 {
    0% {
      transform: scale(1);
    }
    100% {
        transform: scale(1.5);
        opacity: 0;
    100% {
      transform: scale(1.4);
      opacity: 0;
    }
  }
    }
}
  @keyframes ripple2 {
    0% {
      transform: scale(1);
    }
@keyframes ripple3 {
    0% {
        transform: scale(1);
    }
    100% {
      transform: scale(1.5);
      opacity: 0;
    }
  }
    100% {
        transform: scale(1.6);
        opacity: 0;
  @keyframes ripple3 {
    0% {
      transform: scale(1);
    }
    }
}
    100% {
      transform: scale(1.6);
      opacity: 0;
    }
  }
@keyframes slide-top {
    0% {
        -webkit-transform: translateY(0);
        transform: translateY(0);
    }
  @keyframes slide-top {
    0% {
      -webkit-transform: translateY(0);
      transform: translateY(0);
    }
    100% {
        -webkit-transform: translateY(-1050px);
        transform: translateY(-1050px);
    }
}
    100% {
      -webkit-transform: translateY(-1050px);
      transform: translateY(-1050px);
    }
  }
@keyframes slide-top1 {
    0% {
        -webkit-transform: translateY(0);
        transform: translateY(0);
    }
  @keyframes slide-top1 {
    0% {
      -webkit-transform: translateY(0);
      transform: translateY(0);
    }
    100% {
        -webkit-transform: translateY(-1050px);
        transform: translateY(-1050px);
    }
}
    100% {
      -webkit-transform: translateY(-1050px);
      transform: translateY(-1050px);
    }
  }
@keyframes scale-in-center {
    0% {
        transform: scale(0);
        opacity: 1;
    }
  @keyframes scale-in-center {
    0% {
      transform: scale(0);
      opacity: 1;
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}
    100% {
      transform: scale(1);
      opacity: 1;
    }
  }
</style>