@@ -1,12 +1,11 @@
|
||||
# 开发环境 (dev):仅 vite dev --mode dev 加载;build 产物不读任何 .env.*
|
||||
# (.env.base/.env.ld/.env.prod 已删,线上由 /api/env-config?target=front 运行时下发)。
|
||||
# ⚠️ VITE_* 仅本地开发生效。
|
||||
# API 走 /api 同源相对路径(vite proxy 转发到本地后端 8105),
|
||||
# API 走 /api 同源相对路径(vite proxy 转发到本地后端 8109),
|
||||
# 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境
|
||||
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
||||
# 留空 → serviceHost 走同源相对 /api(经 vite 代理到本地后端 8105);填绝对地址会绕过代理直连该域
|
||||
# 留空 → serviceHost 走同源相对 /api(经 vite 代理到本地后端 8109);填绝对地址会绕过代理直连该域
|
||||
VITE_SERVICE_HOST=
|
||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
||||
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
||||
VITE_IS_MOCK=false
|
||||
|
||||
Generated
+9
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "dorm-notice",
|
||||
"name": "acasyllabu",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dorm-notice",
|
||||
"name": "acasyllabu",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@open-file-viewer/core": "^0.1.43",
|
||||
@@ -15,6 +15,7 @@
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"alova": "^3.4.0",
|
||||
"axios": "^0.24.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
@@ -6456,6 +6457,12 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-image": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
|
||||
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"alova": "^3.4.0",
|
||||
"axios": "^0.24.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
|
||||
+7
-2
@@ -25,6 +25,9 @@ function roleOfPath(p: string): "teacher" | "parent" {
|
||||
}
|
||||
|
||||
async function initShell() {
|
||||
// 深链/刷新时初始导航是异步懒加载,不等就绪会读到初始路径 "/",
|
||||
// 下方「登录后跳转第一个模块」就会把 /schedule-status 等直达路由弹回首页
|
||||
await router.isReady();
|
||||
const role = roleOfPath(router.currentRoute.value.path);
|
||||
try {
|
||||
// 端分离:先按 URL 前缀切换当前视角(teacher/parent),再拉身份与菜单
|
||||
@@ -61,8 +64,10 @@ async function initShell() {
|
||||
|
||||
isLogin.value = true;
|
||||
|
||||
// 登录后跳转第一个可用模块("/" 本身即我发布的页,无需跳转)
|
||||
if (router.currentRoute.value.path === "/") {
|
||||
// 登录后跳转第一个可用模块(仅限落在默认页:"/" 与其静态重定向 /timetable;
|
||||
// 深链到 /schedule-status 等具体页时保持原位不动)
|
||||
const curPath = router.currentRoute.value.path;
|
||||
if (curPath === "/" || curPath === "/timetable") {
|
||||
const firstModule = (modList || [])[0];
|
||||
const firstRaw = firstModule?.children?.length
|
||||
? firstModule.children[0]
|
||||
|
||||
@@ -4,14 +4,16 @@ import conf from "@/assets/config/host.config";
|
||||
import type {
|
||||
ClassTreeNode,
|
||||
EffectiveSettingVo,
|
||||
GradeSettingVo,
|
||||
ModNode,
|
||||
MyClassVo,
|
||||
MyClassesVo,
|
||||
MyTimetableVo,
|
||||
NowYearTermVo,
|
||||
SettingGetVo,
|
||||
SubjectVo,
|
||||
TemplateSaveRequest,
|
||||
TimeSettingVo,
|
||||
TimetableOverviewVo,
|
||||
TimetableSaveRequest,
|
||||
TimetableVo,
|
||||
UpFileResultVo,
|
||||
@@ -27,8 +29,6 @@ import type {
|
||||
*
|
||||
* 约定:
|
||||
* - postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交)。
|
||||
* - postForm 同时把参数拼到 URL query:后端 @RequestParam 都收得到,且本地
|
||||
* vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿到参数(不影响线上)。
|
||||
*/
|
||||
|
||||
const BASE = "/api/base";
|
||||
@@ -56,13 +56,11 @@ function postJSON<T = any>(url: string, data?: Record<string, any> | any[]): Met
|
||||
function postForm<T = any>(url: string, data?: Record<string, any>): Method<T> {
|
||||
const pairs = data ? Object.entries(data).filter(([, v]) => v !== undefined && v !== null) : [];
|
||||
const form = pairs.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
||||
// B1 修复:mock 只解析 query,故仅 mock 模式把参数拼进 URL;真后端只发 body,
|
||||
// 否则 Spring @RequestParam 会把 query 与 body 同名值以逗号合并(单值变 "X,X"、数字变 "1,1" 直接 500)。
|
||||
const fullUrl = conf.isMock && form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url;
|
||||
// 只发 body,不拼 URL query:Spring @RequestParam 会把 query 与 body 同名值以逗号合并(单值变 "X,X" 直接 500)
|
||||
return new Method<T>(
|
||||
"POST",
|
||||
alovaInstance,
|
||||
fullUrl,
|
||||
url,
|
||||
{ headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" } },
|
||||
form || undefined
|
||||
);
|
||||
@@ -83,6 +81,26 @@ export function fileUploadUrl(): string {
|
||||
return `${conf.serviceHost}${FILE}/Upload`;
|
||||
}
|
||||
|
||||
/** 年级照片课表打包下载地址(ZIP,条目按年级+班级名命名;前端 fetch 取 Blob 后落盘) */
|
||||
export function gradePhotoZipUrl(gradeName: string): string {
|
||||
return `${conf.serviceHost}${TIMETABLE}/DownloadGradePhotos?gradeName=${encodeURIComponent(gradeName)}`;
|
||||
}
|
||||
|
||||
/** 课表导入模板下载地址(按班级年级生效节次模板生成 xlsx) */
|
||||
export function timetableImportTemplateUrl(classId: number): string {
|
||||
return `${conf.serviceHost}${TIMETABLE}/ImportTemplate?classId=${classId}`;
|
||||
}
|
||||
|
||||
/** 班级电子课表导出地址(xlsx,当前学年学期) */
|
||||
export function timetableExportUrl(classId: number): string {
|
||||
return `${conf.serviceHost}${TIMETABLE}/Export?classId=${classId}`;
|
||||
}
|
||||
|
||||
/** 课表导入上传地址(FormData:classId + file,整表替换该班当前课表) */
|
||||
export function timetableImportUrl(): string {
|
||||
return `${conf.serviceHost}${TIMETABLE}/Import`;
|
||||
}
|
||||
|
||||
class BaseApi {
|
||||
// ========== 用户信息 & 模块权限 ==========
|
||||
/** 获取当前登录用户信息 */
|
||||
@@ -140,10 +158,16 @@ class SyllabusApi {
|
||||
return get<NowYearTermVo>(`${TIMETABLE}/NowYearTerm`);
|
||||
}
|
||||
|
||||
// ========== 排课情况 ==========
|
||||
/** 全校排课情况汇总(每班电子版 / 照片版编排状态,按年级→班级排序) */
|
||||
overview() {
|
||||
return get<TimetableOverviewVo[]>(`${TIMETABLE}/Overview`);
|
||||
}
|
||||
|
||||
// ========== 我的课表(按角色过滤的只读视图) ==========
|
||||
/** 可查看班级列表(管理员/校领导=全部;班主任=所带;任教=任教班级) */
|
||||
/** 可查看班级列表 + 身份标识(管理员/班主任/任教老师,前端按身份切换视图) */
|
||||
myClasses() {
|
||||
return get<MyClassVo[]>(`${TIMETABLE}/MyClasses`);
|
||||
return get<MyClassesVo>(`${TIMETABLE}/MyClasses`);
|
||||
}
|
||||
/** 班级课表(权限校验;任教老师只返回任教科目格) */
|
||||
myGet(classId: number) {
|
||||
@@ -171,10 +195,26 @@ class SyllabusApi {
|
||||
deleteTemplate(id: number) {
|
||||
return postForm<void>(`${SETTING}/DeleteTemplate`, { id });
|
||||
}
|
||||
/** 保存节次模板的「显示全部/只显示节次」偏好(绑定模板记忆,预览课表/我的课表同随此值) */
|
||||
saveTemplateShowBreakRows(id: number, showBreakRows: boolean) {
|
||||
return postForm<void>(`${SETTING}/SaveTemplateShowBreakRows`, { id, showBreakRows });
|
||||
}
|
||||
/** 保存节次模板的「节次名称 显示/隐藏」偏好(绑定模板记忆,预览课表/我的课表同随此值) */
|
||||
saveTemplateShowPeriodName(id: number, showPeriodName: boolean) {
|
||||
return postForm<void>(`${SETTING}/SaveTemplateShowPeriodName`, { id, showPeriodName });
|
||||
}
|
||||
/** 课表编辑页生效设置:时间边界 + 当前学期下指定年级启用模板的行(term 1=上学期 2=下学期) */
|
||||
effectiveForGrade(gradeName: string, term?: number) {
|
||||
return get<EffectiveSettingVo>(`${SETTING}/ForGrade`, { gradeName, term });
|
||||
}
|
||||
/** 年级设置列表(启用 + 使用电子版/照片版;未配置的年级默认全启用) */
|
||||
gradeSettings() {
|
||||
return get<GradeSettingVo[]>(`${SETTING}/Grades`);
|
||||
}
|
||||
/** 批量保存年级设置(按年级名 upsert;作用于「我的课表」电子/照片视图切换) */
|
||||
saveGradeSettings(data: GradeSettingVo[]) {
|
||||
return postJSON<void>(`${SETTING}/SaveGrades`, data);
|
||||
}
|
||||
|
||||
// ========== 附件 ==========
|
||||
fileDownUrl = fileDownUrl;
|
||||
|
||||
@@ -2,7 +2,7 @@ import config from "./host.config";
|
||||
import { AxiosRequestConfig } from "axios";
|
||||
import { PcDebugUser } from "@/types/Basics";
|
||||
|
||||
const serviceUrl = config.isMock ? "" : config.serviceHost,
|
||||
const serviceUrl = config.serviceHost,
|
||||
timeout: number = 15000;
|
||||
|
||||
let baseConfig: AxiosRequestConfig = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseConf } from "@/types/Basics";
|
||||
* ① window.__APP_CONFIG__:线上运行时配置,index.html 引导脚本在应用模块加载前同步拉取
|
||||
* /<contextPath>/api/env-config?target=<部署目录名>(本应用部署于 /{contextPath}/front/,
|
||||
* 故为 front,对应后端键 yh.env-config.source.front,projman 配置管理维护)写入
|
||||
* ② import.meta.env.VITE_*:仅本地开发/演示(vite dev --mode dev 读 .env.dev、--mode mock 读 .env.mock);
|
||||
* ② import.meta.env.VITE_*:仅本地开发(vite dev --mode dev 读 .env.dev);
|
||||
* build 产物不读任何 .env.*(.env.base/.env.ld/.env.prod 已删),VITE_* 恒为 undefined,
|
||||
* 本层自动跳过,产物不含任何环境值
|
||||
* ③ 同域推导兜底:env-config 未配置/拉取失败时框架至少能渲染并调通同源 API;
|
||||
@@ -14,7 +14,7 @@ import { BaseConf } from "@/types/Basics";
|
||||
const env = import.meta.env;
|
||||
|
||||
// ③ 同域推导兜底:context 段取当前路径首段(/{contextPath}/front/ → contextPath,即后端上下文),
|
||||
// 与服务端 context-path 对齐(本地无、base/prod /familyactivity);本地 dev/mock 在根路径运行(无段),
|
||||
// 与服务端 context-path 对齐(本地无、base/prod /familyactivity);本地 dev 在根路径运行(无段),
|
||||
// serviceHost 留空走 vite 同源代理(规避 CAS 过滤器不回 CORS 头)
|
||||
const seg = window.location.pathname.split("/").filter(Boolean);
|
||||
const origin = window.location.origin;
|
||||
@@ -27,7 +27,6 @@ let conf: BaseConf = {
|
||||
ziyuankuHost: "",
|
||||
zhuanxiangHost: "",
|
||||
},
|
||||
isMock: false,
|
||||
isDebug: env.DEV,
|
||||
};
|
||||
|
||||
@@ -41,16 +40,15 @@ const applyConf = (target: any, source: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
// ② 本地开发/演示(build 产物中恒 undefined → 自动跳过)
|
||||
// ② 本地开发(build 产物中恒 undefined → 自动跳过)
|
||||
applyConf(conf, {
|
||||
baseHost: env.VITE_BASE_HOST,
|
||||
ssoHost: env.VITE_SSO_HOST,
|
||||
serviceHost: env.VITE_IS_MOCK === "true" ? "" : env.VITE_SERVICE_HOST,
|
||||
serviceHost: env.VITE_SERVICE_HOST,
|
||||
relayConf: {
|
||||
ziyuankuHost: env.VITE_ZIYUANKU_HOST,
|
||||
zhuanxiangHost: env.VITE_ZHUANXIANG_HOST,
|
||||
},
|
||||
isMock: env.VITE_IS_MOCK === "true",
|
||||
});
|
||||
|
||||
// ① 线上运行时配置(最高优先,覆盖前面所有);relayConf 为嵌套对象,按子键合并,
|
||||
|
||||
Vendored
+4
-11
@@ -6,7 +6,6 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const $$userConfigMap: typeof import('./api/index')['$$userConfigMap']
|
||||
const Button: typeof import('view-ui-plus')['Button']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const Form: typeof import('view-ui-plus')['Form']
|
||||
@@ -16,23 +15,18 @@ declare global {
|
||||
const Modal: typeof import('view-ui-plus')['Modal']
|
||||
const Select: typeof import('view-ui-plus')['Select']
|
||||
const Table: typeof import('view-ui-plus')['Table']
|
||||
const alovaInstance: typeof import('./api/index')['alovaInstance']
|
||||
const api: typeof import('./api/index')['default']
|
||||
const apiDefinitions: typeof import('./api/apiDefinitions')['default']
|
||||
const baseApi: typeof import('./api/module/syllabus')['baseApi']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApis: typeof import('./api/createApis')['createApis']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const dorm: typeof import('./api/module/dorm')['default']
|
||||
const dormApi: typeof import('./api/module/dorm')['dormApi']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const filePreviewUrl: typeof import('./api/module/syllabus')['filePreviewUrl']
|
||||
const fileUploadUrl: typeof import('./api/module/syllabus')['fileUploadUrl']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const gradePhotoZipUrl: typeof import('./api/module/syllabus')['gradePhotoZipUrl']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
@@ -40,9 +34,7 @@ declare global {
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const mountApis: typeof import('./api/createApis')['mountApis']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const noticeApi: typeof import('./api/module/dorm')['noticeApi']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
@@ -64,12 +56,14 @@ declare global {
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const settingApi: typeof import('./api/module/dorm')['settingApi']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const syllabus: typeof import('./api/module/syllabus')['default']
|
||||
const syllabusApi: typeof import('./api/module/syllabus')['syllabusApi']
|
||||
const timetableExportUrl: typeof import('./api/module/syllabus')['timetableExportUrl']
|
||||
const timetableImportTemplateUrl: typeof import('./api/module/syllabus')['timetableImportTemplateUrl']
|
||||
const timetableImportUrl: typeof import('./api/module/syllabus')['timetableImportUrl']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
@@ -94,7 +88,6 @@ declare global {
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const withConfigType: typeof import('./api/createApis')['withConfigType']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isMultiple: !this.multiple || false,
|
||||
uploadFileUrl: (config.isMock ? "" : config.serviceHost) + "/api/base/FileUpload",
|
||||
uploadFileUrl: config.serviceHost + "/api/base/FileUpload",
|
||||
formatArr: ["jpg", "gif", "png", "bmp", "xls", "doc", "txt", "docx", "xlsx", "zip", "rar", "pdf", "ppt", "pptx", "wps"],
|
||||
maxFileSize: "200MB",
|
||||
maxSize: 204800,
|
||||
|
||||
@@ -40,7 +40,7 @@ const previewName = ref("");
|
||||
const fileType = ref("");
|
||||
const blob = ref<Blob | null>(null);
|
||||
|
||||
const fileServiceBase = config.isMock ? "" : config.serviceHost;
|
||||
const fileServiceBase = config.serviceHost;
|
||||
/** 内联流地址(<video> 等场景用;Down 是 attachment 头,视频流不可用) */
|
||||
const previewUrl = computed(() => (props.fileId ? `${fileServiceBase}/api/file/Preview?id=${props.fileId}` : ""));
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: { title: "课程表编排", modId: 107 },
|
||||
},
|
||||
|
||||
// 排课情况(全校班级电子版/照片版编排状态总览;parentId 挂靠课表编排菜单高亮)
|
||||
{
|
||||
path: "/schedule-status",
|
||||
name: "timetable_overview",
|
||||
component: () => import("@/views/syllabus/TimetableOverview.vue"),
|
||||
meta: { title: "排课情况", parentId: 107 },
|
||||
},
|
||||
|
||||
// 我的课表(按角色只读查看:管理员/校领导=全部班级,班主任=所带班级,任教老师=任教班级任教科目)
|
||||
{
|
||||
path: "/mine",
|
||||
|
||||
Vendored
-1
@@ -29,7 +29,6 @@ export interface BaseConf {
|
||||
ssoHost: string;
|
||||
serviceHost: string;
|
||||
relayConf: RelayConf;
|
||||
isMock: Boolean;
|
||||
isDebug?: Boolean;
|
||||
[keys: string]: any; //任意属性
|
||||
}
|
||||
|
||||
@@ -73,6 +73,23 @@ export interface TimetableSaveRequest {
|
||||
slots: TimetableSlotVo[];
|
||||
}
|
||||
|
||||
/** 排课情况(按班级汇总电子版 / 照片版编排状态) */
|
||||
export interface TimetableOverviewVo {
|
||||
classId: number;
|
||||
gradeName: string | null;
|
||||
className: string | null;
|
||||
/** 电子版是否已设置(当前学年学期已保存过节次) */
|
||||
electronicSet: boolean;
|
||||
/** 已排节次数 */
|
||||
periodCount: number;
|
||||
/** 已排科目数(去重) */
|
||||
subjectCount: number;
|
||||
/** 照片版是否已上传 */
|
||||
photoSet: boolean;
|
||||
/** 照片附件ID(缩略图预览用,未上传为 null) */
|
||||
photoFileId: number | null;
|
||||
}
|
||||
|
||||
// ==================== 我的课表(按角色过滤的只读视图) ====================
|
||||
|
||||
/**
|
||||
@@ -97,6 +114,21 @@ export interface MyTimetableVo extends TimetableVo {
|
||||
mySubjects: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 「我的课表」班级列表 + 当前用户身份标识。
|
||||
* 身份可叠加(如管理员同时是班主任/任教),前端按 管理员>班主任>任教老师 优先级展示默认视图。
|
||||
*/
|
||||
export interface MyClassesVo {
|
||||
/** 是否管理员/校领导身份 */
|
||||
admin: boolean;
|
||||
/** 是否班主任身份(任一班的正/副班主任是本人) */
|
||||
headTeacher: boolean;
|
||||
/** 是否任教老师身份(任一班有任教科目) */
|
||||
subjectTeacher: boolean;
|
||||
/** 可查看班级(管理员=全校;班主任/任教=所带∪任教;学生=本人班级) */
|
||||
classes: MyClassVo[];
|
||||
}
|
||||
|
||||
// ==================== 应用设置(时段判定 + 节次模板) ====================
|
||||
|
||||
/**
|
||||
@@ -150,6 +182,10 @@ export interface PeriodTemplateVo {
|
||||
periodMinutes: number;
|
||||
/** 课间时长(分钟),模板加空白行的默认用时 */
|
||||
breakMinutes: number;
|
||||
/** 课表页是否显示空白行(课间/午休):false=只显示节次,随模板保存(预览课表/我的课表同随此值) */
|
||||
showBreakRows: boolean;
|
||||
/** 课表页是否显示「节次」列(节次名称):false=隐藏,随模板保存(预览课表/我的课表同随此值) */
|
||||
showPeriodName: boolean;
|
||||
rows: TemplateRowVo[];
|
||||
}
|
||||
|
||||
@@ -175,6 +211,10 @@ export interface TemplateSaveRequest {
|
||||
periodMinutes: number;
|
||||
/** 课间时长(分钟) */
|
||||
breakMinutes: number;
|
||||
/** 课表页是否显示空白行(课间/午休):false=只显示节次,缺省沿用现值 */
|
||||
showBreakRows?: boolean;
|
||||
/** 课表页是否显示「节次」列(节次名称):false=隐藏,缺省沿用现值 */
|
||||
showPeriodName?: boolean;
|
||||
rows: TemplateRowVo[];
|
||||
}
|
||||
|
||||
@@ -186,6 +226,26 @@ export interface EffectiveSettingVo {
|
||||
teachDays: string;
|
||||
/** 生效模板名称(如 春),无启用模板时为空 */
|
||||
templateName: string | null;
|
||||
/** 生效模板ID(「显示全部/只显示节次」开关回存用),无启用模板时为空 */
|
||||
templateId: number | null;
|
||||
/** 课表页是否显示空白行(课间/午休):false=只显示节次,随生效模板记忆 */
|
||||
showBreakRows: boolean;
|
||||
/** 课表页是否显示「节次」列(节次名称):false=隐藏,随生效模板记忆 */
|
||||
showPeriodName: boolean;
|
||||
/** 年级是否启用课表查看(年级设置,未配置视为启用) */
|
||||
gradeEnabled: boolean;
|
||||
/** 该年级是否使用电子课表(未配置视为使用) */
|
||||
useElectronic: boolean;
|
||||
/** 该年级是否使用照片课表(未配置视为使用) */
|
||||
usePhoto: boolean;
|
||||
}
|
||||
|
||||
/** 年级应用设置(启用 + 使用电子版/照片版;作用于「我的课表」电子/照片视图切换) */
|
||||
export interface GradeSettingVo {
|
||||
gradeName: string;
|
||||
enabled: boolean;
|
||||
useElectronic: boolean;
|
||||
usePhoto: boolean;
|
||||
}
|
||||
|
||||
// ==================== 通用 ====================
|
||||
|
||||
@@ -25,13 +25,15 @@
|
||||
<th>启用时段</th>
|
||||
<th>节次数</th>
|
||||
<th>空白行</th>
|
||||
<th>课表显示</th>
|
||||
<th>节次名称</th>
|
||||
<th>启用</th>
|
||||
<th class="st-col-op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!templates.length">
|
||||
<td colspan="8">
|
||||
<td colspan="10">
|
||||
<div class="st-empty">
|
||||
<b>还没有节次模板</b><br />点击「新增模板」创建(每个启用时段默认一条节次),启用后课表编排页即按模板生成节次。
|
||||
</div>
|
||||
@@ -44,6 +46,28 @@
|
||||
<td>{{ bandsText(tpl.enabledBands) }}</td>
|
||||
<td class="num">{{ periodCount(tpl.rows) }} 节</td>
|
||||
<td class="num">{{ breakCount(tpl.rows) }} 行</td>
|
||||
<td>
|
||||
<label
|
||||
class="st-switch"
|
||||
:class="{ on: tpl.showBreakRows !== false }"
|
||||
title="课表编排/预览课表/我的课表是否显示课间等空白行,切换即保存"
|
||||
>
|
||||
<input type="checkbox" :checked="tpl.showBreakRows !== false" @change="toggleShowBreakRows(tpl)" />
|
||||
<span class="track"><span class="thumb"></span></span>
|
||||
{{ tpl.showBreakRows !== false ? "显示全部" : "只显示节次" }}
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<label
|
||||
class="st-switch"
|
||||
:class="{ on: tpl.showPeriodName !== false }"
|
||||
title="课表编排预览/我的课表是否显示「节次」列(第一节/课间等名称),切换即保存"
|
||||
>
|
||||
<input type="checkbox" :checked="tpl.showPeriodName !== false" @change="toggleShowPeriodName(tpl)" />
|
||||
<span class="track"><span class="thumb"></span></span>
|
||||
{{ tpl.showPeriodName !== false ? "显示" : "隐藏" }}
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<label class="st-switch" :class="{ on: tpl.enabled }">
|
||||
<input type="checkbox" :checked="tpl.enabled" @change="toggleEnabled(tpl)" />
|
||||
@@ -61,7 +85,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="st-hintbar">
|
||||
<span>停用时段的节次行不在此维护、课表页也不显示;节次名称可自定义,留空按时段内顺序显示第X节;空白行(课间 / 午休)同样落在时段里,可自定义名称、备注与时间,不占用节次序号、不能排课程。</span>
|
||||
<span>停用时段的节次行不在此维护、课表页也不显示;节次名称可自定义,留空按时段内顺序显示第X节;空白行(课间 / 午休)同样落在时段里,可自定义名称、备注与时间,不占用节次序号、不能排课程;「课表显示」决定课表编排 / 预览课表 / 我的课表是否显示空白行,「节次名称」决定预览课表 / 我的课表是否显示「节次」列,切换即存。</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -102,14 +126,14 @@
|
||||
{{ g }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="st-field st-days sp5">
|
||||
<div class="st-field st-days sp4">
|
||||
<span>教学日</span>
|
||||
<label v-for="(d, i) in DAY_SHORT" :key="d" class="st-day" :class="{ on: draft.teachDays[i] }">
|
||||
<input type="checkbox" :checked="draft.teachDays[i]" @change="toggleDay(i)" />
|
||||
{{ d }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="st-field st-days sp5">
|
||||
<div class="st-field st-days sp4">
|
||||
<span>时段启用</span>
|
||||
<label
|
||||
v-for="b in BANDS"
|
||||
@@ -123,13 +147,23 @@
|
||||
<i></i>{{ b.label }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="st-field st-switch-field">
|
||||
<span>模板启用</span>
|
||||
<label class="st-switch" :class="{ on: draft.enabled }">
|
||||
<input type="checkbox" :checked="draft.enabled" @change="draft.enabled = !draft.enabled" />
|
||||
<span class="track"><span class="thumb"></span></span>
|
||||
{{ draft.enabled ? "启用" : "停用" }}
|
||||
</label>
|
||||
<div class="st-display-prefs">
|
||||
<div class="st-field st-switch-field">
|
||||
<span title="课表编排预览/我的课表是否显示「节次」列(第一节/课间等名称)">节次名称</span>
|
||||
<label class="st-switch" :class="{ on: draft.showPeriodName }" title="显示:课表出现「节次」列;隐藏:只看时段/学科/时间更简洁,预览课表与我的课表同随此值">
|
||||
<input type="checkbox" :checked="draft.showPeriodName" @change="draft.showPeriodName = !draft.showPeriodName" />
|
||||
<span class="track"><span class="thumb"></span></span>
|
||||
{{ draft.showPeriodName ? "显示" : "隐藏" }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="st-field st-switch-field">
|
||||
<span title="课表编排/预览课表/我的课表是否显示课间等空白行">课表显示</span>
|
||||
<label class="st-switch" :class="{ on: draft.showBreakRows }" title="显示全部:课表页显示课间等空白行;只显示节次:隐藏空白行更紧凑,预览课表与我的课表同随此值">
|
||||
<input type="checkbox" :checked="draft.showBreakRows" @change="draft.showBreakRows = !draft.showBreakRows" />
|
||||
<span class="track"><span class="thumb"></span></span>
|
||||
{{ draft.showBreakRows ? "显示全部" : "只显示节次" }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,6 +260,14 @@
|
||||
{{ fullScreen ? "退出全屏" : "全屏" }}
|
||||
</button>
|
||||
<span class="st-flex"></span>
|
||||
<div class="st-field st-switch-field">
|
||||
<span>模板启用</span>
|
||||
<label class="st-switch" :class="{ on: draft.enabled }" title="启用后学期重叠(全年与上/下学期互通)且年级有交叉的其它模板自动停用">
|
||||
<input type="checkbox" :checked="draft.enabled" @change="draft.enabled = !draft.enabled" />
|
||||
<span class="track"><span class="thumb"></span></span>
|
||||
{{ draft.enabled ? "启用" : "停用" }}
|
||||
</label>
|
||||
</div>
|
||||
<button class="st-btn ghost" type="button" title="预览该模板生成的节次骨架(含教学日列)" @click="previewOpen = true">预览</button>
|
||||
<button class="st-btn primary" type="button" :disabled="savingTpl" @click="saveDraft">
|
||||
{{ savingTpl ? "保存中…" : "保存模板" }}
|
||||
@@ -255,7 +297,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="h-band">时段</th>
|
||||
<th class="h-slot">节次</th>
|
||||
<th v-if="previewShowName" class="h-slot">节次</th>
|
||||
<!-- 星期列即该模板绑定的教学日:只列教学日,且不做周末特殊标记 -->
|
||||
<th v-for="d in previewDays" :key="d">{{ DAY_SHORT[d] }}</th>
|
||||
<th class="h-time">时间</th>
|
||||
@@ -263,22 +305,22 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!draft.rows.length">
|
||||
<td :colspan="previewDays.length + 3" class="st-sheet-empty">还没有行,添加节次或空白行后可在此预览。</td>
|
||||
<td :colspan="previewDays.length + (previewShowName ? 3 : 2)" class="st-sheet-empty">还没有行,添加节次或空白行后可在此预览。</td>
|
||||
</tr>
|
||||
<template v-for="(er, i) in editorRows" :key="i">
|
||||
<tr v-if="er.rowType === 'period'">
|
||||
<td v-if="er.bandFirst" :rowspan="er.bandRows" class="rail" :style="bandStyle(er.band)">{{ bandLabel(er.band) }}</td>
|
||||
<td class="slot">{{ draft.rows[i].name || "第" + cnNum((er.bandIndex || er.periodNo) - 1) + "节" }}</td>
|
||||
<template v-for="(p, idx) in previewRows" :key="idx">
|
||||
<tr v-if="p.er.rowType === 'period'">
|
||||
<td v-if="p.er.bandFirst" :rowspan="p.er.bandRows" class="rail" :style="bandStyle(p.er.band)">{{ bandLabel(p.er.band) }}</td>
|
||||
<td v-if="previewShowName" class="slot">{{ p.row.name || "第" + cnNum((p.er.bandIndex || p.er.periodNo) - 1) + "节" }}</td>
|
||||
<td v-for="d in previewDays" :key="d" class="blank">—</td>
|
||||
<td class="time num">{{ draft.rows[i].start || "--:--" }} – {{ draft.rows[i].end || "--:--" }}</td>
|
||||
<td class="time num">{{ p.row.start || "--:--" }} – {{ p.row.end || "--:--" }}</td>
|
||||
</tr>
|
||||
<tr v-else class="brk">
|
||||
<td v-if="er.bandFirst" :rowspan="er.bandRows" class="rail" :style="bandStyle(er.band)">{{ bandLabel(er.band) }}</td>
|
||||
<td :colspan="er.band !== null ? previewDays.length + 1 : previewDays.length + 2" class="brk">
|
||||
<span class="tag"><i></i>{{ draft.rows[i].name || "课间" }}</span>
|
||||
<span v-if="draft.rows[i].remark" class="cnt" :class="{ long: isLongRemark(draft.rows[i].remark) }">{{ draft.rows[i].remark }}</span>
|
||||
<td v-if="p.er.bandFirst" :rowspan="p.er.bandRows" class="rail" :style="bandStyle(p.er.band)">{{ bandLabel(p.er.band) }}</td>
|
||||
<td :colspan="(p.er.band !== null ? previewDays.length + 1 : previewDays.length + 2) - (previewShowName ? 0 : 1)" class="brk">
|
||||
<span class="tag"><i></i>{{ p.row.name || "课间" }}</span>
|
||||
<span v-if="p.row.remark" class="cnt" :class="{ long: isLongRemark(p.row.remark) }">{{ p.row.remark }}</span>
|
||||
</td>
|
||||
<td class="time num">{{ draft.rows[i].start || "--:--" }} – {{ draft.rows[i].end || "--:--" }}</td>
|
||||
<td class="time num">{{ p.row.start || "--:--" }} – {{ p.row.end || "--:--" }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
@@ -348,6 +390,10 @@ interface DraftTemplate {
|
||||
periodMinutes: number;
|
||||
/** 课间时长(分钟):模板加空白行的默认用时 */
|
||||
breakMinutes: number;
|
||||
/** 课表页是否显示空白行(课间/午休):false=只显示节次,随模板保存 */
|
||||
showBreakRows: boolean;
|
||||
/** 课表页是否显示「节次」列(节次名称):false=隐藏,随模板保存 */
|
||||
showPeriodName: boolean;
|
||||
rows: DraftRow[];
|
||||
}
|
||||
|
||||
@@ -387,6 +433,8 @@ const previewMeta = computed(() => {
|
||||
`启用时段:${bandsText(d.enabledBands)}`,
|
||||
`教学日:${days}`,
|
||||
`节次 ${d.periodMinutes} 分钟 / 课间 ${d.breakMinutes} 分钟`,
|
||||
`课表显示:${d.showBreakRows ? "显示全部" : "只显示节次"}`,
|
||||
`节次名称:${d.showPeriodName ? "显示" : "隐藏"}`,
|
||||
].join(" · ");
|
||||
});
|
||||
|
||||
@@ -502,15 +550,14 @@ function rawBandIndex(start: string): number | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器行派生:每行附时段与分组信息(与 draft.rows 逐位对齐)。
|
||||
* 编辑器行分组:每行附时段与分组信息(与入参行序逐位对齐)。
|
||||
* 行的所属时段取自行数据(band),开始时间不参与时段归属;
|
||||
* 相邻同时段的行(节次与空白行都算)归入一组,组内首行以 rowspan 竖排合并显示时段名
|
||||
* ——空白行同样落在时段里,不单独脱离分组。
|
||||
* bandIndex:节次在该时段组内的顺序(1 起),节次默认名按此时段内自增。
|
||||
*/
|
||||
const editorRows = computed(() => {
|
||||
function groupEditorRows(rows: DraftRow[]) {
|
||||
type Entry = { i: number; band: BandKey | null; gi: number | null };
|
||||
const rows = draft.value?.rows || [];
|
||||
const entries: Entry[] = [];
|
||||
let gi = 0;
|
||||
let prev: BandKey | null = null;
|
||||
@@ -561,8 +608,22 @@ const editorRows = computed(() => {
|
||||
periodNo,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑器行表(与 draft.rows 逐位对齐) */
|
||||
const editorRows = computed(() => groupEditorRows(draft.value?.rows || []));
|
||||
|
||||
/** 模板预览行:跟随「课表显示」偏好——只显示节次时按剔除空白行后的行重新分组(时段 rowspan 相应重算) */
|
||||
const previewRows = computed(() => {
|
||||
const rows = draft.value?.rows || [];
|
||||
const shown = draft.value?.showBreakRows === false ? rows.filter((r) => r.rowType === "period") : rows;
|
||||
// groupEditorRows 输出与入参行逐位对齐,按下标配对取回原始行
|
||||
return groupEditorRows(shown).map((er, i) => ({ er, row: shown[i] }));
|
||||
});
|
||||
|
||||
/** 模板预览是否显示「节次」列:跟随「节次名称」偏好(与预览课表/我的课表观感一致) */
|
||||
const previewShowName = computed(() => draft.value?.showPeriodName !== false);
|
||||
|
||||
/**
|
||||
* 行用时默认值(分钟):取当前模板的「节次时间 / 课间时间」,
|
||||
* 编辑对象即该模板,时长自然绑定模板。
|
||||
@@ -783,6 +844,9 @@ function openNew() {
|
||||
enabledBands: bands,
|
||||
periodMinutes,
|
||||
breakMinutes,
|
||||
// 显示偏好以参照的全年模板为准(无则显示全部、显示节次名称)
|
||||
showBreakRows: src ? src.showBreakRows !== false : true,
|
||||
showPeriodName: src ? src.showPeriodName !== false : true,
|
||||
rows: src ? preparedRows(toDraftRows(src.rows), bands) : defaultRows(),
|
||||
};
|
||||
}
|
||||
@@ -800,6 +864,8 @@ function openCopy(tpl: PeriodTemplateVo) {
|
||||
enabledBands: bands,
|
||||
periodMinutes: templateMinutes(tpl.periodMinutes, 45),
|
||||
breakMinutes: templateMinutes(tpl.breakMinutes, 10),
|
||||
showBreakRows: tpl.showBreakRows !== false,
|
||||
showPeriodName: tpl.showPeriodName !== false,
|
||||
rows: preparedRows(toDraftRows(tpl.rows), bands),
|
||||
};
|
||||
}
|
||||
@@ -816,6 +882,8 @@ function openEdit(tpl: PeriodTemplateVo) {
|
||||
enabledBands: bands,
|
||||
periodMinutes: templateMinutes(tpl.periodMinutes, 45),
|
||||
breakMinutes: templateMinutes(tpl.breakMinutes, 10),
|
||||
showBreakRows: tpl.showBreakRows !== false,
|
||||
showPeriodName: tpl.showPeriodName !== false,
|
||||
rows: preparedRows(toDraftRows(tpl.rows), bands),
|
||||
};
|
||||
}
|
||||
@@ -894,6 +962,40 @@ function removeTemplate(tpl: PeriodTemplateVo) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模板的「课表显示」(显示全部/只显示节次):轻量接口直接落库到该模板,
|
||||
* 课表编排页开关、预览课表与我的课表同随此值。
|
||||
*/
|
||||
async function toggleShowBreakRows(tpl: PeriodTemplateVo) {
|
||||
const next = tpl.showBreakRows === false;
|
||||
try {
|
||||
await syllabusApi.saveTemplateShowBreakRows(tpl.id, next);
|
||||
Message.success(`「${tpl.name}」课表页已设为${next ? "显示全部" : "只显示节次"}`);
|
||||
await loadSetting();
|
||||
} catch (e) {
|
||||
console.error("保存课表显示偏好失败", e);
|
||||
Message.error("课表显示保存失败,请重试");
|
||||
await loadSetting();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模板的「节次名称」(显示/隐藏):轻量接口直接落库到该模板,
|
||||
* 课表编排预览课表与我的课表同随此值(课表编排编辑表不受影响)。
|
||||
*/
|
||||
async function toggleShowPeriodName(tpl: PeriodTemplateVo) {
|
||||
const next = tpl.showPeriodName === false;
|
||||
try {
|
||||
await syllabusApi.saveTemplateShowPeriodName(tpl.id, next);
|
||||
Message.success(`「${tpl.name}」课表已${next ? "显示" : "隐藏"}节次名称`);
|
||||
await loadSetting();
|
||||
} catch (e) {
|
||||
console.error("保存节次名称显示偏好失败", e);
|
||||
Message.error("节次名称保存失败,请重试");
|
||||
await loadSetting();
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 模板编辑器
|
||||
* ============================================================ */
|
||||
@@ -1233,6 +1335,9 @@ async function saveDraft() {
|
||||
enabledBands: d.enabledBands,
|
||||
periodMinutes: pMin,
|
||||
breakMinutes: bMin,
|
||||
// 课表页显示偏好随模板保存
|
||||
showBreakRows: d.showBreakRows,
|
||||
showPeriodName: d.showPeriodName,
|
||||
rows: d.rows.map((r) => ({
|
||||
rowType: r.rowType,
|
||||
// 所属时段随行保存(节次与空白行都落在时段里)
|
||||
@@ -1739,7 +1844,7 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
/* 两行各 12 栏铺满:
|
||||
① 名称3 · 学期2 · 节次时间2 · 课间时间2 · 年级3
|
||||
② 教学日5 · 时段启用5 · 模板启用2 */
|
||||
② 教学日5 · 时段启用5 · 显示偏好2(「节次名称」「课表显示」上下两行,「模板启用」已移至底部操作栏) */
|
||||
> .st-field {
|
||||
grid-column: span 2;
|
||||
min-width: 0;
|
||||
@@ -1747,15 +1852,23 @@ onBeforeUnmount(() => {
|
||||
> .st-field.sp3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
> .st-field.sp5 {
|
||||
> .st-field.sp4 {
|
||||
grid-column: span 5;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
> .st-display-prefs {
|
||||
grid-column: span 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.st-editor-fields > .st-field,
|
||||
.st-editor-fields > .st-field.sp3,
|
||||
.st-editor-fields > .st-field.sp5 {
|
||||
.st-editor-fields > .st-field.sp4,
|
||||
.st-editor-fields > .st-display-prefs {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
+324
-246
@@ -1,34 +1,30 @@
|
||||
<template>
|
||||
<div class="mt-page">
|
||||
<!-- 页头:标题 + 统计 -->
|
||||
<div class="mt-pagehead">
|
||||
<div>
|
||||
<h1 class="mt-h1">我的课表</h1>
|
||||
<p class="mt-sub">管理员/校领导查看全校班级,班主任查看所带班级,任教老师查看任教班级的任教科目,学生查看自己班级。</p>
|
||||
</div>
|
||||
<div class="mt-stats">
|
||||
<div class="mt-stat"><b>{{ yearTermText }}</b><span>当前学年学期</span></div>
|
||||
<div class="mt-stat"><b class="num">{{ classes.length }} 个</b><span>可查看班级</span></div>
|
||||
<div class="mt-stat"><b>{{ scopeText }}</b><span>查看范围</span></div>
|
||||
<div class="mt-stat"><b>{{ activeClassName || "未选择" }}</b><span>当前班级</span></div>
|
||||
<div class="mt-stat"><b class="num">{{ filledCount }} 门</b><span>已排课程</span></div>
|
||||
<div class="mt-stat"><b>{{ photo ? "已上传" : "未上传" }}</b><span>照片课表</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="mt-card">
|
||||
<!-- 工具条:班级 tab + 操作 -->
|
||||
<!-- 工具条:身份视图切换 + 班级 tab + 操作 -->
|
||||
<div class="mt-card-tools">
|
||||
<!-- 身份视图切换:管理员 > 班主任 > 任教老师(多身份时展示,单身份固定无需切换) -->
|
||||
<div v-if="showViewSwitcher" class="mt-idtabs" role="tablist" aria-label="身份视图切换">
|
||||
<button
|
||||
v-for="v in availableViews"
|
||||
:key="v.key"
|
||||
role="tab"
|
||||
type="button"
|
||||
:aria-selected="viewMode === v.key"
|
||||
:class="{ on: viewMode === v.key }"
|
||||
@click="selectView(v.key)"
|
||||
>{{ v.label }}</button>
|
||||
</div>
|
||||
<div class="mt-classtabs" role="tablist" aria-label="班级切换">
|
||||
<template v-if="classes.length">
|
||||
<button
|
||||
v-for="cls in classes"
|
||||
v-for="cls in viewClasses"
|
||||
:key="cls.classId"
|
||||
role="tab"
|
||||
type="button"
|
||||
:aria-selected="cls.classId === activeId"
|
||||
:class="{ on: cls.classId === activeId }"
|
||||
:title="cls.viewAllSubjects ? '可查看全部科目' : '仅显示任教科目:' + (cls.subjects || []).join('、')"
|
||||
:title="tabTitle(cls)"
|
||||
@click="selectClass(cls.classId)"
|
||||
>
|
||||
{{ classLabel(cls) }}
|
||||
@@ -38,8 +34,12 @@
|
||||
<span v-else-if="!loading" class="mt-tabs-empty">暂无可查看的班级</span>
|
||||
</div>
|
||||
<div class="mt-tools-group">
|
||||
<button v-if="photo" class="mt-btn ghost" type="button" @click="photoOpen = true">照片课表</button>
|
||||
<button class="mt-btn primary" type="button" :disabled="!activeId" @click="print">打印课表</button>
|
||||
<button v-if="gradeEnabled && useElectronic && view === 'electronic'" class="mt-btn primary" type="button" :disabled="!activeId" @click="print">打印课表</button>
|
||||
<!-- 电子课表 / 照片课表 视图切换:按年级设置勾选项展示(只开一个时无需切换) -->
|
||||
<div v-if="showViewTabs" class="mt-viewtabs" role="tablist" aria-label="课表版本切换">
|
||||
<button role="tab" type="button" :aria-selected="view === 'electronic'" :class="{ on: view === 'electronic' }" @click="view = 'electronic'">电子课表</button>
|
||||
<button role="tab" type="button" :aria-selected="view === 'photo'" :class="{ on: view === 'photo' }" @click="view = 'photo'">照片课表</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,30 +51,59 @@
|
||||
管理员/校领导需确认学校已建班级;老师需确认已在班级服务中维护班主任或任教关系;学生需确认学籍班级已维护。
|
||||
</div>
|
||||
<div v-else-if="!activeId" class="mt-empty"><b>请选择班级</b></div>
|
||||
<!-- 年级设置(排课情况页按年级统一配置):未启用 / 电子版照片版均未勾选时不展示课表 -->
|
||||
<div v-else-if="!gradeEnabled" class="mt-empty">
|
||||
<b>当前年级未启用课表查看</b><br />
|
||||
请联系管理员在「排课情况」对应年级打开「启用」开关。
|
||||
</div>
|
||||
<div v-else-if="!useElectronic && !usePhoto" class="mt-empty">
|
||||
<b>当前年级未开放课表查看</b><br />
|
||||
请联系管理员在「排课情况」对应年级勾选「使用电子版 / 使用照片版」。
|
||||
</div>
|
||||
<!-- 照片课表视图 -->
|
||||
<div v-else-if="view === 'photo'" class="mt-photo-pane">
|
||||
<img v-if="photoUrl" :src="photoUrl" :alt="(activeClassName || '') + ' 课程表照片'" />
|
||||
<div v-else class="mt-empty">
|
||||
<b>本班尚未上传照片课表</b><br />
|
||||
请到「课程表编排 → 照片版」上传后再查看。
|
||||
</div>
|
||||
</div>
|
||||
<!-- 电子课表视图 -->
|
||||
<div v-else class="mt-table-scroll mt-print-area">
|
||||
<table class="mt-sheet">
|
||||
<caption>
|
||||
<span class="t">{{ activeClassName || "未选班级" }} 课程表</span>
|
||||
<span class="s">{{ [yearTermShort, "生成时间 " + stamp()].filter(Boolean).join(" · ") }}</span>
|
||||
<div class="cap-row">
|
||||
<span class="t">{{ activeClassName || "未选班级" }} 课程表</span>
|
||||
<span v-if="yearTermShort" class="s">{{ yearTermShort }}</span>
|
||||
<!-- 当前身份视图徽标 + 时段色标图例:与班级名称同一行靠右 -->
|
||||
<span class="cap-legend">
|
||||
<span class="mt-scope-chip" :class="{ all: viewMode !== 'teacher' }">
|
||||
<span class="dot"></span>{{ viewChipText }}
|
||||
</span>
|
||||
<span v-for="r in bandLegend" :key="r.key" class="rule">
|
||||
<i :style="{ background: PERIODS[r.key].color }"></i>{{ r.text }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="h-empty"><span class="a">时段</span><span class="b">星期</span></th>
|
||||
<th style="min-width: 70px">节次</th>
|
||||
<th v-if="showPeriodName" style="min-width: 70px">节次</th>
|
||||
<th v-for="ci in dayColumns" :key="ci" :class="{ 'is-weekend': ci >= 5 }">{{ DAYS[ci] }}</th>
|
||||
<th class="h-time" style="min-width: 118px">时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!renderRows.length">
|
||||
<td :colspan="dayColumns.length + 3">
|
||||
<td :colspan="dayColumns.length + (showPeriodName ? 3 : 2)">
|
||||
<div class="mt-empty">当前年级未启用节次模板,请到「课程表编排」或「应用设置」配置后再查看。</div>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="(row, idx) in renderRows" :key="idx">
|
||||
<tr v-if="row.kind === 'slot'">
|
||||
<td v-if="row.bandFirst" :rowspan="row.bandRows" class="rail" :style="railStyle(row.period)">{{ PERIODS[row.period].label }}</td>
|
||||
<td class="slot">{{ row.name || "第" + cnNum(row.bandIndex - 1) + "节" }}</td>
|
||||
<td v-if="showPeriodName" class="slot">{{ row.name || "第" + cnNum(row.bandIndex - 1) + "节" }}</td>
|
||||
<td v-for="ci in dayColumns" :key="ci" :class="displaySlots[row.ri]?.subjects[ci] ? 'subject' : 'blank'" :style="displaySlots[row.ri]?.subjects[ci] ? subjectStyle(displaySlots[row.ri].subjects[ci]) : undefined">
|
||||
{{ displaySlots[row.ri]?.subjects[ci] || "—" }}
|
||||
</td>
|
||||
@@ -82,15 +111,19 @@
|
||||
</tr>
|
||||
<tr v-else class="brk" :class="{ 'in-band': row.inBand }">
|
||||
<td v-if="row.bandFirst" :rowspan="row.bandRows" class="rail" :style="railStyle(row.period!)">{{ PERIODS[row.period!].label }}</td>
|
||||
<!-- 空白行拆两块:节次列显名称、星期列跨列显备注,均居中 -->
|
||||
<template v-if="row.inBand">
|
||||
<!-- 空白行拆两块:节次列显名称、星期列跨列显备注,均居中;隐藏节次列时名称与备注并为一格 -->
|
||||
<template v-if="row.inBand && showPeriodName">
|
||||
<td class="brk b-name"><span class="tag"><i></i>{{ row.name }}</span></td>
|
||||
<td :colspan="dayColumns.length" class="brk b-remark">
|
||||
<span v-if="row.remark" class="cnt" :class="{ long: isLongRemark(row.remark) }">{{ row.remark }}</span>
|
||||
</td>
|
||||
</template>
|
||||
<td v-else-if="row.inBand" :colspan="dayColumns.length" class="brk b-name">
|
||||
<span class="tag"><i></i>{{ row.name }}</span>
|
||||
<span v-if="row.remark" class="cnt" :class="{ long: isLongRemark(row.remark) }">{{ row.remark }}</span>
|
||||
</td>
|
||||
<!-- 无时段兜底:整行合并一块,维持旧观感 -->
|
||||
<td v-else :colspan="dayColumns.length + 2" class="brk merged">
|
||||
<td v-else :colspan="dayColumns.length + (showPeriodName ? 2 : 1)" class="brk merged">
|
||||
<span class="tag"><i></i>{{ row.name }}</span>
|
||||
<span v-if="row.remark" class="cnt" :class="{ long: isLongRemark(row.remark) }">{{ row.remark }}</span>
|
||||
</td>
|
||||
@@ -106,35 +139,10 @@
|
||||
</span>
|
||||
<span class="mt-scope-note">本班仅显示您任教的学科,其余节次留空。</span>
|
||||
</div>
|
||||
<div v-else class="mt-hintbar">
|
||||
<span class="mt-scope-chip all">
|
||||
<span class="dot"></span>{{ activeClass?.headTeacher ? "班主任视图" : "全部科目" }}
|
||||
</span>
|
||||
<span v-for="r in bandLegend" :key="r.key" class="rule">
|
||||
<i :style="{ background: PERIODS[r.key].color }"></i>{{ r.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 照片课表弹窗 -->
|
||||
<div v-if="photoOpen" class="mt-overlay" @mousedown.self="photoOpen = false">
|
||||
<div class="mt-modal" role="dialog" aria-modal="true" aria-labelledby="mtPhotoTitle">
|
||||
<div class="mt-modal-head">
|
||||
<h2 id="mtPhotoTitle">{{ activeClassName || "本班" }} 课程表照片</h2>
|
||||
<button class="x" type="button" aria-label="关闭照片预览" @click="photoOpen = false">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-modal-body">
|
||||
<div class="mt-photo-view"><img :src="photoUrl" :alt="(activeClassName || '') + ' 课程表照片'" /></div>
|
||||
</div>
|
||||
<div class="mt-modal-foot">
|
||||
<button class="mt-btn primary" type="button" @click="photoOpen = false">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -142,7 +150,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { syllabusApi, filePreviewUrl } from "@/api/module/syllabus";
|
||||
import type { MyClassVo, MyTimetableVo, NowYearTermVo, TemplateRowVo, TimeSettingVo } from "@/types/syllabus";
|
||||
import type { MyClassesVo, MyClassVo, MyTimetableVo, NowYearTermVo, TemplateRowVo, TimeSettingVo } from "@/types/syllabus";
|
||||
|
||||
/* ============================================================
|
||||
* 常量(与课程表编排页同源的视觉/数据约定)
|
||||
@@ -227,15 +235,29 @@ const activeId = ref<number | null>(null);
|
||||
const nowYearTerm = ref<NowYearTermVo | null>(null);
|
||||
const timetable = ref<MyTimetableVo | null>(null);
|
||||
const photo = ref<{ photoFileId: number; photoName: string | null } | null>(null);
|
||||
const photoOpen = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
/** 课表视图:电子课表 / 照片课表(年级设置两者都勾选时展示切换 tab) */
|
||||
const view = ref<"electronic" | "photo">("electronic");
|
||||
|
||||
/** 身份视图:管理员 > 班主任 > 任教老师(多身份可点击切换;null=单一身份/学生,固定展示) */
|
||||
type ViewMode = "admin" | "head" | "teacher";
|
||||
const viewMode = ref<ViewMode | null>(null);
|
||||
/** 是否管理员/校领导身份(后端身份标识;班主任/任教身份按班级数据推导) */
|
||||
const isAdmin = ref(false);
|
||||
/** 当前班级年级设置(ForGrade 带出;默认全启用) */
|
||||
const gradeFlags = ref({ gradeEnabled: true, useElectronic: true, usePhoto: true });
|
||||
|
||||
/** 年级生效模板行(节次+空白行);无启用模板时回退内置默认 10 节 */
|
||||
const tplRows = ref<TemplateRowVo[]>(defaultTemplateRows());
|
||||
/** 时段判定边界(旧模板行兜底推时段用) */
|
||||
const bands = ref<TimeSettingVo>({ ...DEFAULT_BANDS });
|
||||
/** 生效模板教学日(7 位 0/1 串,周一~周日);无模板 7 天全开 */
|
||||
const teachDaysStr = ref("1111111");
|
||||
/** 是否显示空白行(课间/午休):随生效模板记忆(课表编排页维护),默认显示全部 */
|
||||
const showBreakRows = ref(true);
|
||||
/** 是否显示「节次」列(节次名称):随生效模板记忆(应用设置维护),默认显示 */
|
||||
const showPeriodName = ref(true);
|
||||
|
||||
function defaultTemplateRows(): TemplateRowVo[] {
|
||||
return DEFAULT_ROWS.map((r) => ({ rowType: "period" as const, name: "", start: r.start, end: r.end }));
|
||||
@@ -246,12 +268,52 @@ function defaultTemplateRows(): TemplateRowVo[] {
|
||||
* ============================================================ */
|
||||
const activeClass = computed(() => classes.value.find((c) => c.classId === activeId.value) || null);
|
||||
|
||||
/** 年级设置派生(undefined 视为启用/使用,兜底旧数据) */
|
||||
const gradeEnabled = computed(() => gradeFlags.value.gradeEnabled !== false);
|
||||
const useElectronic = computed(() => gradeFlags.value.useElectronic !== false);
|
||||
const usePhoto = computed(() => gradeFlags.value.usePhoto !== false);
|
||||
/** 电子 + 照片都开放时才需要版本切换 tab */
|
||||
const showViewTabs = computed(() => gradeEnabled.value && useElectronic.value && usePhoto.value);
|
||||
|
||||
const activeClassName = computed(() => (activeClass.value ? classLabel(activeClass.value) : ""));
|
||||
|
||||
/** 科目可见范围以后端判定为准(班主任/管理员=全部;任教=任教科目) */
|
||||
const viewAll = computed(() => (timetable.value ? timetable.value.viewAllSubjects : true));
|
||||
/** 科目可见范围:任教视图恒为任教科目;其余视图以后端判定为准(班主任/管理员=全部) */
|
||||
const viewAll = computed(() =>
|
||||
viewMode.value === "teacher" ? false : timetable.value ? timetable.value.viewAllSubjects : true
|
||||
);
|
||||
|
||||
const mySubjectText = computed(() => (timetable.value?.mySubjects || []).join(" · "));
|
||||
/** 身份视图可用性:班主任/任教按班级身份数据推导(后端对管理员会话同样下发) */
|
||||
const headAvailable = computed(() => classes.value.some((c) => c.headTeacher));
|
||||
const teachAvailable = computed(() => classes.value.some((c) => (c.subjects || []).length > 0));
|
||||
|
||||
/** 可用身份视图(按 管理员>班主任>任教老师 优先级排序) */
|
||||
const availableViews = computed<Array<{ key: ViewMode; label: string }>>(() => {
|
||||
const out: Array<{ key: ViewMode; label: string }> = [];
|
||||
if (isAdmin.value) out.push({ key: "admin", label: "管理员视图" });
|
||||
if (headAvailable.value) out.push({ key: "head", label: "班主任视图" });
|
||||
if (teachAvailable.value) out.push({ key: "teacher", label: "任教老师视图" });
|
||||
return out;
|
||||
});
|
||||
/** 多身份才需要切换;单身份固定展示无需切换 */
|
||||
const showViewSwitcher = computed(() => availableViews.value.length > 1);
|
||||
|
||||
/** 当前身份视图下的班级:管理员=全部;班主任=所带班级;任教=任教班级 */
|
||||
const viewClasses = computed<MyClassVo[]>(() => {
|
||||
if (viewMode.value === "head") return classes.value.filter((c) => c.headTeacher);
|
||||
if (viewMode.value === "teacher") return classes.value.filter((c) => (c.subjects || []).length > 0);
|
||||
return classes.value;
|
||||
});
|
||||
|
||||
/** 题注身份视图徽标文案(无身份视图时兜底「全部科目」,如学生) */
|
||||
const viewChipText = computed(() => availableViews.value.find((v) => v.key === viewMode.value)?.label || "全部科目");
|
||||
|
||||
/** 科目过滤集合:任教视图取班级任教科目(管理员会话后端不下发 mySubjects,本地过滤);其余视图取后端结果 */
|
||||
const restrictSubjects = computed(() => {
|
||||
if (viewMode.value === "teacher") return activeClass.value?.subjects || [];
|
||||
return timetable.value?.mySubjects || [];
|
||||
});
|
||||
|
||||
const mySubjectText = computed(() => restrictSubjects.value.join(" · "));
|
||||
|
||||
/** 班级 tab 显示名:年级名 + 班级名(班级名已含年级时去重) */
|
||||
function classLabel(c: MyClassVo): string {
|
||||
@@ -261,14 +323,14 @@ function classLabel(c: MyClassVo): string {
|
||||
return g + n;
|
||||
}
|
||||
|
||||
const scopeText = computed(() => {
|
||||
if (!classes.value.length) return "—";
|
||||
// 班主任/管理员的班级全部科目;仅任教的班级只显示任教科目
|
||||
return activeClass.value && !activeClass.value.viewAllSubjects ? "任教科目" : "全部科目";
|
||||
});
|
||||
/** 班级 tab 悬停提示:随当前身份视图说明科目范围(任教视图下即使后端放行也只显示任教科目) */
|
||||
function tabTitle(c: MyClassVo): string {
|
||||
const subs = (c.subjects || []).join("、");
|
||||
if (viewMode.value === "teacher" || !c.viewAllSubjects) return subs ? "仅显示任教科目:" + subs : "仅显示任教科目";
|
||||
return "可查看全部科目";
|
||||
}
|
||||
|
||||
const termName = computed(() => (nowYearTerm.value?.term === 2 ? "第二学期" : "第一学期"));
|
||||
const yearTermText = computed(() => (nowYearTerm.value ? `${nowYearTerm.value.year} · ${termName.value}` : "—"));
|
||||
const yearTermShort = computed(() => (nowYearTerm.value ? `${nowYearTerm.value.year}学年${termName.value}` : ""));
|
||||
|
||||
/** 教学日列下标(0~6 → 周一~周日) */
|
||||
@@ -283,13 +345,11 @@ const bandLegend = computed(() =>
|
||||
.map((k) => ({ key: k, text: PERIODS[k].label }))
|
||||
);
|
||||
|
||||
const filledCount = computed(() => displaySlots.value.reduce((n, s) => n + s.subjects.filter(Boolean).length, 0));
|
||||
|
||||
const photoUrl = computed(() => (photo.value ? filePreviewUrl(photo.value.photoFileId) : ""));
|
||||
|
||||
/** 展示节次:节次/时间取自模板行,学科按节次序号对齐课表数据(任教视角再滤一次非任教科目) */
|
||||
/** 展示节次:节次/时间取自模板行,学科按节次序号对齐课表数据(任教视图再滤一次非任教科目) */
|
||||
const displaySlots = computed<SlotRow[]>(() => {
|
||||
const mySet = new Set((timetable.value?.mySubjects || []).map((s) => s.trim()));
|
||||
const mySet = new Set(restrictSubjects.value.map((s) => s.trim()));
|
||||
const restrict = !viewAll.value;
|
||||
return tplRows.value
|
||||
.filter((r) => r.rowType === "period")
|
||||
@@ -362,7 +422,10 @@ function buildRows(source: TemplateRowVo[]): RenderRow[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
const renderRows = computed<RenderRow[]>(() => buildRows(tplRows.value));
|
||||
/** 课表视图行:跟随生效模板的「显示全部/只显示节次」偏好,隐藏空白行后同时段节次并为一组 */
|
||||
const renderRows = computed<RenderRow[]>(() =>
|
||||
showBreakRows.value ? buildRows(tplRows.value) : buildRows(tplRows.value.filter((r) => r.rowType !== "break"))
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
* 工具函数
|
||||
@@ -427,12 +490,6 @@ function isLongRemark(remark: string): boolean {
|
||||
return (remark || "").length > REMARK_INLINE_MAX;
|
||||
}
|
||||
|
||||
function stamp(): string {
|
||||
const t = new Date();
|
||||
const z = (n: number) => String(n).padStart(2, "0");
|
||||
return `${t.getFullYear()}-${z(t.getMonth() + 1)}-${z(t.getDate())} ${z(t.getHours())}:${z(t.getMinutes())}`;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 数据加载
|
||||
* ============================================================ */
|
||||
@@ -447,9 +504,21 @@ async function loadYearTerm() {
|
||||
|
||||
async function loadClasses() {
|
||||
try {
|
||||
classes.value = (await syllabusApi.myClasses().send()) || [];
|
||||
const vo: MyClassesVo | null = await syllabusApi.myClasses().send();
|
||||
classes.value = vo?.classes || [];
|
||||
isAdmin.value = !!vo?.admin;
|
||||
// 默认身份视图按 管理员 > 班主任 > 任教老师 优先级取
|
||||
viewMode.value = isAdmin.value
|
||||
? "admin"
|
||||
: classes.value.some((c) => c.headTeacher)
|
||||
? "head"
|
||||
: classes.value.some((c) => (c.subjects || []).length > 0)
|
||||
? "teacher"
|
||||
: null;
|
||||
} catch (e: any) {
|
||||
classes.value = [];
|
||||
isAdmin.value = false;
|
||||
viewMode.value = null;
|
||||
console.error("获取可查看班级失败", e);
|
||||
Message.error("获取可查看班级失败,请刷新重试");
|
||||
}
|
||||
@@ -474,10 +543,21 @@ async function loadTimetable() {
|
||||
if (vo?.rows?.length) {
|
||||
tplRows.value = vo.rows;
|
||||
teachDaysStr.value = vo.teachDays || "1111111";
|
||||
showBreakRows.value = vo.showBreakRows !== false;
|
||||
showPeriodName.value = vo.showPeriodName !== false;
|
||||
} else {
|
||||
tplRows.value = defaultTemplateRows();
|
||||
teachDaysStr.value = "1111111";
|
||||
showBreakRows.value = true;
|
||||
showPeriodName.value = true;
|
||||
}
|
||||
// 年级设置随生效设置带出;默认视图取开放的那个(都开放则电子优先)
|
||||
gradeFlags.value = {
|
||||
gradeEnabled: vo?.gradeEnabled !== false,
|
||||
useElectronic: vo?.useElectronic !== false,
|
||||
usePhoto: vo?.usePhoto !== false,
|
||||
};
|
||||
view.value = !useElectronic.value && usePhoto.value ? "photo" : "electronic";
|
||||
return vo;
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -485,6 +565,10 @@ async function loadTimetable() {
|
||||
bands.value = { ...DEFAULT_BANDS };
|
||||
tplRows.value = defaultTemplateRows();
|
||||
teachDaysStr.value = "1111111";
|
||||
showBreakRows.value = true;
|
||||
showPeriodName.value = true;
|
||||
gradeFlags.value = { gradeEnabled: true, useElectronic: true, usePhoto: true };
|
||||
view.value = "electronic";
|
||||
return null;
|
||||
}),
|
||||
syllabusApi
|
||||
@@ -515,11 +599,24 @@ async function selectClass(classId: number) {
|
||||
await loadTimetable();
|
||||
}
|
||||
|
||||
/** 切换身份视图:当前班级不在新视图范围内时自动选中该视图首个班级并重取课表 */
|
||||
async function selectView(mode: ViewMode) {
|
||||
if (viewMode.value === mode) return;
|
||||
viewMode.value = mode;
|
||||
if (!viewClasses.value.some((c) => c.classId === activeId.value)) {
|
||||
const first = viewClasses.value[0];
|
||||
if (first) {
|
||||
activeId.value = first.classId;
|
||||
await loadTimetable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 打印
|
||||
* ============================================================ */
|
||||
function print() {
|
||||
if (!activeId.value) return;
|
||||
if (!activeId.value || view.value !== "electronic") return;
|
||||
document.body.classList.add("mt-printing");
|
||||
window.print();
|
||||
}
|
||||
@@ -528,27 +625,25 @@ function afterPrint() {
|
||||
document.body.classList.remove("mt-printing");
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") photoOpen.value = false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 生命周期
|
||||
* ============================================================ */
|
||||
onMounted(async () => {
|
||||
document.addEventListener("keydown", onKeydown);
|
||||
window.addEventListener("afterprint", afterPrint);
|
||||
await Promise.all([loadYearTerm(), loadClasses()]);
|
||||
if (!classes.value.length) {
|
||||
Message.warning("暂无可查看的班级,请确认角色、任教关系或学籍班级已配置");
|
||||
return;
|
||||
}
|
||||
activeId.value = classes.value[0].classId;
|
||||
await loadTimetable();
|
||||
// 默认视图(管理员>班主任>任教)下的首个班级
|
||||
const first = viewClasses.value[0];
|
||||
if (first) {
|
||||
activeId.value = first.classId;
|
||||
await loadTimetable();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("keydown", onKeydown);
|
||||
window.removeEventListener("afterprint", afterPrint);
|
||||
});
|
||||
</script>
|
||||
@@ -601,57 +696,6 @@ onBeforeUnmount(() => {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
.num {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ==================== 页头 ==================== */
|
||||
.mt-pagehead {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.mt-h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--ink-900);
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
.mt-sub {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--ink-400);
|
||||
}
|
||||
.mt-stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mt-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 92px;
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: 0 1px 2px rgba(34, 38, 46, 0.05);
|
||||
b {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--ink-900);
|
||||
line-height: 1.3;
|
||||
}
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: var(--ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 主卡片 + 工具条 ==================== */
|
||||
.mt-card {
|
||||
@@ -734,6 +778,69 @@ onBeforeUnmount(() => {
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
/* 身份视图切换(管理员/班主任/任教老师多身份时展示;决定下方班级 tab 范围) */
|
||||
.mt-idtabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-subtle);
|
||||
button {
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
color: var(--ink-600);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 200ms var(--ease);
|
||||
&:hover {
|
||||
color: var(--brand-600);
|
||||
background: var(--brand-50);
|
||||
}
|
||||
&.on {
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 6px rgba(89, 121, 234, 0.32);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 电子课表 / 照片课表 版本切换(年级设置同时勾选两个版本时展示) */
|
||||
.mt-viewtabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-subtle);
|
||||
button {
|
||||
height: 28px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-600);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 200ms var(--ease);
|
||||
&:hover {
|
||||
color: var(--brand-600);
|
||||
}
|
||||
&.on {
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 6px rgba(89, 121, 234, 0.32);
|
||||
}
|
||||
}
|
||||
}
|
||||
.mt-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -807,6 +914,12 @@ onBeforeUnmount(() => {
|
||||
caption-side: top;
|
||||
text-align: left;
|
||||
padding: 0 2px 16px;
|
||||
.cap-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
}
|
||||
.t {
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
@@ -814,7 +927,16 @@ onBeforeUnmount(() => {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.s {
|
||||
margin-left: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--ink-400);
|
||||
}
|
||||
/* 视图范围徽标 + 时段图例:同行靠右 */
|
||||
.cap-legend {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--ink-400);
|
||||
}
|
||||
@@ -827,20 +949,27 @@ onBeforeUnmount(() => {
|
||||
background: #eef1f8;
|
||||
border-radius: var(--r-md);
|
||||
&.h-empty {
|
||||
background: linear-gradient(135deg, #eef1f8 0 50%, #fdf3d9 50% 100%);
|
||||
/* 斜角分色沿主对角线:右上半(星期侧)同星期表头浅蓝,左下半(时段侧)同米黄 */
|
||||
background: linear-gradient(45deg, #fdf3d9 0 50%, #eef1f8 50% 100%);
|
||||
position: relative;
|
||||
min-width: 52px;
|
||||
.a {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
bottom: 6px;
|
||||
color: var(--ink-900);
|
||||
left: 7px;
|
||||
bottom: 5px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
/* 时段落在黄色半区,用时间列的字色 */
|
||||
color: #8a6105;
|
||||
}
|
||||
.b {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 6px;
|
||||
color: #8a6105;
|
||||
right: 7px;
|
||||
top: 5px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
/* 星期落在浅蓝半区,用星期表头的字色 */
|
||||
color: var(--ink-900);
|
||||
}
|
||||
}
|
||||
&.h-time {
|
||||
@@ -853,12 +982,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
td {
|
||||
padding: 10px 6px;
|
||||
padding: 5px 3px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
background: #fbfcfe;
|
||||
border-radius: var(--r-md);
|
||||
color: var(--ink-600);
|
||||
/* 单元格被撑高时文字垂直居中(全局 reset 把 td 设成 baseline) */
|
||||
vertical-align: middle;
|
||||
&.rail {
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
@@ -893,7 +1024,7 @@ onBeforeUnmount(() => {
|
||||
color: var(--ink-400);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 6px 12px;
|
||||
padding: 2px 4px;
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -942,7 +1073,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格下方提示条(科目范围 / 时段图例) */
|
||||
/* 表格下方提示条(任教科目范围) */
|
||||
.mt-hintbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -951,118 +1082,65 @@ onBeforeUnmount(() => {
|
||||
padding: 12px 4px 0;
|
||||
font-size: 12px;
|
||||
color: var(--ink-400);
|
||||
.mt-scope-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--brand-50);
|
||||
border: 1px solid var(--brand-100);
|
||||
color: var(--brand-800);
|
||||
font-size: 12px;
|
||||
&.all {
|
||||
background: #eefaf2;
|
||||
border-color: #d3eedd;
|
||||
color: #1f6b41;
|
||||
}
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand);
|
||||
}
|
||||
&.all .dot {
|
||||
background: var(--ok);
|
||||
}
|
||||
}
|
||||
.mt-scope-note {
|
||||
font-size: 12px;
|
||||
color: var(--ink-400);
|
||||
}
|
||||
.rule {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
i {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
/* 视图范围徽标(课表标题行 / 任教提示条共用) */
|
||||
.mt-scope-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--brand-50);
|
||||
border: 1px solid var(--brand-100);
|
||||
color: var(--brand-800);
|
||||
font-size: 12px;
|
||||
&.all {
|
||||
background: #eefaf2;
|
||||
border-color: #d3eedd;
|
||||
color: #1f6b41;
|
||||
}
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand);
|
||||
}
|
||||
&.all .dot {
|
||||
background: var(--ok);
|
||||
}
|
||||
}
|
||||
/* 时段色标图例 */
|
||||
.rule {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: var(--ink-400);
|
||||
i {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 照片课表弹窗 ==================== */
|
||||
.mt-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
/* ==================== 照片课表视图 ==================== */
|
||||
.mt-photo-pane {
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background: rgba(23, 27, 35, 0.45);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.mt-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(920px, 100%);
|
||||
max-height: calc(100vh - 80px);
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--r-xl);
|
||||
box-shadow: 0 18px 50px rgba(23, 27, 35, 0.28);
|
||||
overflow: hidden;
|
||||
}
|
||||
.mt-modal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--ink-900);
|
||||
}
|
||||
.x {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--ink-400);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--ink-900);
|
||||
}
|
||||
}
|
||||
}
|
||||
.mt-modal-body {
|
||||
padding: 16px 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
.mt-modal-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--line-soft);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.mt-photo-view {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
padding: 8px 4px;
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 300px);
|
||||
max-height: calc(100vh - 260px);
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 18px rgba(23, 27, 35, 0.12);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
-1
@@ -6,7 +6,6 @@ interface ImportMetaEnv {
|
||||
readonly VITE_SERVICE_HOST: string;
|
||||
readonly VITE_ZIYUANKU_HOST: string;
|
||||
readonly VITE_ZHUANXIANG_HOST: string;
|
||||
readonly VITE_IS_MOCK: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
Reference in New Issue
Block a user