diff --git a/README.md b/README.md index fed68ea..05a6f99 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 班级小助手(Class Helper) +# 班级小助手(Class Helper) 班级信息管理系统,采用 pnpm monorepo,包含**后端服务**、**Web 管理端**(教师/管理员)与 **桌面客户端**(学生)三端。核心链路: @@ -36,12 +36,12 @@ ## 三套交付产物 -| 产物 | 文件 | 用途 | -| ------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| **服务端 + Web 管理端安装程序** | `release-server/班级小助手服务端-0.1.0-x64-setup.exe`(34 MB) | 装到教师电脑/校服务器即完整系统,**内置 Node 运行时**,双击安装、开机自启、自动建库建号 | -| **学生客户端安装程序** | `packages/desktop-client/release/班级小助手-0.1.0-x64-setup.exe`(107 MB) | 学生机安装(NSIS 安装包) | -| **学生客户端单文件版** | `packages/desktop-client/release/班级小助手-0.1.0-x64-portable.exe`(107 MB) | 免安装直接运行(U 盘分发) | -| Web 管理端(PWA) | 由服务端在 `/` 直接托管 | 浏览器打开即用,可在 Edge/Chrome 中「安装为应用」 | +| 产物 | 文件 | 用途 | +| ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| **服务端 + Web 管理端安装程序** | `release-server/班级小助手服务端-0.1.0-x64-setup.exe`(34 MB) | 装到教师电脑/校服务器即完整系统,**内置 Node 运行时**,双击安装、开机自启、自动建库建号 | +| **学生客户端安装程序** | `packages/desktop-client/release/班级小助手-0.1.0-x64-setup.exe`(107 MB) | 学生机安装(NSIS 安装包) | +| **学生客户端单文件版** | `packages/desktop-client/release/班级小助手-0.1.0-x64-portable.exe`(107 MB) | 免安装直接运行(U 盘分发) | +| Web 管理端(PWA) | 由服务端在 `/` 直接托管 | 浏览器打开即用,可在 Edge/Chrome 中「安装为应用」;**已适配手机小屏**(1Panel 风格抽屉导航) | 生产部署(Windows 安装包 / Docker + MySQL / 手动部署)请看 **[docs/production.md](docs/production.md)**。 @@ -172,7 +172,7 @@ pnpm verify:desktop ```bash # 另开一个终端先启动后端:pnpm dev:server pnpm verify:e2e # 后端 + REST + Socket.IO + RBAC + 上课时段拦截:62 项 -pnpm verify:web # Web 管理端真实点击(含上课时段紧急通知全屏警告):8 项 +pnpm verify:web # Web 管理端真实点击(含手机小屏适配、上课时段紧急通知全屏警告):15 项 pnpm verify:desktop # Electron 客户端(含灵动岛与联网集成):26 项 ``` @@ -215,7 +215,7 @@ pnpm verify:desktop # Electron 客户端(含灵动岛与联网集成) | `pnpm dist:win` | 打包客户端 nsis 安装包 + portable 单文件 EXE | | `pnpm dist:all` | 服务端安装程序 + 客户端安装程序一起打 | | `pnpm verify:e2e` | 后端端到端验收(62 项,需服务端已启动) | -| `pnpm verify:web` | Web 管理端 UI 真实点击测试(Electron 驱动,8 项) | +| `pnpm verify:web` | Web 管理端 UI 真实点击测试(Electron 驱动,15 项,含手机小屏适配) | | `pnpm verify:desktop` | EXE 客户端冒烟验证(26 项,含灵动岛行为/像素级截图校验、侧边栏点击与离线回退) | | `pnpm typecheck` | 全仓库类型检查(含 `vue-tsc`) | | `pnpm lint` / `pnpm lint:fix` | ESLint 检查 / 自动修复 | @@ -227,6 +227,59 @@ pnpm verify:desktop # Electron 客户端(含灵动岛与联网集成) | `pnpm db:studio` | 打开 Prisma Studio | | `pnpm db:switch:mysql` / `db:switch:sqlite` | 切换数据库 provider | +## Web 管理端(含手机小屏适配) + +布局沿用 **1Panel 风格**:固定深色侧边栏(`#1f2d3d` + 蓝色圆角选中态)+ 白色顶栏(当前页标题/连接状态/用户菜单)+ +浅灰底 + 白色圆角卡片内容区。手机小屏下侧边栏收进抽屉,由顶栏汉堡按钮唤出。 + +| 断点 | 布局行为 | +| ---------------- | --------------------------------------------------------------------------------------------- | +| `> 992px` 桌面 | 常驻 210px 深色侧边栏 + 顶栏显示连接状态与"已接收 N 条实时事件" | +| `769–992px` 平板 | 保留侧边栏,内容区内边距压缩(14px) | +| `<= 768px` 手机 | 侧边栏**不渲染**,顶栏汉堡按钮 → 240px 深色抽屉菜单;顶栏显示当前页标题;用户菜单收成头像图标 | + +手机端细节(1Panel 同款思路:控件纵向铺满、表格卡片内滚动、弹窗自适应): + +| 位置 | 小屏行为 | +| ----------- | ----------------------------------------------------------------------------- | +| 抽屉菜单 | 点汉堡滑出、点菜单项自动收起;`aria-expanded` 标记状态;桌面端侧边栏视觉不变 | +| 顶栏 | 只保留汉堡 + 当前页标题 + 连接状态圆点 + 头像;身份信息移入用户下拉菜单第一行 | +| 页面工具栏 | 选择器/搜索框/按钮全部铺满整行(`width: 100%`),方便拇指点击 | +| 表格 | 卡片内横向滚动(`.table-card` + `overflow-x: auto`),页面本身不横向滚动 | +| 弹窗/消息框 | 宽度 `calc(100vw - 24px)`、贴近整屏;表单标签与输入框自适应 | +| 详情抽屉 | 宽 86vw;`el-descriptions` 由 2~3 列自动降为单列,避免文字被压成竖排 | +| 统计卡片 | `.stat-grid` 两列;登录页卡片自适应宽度并保留安全内边距 | + +实拍(`pnpm verify:web` 自动留档到 `docs/screenshots/web-mobile/`): + +| 场景 | 截图 | +| ----------------------------------------- | --------------------------------------------------------------------- | +| 手机抽屉菜单 | ![手机抽屉菜单](docs/screenshots/web-mobile/mobile-1-drawer.png) | +| 手机通知页(工具栏铺满 + 表格卡片内滚动) | ![手机通知页](docs/screenshots/web-mobile/mobile-2-notifications.png) | +| 手机弹窗自适应 | ![手机弹窗](docs/screenshots/web-mobile/mobile-3-dialog.png) | + +实现位置:`src/composables/useResponsive.ts`(断点 + resize 监听)、`src/layouts/AdminLayout.vue`(抽屉/顶栏)、 +`src/styles/index.css`(全局响应式规则)、各视图表格卡片加 `.table-card`。 + +`pnpm verify:web` 的 15 项里,后 7 项专门验证手机端(Electron 真实点击 + 真实尺寸窗口): + +| 校验项 | 实测结果 | +| -------------------------------------- | -------------------------------------------------------------------- | +| 侧边栏收起、顶栏汉堡按钮出现 | ✅ `innerWidth=377` 汉堡=true 桌面侧边栏未渲染 | +| 点汉堡滑出抽屉菜单(菜单项完整) | ✅ 7 项、`left=0`、`transform=none`、遮罩进入动画已结束 | +| 抽屉里真实点击"通知发布"并自动收起抽屉 | ✅ `path=/notifications`、抽屉 `right=0`(已滑出屏幕) | +| 页面无横向溢出 | ✅ `scrollWidth=377 = innerWidth`,越界元素=无(表格自身滚动不计入) | +| 表格在卡片内横向滚动 | ✅ 卡片可视 336px / 内容 576px / 表格 560px | +| 工具栏控件铺满整行 | ✅ 控件宽 338px(视口 377px) | +| 弹窗宽度自适应 | ✅ 弹窗宽 353px、`left=12`(不超出视口且接近整屏) | + +> 踩坑记录:这套检查必须让窗口**真正可见**(`win.showInactive()`)。 +> Chromium 会冻结隐藏页面的 CSS transition,Vue 的 `` 收不到 `transitionend`, +> Element Plus 抽屉会一直停在 `enter-from`(表现为"点了没反应")——冒烟里已按"等抽屉滑到位"断言,避免误判。 + +> 手机访问:与电脑同一局域网时用 `http://<主机IP>:4000` 打开管理端; +> 支持「添加到主屏幕」(PWA manifest + Service Worker)。 + ## EXE 客户端(学生端) ### 进程结构与安全设置 @@ -532,23 +585,23 @@ pnpm db:migrate && pnpm db:seed 打包产物验证(对最终 EXE 实测,非仅开发产物): -| 产物 | 大小 | 冒烟结果 | -| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------ | -| 服务端安装程序(内置 Node + Web 端) | 34MB | ✅ 静默安装(升级保留 `.env` 与数据库)→ 自动建库建号 → 服务就绪 → **对安装实例跑 62/62 e2e + 8/8 UI** | -| `release/win-unpacked/班级小助手.exe` | 234.7MB | ✅ 23/23,`packaged: true`,退出码 0 | -| `release/…-x64-portable.exe`(单文件) | 106.9MB | ✅ 打包后冒烟 **23/23**,`packaged: true`,退出码 0(含灵动岛行为与像素校验) | -| `release/…-x64-setup.exe`(客户端) | 107.2MB | ✅ 构建成功,已嵌入自定义图标;含灵动岛与本次行为修正 | +| 产物 | 大小 | 冒烟结果 | +| -------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- | +| 服务端安装程序(内置 Node + Web 端) | 34MB | ✅ 静默安装(升级保留 `.env` 与数据库)→ 自动建库建号 → 服务就绪 → **对安装实例跑 62/62 e2e + 15/15 UI** | +| `release/win-unpacked/班级小助手.exe` | 234.7MB | ✅ 23/23,`packaged: true`,退出码 0 | +| `release/…-x64-portable.exe`(单文件) | 106.9MB | ✅ 打包后冒烟 **23/23**,`packaged: true`,退出码 0(含灵动岛行为与像素校验) | +| `release/…-x64-setup.exe`(客户端) | 107.2MB | ✅ 构建成功,已嵌入自定义图标;含灵动岛与本次行为修正 | 当前实测: -| 验证 | 结果 | -| ---------------------------------------------------------------------------- | ----------------------------------- | -| `pnpm verify:e2e`(开发环境与**安装后的生产实例**各跑一次) | **62/62** ✅ | -| `pnpm verify:web`(Web 管理端真实点击 + 上课时段紧急通知 + PWA) | **8/8** ✅ | -| `pnpm verify:desktop`(客户端冒烟 + 灵动岛行为 + 像素级截图校验 + 离线回退) | **26/26** ✅ | -| `pnpm typecheck` / `pnpm lint` / `pnpm format:check` | 全部通过 ✅ | -| 安装程序完整生命周期(静默安装 → 启停脚本 → 卸载) | 通过 ✅ | -| Docker / Nginx 部署样例 | 文件已提供,本机无 Docker 未实测 ⚠️ | +| 验证 | 结果 | +| ------------------------------------------------------------------------------- | ----------------------------------- | +| `pnpm verify:e2e`(开发环境与**安装后的生产实例**各跑一次) | **62/62** ✅ | +| `pnpm verify:web`(Web 管理端真实点击 + 手机小屏适配 + 上课时段紧急通知 + PWA) | **15/15** ✅ | +| `pnpm verify:desktop`(客户端冒烟 + 灵动岛行为 + 像素级截图校验 + 离线回退) | **26/26** ✅ | +| `pnpm typecheck` / `pnpm lint` / `pnpm format:check` | 全部通过 ✅ | +| 安装程序完整生命周期(静默安装 → 启停脚本 → 卸载) | 通过 ✅ | +| Docker / Nginx 部署样例 | 文件已提供,本机无 Docker 未实测 ⚠️ | ## 常见问题(本机环境已知坑) diff --git a/docs/screenshots/web-mobile/mobile-1-drawer.png b/docs/screenshots/web-mobile/mobile-1-drawer.png new file mode 100644 index 0000000..dc50bf3 Binary files /dev/null and b/docs/screenshots/web-mobile/mobile-1-drawer.png differ diff --git a/docs/screenshots/web-mobile/mobile-2-notifications.png b/docs/screenshots/web-mobile/mobile-2-notifications.png new file mode 100644 index 0000000..9e5d055 Binary files /dev/null and b/docs/screenshots/web-mobile/mobile-2-notifications.png differ diff --git a/docs/screenshots/web-mobile/mobile-3-dialog.png b/docs/screenshots/web-mobile/mobile-3-dialog.png new file mode 100644 index 0000000..9e58a74 Binary files /dev/null and b/docs/screenshots/web-mobile/mobile-3-dialog.png differ diff --git a/packages/web-admin/src/composables/useResponsive.ts b/packages/web-admin/src/composables/useResponsive.ts new file mode 100644 index 0000000..c273180 --- /dev/null +++ b/packages/web-admin/src/composables/useResponsive.ts @@ -0,0 +1,51 @@ +import { computed, onMounted, onUnmounted, readonly, ref } from 'vue'; + +/** + * 响应式断点(与 1Panel 风格一致:小屏把侧边栏收进抽屉,内容区单列铺满)。 + * + * - `<= 768px` 手机:侧边栏抽屉化、顶部工具栏铺满、表格卡片内横向滚动 + * - `769-992px` 平板:保留侧边栏,内容做适度压缩 + * - `> 992px` 桌面:维持原有双栏布局 + */ +export const MOBILE_MAX_WIDTH = 768; +export const TABLET_MAX_WIDTH = 992; + +function currentWidth(): number { + return typeof window === 'undefined' ? 1280 : window.innerWidth; +} + +/** + * 视口响应式状态。 + * 同一时刻可以有多个组件使用(各自持有监听),只做一次 `innerWidth` 读取 + resize 监听。 + */ +export function useResponsive() { + const width = ref(currentWidth()); + + const sync = (): void => { + width.value = currentWidth(); + }; + + onMounted(() => { + sync(); + window.addEventListener('resize', sync, { passive: true }); + window.addEventListener('orientationchange', sync, { passive: true }); + }); + + onUnmounted(() => { + window.removeEventListener('resize', sync); + window.removeEventListener('orientationchange', sync); + }); + + const isMobile = computed(() => width.value <= MOBILE_MAX_WIDTH); + const isTablet = computed(() => width.value > MOBILE_MAX_WIDTH && width.value <= TABLET_MAX_WIDTH); + const isDesktop = computed(() => width.value > TABLET_MAX_WIDTH); + + return { + width: readonly(width), + isMobile, + isTablet, + isDesktop, + /** 手动同步一次(例如窗口尺寸被外部改变后) */ + sync, + }; +} diff --git a/packages/web-admin/src/layouts/AdminLayout.vue b/packages/web-admin/src/layouts/AdminLayout.vue index 87ed5a2..9fc75de 100644 --- a/packages/web-admin/src/layouts/AdminLayout.vue +++ b/packages/web-admin/src/layouts/AdminLayout.vue @@ -1,16 +1,18 @@