From 30c91094f1ae7b1133e6d59abd533f503083bc49 Mon Sep 17 00:00:00 2001 From: JSR Date: Fri, 20 Mar 2026 19:02:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9C=8B=E6=9D=BF=E5=8D=87=E7=BA=A7=E5=8F=AF?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E5=88=86=E5=89=B2=E5=B8=83=E5=B1=80=E5=B9=B6?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=BC=B9=E7=AA=97=E7=BC=96=E8=BE=91SQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 1044 ++++++++++++++++++------------- src/i18n/locales/en-US.json | 5 + src/i18n/locales/zh-CN.json | 5 + 3 files changed, 627 insertions(+), 427 deletions(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 9bdc732..7f7f4cc 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -1,10 +1,11 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react" +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Alert, Button, Card, Empty, Input, + Modal, Popconfirm, Select, Space, @@ -14,9 +15,12 @@ import { Typography, message, } from "antd" -import { DeleteOutlined, PlayCircleOutlined, PlusOutlined, ReloadOutlined } from "@ant-design/icons" +import { DeleteOutlined, EditOutlined, PlayCircleOutlined, PlusOutlined, ReloadOutlined } from "@ant-design/icons" import { useTranslation } from "react-i18next" +type BoardStudentViewMode = "list" | "card" | "grid" +type SplitDirection = "horizontal" | "vertical" + interface StudentListConfig { id: string name: string @@ -24,10 +28,28 @@ interface StudentListConfig { viewMode: BoardStudentViewMode } +interface LayoutLeafNode { + id: string + type: "leaf" + listId: string +} + +interface LayoutSplitNode { + id: string + type: "split" + direction: SplitDirection + ratio: number + first: LayoutNode + second: LayoutNode +} + +type LayoutNode = LayoutLeafNode | LayoutSplitNode + interface BoardConfig { id: string name: string lists: StudentListConfig[] + layout: LayoutNode } interface BoardPreset { @@ -41,8 +63,6 @@ interface BoardManagerProps { canManage: boolean } -type BoardStudentViewMode = "list" | "card" | "grid" - interface BoardStudentCardData { key: string name: string @@ -53,11 +73,23 @@ interface BoardStudentCardData { answeredCount?: number } +interface DragState { + boardId: string + splitNodeId: string + direction: SplitDirection + startRatio: number + startX: number + startY: number + containerSize: number +} + const makeId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}` +const clampRatio = (value: number) => Math.max(0.15, Math.min(0.85, value)) + const getDefaultSql = () => `SELECT name AS student_name, score, @@ -72,16 +104,90 @@ const createDefaultList = (): StudentListConfig => ({ viewMode: "card", }) -const createDefaultBoard = (): BoardConfig => ({ +const createLeafForList = (listId: string): LayoutLeafNode => ({ id: makeId(), - name: "默认看板", - lists: [createDefaultList()], + type: "leaf", + listId, }) +const createDefaultBoard = (): BoardConfig => { + const list = createDefaultList() + return { + id: makeId(), + name: "默认看板", + lists: [list], + layout: createLeafForList(list.id), + } +} + +const collectLeafListIds = (node: LayoutNode, acc: string[] = []): string[] => { + if (node.type === "leaf") { + acc.push(node.listId) + return acc + } + collectLeafListIds(node.first, acc) + collectLeafListIds(node.second, acc) + return acc +} + +const pruneLayout = (node: LayoutNode, validListIds: Set): LayoutNode | null => { + if (node.type === "leaf") { + return validListIds.has(node.listId) ? node : null + } + + const first = pruneLayout(node.first, validListIds) + const second = pruneLayout(node.second, validListIds) + + if (!first && !second) return null + if (!first) return second + if (!second) return first + + return { ...node, first, second } +} + +const appendLeafToLayout = (layout: LayoutNode, listId: string): LayoutNode => { + return { + id: makeId(), + type: "split", + direction: "horizontal", + ratio: 0.5, + first: layout, + second: createLeafForList(listId), + } +} + +const syncBoardLayout = (board: BoardConfig): BoardConfig => { + const validListIds = new Set(board.lists.map((item) => item.id)) + const pruned = pruneLayout(board.layout, validListIds) + + let layout = pruned + const used = new Set(layout ? collectLeafListIds(layout) : []) + const missing = board.lists.map((item) => item.id).filter((id) => !used.has(id)) + + if (!layout) { + const firstMissing = missing.shift() || board.lists[0]?.id + if (!firstMissing) { + const fallbackList = createDefaultList() + return { + ...board, + lists: [fallbackList], + layout: createLeafForList(fallbackList.id), + } + } + layout = createLeafForList(firstMissing) + } + + for (const listId of missing) { + layout = appendLeafToLayout(layout, listId) + } + + return { ...board, layout } +} + const normalizeBoards = (input: unknown): BoardConfig[] => { if (!Array.isArray(input)) return [createDefaultBoard()] - const boards: BoardConfig[] = input + const boards = input .map((board: any) => { const lists = Array.isArray(board?.lists) ? board.lists @@ -98,11 +204,16 @@ const normalizeBoards = (input: unknown): BoardConfig[] => { .filter((list: StudentListConfig) => list.sql.trim()) : [] - return { + const normalizedLists = lists.length > 0 ? lists : [createDefaultList()] + const fallbackLayout = createLeafForList(normalizedLists[0].id) + const layout = board?.layout && typeof board.layout === "object" ? (board.layout as LayoutNode) : fallbackLayout + + return syncBoardLayout({ id: typeof board?.id === "string" && board.id.trim() ? board.id : makeId(), name: typeof board?.name === "string" && board.name.trim() ? board.name.trim() : "未命名看板", - lists: lists.length > 0 ? lists : [createDefaultList()], - } + lists: normalizedLists, + layout, + }) }) .filter((board: BoardConfig) => board.id) @@ -196,6 +307,32 @@ const getAvatarColor = (name: string): string => { return palette[hash % palette.length] } +const replaceLayoutNode = (node: LayoutNode, targetId: string, replacement: LayoutNode): LayoutNode => { + if (node.id === targetId) return replacement + if (node.type === "leaf") return node + return { + ...node, + first: replaceLayoutNode(node.first, targetId, replacement), + second: replaceLayoutNode(node.second, targetId, replacement), + } +} + +const updateSplitRatio = (node: LayoutNode, splitNodeId: string, ratio: number): LayoutNode => { + if (node.type === "leaf") return node + if (node.id === splitNodeId) return { ...node, ratio: clampRatio(ratio) } + return { + ...node, + first: updateSplitRatio(node.first, splitNodeId, ratio), + second: updateSplitRatio(node.second, splitNodeId, ratio), + } +} + +const removeListFromBoard = (board: BoardConfig, listId: string): BoardConfig => { + const nextLists = board.lists.filter((item) => item.id !== listId) + if (nextLists.length <= 0) return board + return syncBoardLayout({ ...board, lists: nextLists }) +} + export const BoardManager: React.FC = ({ canManage }) => { const { t } = useTranslation() const [messageApi, contextHolder] = message.useMessage() @@ -206,6 +343,10 @@ export const BoardManager: React.FC = ({ canManage }) => { const [runningIds, setRunningIds] = useState>({}) const [resultMap, setResultMap] = useState>({}) const [errorMap, setErrorMap] = useState>({}) + const [dragState, setDragState] = useState(null) + const [editingListId, setEditingListId] = useState(null) + + const panelRefs = useRef>({}) const presets: BoardPreset[] = useMemo( () => [ @@ -265,6 +406,11 @@ ORDER BY reward_points DESC, score DESC`, [boards, activeBoardId] ) + const editingList = useMemo(() => { + if (!activeBoard || !editingListId) return null + return activeBoard.lists.find((item) => item.id === editingListId) || null + }, [activeBoard, editingListId]) + const saveBoards = useCallback( async (nextBoards: BoardConfig[]) => { if (!(window as any).api || !canManage) return @@ -284,6 +430,19 @@ ORDER BY reward_points DESC, score DESC`, [canManage, messageApi, t] ) + const mutateBoards = useCallback( + (updater: (prev: BoardConfig[]) => BoardConfig[]) => { + setBoards((prev) => { + const next = updater(prev).map(syncBoardLayout) + if (canManage) { + saveBoards(next).catch(() => void 0) + } + return next + }) + }, + [canManage, saveBoards] + ) + const fetchBoards = useCallback(async () => { if (!(window as any).api) return @@ -349,9 +508,7 @@ ORDER BY reward_points DESC, score DESC`, }, [fetchBoards]) useEffect(() => { - if (!activeBoardId && boards.length > 0) { - setActiveBoardId(boards[0].id) - } + if (!activeBoardId && boards.length > 0) setActiveBoardId(boards[0].id) }, [activeBoardId, boards]) useEffect(() => { @@ -362,9 +519,7 @@ ORDER BY reward_points DESC, score DESC`, useEffect(() => { const onDataUpdated = (e: Event) => { const detail = (e as CustomEvent<{ category?: string }>).detail - if (!detail?.category || detail.category === "all") { - runAllInBoard(activeBoard).catch(() => void 0) - } + if (!detail?.category || detail.category === "all") runAllInBoard(activeBoard).catch(() => void 0) if (detail.category === "students" || detail.category === "events" || detail.category === "reasons") { runAllInBoard(activeBoard).catch(() => void 0) } @@ -374,15 +529,37 @@ ORDER BY reward_points DESC, score DESC`, return () => window.removeEventListener("ss:data-updated", onDataUpdated) }, [activeBoard, runAllInBoard]) - const mutateBoards = (updater: (prev: BoardConfig[]) => BoardConfig[]) => { - setBoards((prev) => { - const next = updater(prev) - if (canManage) { - saveBoards(next).catch(() => void 0) - } - return next - }) - } + useEffect(() => { + if (!dragState) return + + const onMove = (event: MouseEvent) => { + const delta = + dragState.direction === "horizontal" + ? event.clientX - dragState.startX + : event.clientY - dragState.startY + const ratio = clampRatio(dragState.startRatio + delta / dragState.containerSize) + + mutateBoards((prev) => + prev.map((board) => + board.id === dragState.boardId + ? { + ...board, + layout: updateSplitRatio(board.layout, dragState.splitNodeId, ratio), + } + : board + ) + ) + } + + const onUp = () => setDragState(null) + + window.addEventListener("mousemove", onMove) + window.addEventListener("mouseup", onUp) + return () => { + window.removeEventListener("mousemove", onMove) + window.removeEventListener("mouseup", onUp) + } + }, [dragState, mutateBoards]) const updateBoardName = (boardId: string, name: string) => { mutateBoards((prev) => @@ -392,12 +569,8 @@ ORDER BY reward_points DESC, score DESC`, const addBoard = () => { if (!canManage) return - const newBoard: BoardConfig = { - id: makeId(), - name: t("board.newBoard"), - lists: [createDefaultList()], - } - + const newBoard = createDefaultBoard() + newBoard.name = t("board.newBoard") mutateBoards((prev) => [...prev, newBoard]) setActiveBoardId(newBoard.id) } @@ -410,37 +583,54 @@ ORDER BY reward_points DESC, score DESC`, return prev } const next = prev.filter((board) => board.id !== boardId) - if (activeBoardId === boardId && next.length > 0) { - setActiveBoardId(next[0].id) - } + if (activeBoardId === boardId && next.length > 0) setActiveBoardId(next[0].id) return next }) } - const addList = (boardId: string) => { + const addListBySplit = (boardId: string, leafNodeId: string, direction: SplitDirection) => { if (!canManage) return + mutateBoards((prev) => - prev.map((board) => - board.id === boardId - ? { - ...board, - lists: [ - ...board.lists, - { - id: makeId(), - name: t("board.newList"), - sql: getDefaultSql(), - viewMode: "card", - }, - ], - } - : board - ) + prev.map((board) => { + if (board.id !== boardId) return board + + const newList: StudentListConfig = { + id: makeId(), + name: t("board.newList"), + sql: getDefaultSql(), + viewMode: "card", + } + + const findLeaf = (node: LayoutNode): LayoutLeafNode | null => { + if (node.type === "leaf") return node.id === leafNodeId ? node : null + return findLeaf(node.first) || findLeaf(node.second) + } + + const targetLeaf = findLeaf(board.layout) + if (!targetLeaf) return board + + const replacement: LayoutSplitNode = { + id: makeId(), + type: "split", + direction, + ratio: 0.5, + first: targetLeaf, + second: createLeafForList(newList.id), + } + + return syncBoardLayout({ + ...board, + lists: [...board.lists, newList], + layout: replaceLayoutNode(board.layout, leafNodeId, replacement), + }) + }) ) } const removeList = (boardId: string, listId: string) => { if (!canManage) return + mutateBoards((prev) => prev.map((board) => { if (board.id !== boardId) return board @@ -448,10 +638,7 @@ ORDER BY reward_points DESC, score DESC`, messageApi.warning(t("board.keepAtLeastOneList")) return board } - return { - ...board, - lists: board.lists.filter((list) => list.id !== listId), - } + return removeListFromBoard(board, listId) }) ) } @@ -462,14 +649,7 @@ ORDER BY reward_points DESC, score DESC`, board.id === boardId ? { ...board, - lists: board.lists.map((list) => - list.id === listId - ? { - ...list, - ...patch, - } - : list - ), + lists: board.lists.map((list) => (list.id === listId ? { ...list, ...patch } : list)), } : board ) @@ -486,6 +666,324 @@ ORDER BY reward_points DESC, score DESC`, }) } + const renderStudentView = (list: StudentListConfig) => { + const rows = resultMap[list.id] || [] + const studentCards = toStudentCards(rows) + const useCardView = studentCards.length > 0 + + const columns = + rows.length > 0 + ? Object.keys(rows[0]).map((key) => ({ + title: key, + dataIndex: key, + key, + ellipsis: true, + render: (value: any) => (value === null || value === undefined || value === "" ? "-" : String(value)), + })) + : [] + + if (!useCardView) { + return ( + `${list.id}-${index}`} + dataSource={rows} + columns={columns} + loading={Boolean(runningIds[list.id])} + locale={{ emptyText: }} + scroll={{ x: true }} + pagination={{ pageSize: 20, showSizeChanger: false }} + size="small" + /> + ) + } + + return ( +
+ {studentCards.map((item, index) => { + const avatarColor = getAvatarColor(item.name) + const avatarText = getAvatarText(item.name) + const rankBadge = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : null + const primaryMetric = + item.score !== undefined + ? { label: t("board.metrics.totalScore"), value: item.score } + : 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 + + return ( +
+ + {rankBadge && ( +
{rankBadge}
+ )} + + {list.viewMode === "grid" ? ( +
+
1 ? "14px" : "18px", + boxShadow: `0 4px 10px ${avatarColor}40`, + }} + > + {avatarText} +
+
+
+ {item.name} +
+ {primaryMetric && ( + = 0 ? "success" : "error"} + style={{ fontWeight: "bold", marginInlineEnd: 0 }} + > + {primaryMetric.label}:{primaryMetric.value > 0 ? `+${primaryMetric.value}` : primaryMetric.value} + + )} +
+
+ ) : ( +
+
+ {avatarText} +
+
+
+ {item.name} +
+
+ {item.score !== undefined && ( + = 0 ? "success" : "error"} style={{ margin: 0 }}> + {t("board.metrics.totalScore")}: {item.score > 0 ? `+${item.score}` : item.score} + + )} + {item.rewardPoints !== undefined && ( + + {t("board.metrics.rewardPoints")}: {item.rewardPoints} + + )} + {item.weekChange !== undefined && ( + = 0 ? "success" : "error"} style={{ margin: 0 }}> + {t("board.metrics.weekChange")}: {item.weekChange > 0 ? `+${item.weekChange}` : item.weekChange} + + )} + {item.weekDeducted !== undefined && ( + + {t("board.metrics.weekDeducted")}: {item.weekDeducted} + + )} + {item.answeredCount !== undefined && ( + + {t("board.metrics.todayAnswered")}: {item.answeredCount} + + )} +
+
+
+ )} +
+
+ ) + })} +
+ ) + } + + const renderLeafPanel = (board: BoardConfig, leaf: LayoutLeafNode): React.JSX.Element => { + const list = board.lists.find((item) => item.id === leaf.listId) + if (!list) return + + return ( + {list.name}} + extra={ + + + + + + removeList(board.id, list.id)} + disabled={!canManage || board.lists.length <= 1} + > + + + + } + > + {errorMap[list.id] && } +
{renderStudentView(list)}
+
+ ) + } + + const renderLayoutNode = (board: BoardConfig, node: LayoutNode): React.JSX.Element => { + if (node.type === "leaf") { + return ( +
+ {renderLeafPanel(board, node)} +
+ ) + } + + const isHorizontal = node.direction === "horizontal" + const handleSize = 8 + + return ( +
{ + panelRefs.current[node.id] = el + }} + style={{ + width: "100%", + height: "100%", + display: "flex", + flexDirection: isHorizontal ? "row" : "column", + minHeight: 200, + }} + > +
{renderLayoutNode(board, node.first)}
+
{ + if (!canManage) return + const container = panelRefs.current[node.id] + if (!container) return + const rect = container.getBoundingClientRect() + const containerSize = isHorizontal ? rect.width : rect.height + if (containerSize <= 0) return + + setDragState({ + boardId: board.id, + splitNodeId: node.id, + direction: node.direction, + startRatio: node.ratio, + startX: event.clientX, + startY: event.clientY, + containerSize, + }) + }} + style={{ + flex: `0 0 ${handleSize}px`, + cursor: canManage ? (isHorizontal ? "col-resize" : "row-resize") : "default", + background: "var(--ss-border-color)", + borderRadius: 4, + opacity: canManage ? 0.85 : 0.5, + margin: isHorizontal ? "0 4px" : "4px 0", + }} + title={t("board.dragHandle")} + /> +
+ {renderLayoutNode(board, node.second)} +
+
+ ) + } + + const renderBoardWorkspace = () => { + if (!activeBoard) return + + return ( +
+ {renderLayoutNode(activeBoard, activeBoard.layout)} +
+ ) + } + return (
{contextHolder} @@ -495,9 +993,7 @@ ORDER BY reward_points DESC, score DESC`, {t("board.title")} - - {canManage ? t("board.editable") : t("board.readonly")} - + {canManage ? t("board.editable") : t("board.readonly")} - - removeBoard(activeBoard.id)} - disabled={!canManage} - > + removeBoard(activeBoard.id)} disabled={!canManage}> @@ -564,348 +1045,7 @@ ORDER BY reward_points DESC, score DESC`, )} - {activeBoard.lists.map((list) => { - const rows = resultMap[list.id] || [] - const studentCards = toStudentCards(rows) - const useCardView = studentCards.length > 0 - const columns = - rows.length > 0 - ? Object.keys(rows[0]).map((key) => ({ - title: key, - dataIndex: key, - key, - ellipsis: true, - render: (value: any) => - value === null || value === undefined || value === "" ? "-" : String(value), - })) - : [] - - return ( - updateList(activeBoard.id, list.id, { name: e.target.value })} - placeholder={t("board.listNamePlaceholder")} - disabled={!canManage} - /> - } - extra={ - - - updateList(activeBoard.id, list.id, { viewMode }) - } - options={[ - { value: "list", label: t("board.viewModes.list") }, - { value: "card", label: t("board.viewModes.card") }, - { value: "grid", label: t("board.viewModes.grid") }, - ]} - disabled={!canManage} - /> - - removeList(activeBoard.id, list.id)} - disabled={!canManage} - > - - - - } - style={{ backgroundColor: "var(--ss-card-bg)" }} - > - updateList(activeBoard.id, list.id, { sql: e.target.value })} - disabled={!canManage} - spellCheck={false} - placeholder={t("board.sqlPlaceholder")} - style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace" }} - /> - - {errorMap[list.id] && ( - - )} - -
- {useCardView ? ( -
- {studentCards.map((item, index) => { - const avatarColor = getAvatarColor(item.name) - const avatarText = getAvatarText(item.name) - const rankBadge = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : null - const gridPrimaryValue = - item.score !== undefined - ? item.score - : item.rewardPoints !== undefined - ? item.rewardPoints - : item.weekChange !== undefined - ? item.weekChange - : item.weekDeducted !== undefined - ? -item.weekDeducted - : item.answeredCount - - const gridPrimaryLabel = - item.score !== undefined - ? t("board.metrics.totalScore") - : item.rewardPoints !== undefined - ? t("board.metrics.rewardPoints") - : item.weekChange !== undefined - ? t("board.metrics.weekChange") - : item.weekDeducted !== undefined - ? t("board.metrics.weekDeducted") - : item.answeredCount !== undefined - ? t("board.metrics.todayAnswered") - : null - - return ( -
- - {rankBadge && ( -
- {rankBadge} -
- )} - {list.viewMode === "grid" ? ( -
-
1 ? "14px" : "18px", - boxShadow: `0 4px 10px ${avatarColor}40`, - flexShrink: 0, - }} - > - {avatarText} -
-
-
- {item.name} -
- {gridPrimaryValue !== undefined && gridPrimaryLabel && ( - = 0 ? "success" : "error"} - style={{ fontWeight: "bold", marginInlineEnd: 0 }} - > - {gridPrimaryLabel}:{gridPrimaryValue > 0 ? `+${gridPrimaryValue}` : gridPrimaryValue} - - )} -
-
- ) : ( -
-
- {avatarText} -
-
-
- {item.name} -
-
- {item.score !== undefined && ( - = 0 ? "success" : "error"} style={{ margin: 0 }}> - {t("board.metrics.totalScore")}: {item.score > 0 ? `+${item.score}` : item.score} - - )} - {item.rewardPoints !== undefined && ( - - {t("board.metrics.rewardPoints")}: {item.rewardPoints} - - )} - {item.weekChange !== undefined && ( - = 0 ? "success" : "error"} style={{ margin: 0 }}> - {t("board.metrics.weekChange")}: {item.weekChange > 0 ? `+${item.weekChange}` : item.weekChange} - - )} - {item.weekDeducted !== undefined && ( - - {t("board.metrics.weekDeducted")}: {item.weekDeducted} - - )} - {item.answeredCount !== undefined && ( - - {t("board.metrics.todayAnswered")}: {item.answeredCount} - - )} -
-
-
- )} -
-
- ) - })} -
- ) : ( -
`${list.id}-${index}`} - dataSource={rows} - columns={columns} - loading={Boolean(runningIds[list.id])} - locale={{ - emptyText: , - }} - scroll={{ x: true }} - pagination={{ pageSize: 20, showSizeChanger: false }} - size="small" - /> - )} - - - ) - })} + {renderBoardWorkspace()} ) : ( @@ -913,6 +1053,56 @@ ORDER BY reward_points DESC, score DESC`, )} + + setEditingListId(null)} + onOk={() => setEditingListId(null)} + okText={t("common.confirm")} + cancelText={t("common.cancel")} + width={860} + > + {editingList && activeBoard && ( + + updateList(activeBoard.id, editingList.id, { name: e.target.value })} + placeholder={t("board.listNamePlaceholder")} + disabled={!canManage} + /> + + updateList(activeBoard.id, editingList.id, { viewMode })} + options={[ + { value: "list", label: t("board.viewModes.list") }, + { value: "card", label: t("board.viewModes.card") }, + { value: "grid", label: t("board.viewModes.grid") }, + ]} + disabled={!canManage} + /> + + updateList(activeBoard.id, editingList.id, { sql: e.target.value })} + disabled={!canManage} + spellCheck={false} + placeholder={t("board.sqlPlaceholder")} + style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace" }} + /> + + )} + ) } diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 56ee3b7..0d788a9 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -573,6 +573,11 @@ "addList": "Add Student List", "removeList": "Delete List", "removeListConfirm": "Delete current student list?", + "editList": "Edit", + "sqlEditorTitle": "Edit Student List", + "splitHorizontal": "Split Left/Right", + "splitVertical": "Split Top/Bottom", + "dragHandle": "Drag to resize layout", "listNamePlaceholder": "Enter list name", "newList": "New List", "applyPreset": "Apply Preset", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 90d24c9..1ed1ba0 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -573,6 +573,11 @@ "addList": "新增学生列表", "removeList": "删除列表", "removeListConfirm": "确认删除当前学生列表?", + "editList": "编辑", + "sqlEditorTitle": "编辑学生列表", + "splitHorizontal": "左右拆分", + "splitVertical": "上下拆分", + "dragHandle": "拖动调整布局", "listNamePlaceholder": "输入列表名称", "newList": "新列表", "applyPreset": "套用预设",