Ver código fonte

wuzhen update

wuzhen 1 ano atrás
pai
commit
5f31387fc0

+ 11 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/CheckInGroup.java

@@ -0,0 +1,11 @@
+package org.dromara.zsjy.domain.bo;
+
+/**
+ * 签到验证组
+ * 用于签到时的数据验证,不包含签退相关字段的验证
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+public interface CheckInGroup {
+}

+ 83 - 0
mobile/src/api/internship.js

@@ -0,0 +1,83 @@
+/**
+ * 实习管理API接口
+ * 提供学生实习签到、获取实习信息等功能
+ */
+
+import { request } from '@/common/request.js';
+
+/**
+ * 获取学生实习信息(班主任专用)
+ * @param {number} studentId 学生ID
+ * @returns Promise
+ */
+export const getInternshipInfo = (studentId) => {
+    return request({
+        url: `/zsjy/internship/${studentId}`,
+        method: 'GET'
+    });
+};
+
+/**
+ * 学生获取自己的实习信息(移动端专用)
+ * @returns Promise
+ */
+export const getStudentInternshipInfo = () => {
+    return request({
+        url: '/zsjy/internship/student/info',
+        method: 'GET'
+    });
+};
+
+/**
+ * 学生签到打卡
+ * @param {Object} data 签到数据
+ * @returns Promise
+ */
+export const checkIn = (data) => {
+    return request({
+        url: '/zsjy/internship/daily/checkin',
+        method: 'POST',
+        data
+    });
+};
+
+/**
+ * 获取学生考勤日历数据
+ * @param {number} studentId 学生ID
+ * @param {number} year 年份
+ * @param {number} month 月份
+ * @returns Promise
+ */
+export const getCalendarData = (studentId, year, month) => {
+    return request({
+        url: '/zsjy/internship/daily/calendar',
+        method: 'GET',
+        params: { studentId, year, month }
+    });
+};
+
+/**
+ * 获取考勤统计数据
+ * @param {Object} params 查询参数
+ * @returns Promise
+ */
+export const getStatistics = (params) => {
+    return request({
+        url: '/zsjy/internship/daily/statistics',
+        method: 'GET',
+        params
+    });
+};
+
+/**
+ * 查询实习日常记录列表
+ * @param {Object} params 查询参数
+ * @returns Promise
+ */
+export const getDailyList = (params) => {
+    return request({
+        url: '/zsjy/internship/daily/list',
+        method: 'GET',
+        params
+    });
+}; 

+ 1248 - 0
mobile/src/pages/internship/index.vue

@@ -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>

+ 140 - 0
mobile/src/utils/location.js

@@ -0,0 +1,140 @@
+/**
+ * 定位服务工具类
+ * 提供获取当前位置、地址解析等功能
+ */
+
+/**
+ * 获取当前位置
+ * @returns Promise<{latitude, longitude, address}>
+ */
+export const getCurrentLocation = () => {
+    return new Promise((resolve, reject) => {
+        uni.showLoading({
+            title: '正在获取位置...'
+        });
+
+        uni.getLocation({
+            type: 'gcj02', // 使用国测局坐标系
+            geocode: true, // 解析地址信息
+            success: (res) => {
+                console.log('📍 获取位置成功:', res);
+                
+                const locationData = {
+                    latitude: res.latitude,
+                    longitude: res.longitude,
+                    address: res.address || '位置信息获取中...'
+                };
+
+                // 如果没有地址信息,尝试逆地理编码
+                if (!res.address) {
+                    reverseGeocode(res.latitude, res.longitude)
+                        .then(address => {
+                            locationData.address = address;
+                            resolve(locationData);
+                        })
+                        .catch(() => {
+                            locationData.address = `${res.latitude.toFixed(6)}, ${res.longitude.toFixed(6)}`;
+                            resolve(locationData);
+                        });
+                } else {
+                    resolve(locationData);
+                }
+            },
+            fail: (err) => {
+                console.error('❌ 获取位置失败:', err);
+                
+                let errorMsg = '获取位置失败';
+                if (err.errMsg) {
+                    if (err.errMsg.includes('auth deny')) {
+                        errorMsg = '位置权限被拒绝,请在设置中开启位置权限';
+                    } else if (err.errMsg.includes('locate fail')) {
+                        errorMsg = '定位失败,请检查网络连接和GPS设置';
+                    }
+                }
+                
+                reject(new Error(errorMsg));
+            },
+            complete: () => {
+                uni.hideLoading();
+            }
+        });
+    });
+};
+
+/**
+ * 逆地理编码 - 根据坐标获取地址
+ * @param {number} latitude 纬度
+ * @param {number} longitude 经度
+ * @returns Promise<string>
+ */
+export const reverseGeocode = (latitude, longitude) => {
+    return new Promise((resolve, reject) => {
+        // 这里可以调用第三方地图服务API进行逆地理编码
+        // 暂时返回坐标信息
+        const address = `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`;
+        resolve(address);
+    });
+};
+
+/**
+ * 检查定位权限
+ * @returns Promise<boolean>
+ */
+export const checkLocationPermission = () => {
+    return new Promise((resolve) => {
+        // #ifdef APP-PLUS
+        const permissions = ['android.permission.ACCESS_FINE_LOCATION'];
+        plus.android.requestPermissions(permissions, (result) => {
+            resolve(result.granted && result.granted.length > 0);
+        }, (error) => {
+            console.error('权限检查失败:', error);
+            resolve(false);
+        });
+        // #endif
+        
+        // #ifndef APP-PLUS
+        resolve(true); // 小程序和H5环境默认有权限
+        // #endif
+    });
+};
+
+/**
+ * 打开位置权限设置
+ */
+export const openLocationSettings = () => {
+    uni.showModal({
+        title: '位置权限',
+        content: '需要位置权限才能进行签到,请在设置中开启位置权限',
+        confirmText: '去设置',
+        success: (res) => {
+            if (res.confirm) {
+                // #ifdef APP-PLUS
+                plus.runtime.openURL('app-settings:');
+                // #endif
+                
+                // #ifdef MP-WEIXIN
+                wx.openSetting();
+                // #endif
+            }
+        }
+    });
+};
+
+/**
+ * 计算两点间距离(米)
+ * @param {number} lat1 纬度1
+ * @param {number} lng1 经度1  
+ * @param {number} lat2 纬度2
+ * @param {number} lng2 经度2
+ * @returns {number} 距离(米)
+ */
+export const calculateDistance = (lat1, lng1, lat2, lng2) => {
+    const R = 6371000; // 地球半径(米)
+    const dLat = (lat2 - lat1) * Math.PI / 180;
+    const dLng = (lng2 - lng1) * Math.PI / 180;
+    const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+              Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
+              Math.sin(dLng / 2) * Math.sin(dLng / 2);
+    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+    return R * c;
+};