feat(courseware,docs): 完善课件传输与岛编辑并统一全仓文档
- 增加课件上传、班级可见范围、设备补收与传输进度 - 整合自定义灵动岛编辑器和相关客户端交互 - 同步三端契约、数据库迁移、部署配置与隔离回归 - 按源码统一文档与官网说明,移除过时任务书和审查记录 - 保持产品版本为 1.4.0
This commit is contained in:
1 parent
536379036a
commit
c7f215a09c
127 files changed
+4334
-8482
No files matched your search
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 服务端 + Web 管理端 生产打包脚本:pnpm dist:server
|
||||
*
|
||||
* 交付产物统一落到 releases/server/<版本>/(releases/ 不入库,与客户端/插件同级):
|
||||
* 交付产物统一落到 releases/server/<版本>/(releases/ 不入库,与客户端同级):
|
||||
*
|
||||
* 产出(Windows):
|
||||
* releases/server/<版本>/安装包/ClassHelper 服务端-<版本>-x64-setup.exe Windows 安装程序(NSIS)
|
||||
@@ -50,7 +50,7 @@ const PRODUCT = 'ClassHelper 服务端';
|
||||
const PORT = '4000';
|
||||
const LINUX_ARCH = 'x64';
|
||||
|
||||
// 产物根:releases/server/<版本>/(releases/ 不入库,见 AGENTS.md §4)
|
||||
// 产物根:releases/server/<版本>/(releases/ 不入库,见 AGENTS.md 的现行工程约定)
|
||||
const outRoot = path.join(root, 'releases', 'server', VERSION);
|
||||
const INSTALLER_DIR = path.join(outRoot, '安装包');
|
||||
const GENERATED_DIR = path.join(outRoot, '构建中间');
|
||||
@@ -219,7 +219,7 @@ function copyRecursive(from, to) {
|
||||
function buildRuntime() {
|
||||
// 后端代码:保留 dist 层级,保证代码里 serverRoot 的计算(../..)与开发环境一致
|
||||
// 注:dist 里没有 .d.ts / *.map —— packages/server/tsconfig.build.json 显式关掉了它们
|
||||
//(那三样占了原 dist 的 82%,运行时一个都用不到,见 AGENTS.md §5 第 54 条)。
|
||||
//(那三样占了原 dist 的 82%,运行时一个都用不到,见 AGENTS.md 的现行工程约定)。
|
||||
copyRecursive(path.join(serverDir, 'dist'), path.join(staging, 'server', 'dist'));
|
||||
// 迁移文件(首次启动 AUTO_MIGRATE 使用)
|
||||
copyRecursive(
|
||||
@@ -312,7 +312,7 @@ const sizeMb = (target) => Math.round(dirSize(target) / 1024 / 1024);
|
||||
* 其余方言(postgresql / cockroachdb / sqlserver)的 wasm 一个字节都用不上。
|
||||
*
|
||||
* 原则:只删数据文件与文档,**不碰任何会被 require 的入口**;每一条都写明依据。
|
||||
* 改这里之前先看「为什么安全」,改完务必按 AGENTS.md §8 的清单复验一次
|
||||
* 改这里之前先看「为什么安全」,改完务必按 AGENTS.md 的现行工程约定 的清单复验一次
|
||||
* (尤其是数据库管理模块的「生成客户端 / 一键切换」—— 它是唯一会调用 Prisma CLI 的地方)。
|
||||
*/
|
||||
function pruneRuntime() {
|
||||
|
||||
@@ -39,6 +39,7 @@ const MENU_ITEMS = [
|
||||
{ label: '课表管理', path: '/schedules' },
|
||||
{ label: '作业发布', path: '/homeworks' },
|
||||
{ label: '通知发布', path: '/notifications' },
|
||||
{ label: '课件', path: '/courseware' },
|
||||
{ label: '成绩录入', path: '/grades' },
|
||||
// 按 client:read 展示设备状态入口。
|
||||
{ label: '客户端', path: '/clients' },
|
||||
@@ -575,6 +576,46 @@ async function main() {
|
||||
);
|
||||
|
||||
// 5. 上课时段发布紧急通知:全屏警告 + 3 秒倒计时 + 教师确认后才真正发布
|
||||
await win.webContents.executeJavaScript(
|
||||
`[...document.querySelectorAll('.el-menu-item')].find(e=>e.textContent.trim()==='课件')?.click()`,
|
||||
);
|
||||
await waitFor(win, `Boolean(document.querySelector('.courseware-page input[type=file]'))`);
|
||||
const coursewareName = `UI课件验收-${Date.now()}.pptx`;
|
||||
await win.webContents.executeJavaScript(`(() => {
|
||||
const input=document.querySelector('.courseware-page input[type=file]'), transfer=new DataTransfer();
|
||||
transfer.items.add(new File(['ClassHelper UI 课件验收'],${JSON.stringify(coursewareName)},{type:'application/octet-stream'}));
|
||||
input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));
|
||||
})()`);
|
||||
await sleep(100);
|
||||
await win.webContents.executeJavaScript(
|
||||
`[...document.querySelectorAll('.courseware-page button')].find(b=>b.textContent.trim()==='上传云端')?.click()`,
|
||||
);
|
||||
await waitFor(
|
||||
win,
|
||||
`document.querySelector('.courseware-page .el-table__body')?.textContent.includes(${JSON.stringify(coursewareName)})`,
|
||||
);
|
||||
const coursewareUploaded = await win.webContents.executeJavaScript(
|
||||
`(() => {const row=[...document.querySelectorAll('.courseware-page .el-table__row')].find(e=>e.textContent.includes(${JSON.stringify(coursewareName)}));return Boolean(row&&row.textContent.includes('仅自己')&&row.textContent.includes('演示文稿'));})()`,
|
||||
);
|
||||
record('课件真实上传并按类型归档,未选班级时仅自己可见', coursewareUploaded);
|
||||
if (process.env.UI_SMOKE_SHOTS_DIR)
|
||||
fs.writeFileSync(
|
||||
path.join(process.env.UI_SMOKE_SHOTS_DIR, 'courseware-web.png'),
|
||||
(await win.webContents.capturePage()).toPNG(),
|
||||
);
|
||||
await win.webContents.executeJavaScript(
|
||||
`(() => {const row=[...document.querySelectorAll('.courseware-page .el-table__row')].find(e=>e.textContent.includes(${JSON.stringify(coursewareName)}));[...row.querySelectorAll('button')].find(b=>b.textContent.trim()==='删除')?.click();})()`,
|
||||
);
|
||||
await waitFor(win, `Boolean(document.querySelector('.el-message-box__btns .el-button--primary'))`);
|
||||
await win.webContents.executeJavaScript(
|
||||
`document.querySelector('.el-message-box__btns .el-button--primary')?.click()`,
|
||||
);
|
||||
await waitFor(
|
||||
win,
|
||||
`!document.querySelector('.courseware-page .el-table__body')?.textContent.includes(${JSON.stringify(coursewareName)})`,
|
||||
);
|
||||
record('课件可从原生页面删除并清理测试文件', true);
|
||||
|
||||
// "上课时段"由 run.mjs 的探针提前用 HTTP 建好(今天 00:00-23:59 的课表)
|
||||
const urgentTitle = process.env.UI_SMOKE_URGENT_TITLE ?? 'UI 冒烟紧急通知';
|
||||
const inClass = process.env.UI_SMOKE_INCLASS === '1';
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/** 课件验收始终使用临时库和临时文件目录,不访问正在使用的班级。 */
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const dir = path.join(root, '.cache', 'v14-verification', `courseware-${process.pid}-${Date.now()}`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const env = {
|
||||
...process.env,
|
||||
DATABASE_PROVIDER: 'sqlite',
|
||||
DATABASE_URL: `file:${path.join(dir, 'test.db').replaceAll('\\', '/')}`,
|
||||
COURSEWARE_DIR: path.join(dir, 'files'),
|
||||
TZ: 'Asia/Shanghai',
|
||||
RATE_LIMIT_ENABLED: 'false',
|
||||
LOG_LEVEL: 'warn',
|
||||
};
|
||||
const require = createRequire(path.join(root, 'packages/server/package.json'));
|
||||
const { createClient } = require('@libsql/client');
|
||||
const db = createClient({ url: env.DATABASE_URL });
|
||||
await db.execute('PRAGMA user_version=0');
|
||||
db.close();
|
||||
async function run(args) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, args, { cwd: root, env, stdio: 'inherit', windowsHide: true });
|
||||
child.once('error', reject);
|
||||
child.once('exit', (code) => (code === 0 ? resolve() : reject(new Error(`课件验收失败:${code}`))));
|
||||
});
|
||||
}
|
||||
await run([
|
||||
'packages/server/node_modules/prisma/build/index.js',
|
||||
'migrate',
|
||||
'deploy',
|
||||
'--config',
|
||||
'packages/server/prisma.config.ts',
|
||||
]);
|
||||
await run([
|
||||
'--import',
|
||||
'./packages/server/node_modules/tsx/dist/loader.mjs',
|
||||
'packages/server/scripts/verify-courseware.ts',
|
||||
]);
|
||||
console.log(`隔离验收留档:${dir}`);
|
||||
@@ -102,7 +102,12 @@ try {
|
||||
namespace(fs.readFileSync(path.join(root, 'packages/server/scripts/verify-e2e.mjs'), 'utf8')),
|
||||
{ flag: 'wx' },
|
||||
);
|
||||
await run([testFile], 'e2e.log');
|
||||
if (process.argv.includes('--web')) {
|
||||
env.UI_SMOKE_URL = env.VERIFY_BASE_URL;
|
||||
env.UI_SMOKE_ADMIN = `${prefix}admin`;
|
||||
env.UI_SMOKE_ADMIN_PASS = 'admin123';
|
||||
await run(['scripts/ui-smoke/run.mjs'], 'web.log');
|
||||
} else await run([testFile], 'e2e.log');
|
||||
} finally {
|
||||
if (server && server.exitCode === null) {
|
||||
server.kill();
|
||||
|
||||
@@ -5,13 +5,10 @@
|
||||
* 资源路径、Electron 版本、`app.isPackaged` 都可能不同。交付前应实测 `packaged: true`。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/verify-packaged.mjs
|
||||
* node scripts/verify-packaged.mjs --exe "D:\class\@classhelperdesktop-client\ClassHelper.exe"
|
||||
* node scripts/verify-packaged.mjs --offline
|
||||
* node scripts/verify-packaged.mjs --code G101 --password 123456
|
||||
*
|
||||
* 联网用例需要后端在 http://127.0.0.1:4000(默认自动探测;探测不到时退化为离线用例)。
|
||||
* 退出码:0 = 两次启动都全绿且 `packaged: true`。
|
||||
* node scripts/verify-packaged.mjs --exe "releases/client/<版本>/安装包/ClassHelper-<版本>-x64-portable.exe" --offline
|
||||
* 显式 --visual 才进行桌面视觉验收,显式 --online 才探测并启用联网。
|
||||
* 联网前由调用方准备隔离服务、临时教师和班级,并显式传入测试凭据;本脚本不自动创建它们。
|
||||
* 默认后台快速、离线,两次启动均成功且 packaged=true 才返回 0。
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
|
||||
Reference in new issue
Block a user