|
|
@@ -0,0 +1,1248 @@
|
|
|
+<template>
|
|
|
+ <view class="internship-page">
|
|
|
+ <!-- 顶部用户信息卡片 -->
|
|
|
+ <view class="user-card">
|
|
|
+ <view class="user-info">
|
|
|
+ <view class="avatar-section">
|
|
|
+ <u-avatar
|
|
|
+ :src="userInfo.avatar || '/static/default-avatar.png'"
|
|
|
+ size="60"
|
|
|
+ shape="circle"
|
|
|
+ ></u-avatar>
|
|
|
+ </view>
|
|
|
+ <view class="info-section">
|
|
|
+ <view class="user-name">{{ userInfo.nickName || userInfo.userName || '学生' }}</view>
|
|
|
+ <view class="user-detail">学号:{{ userInfo.userName || '暂无' }}</view>
|
|
|
+ <view class="user-detail">部门:{{ userInfo.deptName || '暂无' }}</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 实习信息卡片 -->
|
|
|
+ <view class="internship-card" v-if="internshipInfo">
|
|
|
+ <view class="card-header">
|
|
|
+ <u-icon name="bookmark" size="18" color="#409eff"></u-icon>
|
|
|
+ <text class="card-title">实习信息</text>
|
|
|
+ </view>
|
|
|
+ <view class="internship-info">
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="label">实习单位:</text>
|
|
|
+ <text class="value">{{ internshipInfo.companyName || '暂无' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="label">实习岗位:</text>
|
|
|
+ <text class="value">{{ internshipInfo.internshipPosition || '暂无' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="label">实习期间:</text>
|
|
|
+ <text class="value">{{ formatDateRange(internshipInfo.internshipStartDate, internshipInfo.internshipEndDate) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="label">状态:</text>
|
|
|
+ <u-tag :text="getStatusText(internshipInfo.internshipStatus)" :type="getStatusType(internshipInfo.internshipStatus)" size="mini"></u-tag>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 今日签到卡片 -->
|
|
|
+ <view class="checkin-card">
|
|
|
+ <view class="card-header">
|
|
|
+ <u-icon name="clock" size="18" color="#67c23a"></u-icon>
|
|
|
+ <text class="card-title">今日签到</text>
|
|
|
+ <text class="date-text">{{ currentDate }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 签到状态显示 -->
|
|
|
+ <view class="checkin-status" v-if="todayRecord">
|
|
|
+ <view class="status-item">
|
|
|
+ <u-icon name="checkmark-circle" size="20" color="#67c23a"></u-icon>
|
|
|
+ <text class="status-text">已签到</text>
|
|
|
+ </view>
|
|
|
+ <view class="time-info">
|
|
|
+ <text class="time-label">签到时间:</text>
|
|
|
+ <text class="time-value">{{ todayRecord.checkInTime }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="location-info" v-if="todayRecord.checkInLocation">
|
|
|
+ <text class="location-label">签到位置:</text>
|
|
|
+ <text class="location-value">{{ todayRecord.checkInLocation }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 签到按钮 -->
|
|
|
+ <view class="checkin-actions" v-else>
|
|
|
+ <u-button
|
|
|
+ type="primary"
|
|
|
+ size="large"
|
|
|
+ :loading="checkinLoading"
|
|
|
+ :disabled="checkinLoading || !canCheckin"
|
|
|
+ @click="handleCheckin"
|
|
|
+ :custom-style="checkinButtonStyle"
|
|
|
+ >
|
|
|
+ <u-icon name="map" size="20" color="#fff" style="margin-right: 8px;"></u-icon>
|
|
|
+ {{ checkinLoading ? '签到中...' : '立即签到' }}
|
|
|
+ </u-button>
|
|
|
+
|
|
|
+ <view class="checkin-tips" v-if="!canCheckin">
|
|
|
+ <u-icon name="info-circle" size="14" color="#909399"></u-icon>
|
|
|
+ <text class="tips-text">{{ checkinTips }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 位置信息卡片 -->
|
|
|
+ <view class="location-card" v-if="currentLocation">
|
|
|
+ <view class="card-header">
|
|
|
+ <u-icon name="map-pin" size="18" color="#f56c6c"></u-icon>
|
|
|
+ <text class="card-title">当前位置</text>
|
|
|
+ <u-button
|
|
|
+ type="info"
|
|
|
+ size="mini"
|
|
|
+ text="重新定位"
|
|
|
+ @click="refreshLocation"
|
|
|
+ :loading="locationLoading"
|
|
|
+ ></u-button>
|
|
|
+ </view>
|
|
|
+ <view class="location-info">
|
|
|
+ <view class="location-text">{{ currentLocation.address }}</view>
|
|
|
+ <view class="coordinates">
|
|
|
+ <text class="coord-text">经度:{{ currentLocation.longitude.toFixed(6) }}</text>
|
|
|
+ <text class="coord-text">纬度:{{ currentLocation.latitude.toFixed(6) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="location-accuracy" v-if="currentLocation.accuracy">
|
|
|
+ <text class="accuracy-text">定位精度:{{ currentLocation.accuracy.toFixed(0) }}米</text>
|
|
|
+ <u-tag
|
|
|
+ :text="getAccuracyStatus(currentLocation.accuracy).text"
|
|
|
+ :type="getAccuracyStatus(currentLocation.accuracy).type"
|
|
|
+ size="mini"
|
|
|
+ style="margin-left: 8px;"
|
|
|
+ ></u-tag>
|
|
|
+ </view>
|
|
|
+ <view class="location-actions">
|
|
|
+ <u-button
|
|
|
+ type="primary"
|
|
|
+ size="mini"
|
|
|
+ plain
|
|
|
+ @click="openLocationInMap"
|
|
|
+ style="margin-top: 8px;"
|
|
|
+ >
|
|
|
+ <u-icon name="map" size="16" color="#2979ff" />
|
|
|
+ <text style="margin-left: 4px;">在地图中查看</text>
|
|
|
+ </u-button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 位置获取失败提示 -->
|
|
|
+ <view class="location-error-card" v-else-if="locationError">
|
|
|
+ <view class="card-header">
|
|
|
+ <u-icon name="map-pin" size="18" color="#f56c6c"></u-icon>
|
|
|
+ <text class="card-title">位置信息</text>
|
|
|
+ <u-button
|
|
|
+ type="primary"
|
|
|
+ size="mini"
|
|
|
+ text="重新定位"
|
|
|
+ @click="refreshLocation"
|
|
|
+ :loading="locationLoading"
|
|
|
+ ></u-button>
|
|
|
+ </view>
|
|
|
+ <view class="error-content">
|
|
|
+ <u-icon name="warning" size="24" color="#e6a23c"></u-icon>
|
|
|
+ <text class="error-text">{{ locationError }}</text>
|
|
|
+ <text class="error-tips">签到需要位置信息,请确保:</text>
|
|
|
+ <text class="error-tips">• 已开启位置权限</text>
|
|
|
+ <text class="error-tips">• GPS/网络定位已开启</text>
|
|
|
+ <text class="error-tips">• 网络连接正常</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 考勤统计 -->
|
|
|
+ <view class="statistics-card">
|
|
|
+ <view class="card-header">
|
|
|
+ <u-icon name="bar-chart" size="18" color="#e6a23c"></u-icon>
|
|
|
+ <text class="card-title">本月考勤</text>
|
|
|
+ </view>
|
|
|
+ <view class="stats-grid">
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-number">{{ monthStats.totalDays || 0 }}</text>
|
|
|
+ <text class="stat-label">应签到</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-number">{{ monthStats.checkedDays || 0 }}</text>
|
|
|
+ <text class="stat-label">已签到</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-number">{{ monthStats.missedDays || 0 }}</text>
|
|
|
+ <text class="stat-label">未签到</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-number">{{ monthStats.exceptionDays || 0 }}</text>
|
|
|
+ <text class="stat-label">异常</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 最近签到记录 -->
|
|
|
+ <view class="records-card">
|
|
|
+ <view class="card-header">
|
|
|
+ <u-icon name="list" size="18" color="#909399"></u-icon>
|
|
|
+ <text class="card-title">最近签到</text>
|
|
|
+ <u-button
|
|
|
+ type="info"
|
|
|
+ size="mini"
|
|
|
+ text="查看更多"
|
|
|
+ @click="viewMoreRecords"
|
|
|
+ ></u-button>
|
|
|
+ </view>
|
|
|
+ <view class="records-list" v-if="recentRecords.length > 0">
|
|
|
+ <view class="record-item" v-for="record in recentRecords" :key="record.dailyId">
|
|
|
+ <view class="record-date">{{ formatDate(record.checkDate) }}</view>
|
|
|
+ <view class="record-time">{{ record.checkInTime || '未签到' }}</view>
|
|
|
+ <view class="record-status">
|
|
|
+ <u-tag
|
|
|
+ :text="record.checkInTime ? '已签到' : '未签到'"
|
|
|
+ :type="record.checkInTime ? 'success' : 'error'"
|
|
|
+ size="mini"
|
|
|
+ ></u-tag>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="empty-records" v-else>
|
|
|
+ <u-icon name="inbox" size="40" color="#c0c4cc"></u-icon>
|
|
|
+ <text class="empty-text">暂无签到记录</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getInternshipInfo, getStudentInternshipInfo, checkIn, getDailyList, getStatistics } from '@/api/internship.js';
|
|
|
+import { getCurrentLocation, checkLocationPermission, openLocationSettings } from '@/utils/location.js';
|
|
|
+import { userManager } from '@/utils/user.js';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'InternshipIndex',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ // 用户信息
|
|
|
+ userInfo: {},
|
|
|
+
|
|
|
+ // 实习信息
|
|
|
+ internshipInfo: null,
|
|
|
+
|
|
|
+ // 签到相关
|
|
|
+ checkinLoading: false,
|
|
|
+ canCheckin: true,
|
|
|
+ checkinTips: '',
|
|
|
+ todayRecord: null,
|
|
|
+
|
|
|
+ // 位置信息
|
|
|
+ currentLocation: null,
|
|
|
+ locationLoading: false,
|
|
|
+ locationError: '',
|
|
|
+
|
|
|
+ // 统计数据
|
|
|
+ monthStats: {
|
|
|
+ totalDays: 0,
|
|
|
+ checkedDays: 0,
|
|
|
+ missedDays: 0,
|
|
|
+ exceptionDays: 0
|
|
|
+ },
|
|
|
+
|
|
|
+ // 签到记录
|
|
|
+ recentRecords: [],
|
|
|
+
|
|
|
+ // 当前日期
|
|
|
+ currentDate: '',
|
|
|
+
|
|
|
+ // 样式
|
|
|
+ checkinButtonStyle: {
|
|
|
+ width: '100%',
|
|
|
+ height: '50px',
|
|
|
+ borderRadius: '25px',
|
|
|
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
+ border: 'none',
|
|
|
+ fontSize: '16px',
|
|
|
+ fontWeight: 'bold'
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ onLoad() {
|
|
|
+ this.initPage();
|
|
|
+ },
|
|
|
+
|
|
|
+ onShow() {
|
|
|
+ // 页面显示时刷新数据
|
|
|
+ this.loadTodayRecord();
|
|
|
+ this.loadStatistics();
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 初始化页面
|
|
|
+ */
|
|
|
+ async initPage() {
|
|
|
+ try {
|
|
|
+ // 设置当前日期
|
|
|
+ this.currentDate = this.formatDate(new Date());
|
|
|
+
|
|
|
+ // 获取用户信息
|
|
|
+ this.userInfo = userManager.getCurrentUser() || {};
|
|
|
+
|
|
|
+ if (!this.userInfo.userId) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请先登录',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并行加载数据
|
|
|
+ await Promise.all([
|
|
|
+ this.loadInternshipInfo(),
|
|
|
+ this.loadTodayRecord(),
|
|
|
+ this.loadRecentRecords(),
|
|
|
+ this.loadStatistics(),
|
|
|
+ this.initLocation()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('初始化页面失败:', error);
|
|
|
+ uni.showToast({
|
|
|
+ title: '数据加载失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载实习信息
|
|
|
+ */
|
|
|
+ async loadInternshipInfo() {
|
|
|
+ try {
|
|
|
+ console.log('🔍 开始加载学生实习信息...');
|
|
|
+ console.log('👤 当前用户信息:', this.userInfo);
|
|
|
+
|
|
|
+ const response = await getStudentInternshipInfo(); // 使用新的学生专用API
|
|
|
+ console.log('📡 API原始响应:', JSON.stringify(response, null, 2));
|
|
|
+
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.internshipInfo = response.data;
|
|
|
+ console.log('✅ 实习信息获取成功:', JSON.stringify(this.internshipInfo, null, 2));
|
|
|
+
|
|
|
+ // 详细字段检查
|
|
|
+ if (this.internshipInfo) {
|
|
|
+ console.log('🏢 实习单位:', this.internshipInfo.companyName);
|
|
|
+ console.log('💼 实习岗位:', this.internshipInfo.internshipPosition);
|
|
|
+ console.log('📅 开始时间:', this.internshipInfo.internshipStartDate);
|
|
|
+ console.log('📅 结束时间:', this.internshipInfo.internshipEndDate);
|
|
|
+ console.log('📊 状态:', this.internshipInfo.internshipStatus);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ 实习信息获取失败:', response.msg);
|
|
|
+ this.showInternshipError(response.msg || '获取实习信息失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 加载实习信息失败:', error);
|
|
|
+
|
|
|
+ // 根据错误类型显示不同信息
|
|
|
+ if (error.response) {
|
|
|
+ if (error.response.status === 403) {
|
|
|
+ this.showInternshipError('暂无权限查看实习信息,请联系管理员');
|
|
|
+ } else if (error.response.status === 401) {
|
|
|
+ this.showInternshipError('登录已失效,请重新登录');
|
|
|
+ } else {
|
|
|
+ this.showInternshipError(`服务器错误:${error.response.status}`);
|
|
|
+ }
|
|
|
+ } else if (error.request) {
|
|
|
+ this.showInternshipError('网络连接失败,请检查网络');
|
|
|
+ } else {
|
|
|
+ this.showInternshipError('请求发送失败,请重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 显示实习信息错误提示
|
|
|
+ */
|
|
|
+ showInternshipError(message) {
|
|
|
+ console.log('🚨 显示错误提示:', message);
|
|
|
+ uni.showToast({
|
|
|
+ title: message,
|
|
|
+ icon: 'none',
|
|
|
+ duration: 3000
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载今日签到记录
|
|
|
+ */
|
|
|
+ async loadTodayRecord() {
|
|
|
+ try {
|
|
|
+ const today = new Date();
|
|
|
+ const params = {
|
|
|
+ studentId: this.userInfo.userId,
|
|
|
+ checkDate: this.formatDate(today),
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 1
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await getDailyList(params);
|
|
|
+ if (response.code === 200 && response.rows && response.rows.length > 0) {
|
|
|
+ this.todayRecord = response.rows[0];
|
|
|
+ this.canCheckin = false;
|
|
|
+ this.checkinTips = '今日已签到';
|
|
|
+ } else {
|
|
|
+ this.todayRecord = null;
|
|
|
+ this.canCheckin = true;
|
|
|
+ this.checkinTips = '';
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载今日记录失败:', error);
|
|
|
+ this.canCheckin = true;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载最近签到记录
|
|
|
+ */
|
|
|
+ async loadRecentRecords() {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ studentId: this.userInfo.userId,
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 5
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await getDailyList(params);
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.recentRecords = response.rows || [];
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载签到记录失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载统计数据
|
|
|
+ */
|
|
|
+ async loadStatistics() {
|
|
|
+ try {
|
|
|
+ const now = new Date();
|
|
|
+ const params = {
|
|
|
+ studentId: this.userInfo.userId,
|
|
|
+ year: now.getFullYear(),
|
|
|
+ month: now.getMonth() + 1
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await getStatistics(params);
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.monthStats = response.data || this.monthStats;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载统计数据失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化位置信息
|
|
|
+ */
|
|
|
+ async initLocation() {
|
|
|
+ try {
|
|
|
+ // 检查位置权限
|
|
|
+ const hasPermission = await checkLocationPermission();
|
|
|
+ if (!hasPermission) {
|
|
|
+ this.canCheckin = false;
|
|
|
+ this.checkinTips = '需要位置权限才能签到';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取当前位置
|
|
|
+ await this.getCurrentLocationInfo();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('初始化位置失败:', error);
|
|
|
+ this.canCheckin = false;
|
|
|
+ this.checkinTips = '无法获取位置信息';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查是否为H5环境
|
|
|
+ */
|
|
|
+ isH5Web() {
|
|
|
+ try {
|
|
|
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
|
+ } catch (_) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查浏览器是否支持地理定位
|
|
|
+ */
|
|
|
+ isBrowserGeolocationSupported() {
|
|
|
+ return this.isH5Web() && !!navigator.geolocation;
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查是否为微信浏览器
|
|
|
+ */
|
|
|
+ isWechatBrowser() {
|
|
|
+ if (typeof navigator === 'undefined') return false;
|
|
|
+ const ua = navigator.userAgent.toLowerCase();
|
|
|
+ return ua.includes('micromessenger');
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前位置信息 - 增强版多重定位方案
|
|
|
+ */
|
|
|
+ async getCurrentLocationInfo() {
|
|
|
+ this.locationLoading = true;
|
|
|
+ this.locationError = '';
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 方案1: 微信定位(微信浏览器环境)
|
|
|
+ if (this.isWechatBrowser()) {
|
|
|
+ try {
|
|
|
+ uni.showToast({
|
|
|
+ title: '正在微信定位...',
|
|
|
+ icon: 'loading',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+ const wechatLocation = await this.getWechatLocation();
|
|
|
+ if (wechatLocation) {
|
|
|
+ this.currentLocation = wechatLocation;
|
|
|
+ this.updateLocationSuccess();
|
|
|
+ uni.showToast({
|
|
|
+ title: '微信定位成功',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 1500
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.log('微信定位失败,尝试其他方案:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方案2: 浏览器HTML5定位(H5环境)
|
|
|
+ if (this.isH5Web() && this.isBrowserGeolocationSupported()) {
|
|
|
+ try {
|
|
|
+ uni.showToast({
|
|
|
+ title: '正在浏览器定位...',
|
|
|
+ icon: 'loading',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+ const browserLocation = await this.getBrowserLocation();
|
|
|
+ if (browserLocation) {
|
|
|
+ this.currentLocation = browserLocation;
|
|
|
+ this.updateLocationSuccess();
|
|
|
+ uni.showToast({
|
|
|
+ title: '浏览器定位成功',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 1500
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.log('浏览器定位失败,尝试其他方案:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方案3: uni-app定位(通用回退方案)
|
|
|
+ try {
|
|
|
+ uni.showToast({
|
|
|
+ title: '正在系统定位...',
|
|
|
+ icon: 'loading',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+ const uniLocation = await this.getUniappLocation();
|
|
|
+ if (uniLocation) {
|
|
|
+ this.currentLocation = uniLocation;
|
|
|
+ this.updateLocationSuccess();
|
|
|
+ uni.showToast({
|
|
|
+ title: '定位成功',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 1500
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.log('uni-app定位失败:', error);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 所有定位方案都失败
|
|
|
+ this.updateLocationError('定位失败,请检查网络连接和位置权限');
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取位置失败:', error);
|
|
|
+ this.updateLocationError(error.message || '获取位置失败');
|
|
|
+ } finally {
|
|
|
+ this.locationLoading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信定位
|
|
|
+ */
|
|
|
+ async getWechatLocation() {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ // 检查微信环境
|
|
|
+ if (!this.isWechatBrowser()) {
|
|
|
+ reject(new Error('不在微信环境中'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 微信JSSDK定位
|
|
|
+ if (typeof wx !== 'undefined' && wx.getLocation) {
|
|
|
+ wx.getLocation({
|
|
|
+ type: 'gcj02',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.latitude && res.longitude) {
|
|
|
+ resolve({
|
|
|
+ latitude: res.latitude,
|
|
|
+ longitude: res.longitude,
|
|
|
+ accuracy: res.accuracy || 50,
|
|
|
+ address: `${res.latitude.toFixed(6)}, ${res.longitude.toFixed(6)}`
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ reject(new Error('微信定位返回数据无效'));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (error) => {
|
|
|
+ console.error('微信定位失败:', error);
|
|
|
+ reject(new Error('微信定位失败'));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ reject(new Error('微信JSSDK未初始化'));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 浏览器HTML5定位
|
|
|
+ */
|
|
|
+ async getBrowserLocation() {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ navigator.geolocation.getCurrentPosition(
|
|
|
+ (position) => {
|
|
|
+ const { latitude, longitude, accuracy } = position.coords;
|
|
|
+ resolve({
|
|
|
+ latitude: latitude,
|
|
|
+ longitude: longitude,
|
|
|
+ accuracy: accuracy || 100,
|
|
|
+ address: `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`
|
|
|
+ });
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ let errorMsg = '浏览器定位失败';
|
|
|
+ switch (error.code) {
|
|
|
+ case error.PERMISSION_DENIED:
|
|
|
+ errorMsg = '位置权限被拒绝';
|
|
|
+ break;
|
|
|
+ case error.POSITION_UNAVAILABLE:
|
|
|
+ errorMsg = '位置信息不可用';
|
|
|
+ break;
|
|
|
+ case error.TIMEOUT:
|
|
|
+ errorMsg = '定位超时';
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ reject(new Error(errorMsg));
|
|
|
+ },
|
|
|
+ {
|
|
|
+ enableHighAccuracy: true,
|
|
|
+ timeout: 15000,
|
|
|
+ maximumAge: 60000
|
|
|
+ }
|
|
|
+ );
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * uni-app定位
|
|
|
+ */
|
|
|
+ async getUniappLocation() {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.getLocation({
|
|
|
+ type: 'gcj02',
|
|
|
+ geocode: true,
|
|
|
+ success: (res) => {
|
|
|
+ resolve({
|
|
|
+ latitude: res.latitude,
|
|
|
+ longitude: res.longitude,
|
|
|
+ accuracy: res.accuracy || 100,
|
|
|
+ address: res.address || `${res.latitude.toFixed(6)}, ${res.longitude.toFixed(6)}`
|
|
|
+ });
|
|
|
+ },
|
|
|
+ fail: (error) => {
|
|
|
+ let errorMsg = 'uni-app定位失败';
|
|
|
+ if (error.errMsg) {
|
|
|
+ if (error.errMsg.includes('auth deny')) {
|
|
|
+ errorMsg = '位置权限被拒绝';
|
|
|
+ } else if (error.errMsg.includes('locate fail')) {
|
|
|
+ errorMsg = '定位失败,请检查GPS设置';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ reject(new Error(errorMsg));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新定位成功状态
|
|
|
+ */
|
|
|
+ updateLocationSuccess() {
|
|
|
+ if (!this.todayRecord) {
|
|
|
+ this.canCheckin = true;
|
|
|
+ this.checkinTips = '';
|
|
|
+ }
|
|
|
+ this.locationError = '';
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新定位失败状态
|
|
|
+ */
|
|
|
+ updateLocationError(message) {
|
|
|
+ this.currentLocation = null;
|
|
|
+ this.locationError = message;
|
|
|
+ if (!this.todayRecord) {
|
|
|
+ this.canCheckin = false;
|
|
|
+ this.checkinTips = message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新位置
|
|
|
+ */
|
|
|
+ refreshLocation() {
|
|
|
+ this.getCurrentLocationInfo();
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 打开位置信息在地图中查看
|
|
|
+ */
|
|
|
+ openLocationInMap() {
|
|
|
+ if (!this.currentLocation) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '无法获取位置信息',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { latitude, longitude } = this.currentLocation;
|
|
|
+ const name = this.currentLocation.address || '当前位置';
|
|
|
+
|
|
|
+ // 使用uni.openLocation打开系统地图
|
|
|
+ uni.openLocation({
|
|
|
+ latitude: latitude,
|
|
|
+ longitude: longitude,
|
|
|
+ name: name,
|
|
|
+ address: this.currentLocation.address || `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`,
|
|
|
+ success: () => {
|
|
|
+ console.log('打开地图成功');
|
|
|
+ },
|
|
|
+ fail: (error) => {
|
|
|
+ console.error('打开地图失败:', error);
|
|
|
+ uni.showToast({
|
|
|
+ title: '打开地图失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理签到
|
|
|
+ */
|
|
|
+ async handleCheckin() {
|
|
|
+ if (!this.canCheckin || this.checkinLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查实习信息
|
|
|
+ if (!this.internshipInfo) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '暂无实习信息',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查位置信息
|
|
|
+ if (!this.currentLocation) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '位置信息',
|
|
|
+ content: '需要获取位置信息才能签到,是否重新获取?',
|
|
|
+ confirmText: '重新获取',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ this.initLocation();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查定位精度
|
|
|
+ if (this.currentLocation.accuracy && this.currentLocation.accuracy > 500) {
|
|
|
+ const confirmResult = await new Promise((resolve) => {
|
|
|
+ uni.showModal({
|
|
|
+ title: '定位精度提醒',
|
|
|
+ content: `当前定位精度较低(${this.currentLocation.accuracy.toFixed(0)}米),可能影响签到准确性,是否继续签到?`,
|
|
|
+ confirmText: '继续签到',
|
|
|
+ cancelText: '重新定位',
|
|
|
+ success: (res) => resolve(res.confirm),
|
|
|
+ fail: () => resolve(false)
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!confirmResult) {
|
|
|
+ // 用户选择重新定位
|
|
|
+ this.refreshLocation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.checkinLoading = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 构建签到数据 - 使用后端兼容的字段名
|
|
|
+ const checkinData = {
|
|
|
+ internshipId: this.internshipInfo.internshipId, // 新增必填字段
|
|
|
+ studentId: this.userInfo.userId,
|
|
|
+ checkDate: new Date().toISOString().split('T')[0], // 格式化为 yyyy-MM-dd
|
|
|
+ checkInTime: new Date().toTimeString().split(' ')[0], // 格式化为 HH:mm:ss
|
|
|
+
|
|
|
+ // 使用兼容字段名(后端Service会自动映射到正确字段)
|
|
|
+ checkInLocation: this.currentLocation.address, // 后端映射为 checkOutLocation
|
|
|
+ latitude: this.currentLocation.latitude, // 后端映射为 checkOutLatitude
|
|
|
+ longitude: this.currentLocation.longitude, // 后端映射为 checkOutLongitude
|
|
|
+ dailySummary: '移动端签到打卡', // 后端映射为 workContent
|
|
|
+
|
|
|
+ status: '1', // 正常状态
|
|
|
+ isException: '0' // 正常,无异常
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('🔍 签到前检查:', {
|
|
|
+ internshipInfo: this.internshipInfo,
|
|
|
+ userInfo: this.userInfo,
|
|
|
+ currentLocation: this.currentLocation,
|
|
|
+ canCheckin: this.canCheckin
|
|
|
+ });
|
|
|
+ console.log('📤 发送签到数据:', JSON.stringify(checkinData, null, 2));
|
|
|
+
|
|
|
+ const response = await checkIn(checkinData);
|
|
|
+
|
|
|
+ if (response.code === 200) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '签到成功',
|
|
|
+ icon: 'success'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 刷新今日记录
|
|
|
+ await this.loadTodayRecord();
|
|
|
+ await this.loadRecentRecords();
|
|
|
+ await this.loadStatistics();
|
|
|
+
|
|
|
+ } else {
|
|
|
+ throw new Error(response.msg || '签到失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 签到详细错误:', {
|
|
|
+ message: error.message,
|
|
|
+ response: error.response,
|
|
|
+ stack: error.stack
|
|
|
+ });
|
|
|
+ uni.showToast({
|
|
|
+ title: error.message || '签到失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ this.checkinLoading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查看更多记录
|
|
|
+ */
|
|
|
+ viewMoreRecords() {
|
|
|
+ // 可以跳转到详细记录页面
|
|
|
+ uni.showToast({
|
|
|
+ title: '功能开发中',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化日期
|
|
|
+ */
|
|
|
+ formatDate(date) {
|
|
|
+ if (!date) return '';
|
|
|
+ const d = new Date(date);
|
|
|
+ const year = d.getFullYear();
|
|
|
+ const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(d.getDate()).padStart(2, '0');
|
|
|
+ return `${year}-${month}-${day}`;
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化日期范围
|
|
|
+ */
|
|
|
+ formatDateRange(startDate, endDate) {
|
|
|
+ if (!startDate || !endDate) return '暂无';
|
|
|
+ return `${this.formatDate(startDate)} 至 ${this.formatDate(endDate)}`;
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取状态文本
|
|
|
+ */
|
|
|
+ getStatusText(status) {
|
|
|
+ const statusMap = {
|
|
|
+ '1': '进行中',
|
|
|
+ '2': '已完成',
|
|
|
+ '3': '已暂停',
|
|
|
+ '4': '已终止'
|
|
|
+ };
|
|
|
+ return statusMap[status] || '未实习';
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取状态类型
|
|
|
+ */
|
|
|
+ getStatusType(status) {
|
|
|
+ const typeMap = {
|
|
|
+ '1': 'success', // 进行中
|
|
|
+ '2': 'info', // 已完成
|
|
|
+ '3': 'warning', // 已暂停
|
|
|
+ '4': 'error' // 已终止
|
|
|
+ };
|
|
|
+ return typeMap[status] || 'default';
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取定位精度状态
|
|
|
+ */
|
|
|
+ getAccuracyStatus(accuracy) {
|
|
|
+ if (accuracy < 100) {
|
|
|
+ return { text: '高精度', type: 'success' };
|
|
|
+ } else if (accuracy < 500) {
|
|
|
+ return { text: '中精度', type: 'info' };
|
|
|
+ } else {
|
|
|
+ return { text: '低精度', type: 'warning' };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.internship-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 用户信息卡片 */
|
|
|
+.user-card {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 20px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ color: white;
|
|
|
+ box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.user-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-section {
|
|
|
+ margin-right: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-section {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.user-name {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.user-detail {
|
|
|
+ font-size: 14px;
|
|
|
+ opacity: 0.9;
|
|
|
+ margin-bottom: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 通用卡片样式 */
|
|
|
+.internship-card,
|
|
|
+.checkin-card,
|
|
|
+.location-card,
|
|
|
+.statistics-card,
|
|
|
+.records-card {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.card-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-left: 8px;
|
|
|
+ flex: 1;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.date-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+/* 实习信息 */
|
|
|
+.internship-info {
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ width: 80px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.value {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* 签到状态 */
|
|
|
+.checkin-status {
|
|
|
+ padding: 16px;
|
|
|
+ background: #f0f9ff;
|
|
|
+ border-radius: 8px;
|
|
|
+ border-left: 4px solid #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.status-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-text {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #67c23a;
|
|
|
+ margin-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.time-info,
|
|
|
+.location-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.time-label,
|
|
|
+.location-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.time-value,
|
|
|
+.location-value {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* 签到操作 */
|
|
|
+.checkin-actions {
|
|
|
+ padding: 16px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.checkin-tips {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-top: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.tips-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-left: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 位置信息 */
|
|
|
+.location-info {
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.location-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.coordinates {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.coord-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.location-accuracy {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 8px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.accuracy-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.location-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+/* 位置获取失败提示 */
|
|
|
+.location-error-card {
|
|
|
+ background: #fffbe6;
|
|
|
+ border: 1px solid #ffe58f;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.error-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding-top: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #faad14;
|
|
|
+ margin-top: 8px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.error-tips {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #faad14;
|
|
|
+ margin-top: 4px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 统计数据 */
|
|
|
+.stats-grid {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-item {
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
+ padding: 12px 8px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin: 0 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-number {
|
|
|
+ display: block;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #409eff;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+/* 签到记录 */
|
|
|
+.records-list {
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.record-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.record-item:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.record-date {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ width: 100px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.record-time {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ flex: 1;
|
|
|
+ margin-left: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.record-status {
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 空状态 */
|
|
|
+.empty-records {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 32px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+</style>
|