From 102882e3fdef6074c9cb58348a71216dffd4f4d3 Mon Sep 17 00:00:00 2001 From: hb Date: Sat, 2 Aug 2025 10:47:16 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9D=83=E9=99=90=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/base/notice.ts | 17 ++ src/pages/base/home/index.vue | 85 ++++++- src/pages/system/launchPage/launchPage.vue | 43 ++-- src/pages/system/login/login.vue | 12 + src/store/modules/user.ts | 8 + src/utils/permission.ts | 253 ++++++++++++++++++++- 6 files changed, 393 insertions(+), 25 deletions(-) create mode 100644 src/api/base/notice.ts diff --git a/src/api/base/notice.ts b/src/api/base/notice.ts new file mode 100644 index 0000000..2ab7eed --- /dev/null +++ b/src/api/base/notice.ts @@ -0,0 +1,17 @@ +import { get } from "@/utils/request"; + +// 通知公告分页查询参数接口 +export interface NoticePageParams { + page: number; + rows: number; + appCode: string; + fbfw?: string; + xqId?: string; + fbNjmcId?: string; + releaseFlag?: string; +} + +// 获取通知公告列表 +export function getNoticeListApi(params: NoticePageParams) { + return get("/api/cms/article/list", params); +} \ No newline at end of file diff --git a/src/pages/base/home/index.vue b/src/pages/base/home/index.vue index c71de15..716f418 100644 --- a/src/pages/base/home/index.vue +++ b/src/pages/base/home/index.vue @@ -27,6 +27,7 @@ @@ -79,8 +80,11 @@ import { ref, computed, onMounted, watch } from "vue"; import { onShow } from "@dcloudio/uni-app"; import XsPicker from "@/pages/base/components/XsPicker/index.vue" import { cmsArticlePageApi, getUserLatestInfoApi } from "@/api/base/server"; +import { getNoticeListApi } from "@/api/base/notice"; import { useUserStore } from "@/store/modules/user"; import { useDataStore } from "@/store/modules/data"; +import { hasPermission } from "@/utils/permission"; + const { getCurXs } = useUserStore(); const { setData, getAppCode } = useDataStore(); @@ -88,6 +92,37 @@ const { setData, getAppCode } = useDataStore(); const { getLastRefreshTime, getRefreshInterval, setLastRefreshTime, updateStudentInfo, updateStudentList } = useUserStore(); const REFRESH_INTERVAL = 10 * 60 * 1000; // 10分钟刷新一次(测试用) +// 获取当前changeTime +const getCurrentChangeTime = () => { + try { + const userDataStr = uni.getStorageSync('app-user'); + if (!userDataStr) return null; + + const userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr; + return userData?.changeTime || null; + } catch (error) { + console.error('获取changeTime失败:', error); + return null; + } +}; + +// 检查权限(带changeTime) +const checkPermission = (permissionKey: string) => { + const changeTime = getCurrentChangeTime(); + return hasPermission(permissionKey, changeTime); +}; + +// 直接权限检查函数,避免缓存问题 +const hasPermissionDirect = (permissionKey: string) => { + if (!permissionKey) return true; + const userStore = useUserStore(); + const permissions = userStore.getAuth; + if (!permissions || permissions.length === 0) return false; + + const uniquePermissions = [...new Set(permissions)]; + return uniquePermissions.includes(permissionKey); +}; + // 检查是否需要刷新学生信息 const checkAndRefreshStudentInfo = async () => { const lastRefreshTime = getLastRefreshTime; @@ -135,37 +170,44 @@ const menuItems = ref([ title: "班级课表", icon: "/static/base/home/book-read-line.png", path: "/pages/base/class-schedule/index", + permissionKey: "school-bjkb", // 班级课表权限编码 }, { title: "成绩查询", icon: "/static/base/home/file-search-line.png", path: "/pages/base/grades/list", + permissionKey: "school-cjcx", // 成绩查询权限编码 }, { title: "在线请假", icon: "/static/base/home/draft-line.png", path: "/pages/base/leave-request/index", + permissionKey: "school-zxqj", // 在线请假权限编码 }, // TODO:需求待协商硬件对接 // { // title: "进出校园", // icon: "/static/base/home/file-transfer-line.png", // path: "/pages/base/campus-access/index", + // permissionKey: "school-jcxy", // 进出校园权限编码 // }, { title: "家校沟通", icon: "/static/base/home/file-transfer-line.png", path: "/pages/base/jl/index", + permissionKey: "school-jxgt", // 家校沟通权限编码 }, { title: "兴趣课", icon: "/static/base/home/file-text-line.png", path: "/pages/base/interest-class/index", + permissionKey: "school-xqk", // 兴趣课权限编码 }, { title: "俱乐部", icon: "/static/base/home/contacts-book-3-line.png", path: "/pages/base/club/index", + permissionKey: "school-jlb", // 俱乐部权限编码 }, ]); @@ -229,13 +271,21 @@ function goToDetail(notice: any) { const getArticleList = async () => { if (curXs.value && curXs.value.njmcId) { - const params = Object.assign({}, pageParams.value, { njmcId: curXs.value.njmcId }); + const params = { + page: pageParams.value.page, + rows: pageParams.value.rows, + appCode: getAppCode, + fbfw: 'JZ', // 发布范围:家长端 + xqId: curXs.value.xqId || curXs.value.xq_id, // 学生所在学期 + fbNjmcId: curXs.value.njmcId, // 学生所在年级 + releaseFlag: 'A' // 发布状态:有效 + }; - cmsArticlePageApi(params).then(res => { - announcements.value = res.rows; - }) - .catch((error) => { - // 接口调用失败 + getNoticeListApi(params).then(res => { + announcements.value = res.rows; + }) + .catch((error) => { + // 接口调用失败 }); } }; @@ -255,6 +305,29 @@ onMounted(async () => { // 初始化时检查是否需要刷新学生信息 await checkAndRefreshStudentInfo(); + + // 检查权限缓存,确保权限数据是最新的 + const userStore = useUserStore(); + const changeTime = userStore.getChangeTime; + if (changeTime) { + // 如果有changeTime,检查是否需要刷新权限 + const { PermissionCacheManager } = await import('@/utils/permission'); + const cacheInfo = PermissionCacheManager.getCacheInfo(); + + if (cacheInfo.hasCache && cacheInfo.changeTime) { + const serverTime = new Date(changeTime).getTime(); + const cacheTime = new Date(cacheInfo.changeTime).getTime(); + + if (serverTime > cacheTime) { + // 服务器时间更新,刷新权限缓存 + const { refreshPermissionCache } = await import('@/utils/permission'); + const currentPermissions = userStore.getAuth; + if (currentPermissions && currentPermissions.length > 0) { + refreshPermissionCache(currentPermissions, changeTime); + } + } + } + } }); // 页面显示时检查是否需要刷新 diff --git a/src/pages/system/launchPage/launchPage.vue b/src/pages/system/launchPage/launchPage.vue index 28b0259..3c6a767 100644 --- a/src/pages/system/launchPage/launchPage.vue +++ b/src/pages/system/launchPage/launchPage.vue @@ -16,6 +16,7 @@ import {onLoad} from "@dcloudio/uni-app"; import {useDataStore} from "@/store/modules/data"; import {useUserStore} from "@/store/modules/user"; import {checkOpenId} from "@/api/system/login"; +import {refreshPermissionCache} from "@/utils/permission"; const { setGlobal } = useDataStore(); const { afterLoginAction } = useUserStore(); @@ -36,24 +37,34 @@ function toHome(data: any) { onLoad(async (data: any) => { setGlobal(data); if (data && data.openId) { - checkOpenId({ openId: data.openId, appCode: "JZ" }) - .then(async (res) => { - if (res.resultCode == 1) { - if (res.result) { - afterLoginAction(res.result); - toHome(data); - return; + try { + const res = await checkOpenId({ openId: data.openId, appCode: "JZ" }); + + if (res.resultCode == 1 && res.result) { + // 执行登录操作 + afterLoginAction(res.result); + + // 如果有changeTime参数,更新权限缓存 + if (data.changeTime) { + const userStore = useUserStore(); + const currentPermissions = userStore.getAuth; + + if (currentPermissions && currentPermissions.length > 0) { + refreshPermissionCache(currentPermissions, data.changeTime); } } - uni.reLaunch({ - url: "/pages/system/login/login", - }); - }) - .catch((err) => { - uni.reLaunch({ - url: "/pages/system/login/login", - }); - }); + + toHome(data); + } else { + uni.reLaunch({ + url: "/pages/system/login/login", + }); + } + } catch (err) { + uni.reLaunch({ + url: "/pages/system/login/login", + }); + } } else { uni.reLaunch({ url: "/pages/system/login/login", diff --git a/src/pages/system/login/login.vue b/src/pages/system/login/login.vue index 3cc65e7..34ecff5 100644 --- a/src/pages/system/login/login.vue +++ b/src/pages/system/login/login.vue @@ -124,6 +124,7 @@ import { loginRegisterJzApi } from "@/api/base/server"; import { useUserStore } from "@/store/modules/user"; import { useDataStore } from "@/store/modules/data"; import {imagUrl} from "@/utils"; +import {refreshPermissionCache} from "@/utils/permission"; const dicOptions = ref([[[]]]); const dicPickerRef = ref(); @@ -305,6 +306,17 @@ async function submit() { hideLoading(); if (res.resultCode == 1) { afterLoginAction(res.result); + + // 如果有changeTime参数,更新权限缓存 + if (res.result && res.result.changeTime) { + const userStore = useUserStore(); + const currentPermissions = userStore.getAuth; + + if (currentPermissions && currentPermissions.length > 0) { + refreshPermissionCache(currentPermissions, res.result.changeTime); + } + } + toHome(); } else { showToast({ title: res.message || "提交失败", icon: "none" }); diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 980b095..18fb319 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -15,6 +15,7 @@ interface UserState { auth: string[]; lastRefreshTime: number; // 上次刷新时间 refreshInterval: number; // 刷新间隔(毫秒) + changeTime: string; // 权限变更时间 ws: any; wsCallback: any; } @@ -32,6 +33,7 @@ export const useUserStore = defineStore({ auth: [], lastRefreshTime: 0, // 上次刷新时间 refreshInterval: 7 * 24 * 60 * 60 * 1000, // 刷新间隔(毫秒) + changeTime: '', // 权限变更时间 ws: null, wsCallback: defWsCallback }), @@ -53,6 +55,9 @@ export const useUserStore = defineStore({ }, getRefreshInterval(): number { return this.refreshInterval; + }, + getChangeTime(): string { + return this.changeTime; } }, actions: { @@ -74,6 +79,9 @@ export const useUserStore = defineStore({ setRefreshInterval(interval: number) { this.refreshInterval = interval; }, + setChangeTime(changeTime: string) { + this.changeTime = changeTime; + }, // 更新学生信息 updateStudentInfo(studentInfo: any) { this.setCurXs(studentInfo); diff --git a/src/utils/permission.ts b/src/utils/permission.ts index 9d61774..c7d5ce5 100644 --- a/src/utils/permission.ts +++ b/src/utils/permission.ts @@ -2,11 +2,258 @@ import {ISROUTERINTERCEPT} from "@/config"; import {getRouter} from "@/utils/uniapp"; import {useUserStore} from "@/store/modules/user"; -export function _auth(autd: string) { - const {getAuth} = useUserStore() - return getAuth.includes(autd); +// 权限缓存接口 +interface PermissionCache { + permissions: string[]; + timestamp: number; + userId: string; + changeTime: string; } +// 存储键名 +const PERMISSION_CACHE_KEY = 'permission_cache'; +const CHANGE_TIME_KEY = 'change_time'; + +// 存储操作 +function setStorage(key: string, value: any): void { + try { + const jsonValue = JSON.stringify(value); + uni.setStorageSync(key, jsonValue); + } catch (error) { + console.error('存储权限缓存失败:', error); + } +} + +function getStorage(key: string): any { + try { + const value = uni.getStorageSync(key); + + if (value) { + const parsedValue = JSON.parse(value); + return parsedValue; + } else { + return null; + } + } catch (error) { + console.error('获取权限缓存失败:', error); + return null; + } +} + +// 从app-user获取changeTime +export function getChangeTimeFromAppUser(): string | null { + try { + const userDataStr = uni.getStorageSync('app-user'); + if (!userDataStr) return null; + + const userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr; + return userData?.changeTime || null; + } catch (error) { + console.error('获取changeTime失败:', error); + return null; + } +} + +// 设置changeTime到app-user +function setChangeTimeToAppUser(changeTime: string): void { + try { + const userDataStr = uni.getStorageSync('app-user'); + if (!userDataStr) return; + + const userData = typeof userDataStr === 'string' ? JSON.parse(userDataStr) : userDataStr; + userData.changeTime = changeTime; + uni.setStorageSync('app-user', JSON.stringify(userData)); + } catch (error) { + console.error('设置changeTime失败:', error); + } +} + +// 获取权限缓存 +function getPermissionCache(): PermissionCache | null { + return getStorage(PERMISSION_CACHE_KEY); +} + +// 设置权限缓存 +function setPermissionCache(permissions: string[], userId: string, changeTime?: string): void { + const cache: PermissionCache = { + permissions, + timestamp: Date.now(), + userId, + changeTime: changeTime || '' + }; + + setStorage(PERMISSION_CACHE_KEY, cache); + + if (changeTime) { + setChangeTimeToAppUser(changeTime); + } +} + +// 清除权限缓存 +function clearPermissionCache(): void { + try { + uni.removeStorageSync(PERMISSION_CACHE_KEY); + } catch (error) { + console.error('清除权限缓存失败:', error); + } +} + +// 检查缓存是否有效 +function isCacheValid(cache: PermissionCache, currentUserId: string): boolean { + if (!cache || !cache.permissions || cache.userId !== currentUserId) { + return false; + } + + // 检查缓存是否过期(7天) + const now = Date.now(); + const cacheAge = now - cache.timestamp; + const maxAge = 7 * 24 * 60 * 60 * 1000; // 7天 + + return cacheAge < maxAge; +} + +// 获取用户权限(带缓存) +function getUserPermissionsWithCache(currentChangeTime?: string): string[] { + const userStore = useUserStore(); + const currentUser = userStore.getUser; + const currentUserId = currentUser?.userId || currentUser?.id; + + if (!currentUserId) { + return userStore.getAuth; + } + + const cache = getPermissionCache(); + + if (cache && isCacheValid(cache, currentUserId)) { + if (currentChangeTime) { + const serverTime = new Date(currentChangeTime).getTime(); + const cacheTime = new Date(cache.changeTime).getTime(); + + if (serverTime > cacheTime) { + const permissions = userStore.getAuth; + if (permissions && permissions.length > 0) { + setPermissionCache(permissions, currentUserId, currentChangeTime); + } + return permissions; + } else { + return cache.permissions; + } + } else { + return cache.permissions; + } + } + + const permissions = userStore.getAuth; + if (permissions && permissions.length > 0) { + setPermissionCache(permissions, currentUserId, currentChangeTime); + } + + return permissions; +} + +// 刷新权限缓存 +export function refreshPermissionCache(permissions?: string[], changeTime?: string): void { + const userStore = useUserStore(); + const currentUser = userStore.getUser; + const currentUserId = currentUser?.userId || currentUser?.id; + + if (!currentUserId) { + return; + } + + const permissionList = permissions || userStore.getAuth; + + const currentCache = getPermissionCache(); + if (currentCache && currentCache.permissions && permissionList) { + const isSame = JSON.stringify(currentCache.permissions.sort()) === JSON.stringify(permissionList.sort()); + if (isSame && currentCache.changeTime === changeTime) { + return; + } + } + + setPermissionCache(permissionList, currentUserId, changeTime); + + if (changeTime) { + userStore.setChangeTime(changeTime); + } +} + +// 清除权限缓存(供外部调用) +export function clearPermissionCachePublic(): void { + clearPermissionCache(); +} + +// 权限检查函数 +export function _auth(autd: string, changeTime?: string) { + const permissions = getUserPermissionsWithCache(changeTime); + return permissions.includes(autd); +} + +export function hasPermission(permissionKey: string, changeTime?: string): boolean { + if (!permissionKey) return true; + const permissions = getUserPermissionsWithCache(changeTime); + // 去重处理,避免重复权限影响判断 + const uniquePermissions = [...new Set(permissions)]; + return uniquePermissions.includes(permissionKey); +} + +export function hasAnyPermission(permissionKeys: string[], changeTime?: string): boolean { + if (!permissionKeys || permissionKeys.length === 0) return true; + const permissions = getUserPermissionsWithCache(changeTime); + // 去重处理,避免重复权限影响判断 + const uniquePermissions = [...new Set(permissions)]; + return permissionKeys.some(key => uniquePermissions.includes(key)); +} + +export function hasAllPermissions(permissionKeys: string[], changeTime?: string): boolean { + if (!permissionKeys || permissionKeys.length === 0) return true; + const permissions = getUserPermissionsWithCache(changeTime); + // 去重处理,避免重复权限影响判断 + const uniquePermissions = [...new Set(permissions)]; + return permissionKeys.every(key => uniquePermissions.includes(key)); +} + +// 获取用户权限 +export function getUserPermissions(changeTime?: string): string[] { + return getUserPermissionsWithCache(changeTime); +} + +// 权限缓存管理器 +export const PermissionCacheManager = { + getCacheInfo() { + const cache = getPermissionCache(); + const changeTime = getChangeTimeFromAppUser(); + return { + cache, + changeTime, + hasCache: !!cache, + cacheAge: cache ? Date.now() - cache.timestamp : 0 + }; + }, + + debugCache() { + const info = this.getCacheInfo(); + console.log('权限缓存信息:', info); + }, + + forceRefresh() { + clearPermissionCache(); + const userStore = useUserStore(); + const permissions = userStore.getAuth; + if (permissions && permissions.length > 0) { + const currentUser = userStore.getUser; + const currentUserId = currentUser?.userId || currentUser?.id; + if (currentUserId) { + setPermissionCache(permissions, currentUserId); + } + } + }, + + clear() { + clearPermissionCache(); + } +}; + function loginPage(url: string) { uni.redirectTo({ url: "/pages/system/login/login?redirect=" + url, From a1bce6f7b50213c9843f2da50d08657d9b9202be Mon Sep 17 00:00:00 2001 From: hb Date: Mon, 4 Aug 2025 15:23:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=9D=83=E9=99=90=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/system/login/index.ts | 8 + src/pages.json | 24 ++ .../base/components/XkCountdown/index.vue | 152 +++++---- src/pages/base/components/XkPicker/index.vue | 7 - src/pages/base/components/XkkcList/index.vue | 43 ++- src/pages/base/components/XsPicker/index.vue | 18 +- .../base/course-selection/club-selection.vue | 87 +++++- src/pages/base/course-selection/detail.vue | 2 +- .../course-selection/enrollment-ended.vue | 291 ++++++++++++++++++ src/pages/base/course-selection/index.vue | 75 ++++- src/pages/base/course-selection/notice.vue | 45 ++- .../base/course-selection/noticeclub.vue | 73 +++++ src/pages/base/home/index.vue | 49 ++- src/pages/system/launchPage/launchPage.vue | 1 + src/pages/system/login/login.vue | 2 +- src/pages/system/subscribe/index.vue | 194 ++++++++++++ src/static/base/home/details.svg | 1 + src/static/base/home/rdcode.jpg | Bin 0 -> 9602 bytes 18 files changed, 963 insertions(+), 109 deletions(-) create mode 100644 src/pages/base/course-selection/enrollment-ended.vue create mode 100644 src/pages/base/course-selection/noticeclub.vue create mode 100644 src/pages/system/subscribe/index.vue create mode 100644 src/static/base/home/details.svg create mode 100644 src/static/base/home/rdcode.jpg diff --git a/src/api/system/login/index.ts b/src/api/system/login/index.ts index 704e17c..c6e7164 100644 --- a/src/api/system/login/index.ts +++ b/src/api/system/login/index.ts @@ -63,6 +63,14 @@ export const checkOpenId = async (param: { export const updateUserApi = async (param: any) => { return await post("/open/login/js/updateUser", param); }; + +export const updateSignFileApi = async (param: { userId: number; signFile: string }) => { + return await post("/api/user/updateSignFile", { + id: param.userId, + signFile: param.signFile + }); +}; + export const findJsByPhoneApi = async (param: any) => { return await get("/api/js/findJsByPhone", param); }; diff --git a/src/pages.json b/src/pages.json index dcc8157..100bf55 100644 --- a/src/pages.json +++ b/src/pages.json @@ -57,6 +57,15 @@ } } }, + { + "path": "pages/system/subscribe/index", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "关注服务号", + "enablePullDownRefresh": false, + "backgroundColor": "#f4f5f7" + } + }, { "path": "pages/system/webView/webView", "style": { @@ -242,6 +251,13 @@ "enablePullDownRefresh": false } }, + { + "path": "pages/base/course-selection/noticeclub", + "style": { + "navigationBarTitleText": "告知书", + "enablePullDownRefresh": false + } + }, { "path": "pages/base/course-selection/enrolled", "style": { @@ -254,6 +270,14 @@ "navigationBarTitleText": "未开放", "enablePullDownRefresh": false } + }, + { + "path": "pages/base/course-selection/enrollment-ended", + "style": { + "navigationBarTitleText": "选课已结束", + "enablePullDownRefresh": false, + "navigationStyle": "custom" + } } ], "globalStyle": { diff --git a/src/pages/base/components/XkCountdown/index.vue b/src/pages/base/components/XkCountdown/index.vue index 0ec2630..03f50a0 100644 --- a/src/pages/base/components/XkCountdown/index.vue +++ b/src/pages/base/components/XkCountdown/index.vue @@ -1,7 +1,6 @@ @@ -48,17 +58,20 @@ const countdownTime = reactive({ const kcStatus = ref(false); -// 倒计时标题文本 -const countdownTitle = computed(() => { - return kcStatus.value ? "距离选课结束还剩" : "距离选课开始还剩"; -}); - // 当前选中的学生 let countdownTimer: number | null = null; const remainTime = ref("00:00:00"); // 添加选课是否已结束的标记 const isEnrollmentEnded = ref(false); +// 添加选课是否已结束的标记(区分选课开始和选课结束) +const isCourseEnded = ref(false); + +// 格式化结束时间 +const formatEndTime = computed(() => { + if (!props.xk || !props.xk.xkjstime) return ''; + return dayjs(props.xk.xkjstime).format('YYYY-MM-DD'); +}); // 启动倒计时 const startCountdown = (endTimeStamp: number) => { @@ -82,26 +95,10 @@ const startCountdown = (endTimeStamp: number) => { countdownTimer = null; } - // 判断当前倒计时是选课开始还是选课结束 - if (!kcStatus.value) { - // 如果是选课开始倒计时结束,则切换到选课结束倒计时 - kcStatus.value = true; // 更新状态为选课已开始 - uni.showToast({ - title: "选课已开始", - icon: "none", - }); - // 开始选课结束倒计时 - const xkjstime = dayjs(props.xk.xkjstime).valueOf(); - startCountdown(xkjstime); - } else { - // 如果是选课结束倒计时结束 - isEnrollmentEnded.value = true; // 标记选课已结束 - emit("over"); - uni.showToast({ - title: "选课已结束", - icon: "none", - }); - } + // 选课开始倒计时结束 + isEnrollmentEnded.value = true; // 标记选课已开始,隐藏倒计时 + isCourseEnded.value = false; // 标记选课未结束 + emit("over"); return; } @@ -134,22 +131,29 @@ const changeXk = (xk: any) => { //获取选课结束时间 const xkjstime = dayjs(xk.xkjstime).valueOf(); - // 检查是否已经超过选课结束时间 - if (now > xkjstime) { - kcStatus.value = true; + // 如果当前时间 >= 选课结束时间,则显示选课已结束 + if (now >= xkjstime) { isEnrollmentEnded.value = true; + isCourseEnded.value = true; countdownTime.hours = "00"; countdownTime.minutes = "00"; countdownTime.seconds = "00"; + return; } - //判断选课是否开始 - else if (now < xkkstime) { - kcStatus.value = false; - startCountdown(xkkstime); - } else { - kcStatus.value = true; - startCountdown(xkjstime); + + // 如果当前时间 >= 选课开始时间 且 < 选课结束时间,显示报名提示 + if (now >= xkkstime && now < xkjstime) { + isEnrollmentEnded.value = true; + isCourseEnded.value = false; + countdownTime.hours = "00"; + countdownTime.minutes = "00"; + countdownTime.seconds = "00"; + return; } + + // 只有当前时间 < 选课开始时间时才显示倒计时 + kcStatus.value = false; + startCountdown(xkkstime); } // 页面卸载前清除定时器 @@ -174,38 +178,37 @@ if (props.xk && props.xk.id) { \ No newline at end of file diff --git a/src/pages/base/components/XkPicker/index.vue b/src/pages/base/components/XkPicker/index.vue index 94063ea..ad09b08 100644 --- a/src/pages/base/components/XkPicker/index.vue +++ b/src/pages/base/components/XkPicker/index.vue @@ -117,13 +117,6 @@ const loadXkList = async () => { const switchXk = (xk: any) => { curXk.value = xk; showFlag.value = false; - if (xk && xk.id) { - // 显示切换成功提示 - uni.showToast({ - title: `已切换到${xk.xkmc}`, - icon: "none", - }); - } emit("change", xk); } diff --git a/src/pages/base/components/XkkcList/index.vue b/src/pages/base/components/XkkcList/index.vue index 1ff678e..b9ab1d9 100644 --- a/src/pages/base/components/XkkcList/index.vue +++ b/src/pages/base/components/XkkcList/index.vue @@ -9,15 +9,17 @@ :class="{ selected: xkkc.isSelected }" @click="toggleSelection(xkkc)" > - {{ xkkc.kcmc }} + + {{ xkkc.kcmc }} + + + + 报名情况: {{ xkkc.hasNum || 0 }} | {{ xkkc.maxNum || 0 }} - 详情 { setCurXs(xs); showFlag.value = false; emit('change', xs); - // 这里可以添加切换学生后的其他操作,如刷新页面数据等 - uni.showToast({ - title: `已切换到${xs.xm}`, - icon: "none", - }); +} + +// 设置当前学生(无提示) +const setCurrentStudent = (xs: any) => { + setCurXs(xs); + emit('change', xs); } // 如果是bar形式,则默认打开选择器 -if (props.isBar -//&& (getCurXs === null || !getCurXs.id) -) { +if (props.isBar) { if (getUser.xsList.length > 1 ) { showPicker(); } else { - switchXs(getUser.xsList[0]); + // 只有一个学生时,直接设置但不显示提示 + setCurrentStudent(getUser.xsList[0]); } } diff --git a/src/pages/base/course-selection/club-selection.vue b/src/pages/base/course-selection/club-selection.vue index c9743bc..f074242 100644 --- a/src/pages/base/course-selection/club-selection.vue +++ b/src/pages/base/course-selection/club-selection.vue @@ -3,12 +3,15 @@ - - - - - - + + + + + + + + + @@ -35,6 +38,7 @@ import XkkcList from "@/pages/base/components/XkkcList/index.vue" import { jzXkQkjApi } from "@/api/base/server"; import { useUserStore } from "@/store/modules/user"; import { useDataStore } from "@/store/modules/data"; +import dayjs from "dayjs"; const { getCurXs, getUser } = useUserStore(); const { setData, getData } = useDataStore(); @@ -46,9 +50,31 @@ const selectedXkkcIds = ref([]); const xsFlag = ref(true); +// 检查选课是否已结束 +const checkEnrollmentStatus = (xk: any) => { + if (!xk || !xk.xkjstime) return; + + const now = new Date().getTime(); + const endTime = new Date(xk.xkjstime).getTime(); + + if (now > endTime) { + // 选课已结束,跳转到结束页面 + const courseInfo = encodeURIComponent(JSON.stringify(xk)); + uni.navigateTo({ + url: `/pages/base/course-selection/enrollment-ended?courseInfo=${courseInfo}` + }); + return true; + } + return false; +}; + // 切换选课 const switchXk = (xk: any) => { curXk.value = xk; + // 检查选课状态 + if (checkEnrollmentStatus(xk)) { + return; + } } const switchXs = (xs: any) => { @@ -57,7 +83,9 @@ const switchXs = (xs: any) => { // 选课时间结束 const xkTimeOver = (val: any) => { - console.log(val); + console.log('选课时间结束:', val); + // 选课开始时不再跳转,让用户继续选课 + // 只有在选课结束时才跳转到选课结束页面 } // 选中课程 @@ -75,6 +103,21 @@ const submit = async () => { }); return; } + + // 检查选课时间 + if (curXk.value && curXk.value.xkkstime) { + const now = dayjs().valueOf(); + const startTime = dayjs(curXk.value.xkkstime).valueOf(); + + if (now < startTime) { + uni.showToast({ + title: "选课还未开始,请耐心等待!", + icon: "none", + }); + return; + } + } + uni.showLoading({ title: "抢课中...", }); @@ -173,6 +216,36 @@ const submit = async () => { flex-direction: column; gap: 15px; + .top-row { + display: flex; + align-items: center; + + :deep(.title-section) { + width: 100%; + } + } + + .bottom-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 15px; + + // 学生选择器样式 + :deep(.xs-bar) { + flex: 1; + min-width: 0; + } + + // 倒计时组件样式 + :deep(.countdown-section) { + flex-shrink: 0; + background: rgba(255, 255, 255, 0.1); + border-radius: 6px; + padding: 6px 10px; + backdrop-filter: blur(10px); + } + } } } diff --git a/src/pages/base/course-selection/detail.vue b/src/pages/base/course-selection/detail.vue index 5895df4..2377bce 100644 --- a/src/pages/base/course-selection/detail.vue +++ b/src/pages/base/course-selection/detail.vue @@ -161,7 +161,7 @@ const courseDetail = computed(() => { location: data.kcdd || "暂无地点信息", price: data.kcje || 0, studyTime: data.studyTime || "暂无上课时间", - xkkcImg: data.xkkcImg || "/static/images/robot-course.jpg", // 默认图片 + xkkcImg: imagUrl(data.xkkcImg), // 使用imagUrl处理图片路径 }; }); diff --git a/src/pages/base/course-selection/enrollment-ended.vue b/src/pages/base/course-selection/enrollment-ended.vue new file mode 100644 index 0000000..9fd5088 --- /dev/null +++ b/src/pages/base/course-selection/enrollment-ended.vue @@ -0,0 +1,291 @@ + + + + + \ No newline at end of file diff --git a/src/pages/base/course-selection/index.vue b/src/pages/base/course-selection/index.vue index 32e04a8..1b4abaa 100644 --- a/src/pages/base/course-selection/index.vue +++ b/src/pages/base/course-selection/index.vue @@ -5,10 +5,11 @@ - - - - + + + + + @@ -35,6 +36,7 @@ import XkkcList from "@/pages/base/components/XkkcList/index.vue" import { jzXkQkjApi } from "@/api/base/server"; import { useUserStore } from "@/store/modules/user"; import { useDataStore } from "@/store/modules/data"; +import dayjs from "dayjs"; const { getCurXs, getUser } = useUserStore(); const { setData, getData } = useDataStore(); @@ -47,9 +49,31 @@ const selectedXkkcIds = ref([]); const xsFlag = ref(true); +// 检查选课是否已结束 +const checkEnrollmentStatus = (xk: any) => { + if (!xk || !xk.xkjstime) return; + + const now = new Date().getTime(); + const endTime = new Date(xk.xkjstime).getTime(); + + if (now > endTime) { + // 选课已结束,跳转到结束页面 + const courseInfo = encodeURIComponent(JSON.stringify(xk)); + uni.navigateTo({ + url: `/pages/base/course-selection/enrollment-ended?courseInfo=${courseInfo}` + }); + return true; + } + return false; +}; + // 切换选课 const switchXk = (xk: any) => { curXk.value = xk; + // 检查选课状态 + if (checkEnrollmentStatus(xk)) { + return; + } } // 切换学生 @@ -59,7 +83,12 @@ const switchXs = (xs: any) => { // 选课时间结束 const xkTimeOver = (val: any) => { - console.log(val); + console.log('选课时间结束:', val); + // 跳转到选课结束页面 + const courseInfo = encodeURIComponent(JSON.stringify(curXk.value)); + uni.navigateTo({ + url: `/pages/base/course-selection/enrollment-ended?courseInfo=${courseInfo}` + }); } // 选中课程 @@ -77,6 +106,21 @@ const submit = async () => { }); return; } + + // 检查选课时间 + if (curXk.value && curXk.value.xkkstime) { + const now = dayjs().valueOf(); + const startTime = dayjs(curXk.value.xkkstime).valueOf(); + + if (now < startTime) { + uni.showToast({ + title: "选课还未开始,请耐心等待!", + icon: "none", + }); + return; + } + } + uni.showLoading({ title: "抢课中...", }); @@ -174,6 +218,27 @@ const submit = async () => { flex-direction: column; gap: 15px; + .bottom-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 15px; + + // 学生选择器样式 + :deep(.xs-bar) { + flex: 1; + min-width: 0; + } + + // 倒计时组件样式 + :deep(.countdown-section) { + flex-shrink: 0; + background: rgba(255, 255, 255, 0.1); + border-radius: 6px; + padding: 6px 10px; + backdrop-filter: blur(10px); + } + } } } diff --git a/src/pages/base/course-selection/notice.vue b/src/pages/base/course-selection/notice.vue index 30632ab..4d4cfb4 100644 --- a/src/pages/base/course-selection/notice.vue +++ b/src/pages/base/course-selection/notice.vue @@ -25,14 +25,19 @@ + + diff --git a/src/pages/base/home/index.vue b/src/pages/base/home/index.vue index 716f418..ceae315 100644 --- a/src/pages/base/home/index.vue +++ b/src/pages/base/home/index.vue @@ -90,7 +90,7 @@ const { setData, getAppCode } = useDataStore(); // 刷新相关变量 const { getLastRefreshTime, getRefreshInterval, setLastRefreshTime, updateStudentInfo, updateStudentList } = useUserStore(); -const REFRESH_INTERVAL = 10 * 60 * 1000; // 10分钟刷新一次(测试用) +const REFRESH_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1周刷新一次(7天) // 获取当前changeTime const getCurrentChangeTime = () => { @@ -209,6 +209,18 @@ const menuItems = ref([ path: "/pages/base/club/index", permissionKey: "school-jlb", // 俱乐部权限编码 }, + { + title: "兴趣课选课", + icon: "/static/base/home/file-text-line.png", + path: "/pages/base/course-selection/notice", + permissionKey: "school-xqkxk", // 兴趣课选课权限编码 + }, + { + title: "俱乐部选课", + icon: "/static/base/home/contacts-book-3-line.png", + path: "/pages/base/course-selection/noticeclub", + permissionKey: "school-jlbxk", // 俱乐部选课权限编码 + }, ]); // 通知公告数据 @@ -328,8 +340,43 @@ onMounted(async () => { } } } + + // 检查用户是否已关注服务号 + await checkSubscribeStatus(); }); +// 检查关注状态 +const checkSubscribeStatus = async () => { + const userStore = useUserStore(); + const userInfo = userStore.getUser; + + if (userInfo && !userInfo.subscribed) { + // 延迟显示关注提醒,避免与启动页面冲突 + setTimeout(() => { + showSubscribeReminder(); + }, 2000); + } +}; + +// 显示关注提醒 +const showSubscribeReminder = () => { + uni.showModal({ + title: '关注服务号', + content: '请先关注我们的服务号,以便接收重要通知', + showCancel: true, + cancelText: '稍后关注', + confirmText: '立即关注', + success: (res) => { + if (res.confirm) { + // 跳转到关注页面 + uni.navigateTo({ + url: '/pages/system/subscribe/index' + }); + } + } + }); +}; + // 页面显示时检查是否需要刷新 onShow(async () => { await checkAndRefreshStudentInfo(); diff --git a/src/pages/system/launchPage/launchPage.vue b/src/pages/system/launchPage/launchPage.vue index 3c6a767..8c79c74 100644 --- a/src/pages/system/launchPage/launchPage.vue +++ b/src/pages/system/launchPage/launchPage.vue @@ -54,6 +54,7 @@ onLoad(async (data: any) => { } } + // 直接跳转到首页,关注检查在首页进行 toHome(data); } else { uni.reLaunch({ diff --git a/src/pages/system/login/login.vue b/src/pages/system/login/login.vue index 34ecff5..3d6d832 100644 --- a/src/pages/system/login/login.vue +++ b/src/pages/system/login/login.vue @@ -265,7 +265,7 @@ function toHome() { }); } else if (getGlobal.type == 2) { uni.reLaunch({ - url: "/pages/base/course-selection/club-selection", + url: "/pages/base/course-selection/noticeclub", }); } else { uni.reLaunch({ diff --git a/src/pages/system/subscribe/index.vue b/src/pages/system/subscribe/index.vue new file mode 100644 index 0000000..81745ba --- /dev/null +++ b/src/pages/system/subscribe/index.vue @@ -0,0 +1,194 @@ + + + + + \ No newline at end of file diff --git a/src/static/base/home/details.svg b/src/static/base/home/details.svg new file mode 100644 index 0000000..e36784f --- /dev/null +++ b/src/static/base/home/details.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/base/home/rdcode.jpg b/src/static/base/home/rdcode.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9950092593088c46fa20685a051f76f55e6deb05 GIT binary patch literal 9602 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 YFs6Pj_x}Dz0l)q@;D5t+-0J)P0yvfpx&QzG literal 0 HcmV?d00001