From ea58770f168c65900d489abc7395df7484944c77 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 20:26:28 +0800 Subject: [PATCH 01/33] =?UTF-8?q?fix:=20=E7=9C=8B=E6=9D=BFSQL=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E6=97=B6=E8=87=AA=E5=8A=A8=E5=8E=BB=E9=99=A4=E5=B0=BE?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index ed8ced0..7e2efb9 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -259,6 +259,8 @@ const resolveSqlTemplate = (sql: string) => { .join(formatIso(at(-30))) } +const trimSqlTailSemicolon = (sql: string) => sql.trimEnd().replace(/;+$/, "") + const parseNumber = (value: unknown): number | undefined => { if (typeof value === "number" && Number.isFinite(value)) return value if (typeof value === "string" && value.trim() !== "") { @@ -1258,7 +1260,15 @@ ORDER BY reward_points DESC, score DESC`, title={t("board.sqlEditorTitle")} open={Boolean(editingList)} onCancel={() => setEditingListId(null)} - onOk={() => setEditingListId(null)} + onOk={() => { + if (editingList && activeBoard && canManage) { + const nextSql = trimSqlTailSemicolon(editingList.sql) + if (nextSql !== editingList.sql) { + updateList(activeBoard.id, editingList.id, { sql: nextSql }) + } + } + setEditingListId(null) + }} okText={t("common.confirm")} cancelText={t("common.cancel")} width={860} From b7eaa6b52631648b5ca7c3b23a926263ac1e2746 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 20:30:40 +0800 Subject: [PATCH 02/33] =?UTF-8?q?docs:=20=E5=BC=BA=E5=8C=96=E7=9C=8B?= =?UTF-8?q?=E6=9D=BFSQL=E6=8F=90=E7=A4=BA=E8=AF=8D=E7=9A=84=E6=89=A3?= =?UTF-8?q?=E5=88=86=E8=B4=9F=E6=95=B0=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/看板SQL生成提示词.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/resources/看板SQL生成提示词.md b/resources/看板SQL生成提示词.md index a38ab20..6ede121 100644 --- a/resources/看板SQL生成提示词.md +++ b/resources/看板SQL生成提示词.md @@ -85,6 +85,25 @@ 3. 对可能为空的数据优先使用 `COALESCE`。 4. 默认不写 `LIMIT`(系统外层会限制);除非用户明确要求更小结果集。 +## 7.1) 积分符号语义(高优先级强制规则) +`score_events.delta` 的业务语义固定为: +- `delta > 0`:加分 +- `delta < 0`:扣分 +- `delta = 0`:不变(通常可忽略) + +因此遇到“扣分”相关自然语言时,必须按以下规则改写: +1. “扣分记录” => `delta < 0` +2. “扣分大于 N 分 / 扣了超过 N 分” => `delta < -N`(或等价写法 `delta < 0 AND ABS(delta) > N`) +3. “扣分至少 N 分” => `delta <= -N`(或等价写法 `delta < 0 AND ABS(delta) >= N`) +4. “加分大于 N 分” => `delta > N` + +错误示例(禁止生成): +- “扣分 > 5” 写成 `delta > 5` + +正确示例(优先生成): +- “扣分 > 5” 写成 `delta < -5` +- “只看扣分” 写成 `delta < 0` + ## 8) 自然周规则(允许生成上个自然周 SQL) 当用户要求“上个自然周/本自然周”时,必须使用模板变量区间表达,不要自行计算数据库日期函数: - 上个自然周:`event_time >= '{ {last_week_start} }' AND event_time < '{ {this_week_start} }'` @@ -104,6 +123,7 @@ - 是否没有系统表(如 `sqlite_master`)? - 是否只用了给定业务表? - 是否所有字段都能在对应表字段清单中找到(尤其检查 `students.name` vs `students.student_name`)? +- 若需求涉及“扣分”,是否已使用负数语义(如 `delta < 0`、`delta < -N`)而非 `delta > N`? 用户需求: { {在这里粘贴用户需求} } From 95b0b29c3ac156cc70c41557abfdca1c092ac274 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 20:33:01 +0800 Subject: [PATCH 03/33] =?UTF-8?q?docs:=20=E5=A2=9E=E5=8A=A0=E7=A7=AF?= =?UTF-8?q?=E5=88=86=E7=9B=B8=E5=85=B3=E6=9F=A5=E8=AF=A2=E5=BF=85=E9=A1=BB?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E6=80=BB=E7=A7=AF=E5=88=86=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/看板SQL生成提示词.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/resources/看板SQL生成提示词.md b/resources/看板SQL生成提示词.md index 6ede121..be906a4 100644 --- a/resources/看板SQL生成提示词.md +++ b/resources/看板SQL生成提示词.md @@ -104,6 +104,25 @@ - “扣分 > 5” 写成 `delta < -5` - “只看扣分” 写成 `delta < 0` +## 7.2) 积分相关查询结果字段强制规则 +若用户意图与“积分”相关(例如:积分排行、积分变化、扣分/加分统计、学生积分看板),最终结果中必须包含“学生当前总积分”字段: +- 字段名统一输出为:`score` +- 来源必须是 `students.score`(不是 `SUM(score_events.delta)`) + +生成要求: +1. 若主表是 `students`:直接选择 `students.score AS score`(或 `s.score AS score`) +2. 若主表不是 `students`(如 `score_events`):必须关联 `students` 并带出总积分 + 标准关联:`LEFT JOIN students s ON s.name = <学生名字段>` +3. 若查询按学生聚合,仍需在结果中包含 `score`,不要省略 + +错误示例(禁止生成): +- 积分相关 SQL 只返回 `student_name`、`week_change`,但没有 `score` +- 用 `SUM(delta)` 命名为 `score` 冒充当前总积分 + +正确示例(优先生成): +- `SELECT e.student_name, COALESCE(s.score, 0) AS score, SUM(e.delta) AS week_change ... LEFT JOIN students s ON s.name = e.student_name ...` +- `SELECT s.name AS student_name, s.score AS score FROM students s ...` + ## 8) 自然周规则(允许生成上个自然周 SQL) 当用户要求“上个自然周/本自然周”时,必须使用模板变量区间表达,不要自行计算数据库日期函数: - 上个自然周:`event_time >= '{ {last_week_start} }' AND event_time < '{ {this_week_start} }'` @@ -124,6 +143,7 @@ - 是否只用了给定业务表? - 是否所有字段都能在对应表字段清单中找到(尤其检查 `students.name` vs `students.student_name`)? - 若需求涉及“扣分”,是否已使用负数语义(如 `delta < 0`、`delta < -N`)而非 `delta > N`? +- 若需求涉及“积分”,结果中是否包含 `students.score` 且命名为 `score`? 用户需求: { {在这里粘贴用户需求} } From 24e909a2ff48918c5933210e00b3056f7d62f73d Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 20:47:28 +0800 Subject: [PATCH 04/33] =?UTF-8?q?feat:=20=E7=9C=8B=E6=9D=BF=E5=AD=A6?= =?UTF-8?q?=E7=94=9F=E5=88=97=E8=A1=A8=E6=94=AF=E6=8C=81=E6=80=BB=E5=88=86?= =?UTF-8?q?=E4=B8=8E=E5=8A=A0=E6=89=A3=E5=88=86=E6=98=BE=E7=A4=BA=E5=88=87?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 82 +++++++++++++++++++++++++++++---- src/i18n/locales/en-US.json | 6 +++ src/i18n/locales/zh-CN.json | 6 +++ 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 7e2efb9..732cc8d 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -19,6 +19,7 @@ import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from "@ant import { useTranslation } from "react-i18next" type BoardStudentViewMode = "list" | "card" | "grid" +type BoardScoreDisplayMode = "total" | "split" type SplitDirection = "horizontal" | "vertical" interface StudentListConfig { @@ -26,6 +27,7 @@ interface StudentListConfig { name: string sql: string viewMode: BoardStudentViewMode + scoreDisplayMode: BoardScoreDisplayMode } interface LayoutLeafNode { @@ -67,6 +69,8 @@ interface BoardStudentCardData { key: string name: string score?: number + addScore?: number + deductScore?: number rewardPoints?: number weekChange?: number weekDeducted?: number @@ -95,18 +99,31 @@ const makeId = () => const clampRatio = (value: number) => Math.max(0.15, Math.min(0.85, value)) -const getDefaultSql = () => `SELECT - name AS student_name, - score, - reward_points -FROM students -ORDER BY score DESC` +const getDefaultSql = () => `WITH score_stat AS ( + SELECT + student_name, + SUM(CASE WHEN delta > 0 THEN delta ELSE 0 END) AS add_score, + SUM(CASE WHEN delta < 0 THEN -delta ELSE 0 END) AS deduct_score + FROM score_events + WHERE settlement_id IS NULL + GROUP BY student_name +) +SELECT + s.name AS student_name, + s.score, + s.reward_points, + COALESCE(ss.add_score, 0) AS add_score, + COALESCE(ss.deduct_score, 0) AS deduct_score +FROM students s +LEFT JOIN score_stat ss ON ss.student_name = s.name +ORDER BY s.score DESC` const createDefaultList = (): StudentListConfig => ({ id: makeId(), name: "学生积分榜", sql: getDefaultSql(), viewMode: "card", + scoreDisplayMode: "total", }) const createLeafForList = (listId: string): LayoutLeafNode => ({ @@ -205,6 +222,10 @@ const normalizeBoards = (input: unknown): BoardConfig[] => { list?.viewMode === "list" || list?.viewMode === "card" || list?.viewMode === "grid" ? list.viewMode : "card", + scoreDisplayMode: + list?.scoreDisplayMode === "total" || list?.scoreDisplayMode === "split" + ? list.scoreDisplayMode + : "total", })) .filter((list: StudentListConfig) => list.sql.trim()) : [] @@ -289,6 +310,10 @@ const toStudentCards = (rows: any[]): BoardStudentCardData[] => { key: `${name}-${index}`, name, score: parseNumber(data.score), + addScore: parseNumber(data.add_score ?? data.addScore ?? data.plus_score ?? data.plusScore), + deductScore: parseNumber( + data.deduct_score ?? data.deductScore ?? data.minus_score ?? data.minusScore + ), rewardPoints: parseNumber(data.reward_points ?? data.rewardPoints), weekChange: parseNumber(data.week_change ?? data.range_change ?? data.change), weekDeducted: parseNumber(data.week_deducted ?? data.deducted), @@ -540,6 +565,7 @@ ORDER BY reward_points DESC, score DESC`, name: item.name, sql: item.sql, viewMode: item.viewMode, + scoreDisplayMode: item.scoreDisplayMode, })) ) }, [activeBoard]) @@ -672,6 +698,7 @@ ORDER BY reward_points DESC, score DESC`, name: t("board.newList"), sql: getDefaultSql(), viewMode: "card", + scoreDisplayMode: "total", } const findLeaf = (node: LayoutNode): LayoutLeafNode | null => { @@ -796,8 +823,22 @@ ORDER BY reward_points DESC, score DESC`, const avatarColor = getAvatarColor(item.name) const avatarText = getAvatarText(item.name) const rankBadge = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : null - const primaryMetric = - item.score !== undefined + const useSplitScore = list.scoreDisplayMode === "split" + const primaryMetric = useSplitScore + ? item.addScore !== undefined + ? { label: t("board.metrics.addScore"), value: item.addScore } + : item.deductScore !== undefined + ? { label: t("board.metrics.deductScore"), value: item.deductScore } + : item.rewardPoints !== undefined + ? { label: t("board.metrics.rewardPoints"), value: item.rewardPoints } + : item.weekChange !== undefined + ? { label: t("board.metrics.weekChange"), value: item.weekChange } + : item.weekDeducted !== undefined + ? { label: t("board.metrics.weekDeducted"), value: item.weekDeducted } + : item.answeredCount !== undefined + ? { label: t("board.metrics.todayAnswered"), value: item.answeredCount } + : null + : item.score !== undefined ? { label: t("board.metrics.totalScore"), value: item.score } : item.rewardPoints !== undefined ? { label: t("board.metrics.rewardPoints"), value: item.rewardPoints } @@ -950,12 +991,23 @@ ORDER BY reward_points DESC, score DESC`, {item.name}
- {item.score !== undefined && ( + {list.scoreDisplayMode === "total" && item.score !== undefined && ( = 0 ? "success" : "error"} style={{ margin: 0 }}> {t("board.metrics.totalScore")}:{" "} {item.score > 0 ? `+${item.score}` : item.score} )} + {list.scoreDisplayMode === "split" && item.addScore !== undefined && ( + + {t("board.metrics.addScore")}:{" "} + {item.addScore > 0 ? `+${item.addScore}` : item.addScore} + + )} + {list.scoreDisplayMode === "split" && item.deductScore !== undefined && ( + + {t("board.metrics.deductScore")}: {item.deductScore} + + )} {item.rewardPoints !== undefined && ( {t("board.metrics.rewardPoints")}: {item.rewardPoints} @@ -1305,6 +1357,18 @@ ORDER BY reward_points DESC, score DESC`, ]} disabled={!canManage} /> + setSearchKeyword(e.target.value)} - onFocus={() => { - if (!disableSearchKeyboard) setShowPinyinKeyboard(true) + { - if (!disableSearchKeyboard) setShowPinyinKeyboard(true) - }} - onKeyDown={(e) => { - if (e.key === "Escape") setShowPinyinKeyboard(false) - }} - placeholder={t("home.searchPlaceholder")} - prefix={} - allowClear - style={{ width: "100%" }} - /> - {!disableSearchKeyboard && showPinyinKeyboard && ( + >
-
- {searchKeyboardLayout === "qwerty26" - ? qwertyKeyRows.map((row, rowIndex) => ( -
- {row.map((keyItem) => ( - - ))} -
- )) - : t9KeyRows.map((row, rowIndex) => ( -
- {row.map((keyItem) => ( - + ))} +
+ )) + : t9KeyRows.map((row, rowIndex) => ( +
- {keyItem.digit === "⌫" ? "⌫" : `${keyItem.digit} ${keyItem.letters}`} - + {row.map((keyItem) => ( + + ))} +
))} -
- ))} -
+
+ + )} - )} - - setLayoutType(v as LayoutType)} - style={{ - width: isMobile ? "calc(50% - 8px)" : isTablet ? "130px" : "140px", - minWidth: isMobile ? "100px" : "120px", - flexShrink: isMobile ? 1 : 0, - }} - options={[ - { value: "grouped", label: t("home.layoutBy.grouped") }, - { value: "squareGrid", label: t("home.layoutBy.squareGrid") }, - ]} - /> - - - {batchToolbar} - + setLayoutType(v as LayoutType)} + style={{ + width: isMobile ? "calc(50% - 8px)" : isTablet ? "130px" : "140px", + minWidth: isMobile ? "100px" : "120px", + flexShrink: isMobile ? 1 : 0, + }} + options={[ + { value: "grouped", label: t("home.layoutBy.grouped") }, + { value: "squareGrid", label: t("home.layoutBy.squareGrid") }, + ]} + /> + + + {batchToolbar} + + + )} {renderQuickNav()} @@ -2449,6 +2467,164 @@ export const Home: React.FC = ({ canEdit, isPortraitMode = false }) = )} + {immersiveMode && ( +
+
+ setSearchKeyword(e.target.value)} + onFocus={() => { + if (!disableSearchKeyboard) setShowPinyinKeyboard(true) + }} + onClick={() => { + if (!disableSearchKeyboard) setShowPinyinKeyboard(true) + }} + onKeyDown={(e) => { + if (e.key === "Escape") setShowPinyinKeyboard(false) + }} + placeholder={t("home.searchPlaceholder")} + prefix={} + allowClear + style={{ width: isPortraitMode ? "170px" : "220px", borderRadius: "999px", flexShrink: 0 }} + /> + setLayoutType(v as LayoutType)} + style={{ width: 126, flexShrink: 0 }} + options={[ + { value: "grouped", label: t("home.layoutBy.grouped") }, + { value: "squareGrid", label: t("home.layoutBy.squareGrid") }, + ]} + /> + + +
{batchToolbar}
+ {!disableSearchKeyboard && showPinyinKeyboard && ( +
+
+ {searchKeyboardLayout === "qwerty26" + ? qwertyKeyRows.map((row, rowIndex) => ( +
+ {row.map((keyItem) => ( + + ))} +
+ )) + : t9KeyRows.map((row, rowIndex) => ( +
+ {row.map((keyItem) => ( + + ))} +
+ ))} +
+
+ )} +
+
+ )} + {isPortraitMode ? ( Date: Fri, 27 Mar 2026 21:02:08 +0800 Subject: [PATCH 11/33] =?UTF-8?q?style:=20=E4=BC=98=E5=8C=96=E6=B2=89?= =?UTF-8?q?=E6=B5=B8=E6=A8=A1=E5=BC=8F=E6=82=AC=E6=B5=AE=E6=A0=8F=E9=80=8F?= =?UTF-8?q?=E6=98=8E=E5=BA=A6=E4=B8=8E=E5=B1=85=E4=B8=AD=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/Home.tsx b/src/components/Home.tsx index c516f4f..0cd52af 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -2475,15 +2475,17 @@ export const Home: React.FC = ({ bottom: isPortraitMode ? "12px" : "16px", transform: "translateX(-50%)", zIndex: 1100, - width: "min(calc(100vw - 20px), 1120px)", + width: "max-content", + maxWidth: "calc(100vw - 20px)", borderRadius: "999px", border: "1px solid color-mix(in srgb, var(--ss-border-color) 80%, transparent)", backgroundColor: "var(--ss-card-bg)", - background: "color-mix(in srgb, var(--ss-card-bg) 78%, transparent)", - backdropFilter: "blur(14px)", - WebkitBackdropFilter: "blur(14px)", + background: "color-mix(in srgb, var(--ss-card-bg) 62%, transparent)", + backdropFilter: "blur(16px)", + WebkitBackdropFilter: "blur(16px)", boxShadow: "0 8px 30px rgba(0, 0, 0, 0.16)", - padding: "10px 12px", + padding: "10px", + overflow: "hidden", }} >
= ({ alignItems: "center", gap: "8px", overflowX: "auto", + justifyContent: "flex-start", scrollbarWidth: "thin", }} > From a1d93e1d76d03026b88c05e8e2db7fa3e15f5d21 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 21:04:23 +0800 Subject: [PATCH 12/33] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20macOS=20?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E6=98=BE=E7=A4=BA=E5=9B=9E=E9=80=80=E4=B8=BA?= =?UTF-8?q?=E5=AE=8C=E6=95=B4UA=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2b46147..55bb376 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -518,13 +518,18 @@ function getArchitecture(): string { if (userAgent.includes("arm64") || userAgent.includes("aarch64")) { return "ARM64" - } else if (userAgent.includes("x64") || userAgent.includes("amd64")) { + } else if ( + userAgent.includes("x64") || + userAgent.includes("amd64") || + userAgent.includes("x86_64") || + userAgent.includes("intel") + ) { return "x64" } else if (userAgent.includes("i386") || userAgent.includes("i686")) { return "x86" } - return userAgent + return "Unknown" } function getPlatform(): string { From de8af343161546d43a16bcd476477f4e08d7a5cf Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 21:08:54 +0800 Subject: [PATCH 13/33] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B2=89?= =?UTF-8?q?=E6=B5=B8=E6=A8=A1=E5=BC=8F=E6=90=9C=E7=B4=A2=E9=94=AE=E7=9B=98?= =?UTF-8?q?=E4=B8=8E=E4=B8=8B=E6=8B=89=E5=B1=82=E7=BA=A7=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 0cd52af..4caf53d 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -117,6 +117,7 @@ export const Home: React.FC = ({ const scrollContainerRef = useRef(null) const groupRefs = useRef>({}) const searchAreaRef = useRef(null) + const immersiveToolbarRef = useRef(null) const [selectedStudent, setSelectedStudent] = useState(null) const [batchMode, setBatchMode] = useState(false) @@ -376,6 +377,10 @@ export const Home: React.FC = ({ setSearchKeyword((prev) => `${prev}${keyValue}`) } + const getImmersivePopupContainer = useCallback((triggerNode: HTMLElement) => { + return immersiveToolbarRef.current ?? triggerNode.parentElement ?? document.body + }, []) + const getDisplayText = (name: string) => { if (!name) return "" return name.length > 2 ? name.substring(name.length - 2) : name @@ -2469,6 +2474,8 @@ export const Home: React.FC = ({ {immersiveMode && (
= ({ WebkitBackdropFilter: "blur(16px)", boxShadow: "0 8px 30px rgba(0, 0, 0, 0.16)", padding: "10px", - overflow: "hidden", + overflow: "visible", }} >
= ({ setLayoutType(v as LayoutType)} + getPopupContainer={getImmersivePopupContainer} style={{ width: 126, flexShrink: 0 }} options={[ { value: "grouped", label: t("home.layoutBy.grouped") }, From e6d1e03238432f03b4d688939e22fe7579404fb9 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 21:36:48 +0800 Subject: [PATCH 14/33] =?UTF-8?q?fix:=20=E7=B2=BE=E7=AE=80=E4=B8=BB?= =?UTF-8?q?=E9=A1=B5=E6=96=87=E6=A1=88=E6=8C=89=E9=92=AE=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/locales/zh-CN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index a6b1188..f82b8fb 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -423,7 +423,7 @@ "operationTitle": "积分操作:{{name}}", "submitOperation": "提交操作", "operationTitleBatch": "积分操作:已选 {{count}} 人", - "undoLastAction": "撤销上一步", + "undoLastAction": "撤销", "undoLastHint": "撤销上一条:{{name}} {{delta}}", "undoUnavailable": "当前没有可撤销的积分操作", "undoLastSuccess": "已撤销上一步积分操作", @@ -592,7 +592,7 @@ }, "rewardExchange": { "title": "奖励兑换", - "enterMode": "进入奖励兑换模式", + "enterMode": "奖励兑换", "exitMode": "退出奖励兑换模式", "modeHint": "当前为奖励兑换模式,点击学生卡可选择可兑换奖励。", "normalHint": "当前显示学生原始积分,点击按钮后将显示可兑换积分。", From 56d56e34ab3940c20def1489e6fc7ec2d81020f4 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 21:47:47 +0800 Subject: [PATCH 15/33] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=B2=89?= =?UTF-8?q?=E6=B5=B8=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2=E8=BF=87=E6=B8=A1?= =?UTF-8?q?=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 57 +++++++++++++++++++++++++++++++++-------- src/assets/main.css | 42 ++++++++++++++++++++++++++++++ src/components/Home.tsx | 50 ++++++++++++++++++------------------ 3 files changed, 114 insertions(+), 35 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 55bb376..f5415fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,8 @@ import { ContentArea } from "./components/ContentArea" import { OOBE } from "./components/OOBE/OOBE" import { ThemeProvider, useTheme } from "./contexts/ThemeContext" +const IMMERSIVE_TRANSITION_MS = 260 + function MainContent(): React.JSX.Element { const { t } = useTranslation() const navigate = useNavigate() @@ -15,6 +17,8 @@ function MainContent(): React.JSX.Element { const [messageApi, contextHolder] = message.useMessage() const { isIosDevice, isAndroidDevice, defaultPortraitMode } = useMemo(getMobileDeviceInfo, []) const [immersiveMode, setImmersiveMode] = useState(false) + const [sidebarVisible, setSidebarVisible] = useState(true) + const sidebarTransitionTimerRef = useRef(null) useEffect(() => { const api = (window as any).api @@ -350,6 +354,30 @@ function MainContent(): React.JSX.Element { }) } + useEffect(() => { + if (sidebarTransitionTimerRef.current) { + window.clearTimeout(sidebarTransitionTimerRef.current) + sidebarTransitionTimerRef.current = null + } + + if (!immersiveMode) { + setSidebarVisible(true) + return + } + + sidebarTransitionTimerRef.current = window.setTimeout(() => { + setSidebarVisible(false) + sidebarTransitionTimerRef.current = null + }, IMMERSIVE_TRANSITION_MS) + + return () => { + if (sidebarTransitionTimerRef.current) { + window.clearTimeout(sidebarTransitionTimerRef.current) + sidebarTransitionTimerRef.current = null + } + } + }, [immersiveMode]) + const isDark = currentTheme?.mode === "dark" const brandColor = currentTheme?.config?.tdesign?.brandColor || "#0052D9" @@ -366,16 +394,25 @@ function MainContent(): React.JSX.Element { > {contextHolder} - {!immersiveMode && ( - + {sidebarVisible && ( +
+ +
)} = ({ )} - {immersiveMode && ( -
+
= ({
)}
-
- )} +
{isPortraitMode ? ( Date: Fri, 27 Mar 2026 21:49:01 +0800 Subject: [PATCH 16/33] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E5=BA=95=E9=83=A8=E7=81=B0=E8=89=B2=E7=A9=BA?= =?UTF-8?q?=E9=9A=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/main.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/assets/main.css b/src/assets/main.css index d3b3279..79996a0 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -228,8 +228,12 @@ html.platform-macos #root { .ss-immersive-sidebar { --ss-sidebar-width: 200px; + height: 100%; + display: flex; + flex-shrink: 0; width: var(--ss-sidebar-width); overflow: hidden; + background: var(--ss-sidebar-bg); opacity: 1; transform: translateX(0); transition: @@ -239,6 +243,10 @@ html.platform-macos #root { will-change: width, opacity, transform; } +.ss-immersive-sidebar .ant-layout-sider { + height: 100% !important; +} + .ss-immersive-sidebar.is-hidden { width: 0; opacity: 0; From 8ee3729596cdaba0d8373ac684670a1874b3ebf0 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 21:49:48 +0800 Subject: [PATCH 17/33] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=80=80?= =?UTF-8?q?=E5=87=BA=E6=B2=89=E6=B5=B8=E6=A8=A1=E5=BC=8F=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E6=A0=8F=E6=97=A0=E8=BF=87=E6=B8=A1=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 66 +++++++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 48 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f5415fc..b8637d5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,8 +7,6 @@ import { ContentArea } from "./components/ContentArea" import { OOBE } from "./components/OOBE/OOBE" import { ThemeProvider, useTheme } from "./contexts/ThemeContext" -const IMMERSIVE_TRANSITION_MS = 260 - function MainContent(): React.JSX.Element { const { t } = useTranslation() const navigate = useNavigate() @@ -17,8 +15,6 @@ function MainContent(): React.JSX.Element { const [messageApi, contextHolder] = message.useMessage() const { isIosDevice, isAndroidDevice, defaultPortraitMode } = useMemo(getMobileDeviceInfo, []) const [immersiveMode, setImmersiveMode] = useState(false) - const [sidebarVisible, setSidebarVisible] = useState(true) - const sidebarTransitionTimerRef = useRef(null) useEffect(() => { const api = (window as any).api @@ -354,30 +350,6 @@ function MainContent(): React.JSX.Element { }) } - useEffect(() => { - if (sidebarTransitionTimerRef.current) { - window.clearTimeout(sidebarTransitionTimerRef.current) - sidebarTransitionTimerRef.current = null - } - - if (!immersiveMode) { - setSidebarVisible(true) - return - } - - sidebarTransitionTimerRef.current = window.setTimeout(() => { - setSidebarVisible(false) - sidebarTransitionTimerRef.current = null - }, IMMERSIVE_TRANSITION_MS) - - return () => { - if (sidebarTransitionTimerRef.current) { - window.clearTimeout(sidebarTransitionTimerRef.current) - sidebarTransitionTimerRef.current = null - } - } - }, [immersiveMode]) - const isDark = currentTheme?.mode === "dark" const brandColor = currentTheme?.config?.tdesign?.brandColor || "#0052D9" @@ -394,26 +366,24 @@ function MainContent(): React.JSX.Element { > {contextHolder} - {sidebarVisible && ( -
- -
- )} +
+ +
Date: Fri, 27 Mar 2026 21:50:22 +0800 Subject: [PATCH 18/33] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8react-awesome-q?= =?UTF-8?q?uery-builder=E5=AE=9E=E7=8E=B0=E8=87=AA=E5=8A=A8=E5=8A=A0?= =?UTF-8?q?=E5=88=86=E8=A7=84=E5=88=99=E6=9E=84=E5=BB=BA=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 - pnpm-lock.yaml | 140 ++-------- src/components/AutoScoreManager.tsx | 42 +-- src/components/autoScore/BuilderUtils.ts | 149 +++++++++++ src/components/autoScore/Rule.tsx | 204 ++++++++++++++ src/components/autoScore/aaa.tsx | 104 ++++++++ src/components/autoScore/actionComponent.tsx | 107 -------- .../autoScore/ruleBuilderOverride.css | 251 ------------------ src/components/autoScore/ruleBuilderUtils.ts | 117 -------- src/components/autoScore/ruleComponent.tsx | 64 ----- 10 files changed, 478 insertions(+), 705 deletions(-) create mode 100644 src/components/autoScore/BuilderUtils.ts create mode 100644 src/components/autoScore/Rule.tsx create mode 100644 src/components/autoScore/aaa.tsx delete mode 100644 src/components/autoScore/actionComponent.tsx delete mode 100644 src/components/autoScore/ruleBuilderOverride.css delete mode 100644 src/components/autoScore/ruleBuilderUtils.ts delete mode 100644 src/components/autoScore/ruleComponent.tsx diff --git a/package.json b/package.json index 8aebb7b..9db5c3a 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,6 @@ "dependencies": { "@ant-design/icons": "^6.1.0", "@react-awesome-query-builder/antd": "6.7.0-alpha.0", - "@react-querybuilder/antd": "^8.14.0", - "@react-querybuilder/dnd": "^8.14.0", "@tauri-apps/api": "^2.5.0", "antd": "^6.3.1", "dayjs": "^1.11.20", @@ -28,9 +26,6 @@ "json-rules-engine": "^7.3.1", "pinyin-pro": "^3.27.0", "react": "^19.2.1", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", - "react-dnd-touch-backend": "^16.0.1", "react-dom": "^19.2.1", "react-i18next": "^16.5.6", "react-querybuilder": "^8.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d409fb3..8b2c5bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,6 @@ importers: '@react-awesome-query-builder/antd': specifier: 6.7.0-alpha.0 version: 6.7.0-alpha.0(@ant-design/icons@6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(antd@6.3.2(moment@2.30.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(dayjs@1.11.20)(moment@2.30.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-querybuilder/antd': - specifier: ^8.14.0 - version: 8.14.0(@ant-design/icons@6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(antd@6.3.2(moment@2.30.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(dayjs@1.11.20)(react-querybuilder@8.14.0(@types/react@19.2.14)(json-logic-js@2.0.5)(react@19.2.4)(redux@5.0.1))(react@19.2.4) - '@react-querybuilder/dnd': - specifier: ^8.14.0 - version: 8.14.0(react-dnd-html5-backend@16.0.1)(react-dnd-touch-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14))(@types/node@22.19.15)(@types/react@19.2.14)(react@19.2.4))(react-querybuilder@8.14.0(@types/react@19.2.14)(json-logic-js@2.0.5)(react@19.2.4)(redux@5.0.1))(react@19.2.4) '@tauri-apps/api': specifier: ^2.5.0 version: 2.10.1 @@ -41,15 +35,6 @@ importers: react: specifier: ^19.2.1 version: 19.2.4 - react-dnd: - specifier: ^16.0.1 - version: 16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14))(@types/node@22.19.15)(@types/react@19.2.14)(react@19.2.4) - react-dnd-html5-backend: - specifier: ^16.0.1 - version: 16.0.1 - react-dnd-touch-backend: - specifier: ^16.0.1 - version: 16.0.1 react-dom: specifier: ^19.2.1 version: 19.2.4(react@19.2.4) @@ -791,24 +776,6 @@ packages: react: ^16.8.4 || ^17.0.1 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.4 || ^17.0.1 || ^18.0.0 || ^19.0.0 - '@react-dnd/asap@5.0.2': - resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==} - - '@react-dnd/invariant@4.0.2': - resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==} - - '@react-dnd/shallowequal@4.0.2': - resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==} - - '@react-querybuilder/antd@8.14.0': - resolution: {integrity: sha512-H6SMCmKtciOIJyXo29oLMMDIG1+ydE7eoIqAIhItsvJN7iLcAPhvKd5+CuHyOWj9tokEAHGpdm4BP+KPFAxTNw==} - peerDependencies: - '@ant-design/icons': '>=5' - antd: '>=5.11.0' - dayjs: '>=1' - react: '>=18' - react-querybuilder: 8.14.0 - '@react-querybuilder/core@8.14.0': resolution: {integrity: sha512-j1pIY0Yyn/dXu9ZST/DVY7TqRmIO1hY/mZ8653DaeHaDzUF37tOdkm/NQDU9RfM0KXIWsJY5zlvYAR1DypZ+7g==} peerDependencies: @@ -823,20 +790,6 @@ packages: sequelize: optional: true - '@react-querybuilder/dnd@8.14.0': - resolution: {integrity: sha512-g0BHs9Vtbm7OhnhIq1pxJNY3JXNmSbA7NVTlOuqoAfFl0D46rZfGInVzoBfpwVpoLtQIXPO25Y5N/1/LcWuSNg==} - peerDependencies: - react: '>=18' - react-dnd: '>=14.0.0' - react-dnd-html5-backend: '>=14.0.0' - react-dnd-touch-backend: '>=14.0.0' - react-querybuilder: 8.14.0 - peerDependenciesMeta: - react-dnd-html5-backend: - optional: true - react-dnd-touch-backend: - optional: true - '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -889,66 +842,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1012,30 +978,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.10.1': resolution: {integrity: sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': resolution: {integrity: sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.10.1': resolution: {integrity: sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.10.1': resolution: {integrity: sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.10.1': resolution: {integrity: sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==} @@ -1372,9 +1343,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - dnd-core@16.0.1: - resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==} - doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -2029,27 +1997,6 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - react-dnd-html5-backend@16.0.1: - resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==} - - react-dnd-touch-backend@16.0.1: - resolution: {integrity: sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==} - - react-dnd@16.0.1: - resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==} - peerDependencies: - '@types/hoist-non-react-statics': '>= 3.3.1' - '@types/node': '>= 12' - '@types/react': '>= 16' - react: '>= 16.14' - peerDependenciesMeta: - '@types/hoist-non-react-statics': - optional: true - '@types/node': - optional: true - '@types/react': - optional: true - react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -3210,20 +3157,6 @@ snapshots: - '@types/react-dom' - react-native - '@react-dnd/asap@5.0.2': {} - - '@react-dnd/invariant@4.0.2': {} - - '@react-dnd/shallowequal@4.0.2': {} - - '@react-querybuilder/antd@8.14.0(@ant-design/icons@6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(antd@6.3.2(moment@2.30.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(dayjs@1.11.20)(react-querybuilder@8.14.0(@types/react@19.2.14)(json-logic-js@2.0.5)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': - dependencies: - '@ant-design/icons': 6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - antd: 6.3.2(moment@2.30.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - dayjs: 1.11.20 - react: 19.2.4 - react-querybuilder: 8.14.0(@types/react@19.2.14)(json-logic-js@2.0.5)(react@19.2.4)(redux@5.0.1) - '@react-querybuilder/core@8.14.0(json-logic-js@2.0.5)': dependencies: '@ts-jison/lexer': 0.4.1-alpha.1 @@ -3233,15 +3166,6 @@ snapshots: optionalDependencies: json-logic-js: 2.0.5 - '@react-querybuilder/dnd@8.14.0(react-dnd-html5-backend@16.0.1)(react-dnd-touch-backend@16.0.1)(react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14))(@types/node@22.19.15)(@types/react@19.2.14)(react@19.2.4))(react-querybuilder@8.14.0(@types/react@19.2.14)(json-logic-js@2.0.5)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': - dependencies: - react: 19.2.4 - react-dnd: 16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14))(@types/node@22.19.15)(@types/react@19.2.14)(react@19.2.4) - react-querybuilder: 8.14.0(@types/react@19.2.14)(json-logic-js@2.0.5)(react@19.2.4)(redux@5.0.1) - optionalDependencies: - react-dnd-html5-backend: 16.0.1 - react-dnd-touch-backend: 16.0.1 - '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.1.0 @@ -3811,12 +3735,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - dnd-core@16.0.1: - dependencies: - '@react-dnd/asap': 5.0.2 - '@react-dnd/invariant': 4.0.2 - redux: 4.2.1 - doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -4594,28 +4512,6 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-is: 18.3.1 - react-dnd-html5-backend@16.0.1: - dependencies: - dnd-core: 16.0.1 - - react-dnd-touch-backend@16.0.1: - dependencies: - '@react-dnd/invariant': 4.0.2 - dnd-core: 16.0.1 - - react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14))(@types/node@22.19.15)(@types/react@19.2.14)(react@19.2.4): - dependencies: - '@react-dnd/invariant': 4.0.2 - '@react-dnd/shallowequal': 4.0.2 - dnd-core: 16.0.1 - fast-deep-equal: 3.1.3 - hoist-non-react-statics: 3.3.2 - react: 19.2.4 - optionalDependencies: - '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.14) - '@types/node': 22.19.15 - '@types/react': 19.2.14 - react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 diff --git a/src/components/AutoScoreManager.tsx b/src/components/AutoScoreManager.tsx index 3ac5a76..f6c90ac 100644 --- a/src/components/AutoScoreManager.tsx +++ b/src/components/AutoScoreManager.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react" import { HolderOutlined } from "@ant-design/icons" import { useTranslation } from "react-i18next" -import RuleComponent from "./autoScore/ruleComponent" +import DemoQueryBuilder from "./autoScore/aaa" import { Card, Form, @@ -426,44 +426,8 @@ export const AutoScoreManager: React.FC = () => { - - - {/* - - {triggerList.map((trigger, index) => renderTriggerItem(trigger, index))} - {' '} - - - - */} - - {/* - - {actionList.map((action) => renderActionItem(action))} - - - */} - + +
- ))} - - - -
- ) -} - -export default ActionComponent diff --git a/src/components/autoScore/ruleBuilderOverride.css b/src/components/autoScore/ruleBuilderOverride.css deleted file mode 100644 index 045ac08..0000000 --- a/src/components/autoScore/ruleBuilderOverride.css +++ /dev/null @@ -1,251 +0,0 @@ -.ruleGroup { - background-color: var(--ss-card-bg, #fff); - border: 1px solid var(--ss-border-color, #d9d9d9); - border-radius: 8px; - padding: 16px; - margin-bottom: 12px; -} - -.ruleGroup-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 12px; - flex-wrap: wrap; -} - -.ruleGroup-combinators, -.ruleGroup-addRule, -.ruleGroup-addGroup { - margin-right: 8px; -} - -.ruleGroup-body { - padding-left: 12px; - border-left: 2px solid var(--ss-primary-color, #1677ff); - margin-left: 4px; -} - -.rule { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background-color: var(--ss-item-bg, rgba(0, 0, 0, 0.02)); - border-radius: 6px; - margin-bottom: 8px; - flex-wrap: wrap; -} - -.rule:hover { - background-color: var(--ss-item-hover, rgba(0, 0, 0, 0.04)); -} - -.rule-fields, -.rule-operators, -.rule-value, -.rule-subfields { - min-width: 120px; -} - -.rule-fields .ant-select-selector, -.rule-operators .ant-select-selector, -.rule-subfields .ant-select-selector { - background-color: var(--ss-input-bg, #fff) !important; - border-color: var(--ss-border-color, #d9d9d9) !important; - color: var(--ss-text-main, rgba(0, 0, 0, 0.88)) !important; -} - -.rule-fields .ant-select-selector:hover, -.rule-operators .ant-select-selector:hover, -.rule-subfields .ant-select-selector:hover { - border-color: var(--ss-primary-color, #1677ff) !important; -} - -.rule-fields .ant-select-focused .ant-select-selector, -.rule-operators .ant-select-focused .ant-select-selector, -.rule-subfields .ant-select-focused .ant-select-selector { - border-color: var(--ss-primary-color, #1677ff) !important; - box-shadow: 0 0 0 2px rgba(var(--ss-primary-rgb, 22, 119, 255), 0.1) !important; -} - -.rule-value .ant-input, -.rule-value .ant-input-number, -.rule-value .ant-select-selector { - background-color: var(--ss-input-bg, #fff) !important; - border-color: var(--ss-border-color, #d9d9d9) !important; - color: var(--ss-text-main, rgba(0, 0, 0, 0.88)) !important; -} - -.rule-value .ant-input:hover, -.rule-value .ant-input-number:hover, -.rule-value .ant-select-selector:hover { - border-color: var(--ss-primary-color, #1677ff) !important; -} - -.rule-value .ant-input:focus, -.rule-value .ant-input-number-focused, -.rule-value .ant-select-focused .ant-select-selector { - border-color: var(--ss-primary-color, #1677ff) !important; - box-shadow: 0 0 0 2px rgba(var(--ss-primary-rgb, 22, 119, 255), 0.1) !important; -} - -.rule-value .ant-input-number-input { - color: var(--ss-text-main, rgba(0, 0, 0, 0.88)) !important; -} - -.rule-remove, -.ruleGroup-remove { - color: #fff !important; - background-color: var(--ant-color-error, #ff4d4f) !important; - border-color: var(--ant-color-error, #ff4d4f) !important; - transition: all 0.2s; -} - -.rule-remove:hover, -.ruleGroup-remove:hover { - color: #fff !important; - background-color: #ff7875 !important; - border-color: #ff7875 !important; -} - -.ruleGroup-addRule, -.ruleGroup-addGroup { - color: var(--ss-primary-color, #1677ff) !important; - border-color: var(--ss-primary-color, #1677ff) !important; - background-color: transparent !important; -} - -.ruleGroup-addRule:hover, -.ruleGroup-addGroup:hover { - color: #fff !important; - background-color: var(--ss-primary-color, #1677ff) !important; -} - -.betweenRules { - display: flex; - align-items: center; - justify-content: center; - margin: 8px 0; - color: var(--ss-text-secondary, rgba(0, 0, 0, 0.45)); - font-size: 12px; -} - -.betweenRules::before, -.betweenRules::after { - content: ""; - flex: 1; - height: 1px; - background-color: var(--ss-border-color, #d9d9d9); -} - -.betweenRules::before { - margin-right: 12px; -} - -.betweenRules::after { - margin-left: 12px; -} - -.dragHandle { - cursor: grab; - color: var(--ss-text-secondary, rgba(0, 0, 0, 0.45)); - padding: 4px; - margin-right: 4px; -} - -.dragHandle:hover { - color: var(--ss-text-main, rgba(0, 0, 0, 0.88)); -} - -.queryBuilder .ant-select-dropdown { - background-color: var(--ss-card-bg, #fff); -} - -.queryBuilder .ant-select-item { - color: var(--ss-text-main, rgba(0, 0, 0, 0.88)); -} - -.queryBuilder .ant-select-item-option-active { - background-color: var(--ss-item-hover, rgba(0, 0, 0, 0.04)); -} - -.queryBuilder .ant-select-item-option-selected { - background-color: rgba(var(--ss-primary-rgb, 22, 119, 255), 0.1); - color: var(--ss-primary-color, #1677ff); -} - -.queryBuilder .ant-btn { - border-radius: 6px; -} - -.queryBuilder .ant-select-selector, -.queryBuilder .ant-input, -.queryBuilder .ant-input-number { - border-radius: 6px; -} - -[theme-mode="dark"] .ruleGroup { - background-color: var(--ss-card-bg, #1f1f1f); - border-color: var(--ss-border-color, #424242); -} - -[theme-mode="dark"] .rule { - background-color: var(--ss-item-bg, rgba(255, 255, 255, 0.04)); -} - -[theme-mode="dark"] .rule:hover { - background-color: var(--ss-item-hover, rgba(255, 255, 255, 0.08)); -} - -[theme-mode="dark"] .rule-fields .ant-select-selector, -[theme-mode="dark"] .rule-operators .ant-select-selector, -[theme-mode="dark"] .rule-subfields .ant-select-selector, -[theme-mode="dark"] .rule-value .ant-input, -[theme-mode="dark"] .rule-value .ant-input-number, -[theme-mode="dark"] .rule-value .ant-select-selector { - background-color: var(--ss-input-bg, #141414) !important; - border-color: var(--ss-border-color, #424242) !important; - color: var(--ss-text-main, rgba(255, 255, 255, 0.85)) !important; -} - -[theme-mode="dark"] .rule-value .ant-input-number-input { - color: var(--ss-text-main, rgba(255, 255, 255, 0.85)) !important; -} - -[theme-mode="dark"] .betweenRules { - color: var(--ss-text-secondary, rgba(255, 255, 255, 0.45)); -} - -[theme-mode="dark"] .betweenRules::before, -[theme-mode="dark"] .betweenRules::after { - background-color: var(--ss-border-color, #424242); -} - -[theme-mode="dark"] .dragHandle { - color: var(--ss-text-secondary, rgba(255, 255, 255, 0.45)); -} - -[theme-mode="dark"] .dragHandle:hover { - color: var(--ss-text-main, rgba(255, 255, 255, 0.85)); -} - -[theme-mode="dark"] .queryBuilder .ant-select-dropdown { - background-color: var(--ss-card-bg, #1f1f1f); -} - -[theme-mode="dark"] .queryBuilder .ant-select-item { - color: var(--ss-text-main, rgba(255, 255, 255, 0.85)); -} - -[theme-mode="dark"] .queryBuilder .ant-select-item-option-active { - background-color: var(--ss-item-hover, rgba(255, 255, 255, 0.08)); -} - -[theme-mode="dark"] .queryBuilder .ant-select-item-option-selected { - background-color: rgba(var(--ss-primary-rgb, 22, 119, 255), 0.2); -} - -.ruleGroup-header .ant-select-selector { - min-width: 80px; -} diff --git a/src/components/autoScore/ruleBuilderUtils.ts b/src/components/autoScore/ruleBuilderUtils.ts deleted file mode 100644 index 4b9c01d..0000000 --- a/src/components/autoScore/ruleBuilderUtils.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { RuleGroupType, Field, RuleType } from "react-querybuilder" -import { defaultOperators } from "react-querybuilder" -import { fetchAllTags } from "../TagEditorDialog" - -const tags = await fetchAllTags() - -export interface AutoScoreTrigger { - event: string - value?: string - relation?: "AND" | "OR" -} - -export interface AutoScoreAction { - event: string - value?: string - reason?: string -} - -export interface AutoScoreRuleData { - triggers: AutoScoreTrigger[] - actions: AutoScoreAction[] -} - -export const validator = (r: RuleType) => !!r.value - -export const getFields = (t: (key: string) => string): Field[] => [ - { - name: "student_has_tag", - label: t("triggers.studentTag.label"), - placeholder: t("triggers.studentTag.placeholder"), - valueEditorType: "multiselect", - values: tags.map((tag: { id: number; name: string }) => tag.name), - defaultValue: tags.length > 0 ? [tags[0].name] : [], - operators: defaultOperators.filter((op) => op.name === "in"), - }, - { - name: "interval_time_passed", - label: t("triggers.intervalTime.label"), - matchModes: ["all"], - subproperties: [ - { - name: "month", - label: t("triggers.intervalTime.monthLabel"), - inputType: "number", - datatype: "month", - operators: ["="], - }, - /* { name: 'week', label: t('triggers.intervalTime.weekLabel'), inputType: 'week', datatype: 'week', operators: ['='] }, - */ { - name: "time", - label: t("triggers.intervalTime.timeLabel"), - inputType: "time", - datatype: "time", - operators: ["="], - }, - ], - }, -] - -export const defaultQuery: RuleGroupType = { - combinator: "and", - rules: [{ field: "interval_time_passed", operator: "=", value: "1440" }], -} - -export function queryToAutoScoreRule(): AutoScoreRuleData { - const triggers: AutoScoreTrigger[] = [] - - /* const processRuleGroup = (group: RuleGroupType, relation: 'AND' | 'OR' = 'AND') => { - group.rules.forEach((rule, index) => { - if ('rules' in rule) { - processRuleGroup(rule, group.combinator === 'and' ? 'AND' : 'OR') - } else { - const trigger: AutoScoreTrigger = { - event: rule.field, - value: rule.value as string - } - - if (index > 0) { - trigger.relation = relation - } - - triggers.push(trigger) - } - }) - } */ - - /* processRuleGroup(query) */ - - return { - triggers, - actions: [], - } -} - -export function autoScoreRuleToQuery(ruleData: AutoScoreRuleData): RuleGroupType { - const rules: any[] = [] - let currentCombinator: "and" | "or" = "and" - - ruleData.triggers.forEach((trigger, index) => { - if (index === 0) { - currentCombinator = "and" - } else if (trigger.relation === "OR") { - currentCombinator = "or" - } - - rules.push({ - field: trigger.event, - operator: "=", - value: trigger.value || "", - }) - }) - - return { - combinator: currentCombinator, - rules: rules.length > 0 ? rules : defaultQuery.rules, - } -} diff --git a/src/components/autoScore/ruleComponent.tsx b/src/components/autoScore/ruleComponent.tsx deleted file mode 100644 index b9da023..0000000 --- a/src/components/autoScore/ruleComponent.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { formatQuery, QueryBuilder, RuleGroupType } from "react-querybuilder" -import { useState, useEffect } from "react" -import { useTranslation } from "react-i18next" -import { QueryBuilderDnD } from "@react-querybuilder/dnd" -import * as ReactDnD from "react-dnd" -import { QueryBuilderAntD } from "@react-querybuilder/antd" -import * as ReactDndHtml5Backend from "react-dnd-html5-backend" -import * as ReactDndTouchBackend from "react-dnd-touch-backend" -import { - getFields, - defaultQuery, - autoScoreRuleToQuery, - queryToAutoScoreRule, - type AutoScoreRuleData, -} from "./ruleBuilderUtils" -import { Card } from "antd" - -import "./ruleBuilderOverride.css" - -interface RuleComponentProps { - initialData?: AutoScoreRuleData - onChange?: (data: AutoScoreRuleData) => void -} - -export const RuleComponent: React.FC = ({ initialData, onChange }) => { - const { t } = useTranslation() - const [query, setQuery] = useState( - initialData ? autoScoreRuleToQuery(initialData) : defaultQuery - ) - - useEffect(() => { - if (initialData) { - setQuery(autoScoreRuleToQuery(initialData)) - } - }, [initialData]) - - const handleQueryChange = (newQuery: RuleGroupType) => { - setQuery(newQuery) - if (onChange) { - const ruleData = queryToAutoScoreRule() - onChange(ruleData) - } - } - - return ( -
- - - - - - -
-
{formatQuery(query, "json")}
-
-
-
- ) -} - -export default RuleComponent From 71862d996d57f1ff3f6c546755b73c845bf54c4f Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 21:53:04 +0800 Subject: [PATCH 19/33] =?UTF-8?q?feat:=20=E6=B2=89=E6=B5=B8=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=BA=95=E6=A0=8F=E9=95=BF=E5=BA=A6=E5=8F=98=E5=8C=96?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9B=9E=E5=BC=B9=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/main.css | 5 +++-- src/components/Home.tsx | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/assets/main.css b/src/assets/main.css index 79996a0..c889ddc 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -258,9 +258,10 @@ html.platform-macos #root { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); transition: + width 420ms cubic-bezier(0.2, 1.2, 0.26, 1), opacity 200ms ease, - transform 260ms cubic-bezier(0.22, 1, 0.36, 1); - will-change: opacity, transform; + transform 320ms cubic-bezier(0.2, 1.2, 0.26, 1); + will-change: width, opacity, transform; } .ss-immersive-toolbar.is-hidden { diff --git a/src/components/Home.tsx b/src/components/Home.tsx index be0c2fb..a6a1a99 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -118,6 +118,8 @@ export const Home: React.FC = ({ const groupRefs = useRef>({}) const searchAreaRef = useRef(null) const immersiveToolbarRef = useRef(null) + const immersiveToolbarContentRef = useRef(null) + const [immersiveToolbarWidth, setImmersiveToolbarWidth] = useState(null) const [selectedStudent, setSelectedStudent] = useState(null) const [batchMode, setBatchMode] = useState(false) @@ -321,6 +323,33 @@ export const Home: React.FC = ({ return () => document.removeEventListener("mousedown", onDocumentClick) }, []) + useEffect(() => { + if (!immersiveMode) return + const contentEl = immersiveToolbarContentRef.current + if (!contentEl) return + + let frameId: number | null = null + const updateToolbarWidth = () => { + if (frameId !== null) cancelAnimationFrame(frameId) + frameId = requestAnimationFrame(() => { + const contentWidth = Math.ceil(contentEl.getBoundingClientRect().width) + const nextWidth = contentWidth + 20 + setImmersiveToolbarWidth((prev) => (prev === nextWidth ? prev : nextWidth)) + }) + } + + updateToolbarWidth() + const observer = new ResizeObserver(updateToolbarWidth) + observer.observe(contentEl) + window.addEventListener("resize", updateToolbarWidth) + + return () => { + observer.disconnect() + window.removeEventListener("resize", updateToolbarWidth) + if (frameId !== null) cancelAnimationFrame(frameId) + } + }, [immersiveMode]) + useEffect(() => { return () => { if (longPressTimerRef.current !== null) { @@ -2481,9 +2510,8 @@ export const Home: React.FC = ({ position: "fixed", left: "50%", bottom: isPortraitMode ? "12px" : "16px", - transform: "translateX(-50%)", zIndex: 1100, - width: "max-content", + width: immersiveToolbarWidth ? `${immersiveToolbarWidth}px` : "max-content", maxWidth: "calc(100vw - 20px)", borderRadius: "999px", border: "1px solid color-mix(in srgb, var(--ss-border-color) 80%, transparent)", @@ -2497,7 +2525,10 @@ export const Home: React.FC = ({ }} >
{ + searchAreaRef.current = node + immersiveToolbarContentRef.current = node + }} style={{ display: "flex", alignItems: "center", From 8453a382478f822c077e85c57ffe640983088608 Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Fri, 27 Mar 2026 21:53:45 +0800 Subject: [PATCH 20/33] update: AGENTS.md --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5ccefd3..f4f3df7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,10 +2,10 @@ ## 代码规范 - 所有代码必须符合 Rust 语言规范。 -- 所有代码跑`pnpm run typecheck` or `cargo typecheck`必须通过。 +- 所有代码跑`pnpm run typecheck` or `cargo typecheck` 必须通过。 ## 提交与推送要求 -- 每次代码修改后,必须进行一次 Git 提交(commit)并推送(git push)。 +- 每次代码修改后,除非用户说明进行一次 Git 提交(commit)并推送(git push)否则不进行。 - 提交信息必须使用中文。 - 提交信息必须遵循 Conventional Commits 规范,格式示例:`feat: 新增登录页表单校验`、`fix: 修复导出报告空指针问题`。 - 每次提交完成后,必须将对应提交推送到远端仓库(`git push`)。 From 41f05c134e51de40a7790153b1469ffd25dcbca6 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 27 Mar 2026 21:54:03 +0800 Subject: [PATCH 21/33] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B2=89?= =?UTF-8?q?=E6=B5=B8=E5=BA=95=E6=A0=8F=E5=AE=BD=E5=BA=A6=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E6=8C=81=E7=BB=AD=E6=94=B6=E7=BC=A9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/Home.tsx b/src/components/Home.tsx index a6a1a99..70223be 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -120,6 +120,8 @@ export const Home: React.FC = ({ const immersiveToolbarRef = useRef(null) const immersiveToolbarContentRef = useRef(null) const [immersiveToolbarWidth, setImmersiveToolbarWidth] = useState(null) + const immersiveToolbarHorizontalPadding = 20 + const immersiveToolbarMinWidth = 320 const [selectedStudent, setSelectedStudent] = useState(null) const [batchMode, setBatchMode] = useState(false) @@ -324,7 +326,10 @@ export const Home: React.FC = ({ }, []) useEffect(() => { - if (!immersiveMode) return + if (!immersiveMode) { + setImmersiveToolbarWidth(null) + return + } const contentEl = immersiveToolbarContentRef.current if (!contentEl) return @@ -332,8 +337,11 @@ export const Home: React.FC = ({ const updateToolbarWidth = () => { if (frameId !== null) cancelAnimationFrame(frameId) frameId = requestAnimationFrame(() => { - const contentWidth = Math.ceil(contentEl.getBoundingClientRect().width) - const nextWidth = contentWidth + 20 + const contentWidth = Math.ceil(contentEl.scrollWidth) + const nextWidth = Math.max( + immersiveToolbarMinWidth, + contentWidth + immersiveToolbarHorizontalPadding + ) setImmersiveToolbarWidth((prev) => (prev === nextWidth ? prev : nextWidth)) }) } @@ -348,7 +356,7 @@ export const Home: React.FC = ({ window.removeEventListener("resize", updateToolbarWidth) if (frameId !== null) cancelAnimationFrame(frameId) } - }, [immersiveMode]) + }, [immersiveMode, immersiveToolbarHorizontalPadding, immersiveToolbarMinWidth]) useEffect(() => { return () => { From 508a2c00c2717a9aeddb36af8e8908b8a72a5480 Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Fri, 27 Mar 2026 21:56:41 +0800 Subject: [PATCH 22/33] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4aaa.tsx?= =?UTF-8?q?=E5=B9=B6=E5=BC=95=E5=85=A5Rule=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AutoScoreManager.tsx | 6 +- src/components/autoScore/aaa.tsx | 104 ---------------------------- 2 files changed, 3 insertions(+), 107 deletions(-) delete mode 100644 src/components/autoScore/aaa.tsx diff --git a/src/components/AutoScoreManager.tsx b/src/components/AutoScoreManager.tsx index f6c90ac..20c3e8f 100644 --- a/src/components/AutoScoreManager.tsx +++ b/src/components/AutoScoreManager.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react" import { HolderOutlined } from "@ant-design/icons" import { useTranslation } from "react-i18next" -import DemoQueryBuilder from "./autoScore/aaa" +import Rule from "./autoScore/Rule" import { Card, Form, @@ -426,8 +426,8 @@ export const AutoScoreManager: React.FC = () => { - - + +
+ +
+
+ )} setWizardVisible(false)} /> diff --git a/src/assets/main.css b/src/assets/main.css index c889ddc..dac3a70 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -270,9 +270,74 @@ html.platform-macos #root { pointer-events: none; } -@media (prefers-reduced-motion: reduce) { - .ss-immersive-sidebar, - .ss-immersive-toolbar { - transition: none !important; +.ss-settings-mobile-nav-list { + display: flex; + flex-direction: column; +} + +.ss-settings-mobile-nav-item.ant-btn { + min-height: 48px; + width: 100%; + border-radius: 0; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + color: var(--ss-text-main); + border-bottom: 1px solid var(--ss-divider, rgba(5, 5, 5, 0.06)); +} + +.ss-settings-mobile-nav-item.ant-btn:last-child { + border-bottom: none; +} + +.ss-settings-mobile-nav-item.ant-btn:hover, +.ss-settings-mobile-nav-item.ant-btn:focus-visible { + background: var(--ss-item-hover, rgba(0, 0, 0, 0.04)); +} + +.ss-settings-mobile-nav-item.ant-btn.is-active { + color: var(--ant-color-primary, #1677ff); + background: color-mix(in srgb, var(--ant-color-primary, #1677ff) 10%, transparent); +} + +.ss-settings-mobile-nav-item.ant-btn[disabled] { + color: var(--ss-text-secondary); + opacity: 0.65; +} + +.ss-settings-mobile-nav-item-label { + font-size: 15px; + font-weight: 500; +} + +.ss-settings-mobile-nav-item-arrow { + font-size: 12px; +} + +.ss-route-page { + min-height: 100%; +} + +.ss-route-page.is-subpage-enter { + animation: ss-subpage-enter 240ms cubic-bezier(0.22, 1, 0.36, 1); +} + +@keyframes ss-subpage-enter { + from { + opacity: 0; + transform: translate3d(20px, 0, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +@media (prefers-reduced-motion: reduce) { + .ss-immersive-sidebar, + .ss-immersive-toolbar, + .ss-route-page.is-subpage-enter { + transition: none !important; + animation: none !important; } } diff --git a/src/components/ContentArea.tsx b/src/components/ContentArea.tsx index bfd617f..839827a 100644 --- a/src/components/ContentArea.tsx +++ b/src/components/ContentArea.tsx @@ -6,7 +6,7 @@ import { FullscreenOutlined, FullscreenExitOutlined, } from "@ant-design/icons" -import { Routes, Route, Navigate } from "react-router-dom" +import { Routes, Route, Navigate, useLocation } from "react-router-dom" import { useTranslation } from "react-i18next" import { WindowControls } from "./WindowControls" import appLogo from "../assets/logoHD.svg" @@ -67,6 +67,8 @@ interface ContentAreaProps { immersiveMode: boolean isHomePage: boolean onToggleImmersiveMode: () => void + showSidebarToggle?: boolean + bottomInset?: number } export function ContentArea({ @@ -83,8 +85,13 @@ export function ContentArea({ immersiveMode, isHomePage, onToggleImmersiveMode, + showSidebarToggle = true, + bottomInset = 0, }: ContentAreaProps): React.JSX.Element { const { t } = useTranslation() + const location = useLocation() + const isSubPage = location.pathname !== "/" && !location.pathname.startsWith("/home") + const shouldAnimateSubPage = isPortraitMode && isSubPage useEffect(() => { let cancelled = false @@ -156,7 +163,7 @@ export function ContentArea({ } as React.CSSProperties } > - {!immersiveMode && ( + {!immersiveMode && showSidebarToggle && (
- + } > - - - } - /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - +
+ + + } + /> + } + /> + + } + /> + } /> + } /> + } /> + } /> + } /> + } + /> + } + /> + } /> + +
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index bf3264a..c47d8e3 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from "react" +import { RightOutlined } from "@ant-design/icons" import { Tabs, Card, @@ -17,6 +18,7 @@ import { ThemeQuickSettings } from "./ThemeQuickSettings" import { useTranslation } from "react-i18next" import { changeLanguage, getCurrentLanguage, languageOptions, AppLanguage } from "../i18n" import { useResponsive } from "../hooks/useResponsive" +import { useLocation, useNavigate } from "react-router-dom" type permissionLevel = "admin" | "points" | "view" type appSettings = { @@ -46,8 +48,13 @@ const withTimeout = async ( } } -export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => { +export const Settings: React.FC<{ + permission: permissionLevel + mobileNavigationEnabled?: boolean +}> = ({ permission, mobileNavigationEnabled = false }) => { const { t } = useTranslation() + const navigate = useNavigate() + const location = useLocation() const breakpoint = useResponsive() const isMobile = breakpoint === "xs" || breakpoint === "sm" const [activeTab, setActiveTab] = useState("appearance") @@ -132,6 +139,25 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission ) }, [permission, t]) + const mobilePageItems = useMemo( + () => [ + { key: "students", path: "/students", label: t("sidebar.students"), disabled: !canAdmin }, + { key: "score", path: "/score", label: t("sidebar.score"), disabled: false }, + { key: "auto-score", path: "/auto-score", label: t("sidebar.autoScore"), disabled: false }, + { + key: "reward-settings", + path: "/reward-settings", + label: t("sidebar.rewardSettings"), + disabled: !canAdmin, + }, + { key: "boards", path: "/boards", label: t("sidebar.boards"), disabled: false }, + { key: "leaderboard", path: "/leaderboard", label: t("sidebar.leaderboard"), disabled: false }, + { key: "settlements", path: "/settlements", label: t("sidebar.settlements"), disabled: false }, + { key: "reasons", path: "/reasons", label: t("sidebar.reasons"), disabled: !canAdmin }, + ], + [canAdmin, t] + ) + const emitDataUpdated = (category: "events" | "students" | "reasons" | "all") => { window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category } })) } @@ -1277,6 +1303,35 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission {permissionTag} + {mobileNavigationEnabled && isMobile && ( + +
+ {mobilePageItems.map((item) => ( + + ))} +
+
+ )} + Date: Sat, 28 Mar 2026 09:27:55 +0800 Subject: [PATCH 29/33] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E7=AB=AF=E8=BF=9B=E5=85=A5=E6=B2=89=E6=B5=B8=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 ++ src/components/ContentArea.tsx | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index c501b62..2287082 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -353,6 +353,7 @@ function MainContent(): React.JSX.Element { const isDark = currentTheme?.mode === "dark" const brandColor = currentTheme?.config?.tdesign?.brandColor || "#0052D9" + const isMobileDevice = isIosDevice || isAndroidDevice const showMobileBottomNav = isPortraitMode && !immersiveMode return ( @@ -395,6 +396,7 @@ function MainContent(): React.JSX.Element { onLogout={logout} showWindowControls={!isIosDevice && !isAndroidDevice} isPortraitMode={isPortraitMode} + isMobileDevice={isMobileDevice} sidebarCollapsed={sidebarCollapsed} floatingExpand={isPortraitMode} floatingExpanded={floatingSidebarExpanded} diff --git a/src/components/ContentArea.tsx b/src/components/ContentArea.tsx index 839827a..628f23e 100644 --- a/src/components/ContentArea.tsx +++ b/src/components/ContentArea.tsx @@ -60,6 +60,7 @@ interface ContentAreaProps { onLogout: () => void showWindowControls: boolean isPortraitMode: boolean + isMobileDevice: boolean sidebarCollapsed: boolean floatingExpand: boolean floatingExpanded: boolean @@ -78,6 +79,7 @@ export function ContentArea({ onLogout, showWindowControls, isPortraitMode, + isMobileDevice, sidebarCollapsed, floatingExpand, floatingExpanded, @@ -228,7 +230,7 @@ export function ContentArea({ } > - {(immersiveMode || isHomePage) && ( + {(immersiveMode || (isHomePage && !isMobileDevice)) && (
{batchToolbar}
- {!disableSearchKeyboard && showPinyinKeyboard && ( + {canShowSearchKeyboard && showPinyinKeyboard && (
- - { + if (!(window as any).api) return + const next = String(v) as "t9" | "qwerty26" + const res = await (window as any).api.setSetting("search_keyboard_layout", next) + if (res.success) { + setSettings((prev) => ({ ...prev, search_keyboard_layout: next })) + messageApi.success(t("settings.general.saved")) + } else { + messageApi.error(res.message || t("settings.general.saveFailed")) + } + }} + style={{ width: "320px" }} + disabled={!canAdmin} + options={[ + { value: "qwerty26", label: t("settings.searchKeyboard.options.qwerty26") }, + { value: "t9", label: t("settings.searchKeyboard.options.t9") }, + ]} + /> +
+ {t("settings.searchKeyboard.hint")} +
+
- - { - if (!(window as any).api) return - const res = await (window as any).api.setSetting( - "disable_search_keyboard", - checked - ) - if (res.success) { - setSettings((prev) => ({ ...prev, disable_search_keyboard: checked })) - messageApi.success(t("settings.general.saved")) - } else { - messageApi.error(res.message || t("settings.general.saveFailed")) - } - }} - disabled={!canAdmin} - /> -
- {t("settings.searchKeyboard.disableHint")} -
-
+ + { + if (!(window as any).api) return + const res = await (window as any).api.setSetting( + "disable_search_keyboard", + checked + ) + if (res.success) { + setSettings((prev) => ({ ...prev, disable_search_keyboard: checked })) + messageApi.success(t("settings.general.saved")) + } else { + messageApi.error(res.message || t("settings.general.saveFailed")) + } + }} + disabled={!canAdmin} + /> +
+ {t("settings.searchKeyboard.disableHint")} +
+
+ + )} handleMobileBottomNavChange(v as string[])} + style={{ width: "100%", maxWidth: "520px" }} + disabled={!canAdmin} + options={MOBILE_NAV_ITEMS.map((item) => ({ + value: item.key, + label: t(item.labelKey), + disabled: Boolean(item.adminOnly) && !canAdmin, + }))} + /> +
+ {t("settings.mobile.bottomNav.hint")} +
+
+ )}
), diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index e194c5e..842b71f 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -34,7 +34,8 @@ "pleaseSelect": "Please select", "pleaseEnter": "Please enter", "readOnly": "Read-only mode", - "none": "None" + "none": "None", + "more": "More" }, "oobe": { "title": "Welcome to SecScore", @@ -161,6 +162,13 @@ "saveFailed": "Save failed" }, "zoomHint": "Adjust the overall size of the application interface", + "mobile": { + "navigationTitle": "Page Shortcuts", + "bottomNav": { + "label": "Bottom Navigation Features", + "hint": "Choose which features appear in the mobile bottom bar. The bar shows up to 4 items; extra items go under \"More\"." + } + }, "zoomUpdated": "Interface zoom updated", "updateFailed": "Update failed", "security": { diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index f82b8fb..3037249 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -34,7 +34,8 @@ "pleaseSelect": "请选择", "pleaseEnter": "请输入", "readOnly": "当前为只读权限", - "none": "无" + "none": "无", + "more": "更多" }, "oobe": { "title": "欢迎使用 SecScore", @@ -161,6 +162,13 @@ "saveFailed": "保存失败" }, "zoomHint": "调节应用界面的整体大小", + "mobile": { + "navigationTitle": "页面入口", + "bottomNav": { + "label": "底部导航功能", + "hint": "可选择要出现在手机底部导航的功能。底栏最多展示 4 项,其余会自动收纳到“更多”。" + } + }, "zoomUpdated": "界面缩放已更新", "updateFailed": "更新失败", "security": { diff --git a/src/preload/types.ts b/src/preload/types.ts index 78c6b22..8c527ce 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -35,6 +35,7 @@ export type settingsKey = | "dashboards_config" | "pg_connection_string" | "pg_connection_status" + | "mobile_bottom_nav_items" export interface settingsSpec { is_wizard_completed: boolean @@ -53,6 +54,7 @@ export interface settingsSpec { type: "sqlite" | "postgresql" error?: string } + mobile_bottom_nav_items: string[] } const api = { diff --git a/src/shared/mobileNavigation.ts b/src/shared/mobileNavigation.ts new file mode 100644 index 0000000..7142aa4 --- /dev/null +++ b/src/shared/mobileNavigation.ts @@ -0,0 +1,55 @@ +export const MOBILE_NAV_ALL_KEYS = [ + "home", + "students", + "score", + "auto-score", + "reward-settings", + "boards", + "leaderboard", + "settlements", + "reasons", + "settings", +] as const + +export type MobileNavKey = (typeof MOBILE_NAV_ALL_KEYS)[number] + +export interface MobileNavItemConfig { + key: MobileNavKey + path: string + labelKey: string + adminOnly?: boolean +} + +export const MOBILE_NAV_ITEMS: MobileNavItemConfig[] = [ + { key: "home", path: "/home", labelKey: "sidebar.home" }, + { key: "students", path: "/students", labelKey: "sidebar.students", adminOnly: true }, + { key: "score", path: "/score", labelKey: "sidebar.score" }, + { key: "auto-score", path: "/auto-score", labelKey: "sidebar.autoScore" }, + { + key: "reward-settings", + path: "/reward-settings", + labelKey: "sidebar.rewardSettings", + adminOnly: true, + }, + { key: "boards", path: "/boards", labelKey: "sidebar.boards" }, + { key: "leaderboard", path: "/leaderboard", labelKey: "sidebar.leaderboard" }, + { key: "settlements", path: "/settlements", labelKey: "sidebar.settlements" }, + { key: "reasons", path: "/reasons", labelKey: "sidebar.reasons", adminOnly: true }, + { key: "settings", path: "/settings", labelKey: "sidebar.settings" }, +] + +const MOBILE_NAV_KEY_SET = new Set(MOBILE_NAV_ALL_KEYS) + +export const sanitizeMobileNavKeys = ( + input: unknown, + fallback: MobileNavKey[] = MOBILE_NAV_ITEMS.map((item) => item.key) +): MobileNavKey[] => { + if (!Array.isArray(input)) return fallback + const deduped: MobileNavKey[] = [] + for (const rawKey of input) { + if (typeof rawKey !== "string" || !MOBILE_NAV_KEY_SET.has(rawKey)) continue + const key = rawKey as MobileNavKey + if (!deduped.includes(key)) deduped.push(key) + } + return deduped.length > 0 ? deduped : fallback +} diff --git a/vite.config.ts b/vite.config.ts index f866ea9..6812fc3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ server: { host: process.env.TAURI_DEV_HOST || false, port: 1420, - strictPort: true, + strictPort: false, hmr: process.env.TAURI_DEV_HOST ? { protocol: "ws",