Compare commits

...

12 Commits

Author SHA1 Message Date
KirinBaka
3db0a652ce feat: timetable refresh in 3 minutes 2025-05-07 15:36:33 +08:00
Kirin@BNBU
1049c8c8df Create run.bat 2025-05-07 15:14:54 +08:00
Kirin@BNBU
fd3dd85963 chore: update filename 2025-04-22 15:44:41 +08:00
e8ce8492cf fix: clear api branch 2025-04-22 15:38:28 +08:00
KirinBaka
ffc89fd069 feat: prefix and anti multi-check 2025-04-22 15:34:22 +08:00
KirinBaka
bebf2c9640 一些微小的贡献 2025-04-22 15:19:28 +08:00
a04f1c22c9 fix width overflow 2023-05-22 21:08:56 +08:00
89124b9ad9 seperate regular 2023-04-26 23:19:42 +08:00
6d2c0d8cb3 fix 2023-04-26 22:41:36 +08:00
7f2141e832 fix store.update 2023-02-17 09:40:15 +08:00
55f48de4db hide download button 2023-02-17 00:00:48 +08:00
7738e061fc center UI 2023-02-16 23:55:05 +08:00
22 changed files with 251 additions and 93 deletions

2
.gitignore vendored
View File

@@ -1,8 +1,10 @@
/data
/html.html
/json
/hours.json
/regular.json
/record.json
/store.json
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies

View File

@@ -1,14 +1,17 @@
const API_PREFIX = "";
export const get = async (url: string) => {
const resp = await fetch(url);
const resp = await fetch(`${API_PREFIX}${url}`);
const json = await resp.json();
return json;
};
export const post = async (
url: string,
json: any,
headers: Record<string, string> = {}
) => {
const resp = await fetch(url, {
const resp = await fetch(`${API_PREFIX}${url}`, {
method: "POST",
headers: { "Content-Type": "application/json", ...headers },
body: JSON.stringify(json),

View File

@@ -35,11 +35,13 @@ const downloadObjectAsJson = (exportObj: any, exportName: string) => {
const Timetable = ({
user,
isRegular = false,
disableNetwork = false,
disableConflictCheck = false,
replaceInputType = "checkbox",
apiRecordEndPoint = "/api/record",
openRecordMode = false,
hideDownloadButton = true,
}) => {
const [editable, setEditable] = React.useState(true);
const ref = React.useRef();
@@ -77,10 +79,13 @@ const Timetable = ({
if (disableNetwork) return;
// Immediately revert the checkbox state
target.checked = !target.checked;
// post request
const json = await post("/api/record", {
name: target.name,
checked: target.checked,
checked: !target.checked,
user,
});
if (json.error !== undefined) {
@@ -98,6 +103,7 @@ const Timetable = ({
const handleInput = (event: React.ChangeEvent<HTMLInputElement>): boolean => {
const { target } = event;
// validate
if (target?.children[0]?.tagName !== "TABLE") {
console.log("not a table");
@@ -113,7 +119,7 @@ const Timetable = ({
tds.length = 0;
const table = target.children[0];
table.setAttribute("border", "1");
table.setAttribute("border", "0");
// mark cell
const conflictsTmp: ConflictsTmp = {};
@@ -125,7 +131,18 @@ const Timetable = ({
for (const td_index in tr.children) {
const td: HTMLTableCellElement = tr.children[td_index];
if (td.tagName !== "TD") continue;
if (td.getAttribute("bgcolor")?.toUpperCase() !== "#39CEFF") {
// const position (assigned by supervisor)
if (td?.textContent?.trim() === user) {
const constSelected = document.createElement("input");
constSelected.setAttribute("type", "checkbox");
constSelected.setAttribute("checked", "1");
constSelected.setAttribute("disabled", "1");
td.innerHTML = "";
td.appendChild(constSelected);
}
row.push(null);
continue;
}
@@ -189,7 +206,7 @@ const Timetable = ({
}
const occupied: string[] = json.occupied;
const myselect: string[] = json.myselect;
console.log(json);
// console.log(json);
for (const index in indexToElement) {
if (occupied.includes(index)) {
indexToElement[index].style.display = "none";
@@ -209,11 +226,11 @@ const Timetable = ({
}
};
React.useEffect(() => {
if (disableNetwork) return;
if (disableNetwork || isRegular) return;
const interval = setInterval(() => {
refresh();
}, 1000);
}, 1500);
return () => {
clearInterval(interval);
};
@@ -221,48 +238,62 @@ const Timetable = ({
React.useEffect(() => {
const main = async () => {
const json = await get("/api/html");
const json = await get(isRegular ? "/api/html-regular" : "/api/html");
ref.current.innerHTML = json.html;
handleInput({ target: ref.current });
refresh();
};
main();
// Auto refresh the entire timetable every 3 minutes
const fullRefreshInterval = setInterval(() => {
main();
}, 3 * 60 * 1000); // 3 minutes in milliseconds
return () => {
clearInterval(fullRefreshInterval);
};
}, []);
const DownloadMarks = async () => {
console.log("download marks", marks);
const data = {
user,
selections: {},
};
for (const row of marks) {
for (const input of row) {
if (input === null) continue;
if (input.checked) {
data.selections[input.name] = 1;
} else if (parseFloat(input.value)) {
data.selections[input.name] = parseFloat(input.value);
}
}
}
console.log(data);
downloadObjectAsJson(data, user);
};
return (
<>
<button
onClick={async () => {
console.log("download marks", marks);
const data = {
user,
selections: {},
};
for (const row of marks) {
for (const input of row) {
if (input === null) continue;
if (input.checked) {
data.selections[input.name] = 1;
} else if (parseFloat(input.value)) {
data.selections[input.name] = parseFloat(input.value);
}
}
}
console.log(data);
downloadObjectAsJson(data, user);
}}
>
DownloadSelection
</button>
<span>Login as {user}</span>
<h2 style={{ textAlign: "center" }}>ITSC </h2>
<h3 style={{ textAlign: "center" }}>Login as {user}</h3>
<h3 style={{ textAlign: "center" }}></h3>
<div
align="center"
ref={ref}
contentEditable={editable}
style={{
overflow: "scroll",
}}
onInput={handleInput}
></div>{" "}
></div>
{!hideDownloadButton && (
<p style={{ display: "flex", justifyContent: "center" }}>
<button onClick={DownloadMarks}>Download Selection</button>
</p>
)}
<div style={{ display: "none" }} id="download-dom"></div>
</>
);

View File

@@ -1,3 +1,4 @@
import { NodeNextRequest } from "next/dist/server/base-http/node";
import React from "react";
const UserInputWrap = ({ children, setUser }) => {
@@ -5,31 +6,52 @@ const UserInputWrap = ({ children, setUser }) => {
const [begin, setBegin] = React.useState(false);
React.useEffect(() => {
setInputUser(localStorage.getItem("user") || "");
setInputUser(localStorage.getItem("username") || "");
}, []);
return (
<>
{!begin && (
<div>
<input
placeholder="在这输你的名字"
value={inputUser}
onChange={(event) => setInputUser(event.target.value)}
/>
<button
onClick={() => {
if (inputUser.trim() === "") {
alert("姓名不能为空");
return;
}
setUser(inputUser.trim());
setBegin(true);
localStorage.setItem("user", inputUser.trim());
}}
>
Login
</button>
<div
style={{
display: "grid",
placeItems: "center",
alignItems: "center",
// minHeight: "100vh",
}}
>
<h2 style={{ textAlign: "center" }}>ITSC </h2>
<div>
<input
style={{
margin: "0.5em",
padding: "0.5em",
borderRadius: "0.39em",
}}
placeholder="你的名字..."
value={inputUser}
onChange={(event) => setInputUser(event.target.value)}
size={10}
/>
<button
style={{
margin: "0.5em",
padding: "0.5em",
backgroundColor: "#39ceff",
}}
onClick={() => {
if (inputUser.trim() === "") {
alert("姓名不能为空");
return;
}
setUser(inputUser.trim());
setBegin(true);
localStorage.setItem("username", inputUser.trim());
}}
>
Login
</button>
</div>
</div>
)}
{begin && children}

View File

@@ -1,6 +1,6 @@
const obj = {
begin: false,
limit: 2,
token: process.env.TOKEN || "woshimima",
token: process.env.TOKEN ?? "woshimima",
};
export default obj;

BIN
data-init.zip Normal file

Binary file not shown.

BIN
data.zip Normal file

Binary file not shown.

View File

@@ -4,9 +4,9 @@
"private": true,
"scripts": {
"esbuild": "./node_modules/.bin/esbuild --bundle --tsconfig=entrypoints/tsconfig.json --alias:'@'='./' --outdir=entrypoints --splitting --format=esm entrypoints/index.tsx entrypoints/edit/index.tsx entrypoints/report/index.tsx entrypoints/control/index.tsx --minify",
"dev": "next dev",
"dev": "next dev -p 4000",
"build": "next build",
"start": "next start",
"start": "next start -p 4000",
"lint": "next lint"
},
"dependencies": {

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { store, html } from "@/store";
import config from "@/config";
export default function handler(
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Record<string, string>>
) {
@@ -16,5 +16,6 @@ export default function handler(
const json = req.body;
store.update(json);
}
await store.save();
res.status(200).json(store.get());
}

View File

@@ -4,12 +4,14 @@ import config from "@/config";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
if (req.headers.token !== config.token) {
console.log("api::config: wrong token", req.headers.token, config.token);
res.status(403).json({ error: "wrong token" });
return;
}
// update config
config.begin = req.body.begin ?? config.begin;
config.limit = req.body.limit ?? config.limit;
console.log("api::config: update config", config);
}
res.status(200).json(config);
}

16
pages/api/html-regular.ts Normal file
View File

@@ -0,0 +1,16 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { htmlRegular } from "@/store";
import config from "@/config";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
if (req.headers.token !== config.token) {
res.status(403).json({ error: "wrong token" });
return;
}
htmlRegular.set(req.body.html);
}
res.status(200).json({
html: htmlRegular.get(),
});
}

View File

@@ -5,15 +5,16 @@ import config from "@/config";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const store = storeProxy.get();
// console.log("api::store.null()", store);
if (req.method === "POST") {
if (!config.begin) {
res.status(400).json({
error: "还没到开时间哦",
error: "还没到开时间哦",
});
return;
}
const json = req.body;
console.log("request", json);
console.log("api::request: new request", json);
if (json.checked) {
let count = 0;
for (const name in store) {
@@ -21,33 +22,42 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
count += 1;
if (count >= config.limit) {
res.status(403).json({
error: `超过选择数量限制: ${config.limit}`,
error: `超过选择数量限制,您至多选 ${config.limit} 个班次`,
});
return;
}
}
}
// check whether it is alreadly occupied
if (store[json.name] !== undefined) {
// check whether the user repeatly select
if (store[json.name] === json.user) {
console.log("api::request: repeat select", json);
res.status(403).json({
error: `当前位置已被他人占用`,
error: `您已经选择了这个班次,请勿重复选择`,
});
return;
}
// check whether it is already occupied
else if (store[json.name] !== undefined) {
console.log("api::request: occupied", json);
res.status(403).json({
error: `当前位置已被他人占用,请选择其他班次`,
});
return;
}
store[json.name] = json.user;
} else {
// console.log(store, json);
// check whether the request name match the taken name
if (store[json.name] !== json.user) {
res.status(403).json({
error: `失败:您未选择到当前位置`,
error: `您已经取消了这个班次,请勿重复点击复选框`,
})
return;
}
delete store[json.name];
}
}
console.log("query", req.query);
const resp = {
const resp: { occupied: string[], myselect: string[] } = { // try to fix
occupied: [],
myselect: [],
};

View File

@@ -8,7 +8,7 @@ const read = promisify(fs.readFile);
// 索引与工时
const indexToHour: Record<string, number> = JSON.parse(
fs.readFileSync("./hours.json", "utf8")
fs.readFileSync("./data/hours.json", "utf8")
).selections;
export default async function handler(
@@ -18,7 +18,7 @@ export default async function handler(
// 读入全部json文件
// users: {姓名: {坐标: 权重}}
const users: Record<string, Record<string, number>> = {};
const files = await g("./json/*.json");
const files = await g("./data/json/*.json");
for (const file of files) {
const jsonStr = await read(file, "utf8");
const json: {

View File

@@ -5,8 +5,9 @@ import { get, post } from "@/common";
const ControlPage = () => {
const [isBegin, setIsBegin] = React.useState(false);
const [inputLimit, setInputLimit] = React.useState("2");
const [token, setToken] = React.useState("");
const toggleBegin = async () => {
const json = await post("/api/config", { begin: !isBegin });
const json = await post("/api/config", { begin: !isBegin }, { token });
setIsBegin(json.begin);
};
const refresh = async () => {
@@ -26,9 +27,16 @@ const ControlPage = () => {
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<p>
<input
value={token}
onChange={(event) => setToken(event.target.value)}
placeholder="Token"
/>
</p>
<p>
<button onClick={() => toggleBegin()}>
{isBegin ? "Begin" : "Pause"}
{isBegin ? "Pause" : "Begin"}
</button>
</p>
<p>
@@ -40,9 +48,13 @@ const ControlPage = () => {
/>
<button
onClick={() => {
post("/api/config", {
limit: parseInt(inputLimit) || 2,
});
post(
"/api/config",
{
limit: parseInt(inputLimit) || 2,
},
{ token }
);
}}
>
Set Limit

39
pages/edit-regular.tsx Normal file
View File

@@ -0,0 +1,39 @@
import React from "react";
import { get, post } from "@/common";
const EditPage = () => {
const [token, setToken] = React.useState("");
const ref = React.useRef();
const upload = async () => {
const html = ref.current.innerHTML;
await post("/api/html-regular", { html }, { token });
alert("Upload success");
refresh();
};
const refresh = async () => {
const html = await get("/api/html-regular");
ref.current.innerHTML = html.html;
};
React.useEffect(() => {
refresh();
}, []);
return (
<>
<input
value={token}
placeholder={"token"}
onChange={(event) => setToken(event.target.value)}
/>
<button onClick={() => upload()}>Upload</button>
<div
ref={ref}
onInput={(event) => {
console.log(event.currentTarget.innerHTML);
}}
contentEditable="true"
></div>
</>
);
};
export default EditPage;

View File

@@ -9,9 +9,9 @@ export default function Home() {
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title> </title>
<meta name="description" content="ITSC 学 生 助 理 抢 班 系 统" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>

View File

@@ -22,6 +22,7 @@ const ReportPage = () => {
disableNetwork={true}
apiRecordEndPoint="/api/regular"
openRecordMode={true}
isRegular={true}
/>
</UserInputWrap>
</main>

View File

@@ -1,14 +1,13 @@
import React from "react";
import Head from "next/head";
import { get, post } from "@/common";
const ReportPage = () => {
const ref = React.useRef();
const getReport = async () => {
const resp = await fetch("/api/html").then((resp) => resp.json());
const resp = await get("/api/html");
ref.current.innerHTML = resp.html;
const json: Record<string, string> = await fetch("/api/tool").then((resp) =>
resp.json()
);
const json: Record<string, string> = await get("/api/tool");
const table = ref.current.children[0];
const tbody = table.children[table.children.length - 1];
for (const tr_index in tbody.children) {

View File

@@ -1,14 +1,13 @@
import React from "react";
import Head from "next/head";
import { get, post } from "@/common";
const ReportPage = () => {
const ref = React.useRef();
const getReport = async () => {
const resp = await fetch("/api/html").then((resp) => resp.json());
const resp = await get("/api/html");
ref.current.innerHTML = resp.html;
const json: Record<string, string> = await fetch("/api/admin").then(
(resp) => resp.json()
);
const json: Record<string, string> = await get("/api/admin");
const table = ref.current.children[0];
const tbody = table.children[table.children.length - 1];
for (const tr_index in tbody.children) {

View File

@@ -7,7 +7,7 @@ export default function Home() {
const [begin, setBegin] = React.useState(false);
React.useEffect(() => {
setUser(localStorage.getItem("user") || "");
setUser(localStorage.getItem("username") || "");
}, []);
return (
@@ -34,7 +34,7 @@ export default function Home() {
}
setUser(user.trim());
setBegin(true);
localStorage.setItem("user", user);
localStorage.setItem("username", user);
}}
>
Login
@@ -47,6 +47,7 @@ export default function Home() {
disableConflictCheck={true}
disableNetwork={true}
replaceInputType={"number"}
hideDownloadButton={false}
/>
)}
</main>

2
run.bat Normal file
View File

@@ -0,0 +1,2 @@
npm start
pause

View File

@@ -26,16 +26,28 @@ class Store {
delete this.record[key];
}
public async update(record: Record<string, string>) {
await write(this.filename, JSON.stringify(this.record), "utf8");
this.record = record;
await this.save();
}
public async save() {
// try first, then catch
try {
console.log("store::index: save record", this.record);
await write(this.filename, JSON.stringify(this.record), "utf8");
} catch {
console.error("store::index: save record error, filename:", this.filename);
}
}
}
class HTML {
html: string;
constructor() {
filename: string;
constructor(filename: string) {
this.filename = filename;
// load from file
try {
this.html = fs.readFileSync("./html.html", "utf8");
this.html = fs.readFileSync(this.filename, "utf8");
} catch {
this.html = "";
}
@@ -46,10 +58,16 @@ class HTML {
public async set(html: string) {
this.html = html;
// store into file
await write("./html.html", html, "utf8");
// try first, then catch
try {
await write(this.filename, html, "utf8");
} catch {
console.error("store::index: save record error, filename:", this.filename);
}
}
}
export const html = new HTML();
export const store = new Store("store.json");
export const regular = new Store("regular.json");
export const html = new HTML("./data/html-current.html");
export const htmlRegular = new HTML("./data/html-regular.html");
export const store = new Store("./data/store.json");
export const regular = new Store("./data/regular.json");