新苗调整

This commit is contained in:
hebo 2026-06-23 11:16:12 +08:00
parent 8bdde206b0
commit 2b1dad9e29
11 changed files with 675 additions and 303 deletions

View File

@ -55,12 +55,21 @@ export const dqpkApi = async () => {
};
/**
*
* yfzc_pkkb/
*/
export const drpkkbApi = async (params: any) => {
return await get("/mobile/jz/pkkb/drpkkb", params);
};
/** 某周每日排课课表(家长端:仅 yfzc_pkkb不叠加调换课/代课) */
export const drpkkbWeekApi = async (params: {
bjId: string;
xqId: string;
zc: number;
}) => {
return await get("/mobile/jz/pkkb/drpkkbWeek", params);
};
/**
*
*/
@ -125,6 +134,13 @@ export const jzXsQjActivitiHistoryApi = async (params: any) => {
return await get("/activiti/history/historicFlow", params);
};
/**
* findPage
*/
export const xqFindPageForSelectApi = async () => {
return await get("/api/xq/findPage", { page: 1, rows: 9999, status: "A" });
};
/**
* ID查询作品执行数据
*/
@ -134,6 +150,7 @@ export const zpzxFindByKcParamsApi = async (params: {
njmcId?: string;
bjId?: string;
xsId?: string;
xqId?: string;
}) => {
return await get("/api/zpzx/findByKcParams", params);
};

View File

@ -65,12 +65,10 @@
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted, nextTick } from "vue";
import { dqpkApi, drpkkbApi } from "@/api/base/server";
import { ref, computed, onMounted, nextTick } from "vue";
import { dqpkApi, drpkkbWeekApi } from "@/api/base/server";
import { useUserStore } from "@/store/modules/user";
import { useCommonStore } from "@/store/modules/common";
const { getCurXs } = useUserStore();
const { getCacheList }= useCommonStore();
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import weekOfYear from "dayjs/plugin/weekOfYear";
@ -80,122 +78,197 @@ dayjs.locale("zh-cn");
dayjs.extend(weekOfYear);
dayjs.extend(isoWeek);
let dqZc = 0;
let xqId = '';
let xqId = "";
//
const dnDjz = ref(dayjs().isoWeek());
//
const curZc = ref<any>({});
//
const curRqIndex = ref(0)
//
const zcList = ref<any>([])
//
const curRqIndex = ref(0);
const zcList = ref<any[]>([]);
const rqList = computed(() => {
if (!curZc.value || !curZc.value.drList || !curZc.value.drList.length) {
if (!curZc.value?.drList?.length) {
return [];
}
return curZc.value.drList.filter((dr: any) => {
return (!dr.holiday && dr.zj <= 5) || (dr.holiday && dr.holiday.type != 2);
});
return filterDrList(curZc.value.drList);
});
//
const sjList = ref<any>([])
const sjList = ref<any[]>([]);
/** 当前周整周课表缓存key 为 YYYY-MM-DD */
const weekDaySjMap = ref<Record<string, any[]>>({});
const weekCacheZcDjz = ref<number | null>(null);
const isLoading = ref(false);
//
const showWeekPicker = ref(false);
const weekPopup = ref<any>(null);
const targetScrollTop = ref(0);
const targetScrollTop = ref(0); // scroll-top
function filterDrList(drList: any[]) {
if (!drList?.length) {
return [];
}
return drList.filter((dr: any) => {
return (!dr.holiday && dr.zj <= 5) || (dr.holiday && dr.holiday.type != 2);
});
}
//
const openWeekPicker = async () => {
showWeekPicker.value = true;
//
await nextTick();
if (weekPopup.value) {
weekPopup.value.open("bottom");
} else {
console.error("Week popup ref is not available.");
function dayRqToYmd(day: any): string {
if (!day) {
return "";
}
if (day.rq != null && day.rq !== "") {
return dayjs(day.rq).format("YYYY-MM-DD");
}
const rqNum = day.rqNum;
if (rqNum != null && rqNum !== "") {
const s = String(rqNum);
if (s.length === 8) {
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
}
}
return "";
}
function isDateInWeekRawDrList(zc: any, ymd: string): boolean {
if (!ymd || !zc?.drList?.length) {
return false;
}
return (zc.drList as any[]).some((dr) => dayRqToYmd(dr) === ymd);
}
function findZcIndexByCalendarDate(list: any[], ymd: string): number {
for (let i = 0; i < list.length; i++) {
if (isDateInWeekRawDrList(list[i], ymd)) {
return i;
}
}
return -1;
}
function findRqListIndexForCalendarToday(drs: any[]): number {
const today = dayjs().format("YYYY-MM-DD");
return drs.findIndex((d) => dayRqToYmd(d) === today);
}
/**
* 定位当前周 + 今天
* 1. 按校历日期找含今天的周含周末不用过滤后的列表
* 2. 否则按 dnDjzISO 匹配当前周标签
* 3. 不用缓存里可能过期的 result.zc
* 4. 若今天在本周校历内但不在可展示日周末/放假 下一周第一个可展示日
*/
function resolveInitialWeekAndDayIndex(list: any[]): { zcIdx: number; dayIdx: number } {
if (!list.length) {
return { zcIdx: 0, dayIdx: 0 };
}
const today = dayjs().format("YYYY-MM-DD");
let zcIdx = findZcIndexByCalendarDate(list, today);
if (zcIdx < 0) {
const isoWeekNum = dayjs().isoWeek();
zcIdx = list.findIndex((z: any) => z.dnDjz === isoWeekNum);
}
if (zcIdx < 0) {
zcIdx = 0;
}
let drs = filterDrList(list[zcIdx]?.drList || []);
let dayIdx = findRqListIndexForCalendarToday(drs);
if (dayIdx < 0 && isDateInWeekRawDrList(list[zcIdx], today)) {
const nextZcIdx = zcIdx + 1;
if (nextZcIdx < list.length) {
zcIdx = nextZcIdx;
drs = filterDrList(list[zcIdx]?.drList || []);
}
dayIdx = 0;
} else if (dayIdx < 0) {
dayIdx = 0;
}
return { zcIdx, dayIdx };
}
async function loadWeekSchedule(zc: any) {
weekDaySjMap.value = {};
weekCacheZcDjz.value = null;
sjList.value = [];
const bjId = getCurXs?.bjId;
if (!bjId || !xqId || !zc?.djz) {
return;
}
isLoading.value = true;
try {
const res: any = await drpkkbWeekApi({
bjId: String(bjId),
xqId,
zc: zc.djz,
});
weekCacheZcDjz.value = zc.djz;
if (res?.resultCode === 1 && Array.isArray(res.result)) {
for (const day of res.result) {
const key = day.rq ? dayjs(day.rq).format("YYYY-MM-DD") : "";
if (key) {
weekDaySjMap.value[key] = Array.isArray(day.sjList) ? day.sjList : [];
}
}
}
} catch (error) {
console.error("加载周课表失败:", error);
uni.showToast({ title: "加载课表失败", icon: "none" });
} finally {
isLoading.value = false;
}
}
//
const closeWeekPicker = () => {
function applyDaySchedule(index: number) {
curRqIndex.value = index;
if (!rqList.value.length) {
sjList.value = [];
return;
}
const safeIdx = Math.max(0, Math.min(index, rqList.value.length - 1));
curRqIndex.value = safeIdx;
const rqKey = dayRqToYmd(rqList.value[safeIdx]);
sjList.value = weekDaySjMap.value[rqKey] || [];
}
const openWeekPicker = async () => {
showWeekPicker.value = true;
await nextTick();
if (weekPopup.value) {
weekPopup.value.close();
weekPopup.value.open("bottom");
}
};
//
const closeWeekPicker = () => {
weekPopup.value?.close();
};
const popupChange = (e: { show: boolean }) => {
if (!e.show) {
showWeekPicker.value = false;
}
};
//
const selectWeek = (zc: any) => {
if (curZc.value && zc && zc.djz === curZc.value.djz) {
//
const selectWeek = async (zc: any) => {
if (curZc.value?.djz === zc?.djz) {
closeWeekPicker();
return;
}
//
Object.assign(curZc.value, zc);
//
closeWeekPicker();
//
selectDay(0);
await loadWeekSchedule(zc);
const drs = filterDrList(zc.drList || []);
const todayIdx = findRqListIndexForCalendarToday(drs);
applyDaySchedule(todayIdx >= 0 ? todayIdx : 0);
};
//
const selectDay = (index: number) => {
curRqIndex.value = index;
if (!rqList.value.length) {
if (weekCacheZcDjz.value !== curZc.value?.djz) {
void loadWeekSchedule(curZc.value).then(() => applyDaySchedule(index));
return;
}
const params = {
jsId: "",
bjId: getCurXs.bjId,
xqId: xqId,
zc: curZc.value.djz,
rq: rqList.value[index].rq, //
zj: rqList.value[index].zj,
txDate: null
};
if (params.zj > 5) {
const holiday = rqList.value[index].holiday || {};
if (holiday && holiday.type === 3 && holiday.txDate) {
params.txDate = holiday.txDate;
const txZc = rqList.value[index].txZc;
if (txZc) {
params.zc = txZc;
}
}
}
drpkkbApi(params).then(res => {
// result
if (res && res.resultCode === 1) {
sjList.value = res.result;
} else {
//
console.warn("检查报名状态接口返回错误:", res);
}
})
.catch((error) => {
//
console.error("调用检查报名状态接口失败:", error);
});
applyDaySchedule(index);
};
//
const getCourseColorClass = (subject: string | undefined): string => {
if (!subject) return "";
if (subject.includes("语文")) return "color-lang";
@ -205,33 +278,23 @@ const getCourseColorClass = (subject: string | undefined): string => {
};
onMounted(async () => {
//
const res = await getCacheList(dqpkApi, "dqPkSz");
const result = res.result;
dqZc = res.result.zc;
xqId = res.result.xq.id;
zcList.value = result.zcList;
sjList.value = [];
try {
// getCacheList result.zc
const res: any = await dqpkApi();
const result = res?.result ?? res;
if (!result?.xq?.id) {
return;
}
xqId = result.xq.id;
zcList.value = result.zcList || [];
sjList.value = [];
//
const today = dayjs();
const currentWeekday = today.day(); // 0-60
const currentWeekdayIndex = currentWeekday === 0 ? 6 : currentWeekday - 1; // 1-71
//
let zc = zcList.value.find((item:any) => item.djz === dqZc);
if (!zc) {
zc = zcList.value[0];
}
//
selectWeek(zc);
//
if (rqList.value && rqList.value.length > 0) {
//
const targetIndex = Math.min(currentWeekdayIndex, rqList.value.length - 1);
selectDay(targetIndex);
const { zcIdx, dayIdx } = resolveInitialWeekAndDayIndex(zcList.value);
Object.assign(curZc.value, zcList.value[zcIdx] || {});
await loadWeekSchedule(curZc.value);
applyDaySchedule(dayIdx);
} catch (error) {
console.error("初始化课表失败:", error);
}
});
</script>

View File

@ -10,7 +10,7 @@
<!-- 顶部蓝色背景与教师端一致 -->
<view class="blue-header">
<view class="header-content">
<text class="exam-title">{{ kscc.ksmc || '考试场次' }}</text>
<text class="exam-title">{{ kscc.ksmc || '学情场次' }}</text>
<view class="student-info-section">
<text class="student-name">{{ curXs.xm || curXs.name || '学生姓名' }}</text>
<text class="student-class" v-if="curXs.njmc || curXs.bjmc">
@ -45,11 +45,11 @@
:class="{ active: activeTab === 'scores' }"
@click="switchTab('scores')"
>
<text>学科成绩</text>
<text>学科等级</text>
</view>
</view>
<!-- 学科成绩视图与教师端布局一致分数按 sfXsFs 显示 -->
<!-- 学科等级视图与教师端布局一致分数按 sfXsFs 显示 -->
<view class="score-view" v-if="activeTab === 'scores'">
<view v-if="ksccKscjList.length > 0" class="score-content">
<view
@ -152,7 +152,7 @@ onLoad((options?: Record<string, string>) => {
}
kscc.value = {
id: kid,
ksmc: name || "考试场次",
ksmc: name || "学情场次",
};
if (q.njmcId != null && String(q.njmcId).trim()) {
try {
@ -198,16 +198,16 @@ const radarData = {
//
const sysKmList = ref<any>([]);
//
//
const ksccKmList = ref<any>([]);
//
const totalKmFs = ref<number>(0);
//
const kmDjList = ref<any>([]);
//
//
const djList = ref<any>([]);
//
//
const ksccKscjList = ref<any>([]);
//
@ -361,7 +361,7 @@ function resolveOverallKsDj(sumScaled: number, examFullMarksScaled: number): any
return pickBestDjMatch(ksDjRows, scoreForMatch) || {};
}
// kspj/detail
// kspj/detail
const initKsdj = () => {
djList.value.forEach((ksdj: any) => {
ksdj.zdf = floatToInt(ksdj.zdf, fsScale);
@ -417,7 +417,7 @@ onMounted(async () => {
}
const ksccId = kscc.value?.id;
if (!ksccId) {
uni.showToast({ title: "请先选择考试场次", icon: "none" });
uni.showToast({ title: "请先选择学情场次", icon: "none" });
return;
}
const res = await xsKscjApi({
@ -640,7 +640,7 @@ onMounted(async () => {
}
}
/* 学科成绩视图(与教师端一致) */
/* 学科等级视图(与教师端一致) */
.score-view {
margin-top: 15px;
display: flex;

View File

@ -1,9 +1,9 @@
<template>
<view class="page">
<view class="head-tip">请选择考试场次</view>
<view class="head-tip">请选择学情场次</view>
<scroll-view scroll-y class="scroll">
<view v-if="loading" class="state">加载中...</view>
<view v-else-if="!treeData.length" class="state">暂无考试场次数据</view>
<view v-else-if="!treeData.length" class="state">暂无学情场次数据</view>
<view v-else class="sections">
<view v-for="xq in treeData" :key="xqKey(xq)" class="section">
<text class="section-title">{{ xq.title || xq.label || "学期" }}</text>
@ -16,7 +16,7 @@
<text class="kscc-name">{{ c.title || c.ksmc || "—" }}</text>
<text class="arrow"></text>
</view>
<view v-if="!(xq.children || []).length" class="kscc-empty">该学期下暂无场次</view>
<view v-if="!(xq.children || []).length" class="kscc-empty">该学期下暂无学情场次</view>
</view>
</view>
</scroll-view>
@ -68,7 +68,7 @@ function goDetail(xq: any, c: any) {
const xqTitle = String(xq?.title ?? xq?.label ?? "").trim();
const ksTitle = String(c?.title ?? c?.ksmc ?? "").trim();
const idEnc = encodeURIComponent(String(ksccId));
const ksEnc = encodeURIComponent(ksTitle || "考试场次");
const ksEnc = encodeURIComponent(ksTitle || "学情场次");
uni.navigateTo({
url: `/pages/base/grades/detail?ksccId=${idEnc}&ksmc=${ksEnc}`,
});

View File

@ -67,15 +67,18 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch, reactive } from "vue";
import { onShow } from "@dcloudio/uni-app";
import XsPicker from "@/pages/base/components/XsPicker/index.vue"
import { getNoticeListApi } from "@/api/base/notice";
import { getMobileMenuApi } from "@/api/system/menu";
import { getPermissionChangeTimeApi } from "@/api/system/config";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { useMenuStore } from "@/store/modules/menu";
import { type MobileMenuTreeNode } from "@/api/system/menu";
import { PageUtils } from "@/utils/pageUtil";
import { useDebounce } from "@/utils/debounce";
import { resolveJzdUserId } from "@/utils/menuCache";
const userStore = useUserStore();
const { getCurXs } = userStore;
@ -218,23 +221,38 @@ const getArticleList = async () => {
}
};
async function loadHomeMenu() {
const userId = resolveJzdUserId(userStore.getUser);
if (!userId || !userStore.getToken) {
menuItems.value = [];
return;
}
if (menuStore.isMenuCacheForUser(userId)) {
menuItems.value = flattenMenuToItems(menuStore.getMobileMenu);
return;
}
try {
const timeRes = await getPermissionChangeTimeApi();
const serverChangeTime = String((timeRes as any)?.result ?? "");
if (serverChangeTime) userStore.setChangeTime(serverChangeTime);
const r = await getMobileMenuApi();
if (r?.result && Array.isArray(r.result) && r.result.length > 0) {
menuStore.setMobileMenu(r.result, userId);
menuItems.value = flattenMenuToItems(r.result);
} else {
menuItems.value = [];
}
} catch (_e) {
menuItems.value = [];
}
}
onMounted(async () => {
setGlobal({ from: 'home' });
// launchPage menuStore
const cachedMenu = menuStore.getMobileMenu;
if (cachedMenu?.length > 0) {
menuItems.value = flattenMenuToItems(cachedMenu);
} else if (userStore.getToken) {
// launchPage token
try {
const r = await getMobileMenuApi();
if (r?.result && Array.isArray(r.result) && r.result.length > 0) {
menuStore.setMobileMenu(r.result);
menuItems.value = flattenMenuToItems(r.result);
}
} catch (_e) {}
}
await loadHomeMenu();
//
if (curXs.value && curXs.value.njmcId) {
@ -250,6 +268,13 @@ onMounted(async () => {
await checkSubscribeStatus();
});
onShow(() => {
const userId = resolveJzdUserId(userStore.getUser);
if (!menuStore.isMenuCacheForUser(userId)) {
void loadHomeMenu();
}
});
//
const checkSubscribeStatus = async () => {
const userInfo = userStore.getUser;

View File

@ -16,7 +16,7 @@
<view class="banner-title-wrapper">
<text class="banner-icon">{{ activeTab === 0 ? '📝' : '📚' }}</text>
<view class="banner-text">
<text class="banner-title">{{ activeTab === 0 ? '作品任务' : '作品资源' }}</text>
<text class="banner-title">{{ activeTab === 0 ? '作品任务' : '课程资源' }}</text>
<text class="banner-subtitle">{{
activeTab === 0 ? '独立项目选择 · 学习成长' : '课程资料 · 在线查看'
}}</text>
@ -29,18 +29,45 @@
</view>
</view>
<view class="tab-bar">
<view class="tab-item" :class="{ active: activeTab === 0 }" @click="activeTab = 0">
<text class="tab-text">作品任务</text>
<view class="header-panel">
<view class="filter-bar">
<view class="filter-item">
<uni-data-select
v-model="searchForm.xnmc"
:localdata="xnmcList"
placeholder="选择学年"
@change="handleXnChange"
class="filter-select"
/>
</view>
<view class="filter-item">
<uni-data-select
v-model="searchForm.xqId"
:localdata="xqList"
placeholder="选择学期"
@change="handleXqChange"
class="filter-select"
/>
</view>
</view>
<view class="tab-item" :class="{ active: activeTab === 1 }" @click="activeTab = 1">
<text class="tab-text">作品资源</text>
<view class="tab-bar">
<view class="tab-item" :class="{ active: activeTab === 0 }" @click="activeTab = 0">
<text class="tab-text">作品任务</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 1 }" @click="activeTab = 1">
<text class="tab-text">课程资源</text>
</view>
</view>
</view>
<!-- 任务列表 -->
<scroll-view scroll-y class="list-scroll-view">
<view v-if="isLoading && taskList.length === 0" class="loading-indicator">
<scroll-view scroll-y class="list-scroll-view" :enable-back-to-top="true">
<view v-if="!xqIdsForApi" class="empty-state empty-state--filter">
<view class="empty-icon">📅</view>
<text class="empty-text">请先选择学年并选择具体学期或全部学期</text>
</view>
<view v-else-if="isLoading && taskList.length === 0" class="loading-indicator">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
@ -97,7 +124,7 @@
<view v-else class="empty-state">
<view class="empty-icon">📭</view>
<text class="empty-text">{{ activeTab === 0 ? '暂无作品任务' : '暂无作品资源' }}</text>
<text class="empty-text">{{ activeTab === 0 ? '暂无作品任务' : '暂无课程资源' }}</text>
</view>
</scroll-view>
</view>
@ -105,12 +132,41 @@
</template>
<script lang="ts" setup>
import { ref, computed, onUnmounted } from "vue";
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
import { onShow, onLoad } from "@dcloudio/uni-app";
import { useUserStore } from "@/store/modules/user";
import { useCommonStore } from "@/store/modules/common";
import { zpzxFindByKcParamsApi } from "@/api/base/server";
import { getPersistedDqXqAndZc } from "@/utils/xszpPersistXq";
const userStore = useUserStore();
const commonStore = useCommonStore();
const searchForm = reactive({
xnmc: "",
xqId: "",
});
const xnmcList = ref<Array<{ value: string; text: string }>>([]);
const allXqList = ref<Array<{ id: string; xqmc: string; xnmc: string }>>([]);
const xqList = ref<Array<{ value: string; text: string }>>([]);
const currentXqId = ref("");
const xqListReady = ref(false);
/** 传给接口的学期 id单选为单个 id选「全部学期」为当前学年下所有学期 id 逗号拼接 */
const xqIdsForApi = computed(() => {
if (searchForm.xqId) {
return searchForm.xqId;
}
const year = searchForm.xnmc;
if (!year) {
return "";
}
return allXqList.value
.filter((it) => it.xnmc === year)
.map((it) => String(it.id))
.filter(Boolean)
.join(",");
});
interface TaskItem {
id: string;
@ -190,18 +246,101 @@ onLoad(async (options) => {
// openId onShow isLoginReady false onLoad
// onShow onLoad
if (options && options.openId) {
if (options && options.openId && xqListReady.value) {
loadTaskList();
}
});
onMounted(async () => {
await loadXqList();
xqListReady.value = true;
if (isLoginReady.value) {
loadTaskList();
}
});
onShow(() => {
//
if (isLoginReady.value) {
//
if (isLoginReady.value && xqListReady.value) {
loadTaskList();
}
});
function handleXnChange() {
refreshXqListByYear();
loadTaskList();
}
function handleXqChange() {
loadTaskList();
}
function refreshXqListByYear() {
const year = searchForm.xnmc;
const list = allXqList.value
.filter((item) => item.xnmc === year)
.map((item) => ({ value: item.id, text: item.xqmc }));
xqList.value = [{ value: "", text: "全部学期" }, ...list];
const curId = currentXqId.value;
if (curId && list.some((x) => String(x.value) === String(curId))) {
searchForm.xqId = curId;
} else {
searchForm.xqId = "";
}
}
const loadXqList = async () => {
try {
const response: any = await commonStore.getXqListForSelect();
const data =
response?.rows ||
response?.result ||
response?.data ||
(Array.isArray(response) ? response : []);
const dq = await getPersistedDqXqAndZc(commonStore);
const dqXqId = dq?.xqId != null ? String(dq.xqId) : "";
const cur =
data.find((item: any) => String(item?.id) === dqXqId) ||
data.find((item: any) => String(item?.dqxq) === "是");
currentXqId.value = cur?.id != null ? String(cur.id) : "";
const curXnmc = cur?.xnmc != null ? String(cur.xnmc).trim() : "";
allXqList.value = data.map((item: any) => ({
id: item.id,
xqmc: item.xqmc || item.name,
xnmc: item.xnmc || "",
}));
const yearSet = new Set<string>();
allXqList.value.forEach((item) => {
if (item.xnmc) yearSet.add(item.xnmc);
});
xnmcList.value = Array.from(yearSet).map((y) => ({ value: y, text: y }));
if (xnmcList.value.length > 0) {
if (curXnmc && xnmcList.value.some((x) => x.value === curXnmc)) {
searchForm.xnmc = curXnmc;
} else {
searchForm.xnmc = xnmcList.value[0].value;
}
refreshXqListByYear();
} else {
xqList.value = allXqList.value.map((item) => ({
value: item.id,
text: item.xqmc,
}));
if (currentXqId.value) searchForm.xqId = currentXqId.value;
else if (xqList.value.length)
searchForm.xqId = String(xqList.value[0].value);
}
} catch (e) {
console.error(e);
xnmcList.value = [];
allXqList.value = [];
xqList.value = [];
currentXqId.value = "";
}
};
//
uni.$on('refreshTaskList', () => {
console.log('收到刷新任务列表事件');
@ -215,7 +354,11 @@ onUnmounted(() => {
const loadTaskList = async () => {
//
if (!isLoginReady.value) {
if (!isLoginReady.value || !xqListReady.value) {
return;
}
if (!xqIdsForApi.value) {
taskList.value = [];
return;
}
@ -239,6 +382,9 @@ const loadTaskList = async () => {
if (kcId.value) {
params.kcId = kcId.value;
}
if (xqIdsForApi.value) {
params.xqId = xqIdsForApi.value;
}
const response = await zpzxFindByKcParamsApi(params);
@ -320,7 +466,7 @@ const viewTaskDetail = (task: TaskItem) => {
});
};
/** 作品资源(资料查看)详情 */
/** 课程资源(资料查看)详情 */
const viewResourceDetail = (task: TaskItem) => {
const params = new URLSearchParams();
params.append('zpId', task.zpId);
@ -384,14 +530,16 @@ const formatTime = (timeStr?: string) => {
display: flex;
flex-direction: column;
height: 100%;
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);
min-height: 100vh;
background: #eef2f8;
box-sizing: border-box;
}
//
.page-banner {
background: linear-gradient(135deg, #4e73df 0%, #2e59d9 100%);
padding: 20px 16px;
box-shadow: 0 4px 12px rgba(78, 115, 223, 0.3);
flex-shrink: 0;
background: linear-gradient(145deg, #3d5fc4 0%, #2e59d9 55%, #2547b8 100%);
padding: 18px 16px 16px;
box-shadow: 0 4px 12px rgba(46, 89, 217, 0.2);
.banner-content {
display: flex;
@ -402,89 +550,137 @@ const formatTime = (timeStr?: string) => {
.banner-title-wrapper {
display: flex;
align-items: center;
gap: 12px;
gap: 10px;
flex: 1;
min-width: 0;
}
.banner-icon {
font-size: 32px;
font-size: 28px;
line-height: 1;
flex-shrink: 0;
}
.banner-text {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.banner-title {
font-size: 24px;
font-weight: bold;
font-size: 20px;
font-weight: 700;
color: #ffffff;
line-height: 1.2;
line-height: 1.25;
}
.banner-subtitle {
font-size: 13px;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 1px;
font-size: 12px;
color: rgba(255, 255, 255, 0.88);
line-height: 1.4;
}
.task-count {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
padding: 10px 16px;
border-radius: 12px;
min-width: 60px;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.25);
padding: 8px 14px;
border-radius: 14px;
min-width: 56px;
}
.count-number {
font-size: 24px;
font-weight: bold;
font-size: 22px;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.count-label {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
color: rgba(255, 255, 255, 0.92);
margin-top: 4px;
}
}
.header-panel {
flex-shrink: 0;
width: 100%;
margin: 0;
padding: 12px 12px 10px;
background: #ffffff;
border-radius: 0;
box-shadow: none;
border-bottom: 1px solid #e9ecef;
box-sizing: border-box;
}
.filter-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #eef1f6;
}
.filter-item {
flex: 1;
min-width: 0;
}
.filter-select {
width: 100%;
}
.filter-item :deep(.uni-stat__select) {
flex: 1;
min-width: 0;
}
.filter-item :deep(.uni-select) {
border: 1px solid #e2e8f0 !important;
border-radius: 10px !important;
background: #f8fafc !important;
min-height: 36px;
}
.filter-item :deep(.uni-select__input-text) {
font-size: 13px !important;
color: #334155 !important;
}
.tab-bar {
display: flex;
background: #ffffff;
margin: 0 16px;
margin-top: -10px;
margin-bottom: 12px;
border-radius: 12px;
padding: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
position: relative;
z-index: 2;
background: #f1f5f9;
border-radius: 10px;
padding: 3px;
gap: 3px;
}
.tab-item {
flex: 1;
text-align: center;
padding: 10px 0;
border-radius: 10px;
transition: background 0.2s ease, color 0.2s ease;
padding: 9px 0;
border-radius: 9px;
transition: background 0.2s ease, box-shadow 0.2s ease;
.tab-text {
font-size: 15px;
color: #666;
font-size: 14px;
color: #64748b;
font-weight: 500;
}
&.active {
background: linear-gradient(135deg, #4e73df 0%, #2e59d9 100%);
background: #ffffff;
box-shadow: 0 2px 8px rgba(46, 89, 217, 0.15);
.tab-text {
color: #ffffff;
color: #2e59d9;
font-weight: 600;
}
}
@ -492,7 +688,10 @@ const formatTime = (timeStr?: string) => {
.list-scroll-view {
flex: 1;
padding: 0;
min-height: 0;
width: 100%;
padding: 8px 12px 16px;
box-sizing: border-box;
}
.loading-indicator {
@ -500,21 +699,21 @@ const formatTime = (timeStr?: string) => {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
padding: 48px 0;
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #409eff;
width: 36px;
height: 36px;
border: 3px solid #e2e8f0;
border-top: 3px solid #4e73df;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.loading-text {
color: #999;
font-size: 14px;
color: #94a3b8;
font-size: 13px;
}
}
@ -524,26 +723,23 @@ const formatTime = (timeStr?: string) => {
}
.task-card {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 16px;
margin: 0 0 16px 0;
padding: 16px;
box-shadow:
0 2px 16px rgba(0, 0, 0, 0.08),
0 0 0 1px rgba(78, 115, 223, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #ffffff;
border-radius: 14px;
margin: 0 0 12px;
padding: 14px 16px;
border: 1px solid #e8edf5;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: visible;
overflow: hidden;
&--resource {
cursor: pointer;
}
&:active {
transform: translateY(2px) scale(0.98);
box-shadow:
0 1px 8px rgba(0, 0, 0, 0.12),
0 0 0 1px rgba(78, 115, 223, 0.15);
transform: scale(0.99);
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
}
}
@ -565,9 +761,9 @@ const formatTime = (timeStr?: string) => {
}
.task-title {
font-size: 16px;
font-size: 15px;
font-weight: 600;
color: #333;
color: #1e293b;
flex: 1;
}
@ -634,39 +830,37 @@ const formatTime = (timeStr?: string) => {
.card-footer {
padding-top: 12px;
border-top: 1px solid #f0f0f0;
border-top: 1px solid #f1f5f9;
display: flex;
gap: 12px;
gap: 10px;
justify-content: space-between;
.action-btn {
flex: 1;
height: 36px;
line-height: 36px;
border-radius: 8px;
font-size: 14px;
height: 34px;
line-height: 34px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
border: none;
transition: all 0.3s;
transition: opacity 0.2s;
&.challenge-btn {
background: linear-gradient(135deg, #4e73df 0%, #2e59d9 100%);
color: #ffffff;
&:active {
background: linear-gradient(135deg, #2e59d9 0%, #1e3fa8 100%);
transform: scale(0.98);
opacity: 0.88;
}
}
&.detail-btn {
background: #f5f7fa;
color: #4e73df;
border: 1px solid #e0e6ed;
background: #f1f5f9;
color: #2e59d9;
border: 1px solid #e2e8f0;
&:active {
background: #e8ecf1;
transform: scale(0.98);
opacity: 0.88;
}
}
}
@ -677,18 +871,29 @@ const formatTime = (timeStr?: string) => {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
padding: 56px 20px;
background: #ffffff;
border-radius: 14px;
border: 1px dashed #dbe3ef;
&--filter {
padding: 40px 20px;
background: #f8fafc;
border-color: #e2e8f0;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.6;
font-size: 48px;
margin-bottom: 12px;
opacity: 0.75;
}
.empty-text {
font-size: 16px;
color: #4a5568;
font-weight: 600;
font-size: 14px;
color: #64748b;
font-weight: 500;
text-align: center;
line-height: 1.5;
}
}

View File

@ -20,6 +20,7 @@ import { checkOpenId } from "@/api/system/login";
import { getMobileMenuApi } from "@/api/system/menu";
import { getPermissionChangeTimeApi } from "@/api/system/config";
import { PageUtils } from "@/utils/pageUtil";
import { resolveJzdUserId } from "@/utils/menuCache";
const dataStore = useDataStore();
const { setGlobal } = dataStore;
@ -33,6 +34,37 @@ const toLogin = () => {
});
};
async function syncMobileMenu(userId: string, savedChangeTime: string) {
if (!userId) return;
let needFetchMenu = true;
let serverChangeTime = "";
try {
const timeRes = await getPermissionChangeTimeApi();
serverChangeTime = String((timeRes as any)?.result ?? "");
needFetchMenu = menuStore.shouldRefreshMenu(
userId,
serverChangeTime,
savedChangeTime
);
if (serverChangeTime) userStore.setChangeTime(serverChangeTime);
} catch (_e) {
needFetchMenu = true;
}
if (!needFetchMenu && menuStore.isMenuCacheForUser(userId)) {
return;
}
try {
const r = await getMobileMenuApi();
if (r?.result && Array.isArray(r.result) && r.result.length > 0) {
menuStore.setMobileMenu(r.result, userId);
}
} catch (_e) {}
}
//
const initGlobalData = (data: any) => {
let gData = { ...(data || {}) };
@ -71,36 +103,11 @@ const initGlobalData = (data: any) => {
onLoad(async (data: any) => {
const gData = initGlobalData(data);
//
// + userId
if (gData.fromLogin === "1") {
let needFetchMenu = true;
try {
const timeRes = await getPermissionChangeTimeApi();
const serverChangeTime = String((timeRes as any)?.result ?? "");
const localChangeTime = userStore.getChangeTime || "";
const localMenu = menuStore.getMobileMenu || [];
needFetchMenu =
serverChangeTime !== localChangeTime || !localMenu?.length;
console.log("[launchPage] fromLogin", {
timeResRaw: JSON.stringify(timeRes),
serverChangeTime,
localChangeTime,
localMenuLength: localMenu?.length ?? 0,
needFetchMenu,
});
if (serverChangeTime) userStore.setChangeTime(serverChangeTime);
} catch (_e) {
needFetchMenu = true;
console.warn("[launchPage] fromLogin getPermissionChangeTime 异常", _e);
}
if (needFetchMenu) {
try {
const r = await getMobileMenuApi();
if (r?.result && Array.isArray(r.result) && r.result.length > 0) {
menuStore.setMobileMenu(r.result);
}
} catch (_e) {}
}
const userId = resolveJzdUserId(userStore.getUser);
const savedChangeTime = userStore.getChangeTime || "";
await syncMobileMenu(userId, savedChangeTime);
PageUtils.toHome(gData.lxId, gData.action);
return;
}
@ -110,40 +117,37 @@ onLoad(async (data: any) => {
const res = await checkOpenId({ openId: gData.openId, appCode: "JZ" });
if (res.resultCode == 1 && res.result) {
// afterLoginAction afterLoginAction logout changeTime menuStore
const loginUserId = resolveJzdUserId(res.result);
const savedChangeTime = userStore.getChangeTime || "";
const savedMenu = menuStore.getMobileMenu || [];
// H5 token
const canRestoreCache = menuStore.isMenuCacheForUser(loginUserId);
const savedMenu = canRestoreCache ? [...menuStore.getMobileMenu] : [];
userStore.afterLoginAction(res.result);
//
let needFetchMenu = true;
try {
const timeRes = await getPermissionChangeTimeApi();
const serverChangeTime = String((timeRes as any)?.result ?? "");
needFetchMenu =
serverChangeTime !== savedChangeTime || !savedMenu?.length;
console.log("[launchPage] openId", {
timeResRaw: JSON.stringify(timeRes),
needFetchMenu = menuStore.shouldRefreshMenu(
loginUserId,
serverChangeTime,
savedChangeTime,
savedMenuLength: savedMenu?.length ?? 0,
needFetchMenu,
});
savedChangeTime
);
if (serverChangeTime) userStore.setChangeTime(serverChangeTime);
} catch (_e) {
needFetchMenu = true;
console.warn("[launchPage] openId getPermissionChangeTime 异常", _e);
}
if (needFetchMenu) {
try {
const r = await getMobileMenuApi();
if (r?.result && Array.isArray(r.result) && r.result.length > 0) {
menuStore.setMobileMenu(r.result);
menuStore.setMobileMenu(r.result, loginUserId);
}
} catch (_e) {}
} else {
menuStore.setMobileMenu(savedMenu); // logout
} else if (savedMenu.length > 0) {
menuStore.setMobileMenu(savedMenu, loginUserId);
}
PageUtils.toHome(gData.lxId, gData.action);

View File

@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { jsFindByIdApi } from "@/api/base/jsApi";
import { xkkclxFindAllApi, getXkkcDetailByIdApi } from "@/api/base/xkApi";
import { clear } from "console";
import { xqFindPageForSelectApi, gzlGetDqXqAndZcApi } from "@/api/base/server";
/**
*
@ -155,6 +155,20 @@ export const useCommonStore = defineStore({
}
return Promise.resolve(this.data.xkkc[id]);
},
/** 学期列表findPage供下拉选择Pinia 持久化缓存) */
async getXqListForSelect(): Promise<any> {
return this.getCacheList(xqFindPageForSelectApi, "xqFindPage");
},
/** 当前学期与周次Pinia 持久化,供学年学期筛选默认项) */
async getDqXqAndZc(): Promise<any> {
if (!this.data.dqXqAndZc) {
const res = await gzlGetDqXqAndZcApi();
if (res && res.resultCode === 1) {
this.data.dqXqAndZc = res.result;
}
}
return Promise.resolve(this.data.dqXqAndZc);
},
},
persist: {
enabled: true,

View File

@ -6,24 +6,53 @@ export const useMenuStore = defineStore({
state: () => ({
/** 树形菜单数据,持久化到 localStorage key: app-Menu-jzd家长端与教师端 app-Menu-jsd 区分,避免同域下菜单互相覆盖) */
mobileMenu: [] as MobileMenuTreeNode[],
/** 菜单所属用户 ID换账号时与当前用户不一致则丢弃缓存 */
menuUserId: "" as string,
}),
getters: {
getMobileMenu(): MobileMenuTreeNode[] {
return this.mobileMenu || [];
},
getMenuUserId(): string {
return this.menuUserId || "";
},
},
actions: {
/** 清空菜单 */
clearMenu() {
this.mobileMenu = [];
this.menuUserId = "";
},
/**
*
* @param menu
* @param userId ID
*/
setMobileMenu(menu: MobileMenuTreeNode[]) {
setMobileMenu(menu: MobileMenuTreeNode[], userId?: string | number) {
this.clearMenu();
this.mobileMenu = menu && Array.isArray(menu) ? [...menu] : [];
if (userId != null && userId !== "") {
this.menuUserId = String(userId);
}
},
/** 缓存是否属于指定用户且非空 */
isMenuCacheForUser(userId: string | number | undefined | null): boolean {
if (userId == null || userId === "" || !this.menuUserId || !this.mobileMenu?.length) {
return false;
}
return String(userId) === String(this.menuUserId);
},
/**
*
*/
shouldRefreshMenu(
userId: string | number | undefined | null,
serverChangeTime: string,
localChangeTime: string
): boolean {
if (!this.isMenuCacheForUser(userId)) return true;
if (serverChangeTime && serverChangeTime !== (localChangeTime || "")) return true;
return false;
},
},
persist: {

7
src/utils/menuCache.ts Normal file
View File

@ -0,0 +1,7 @@
/** 从家长端登录用户信息解析 userId */
export function resolveJzdUserId(user: unknown): string {
if (!user || typeof user !== "object") return "";
const u = user as Record<string, unknown>;
const id = u.userId ?? u.id;
return id != null && id !== "" ? String(id) : "";
}

View File

@ -0,0 +1,8 @@
import type { useCommonStore } from "@/store/modules/common";
type CommonStore = ReturnType<typeof useCommonStore>;
/** Pinia 持久化common.data.dqXqAndZc */
export async function getPersistedDqXqAndZc(store: CommonStore): Promise<any> {
return (await store.getDqXqAndZc()) ?? {};
}