diff --git a/.env.dev b/.env.dev index 3522ccc..a1f4161 100644 --- a/.env.dev +++ b/.env.dev @@ -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 diff --git a/package-lock.json b/package-lock.json index cf19394..a3af5bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2dc2009..9559357 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.vue b/src/App.vue index f88a2bb..36ed058 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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] diff --git a/src/api/module/syllabus.ts b/src/api/module/syllabus.ts index a253a4d..00d94f7 100644 --- a/src/api/module/syllabus.ts +++ b/src/api/module/syllabus.ts @@ -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(url: string, data?: Record | any[]): Met function postForm(url: string, data?: Record): Method { 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( "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(`${TIMETABLE}/NowYearTerm`); } + // ========== 排课情况 ========== + /** 全校排课情况汇总(每班电子版 / 照片版编排状态,按年级→班级排序) */ + overview() { + return get(`${TIMETABLE}/Overview`); + } + // ========== 我的课表(按角色过滤的只读视图) ========== - /** 可查看班级列表(管理员/校领导=全部;班主任=所带;任教=任教班级) */ + /** 可查看班级列表 + 身份标识(管理员/班主任/任教老师,前端按身份切换视图) */ myClasses() { - return get(`${TIMETABLE}/MyClasses`); + return get(`${TIMETABLE}/MyClasses`); } /** 班级课表(权限校验;任教老师只返回任教科目格) */ myGet(classId: number) { @@ -171,10 +195,26 @@ class SyllabusApi { deleteTemplate(id: number) { return postForm(`${SETTING}/DeleteTemplate`, { id }); } + /** 保存节次模板的「显示全部/只显示节次」偏好(绑定模板记忆,预览课表/我的课表同随此值) */ + saveTemplateShowBreakRows(id: number, showBreakRows: boolean) { + return postForm(`${SETTING}/SaveTemplateShowBreakRows`, { id, showBreakRows }); + } + /** 保存节次模板的「节次名称 显示/隐藏」偏好(绑定模板记忆,预览课表/我的课表同随此值) */ + saveTemplateShowPeriodName(id: number, showPeriodName: boolean) { + return postForm(`${SETTING}/SaveTemplateShowPeriodName`, { id, showPeriodName }); + } /** 课表编辑页生效设置:时间边界 + 当前学期下指定年级启用模板的行(term 1=上学期 2=下学期) */ effectiveForGrade(gradeName: string, term?: number) { return get(`${SETTING}/ForGrade`, { gradeName, term }); } + /** 年级设置列表(启用 + 使用电子版/照片版;未配置的年级默认全启用) */ + gradeSettings() { + return get(`${SETTING}/Grades`); + } + /** 批量保存年级设置(按年级名 upsert;作用于「我的课表」电子/照片视图切换) */ + saveGradeSettings(data: GradeSettingVo[]) { + return postJSON(`${SETTING}/SaveGrades`, data); + } // ========== 附件 ========== fileDownUrl = fileDownUrl; diff --git a/src/assets/config/api.config.ts b/src/assets/config/api.config.ts index 7b82fad..f142e1c 100644 --- a/src/assets/config/api.config.ts +++ b/src/assets/config/api.config.ts @@ -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 = { diff --git a/src/assets/config/host.config.ts b/src/assets/config/host.config.ts index 31aebc5..913c836 100644 --- a/src/assets/config/host.config.ts +++ b/src/assets/config/host.config.ts @@ -5,7 +5,7 @@ import { BaseConf } from "@/types/Basics"; * ① window.__APP_CONFIG__:线上运行时配置,index.html 引导脚本在应用模块加载前同步拉取 * //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 为嵌套对象,按子键合并, diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 1c90538..9f25dd1 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -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 { diff --git a/src/components/AttachmentUpload.vue b/src/components/AttachmentUpload.vue index 5dda161..af76aed 100644 --- a/src/components/AttachmentUpload.vue +++ b/src/components/AttachmentUpload.vue @@ -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, diff --git a/src/components/FilePreviewModal.vue b/src/components/FilePreviewModal.vue index 131fb82..1425c80 100644 --- a/src/components/FilePreviewModal.vue +++ b/src/components/FilePreviewModal.vue @@ -40,7 +40,7 @@ const previewName = ref(""); const fileType = ref(""); const blob = ref(null); -const fileServiceBase = config.isMock ? "" : config.serviceHost; +const fileServiceBase = config.serviceHost; /** 内联流地址(