diff --git a/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/CheckInGroup.java b/backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/CheckInGroup.java new file mode 100644 index 00000000..023010f5 --- /dev/null +++ b/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 { +} diff --git a/mobile/src/api/internship.js b/mobile/src/api/internship.js new file mode 100644 index 00000000..53a12c31 --- /dev/null +++ b/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 + }); +}; \ No newline at end of file diff --git a/mobile/src/pages/internship/index.vue b/mobile/src/pages/internship/index.vue new file mode 100644 index 00000000..ca7c5b41 --- /dev/null +++ b/mobile/src/pages/internship/index.vue @@ -0,0 +1,1248 @@ + + + + + diff --git a/mobile/src/utils/location.js b/mobile/src/utils/location.js new file mode 100644 index 00000000..7b85a7ed --- /dev/null +++ b/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 + */ +export const reverseGeocode = (latitude, longitude) => { + return new Promise((resolve, reject) => { + // 这里可以调用第三方地图服务API进行逆地理编码 + // 暂时返回坐标信息 + const address = `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`; + resolve(address); + }); +}; + +/** + * 检查定位权限 + * @returns Promise + */ +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; +}; \ No newline at end of file