mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 20:29:03 +08:00
feat:android build
This commit is contained in:
+4
-5
@@ -7,11 +7,10 @@
|
||||
--ss-safe-area-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--ss-safe-area-left: env(safe-area-inset-left, 0px);
|
||||
--ss-font-family:
|
||||
"PingFang SC", "PingFangTC-Regular", "Hiragino Sans GB", "Hiragino Sans",
|
||||
"STHeiti", "Heiti SC", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC",
|
||||
"Microsoft YaHei UI", "Microsoft YaHei", "微软雅黑", -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
sans-serif;
|
||||
"PingFang SC", "PingFangTC-Regular", "Hiragino Sans GB", "Hiragino Sans", "STHeiti", "Heiti SC",
|
||||
"Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "Microsoft YaHei UI",
|
||||
"Microsoft YaHei", "微软雅黑", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
|
||||
+144
-33
@@ -211,11 +211,15 @@ const normalizeBoards = (input: unknown): BoardConfig[] => {
|
||||
|
||||
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
|
||||
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() : "未命名看板",
|
||||
name:
|
||||
typeof board?.name === "string" && board.name.trim() ? board.name.trim() : "未命名看板",
|
||||
lists: normalizedLists,
|
||||
layout,
|
||||
})
|
||||
@@ -283,7 +287,9 @@ const toStudentCards = (rows: any[]): BoardStudentCardData[] => {
|
||||
|
||||
if (cards.every((item) => item.score === undefined)) return cards
|
||||
|
||||
return cards.sort((a, b) => (b.score ?? Number.MIN_SAFE_INTEGER) - (a.score ?? Number.MIN_SAFE_INTEGER))
|
||||
return cards.sort(
|
||||
(a, b) => (b.score ?? Number.MIN_SAFE_INTEGER) - (a.score ?? Number.MIN_SAFE_INTEGER)
|
||||
)
|
||||
}
|
||||
|
||||
const getAvatarText = (name: string): string => {
|
||||
@@ -312,7 +318,11 @@ const getAvatarColor = (name: string): string => {
|
||||
return palette[hash % palette.length]
|
||||
}
|
||||
|
||||
const replaceLayoutNode = (node: LayoutNode, targetId: string, replacement: LayoutNode): LayoutNode => {
|
||||
const replaceLayoutNode = (
|
||||
node: LayoutNode,
|
||||
targetId: string,
|
||||
replacement: LayoutNode
|
||||
): LayoutNode => {
|
||||
if (node.id === targetId) return replacement
|
||||
if (node.type === "leaf") return node
|
||||
return {
|
||||
@@ -551,8 +561,13 @@ 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 === "students" || detail.category === "events" || detail.category === "reasons") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -595,7 +610,9 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
|
||||
const updateBoardName = (boardId: string, name: string) => {
|
||||
mutateBoards((prev) =>
|
||||
prev.map((board) => (board.id === boardId ? { ...board, name: name || t("board.untitledBoard") } : board))
|
||||
prev.map((board) =>
|
||||
board.id === boardId ? { ...board, name: name || t("board.untitledBoard") } : board
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -727,7 +744,8 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
dataIndex: key,
|
||||
key,
|
||||
ellipsis: true,
|
||||
render: (value: any) => (value === null || value === undefined || value === "" ? "-" : String(value)),
|
||||
render: (value: any) =>
|
||||
value === null || value === undefined || value === "" ? "-" : String(value),
|
||||
}))
|
||||
: []
|
||||
|
||||
@@ -738,7 +756,11 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
dataSource={rows}
|
||||
columns={columns}
|
||||
loading={Boolean(runningIds[list.id])}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("common.noData")} /> }}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("common.noData")} />
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
pagination={{ pageSize: 20, showSizeChanger: false }}
|
||||
size="small"
|
||||
@@ -777,7 +799,10 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
: null
|
||||
|
||||
return (
|
||||
<div key={item.key} style={{ ...(list.viewMode === "grid" ? { aspectRatio: "1 / 1" } : null) }}>
|
||||
<div
|
||||
key={item.key}
|
||||
style={{ ...(list.viewMode === "grid" ? { aspectRatio: "1 / 1" } : null) }}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -789,13 +814,22 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
padding: list.viewMode === "grid" ? "8px" : list.viewMode === "list" ? "10px 12px" : "12px 14px",
|
||||
padding:
|
||||
list.viewMode === "grid"
|
||||
? "8px"
|
||||
: list.viewMode === "list"
|
||||
? "10px 12px"
|
||||
: "12px 14px",
|
||||
height: list.viewMode === "grid" ? "100%" : undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{rankBadge && (
|
||||
<div style={{ position: "absolute", top: "-10px", left: "-10px", fontSize: "24px" }}>{rankBadge}</div>
|
||||
<div
|
||||
style={{ position: "absolute", top: "-10px", left: "-10px", fontSize: "24px" }}
|
||||
>
|
||||
{rankBadge}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{list.viewMode === "grid" ? (
|
||||
@@ -827,7 +861,15 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
>
|
||||
{avatarText}
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 6, width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
@@ -842,10 +884,17 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
</div>
|
||||
{primaryMetric && (
|
||||
<Tag
|
||||
color={typeof primaryMetric.value === "number" && primaryMetric.value >= 0 ? "success" : "error"}
|
||||
color={
|
||||
typeof primaryMetric.value === "number" && primaryMetric.value >= 0
|
||||
? "success"
|
||||
: "error"
|
||||
}
|
||||
style={{ fontWeight: "bold", marginInlineEnd: 0 }}
|
||||
>
|
||||
{primaryMetric.label}:{primaryMetric.value > 0 ? `+${primaryMetric.value}` : primaryMetric.value}
|
||||
{primaryMetric.label}:
|
||||
{primaryMetric.value > 0
|
||||
? `+${primaryMetric.value}`
|
||||
: primaryMetric.value}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
@@ -885,7 +934,8 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
<div style={{ marginTop: 4, display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||
{item.score !== undefined && (
|
||||
<Tag color={item.score >= 0 ? "success" : "error"} style={{ margin: 0 }}>
|
||||
{t("board.metrics.totalScore")}: {item.score > 0 ? `+${item.score}` : item.score}
|
||||
{t("board.metrics.totalScore")}:{" "}
|
||||
{item.score > 0 ? `+${item.score}` : item.score}
|
||||
</Tag>
|
||||
)}
|
||||
{item.rewardPoints !== undefined && (
|
||||
@@ -894,8 +944,12 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
</Tag>
|
||||
)}
|
||||
{item.weekChange !== undefined && (
|
||||
<Tag color={item.weekChange >= 0 ? "success" : "error"} style={{ margin: 0 }}>
|
||||
{t("board.metrics.weekChange")}: {item.weekChange > 0 ? `+${item.weekChange}` : item.weekChange}
|
||||
<Tag
|
||||
color={item.weekChange >= 0 ? "success" : "error"}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
{t("board.metrics.weekChange")}:{" "}
|
||||
{item.weekChange > 0 ? `+${item.weekChange}` : item.weekChange}
|
||||
</Tag>
|
||||
)}
|
||||
{item.weekDeducted !== undefined && (
|
||||
@@ -934,11 +988,15 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "var(--ss-card-bg)",
|
||||
border: isSelected ? "1px solid var(--ant-color-primary, #1677ff)" : "1px solid var(--ss-border-color)",
|
||||
border: isSelected
|
||||
? "1px solid var(--ant-color-primary, #1677ff)"
|
||||
: "1px solid var(--ss-border-color)",
|
||||
boxShadow: isSelected ? "0 8px 18px rgba(22, 119, 255, 0.14)" : undefined,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
styles={{ body: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column", padding: 12 } }}
|
||||
styles={{
|
||||
body: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column", padding: 12 },
|
||||
}}
|
||||
title={<span style={{ fontWeight: 600 }}>{list.name}</span>}
|
||||
extra={
|
||||
<Space size={6}>
|
||||
@@ -950,15 +1008,24 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
onConfirm={() => removeList(board.id, list.id)}
|
||||
disabled={!canManage || board.lists.length <= 1}
|
||||
>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} disabled={!canManage || board.lists.length <= 1}>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={!canManage || board.lists.length <= 1}
|
||||
>
|
||||
{t("board.removeList")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{errorMap[list.id] && <Alert style={{ marginBottom: 12 }} type="error" message={errorMap[list.id]} showIcon />}
|
||||
<div style={{ flex: 1, overflowY: "auto", overflowX: "hidden" }}>{renderStudentView(list)}</div>
|
||||
{errorMap[list.id] && (
|
||||
<Alert style={{ marginBottom: 12 }} type="error" message={errorMap[list.id]} showIcon />
|
||||
)}
|
||||
<div style={{ flex: 1, overflowY: "auto", overflowX: "hidden" }}>
|
||||
{renderStudentView(list)}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -988,7 +1055,9 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
minHeight: 200,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: `${node.ratio} 1 0`, minWidth: 0, minHeight: 0 }}>{renderLayoutNode(board, node.first)}</div>
|
||||
<div style={{ flex: `${node.ratio} 1 0`, minWidth: 0, minHeight: 0 }}>
|
||||
{renderLayoutNode(board, node.first)}
|
||||
</div>
|
||||
<div
|
||||
onMouseDown={(event) => {
|
||||
if (!canManage) return
|
||||
@@ -1047,13 +1116,31 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16, width: "100%", minHeight: 0, flex: 1 }}>
|
||||
<div style={{ display: "flex", width: "100%", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
width: "100%",
|
||||
minHeight: 0,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Space align="center" size={8}>
|
||||
<Typography.Title level={2} style={{ margin: 0, color: "var(--ss-text-main)" }}>
|
||||
{t("board.title")}
|
||||
</Typography.Title>
|
||||
<Tag color={canManage ? "success" : "default"}>{canManage ? t("board.editable") : t("board.readonly")}</Tag>
|
||||
<Tag color={canManage ? "success" : "default"}>
|
||||
{canManage ? t("board.editable") : t("board.readonly")}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button
|
||||
@@ -1070,7 +1157,11 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
>
|
||||
{t("board.splitVertical")}
|
||||
</Button>
|
||||
<Button icon={<EditOutlined />} onClick={openRenameModal} disabled={!canManage || !activeBoard}>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={openRenameModal}
|
||||
disabled={!canManage || !activeBoard}
|
||||
>
|
||||
{t("board.renameBoard")}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
@@ -1100,7 +1191,16 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
/>
|
||||
|
||||
{activeBoard ? (
|
||||
<div style={{ width: "100%", minHeight: 0, flex: 1, display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
minHeight: 0,
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{saving && (
|
||||
<Typography.Text type="secondary" style={{ flexShrink: 0 }}>
|
||||
{t("board.saving")}
|
||||
@@ -1123,7 +1223,11 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
okText={t("common.confirm")}
|
||||
cancelText={t("common.cancel")}
|
||||
>
|
||||
<Input value={renameValue} onChange={(e) => setRenameValue(e.target.value)} placeholder={t("board.boardNamePlaceholder")} />
|
||||
<Input
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
placeholder={t("board.boardNamePlaceholder")}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
@@ -1147,14 +1251,19 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
<Select
|
||||
style={{ width: 260 }}
|
||||
placeholder={t("board.applyPreset")}
|
||||
options={presets.map((preset) => ({ value: preset.id, label: `${preset.name} · ${preset.description}` }))}
|
||||
options={presets.map((preset) => ({
|
||||
value: preset.id,
|
||||
label: `${preset.name} · ${preset.description}`,
|
||||
}))}
|
||||
onChange={(presetId) => applyPreset(activeBoard.id, editingList.id, presetId)}
|
||||
disabled={!canManage}
|
||||
/>
|
||||
<Select
|
||||
style={{ width: 160 }}
|
||||
value={editingList.viewMode}
|
||||
onChange={(viewMode: BoardStudentViewMode) => updateList(activeBoard.id, editingList.id, { viewMode })}
|
||||
onChange={(viewMode: BoardStudentViewMode) =>
|
||||
updateList(activeBoard.id, editingList.id, { viewMode })
|
||||
}
|
||||
options={[
|
||||
{ value: "list", label: t("board.viewModes.list") },
|
||||
{ value: "card", label: t("board.viewModes.card") },
|
||||
@@ -1170,7 +1279,9 @@ ORDER BY reward_points DESC, score DESC`,
|
||||
disabled={!canManage}
|
||||
spellCheck={false}
|
||||
placeholder={t("board.sqlPlaceholder")}
|
||||
style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace" }}
|
||||
style={{
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@@ -34,7 +34,11 @@ interface reasonItem {
|
||||
|
||||
const deepClone = <T,>(v: T): T => JSON.parse(JSON.stringify(v)) as T
|
||||
|
||||
const withTimeout = async (promise: Promise<any>, ms: number, timeoutMessage: string): Promise<any> => {
|
||||
const withTimeout = async (
|
||||
promise: Promise<any>,
|
||||
ms: number,
|
||||
timeoutMessage: string
|
||||
): Promise<any> => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
return await Promise.race<any>([
|
||||
@@ -115,17 +119,22 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const steps: oobeStep[] = ["entry", "language", "theme", "password", "students", "reasons", "start"]
|
||||
const steps: oobeStep[] = [
|
||||
"entry",
|
||||
"language",
|
||||
"theme",
|
||||
"password",
|
||||
"students",
|
||||
"reasons",
|
||||
"start",
|
||||
]
|
||||
const stepIndex = steps.indexOf(currentStep) + 1
|
||||
const totalSteps = steps.length
|
||||
|
||||
const primaryColor = workingTheme?.config?.tdesign?.brandColor || "#1677FF"
|
||||
const isDark = workingTheme?.mode === "dark"
|
||||
|
||||
const showOobeMessage = (
|
||||
type: "success" | "error" | "warning" | "info",
|
||||
content: string
|
||||
) => {
|
||||
const showOobeMessage = (type: "success" | "error" | "warning" | "info", content: string) => {
|
||||
messageApi.open({
|
||||
type,
|
||||
content,
|
||||
@@ -392,7 +401,9 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
||||
"同步预检查超时,请稍后重试"
|
||||
)
|
||||
if (!previewRes?.success || !previewRes?.data?.can_sync) {
|
||||
throw new Error(previewRes?.message || previewRes?.data?.message || t("settings.database.uploadFailed"))
|
||||
throw new Error(
|
||||
previewRes?.message || previewRes?.data?.message || t("settings.database.uploadFailed")
|
||||
)
|
||||
}
|
||||
|
||||
if (previewRes?.data?.need_sync) {
|
||||
@@ -403,7 +414,9 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
||||
)
|
||||
if (!syncApplyRes?.success || !syncApplyRes?.data?.success) {
|
||||
throw new Error(
|
||||
syncApplyRes?.data?.message || syncApplyRes?.message || t("settings.database.uploadFailed")
|
||||
syncApplyRes?.data?.message ||
|
||||
syncApplyRes?.message ||
|
||||
t("settings.database.uploadFailed")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -942,7 +955,9 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{currentStep !== "language" && <Button onClick={handlePrev}>{t("common.prev")}</Button>}
|
||||
{currentStep !== "language" && (
|
||||
<Button onClick={handlePrev}>{t("common.prev")}</Button>
|
||||
)}
|
||||
{currentStep !== "start" && <Button onClick={handleSkip}>{t("oobe.skip")}</Button>}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -78,7 +78,10 @@ export const RewardExchange: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
}, [fetchData])
|
||||
|
||||
const sortedStudents = useMemo(
|
||||
() => [...students].sort((a, b) => b.reward_points - a.reward_points || a.name.localeCompare(b.name, "zh-CN")),
|
||||
() =>
|
||||
[...students].sort(
|
||||
(a, b) => b.reward_points - a.reward_points || a.name.localeCompare(b.name, "zh-CN")
|
||||
),
|
||||
[students]
|
||||
)
|
||||
|
||||
@@ -128,7 +131,12 @@ export const RewardExchange: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
}
|
||||
|
||||
const columns: ColumnsType<RedemptionItem> = [
|
||||
{ title: t("rewardExchange.recordStudent"), dataIndex: "student_name", key: "student_name", width: 120 },
|
||||
{
|
||||
title: t("rewardExchange.recordStudent"),
|
||||
dataIndex: "student_name",
|
||||
key: "student_name",
|
||||
width: 120,
|
||||
},
|
||||
{ title: t("rewardExchange.recordReward"), dataIndex: "reward_name", key: "reward_name" },
|
||||
{
|
||||
title: t("rewardExchange.recordCost"),
|
||||
@@ -149,7 +157,14 @@ export const RewardExchange: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
{contextHolder}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0, color: "var(--ss-text-main)" }}>{t("rewardExchange.title")}</h2>
|
||||
<Button
|
||||
type={exchangeMode ? "default" : "primary"}
|
||||
@@ -187,7 +202,9 @@ export const RewardExchange: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
onClick={() => handleStudentClick(student)}
|
||||
style={{
|
||||
cursor: exchangeMode ? "pointer" : "default",
|
||||
border: exchangeMode ? "1px solid var(--ant-color-primary, #1677ff)" : "1px solid var(--ss-border-color)",
|
||||
border: exchangeMode
|
||||
? "1px solid var(--ant-color-primary, #1677ff)"
|
||||
: "1px solid var(--ss-border-color)",
|
||||
}}
|
||||
bodyStyle={{ padding: "12px" }}
|
||||
>
|
||||
@@ -196,7 +213,9 @@ export const RewardExchange: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
{points > 0 ? `+${points}` : points}
|
||||
</Tag>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: "var(--ss-text-secondary)" }}>
|
||||
{exchangeMode ? t("rewardExchange.remainingRewardPoints") : t("rewardExchange.currentScore")}
|
||||
{exchangeMode
|
||||
? t("rewardExchange.remainingRewardPoints")
|
||||
: t("rewardExchange.currentScore")}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
@@ -228,11 +247,15 @@ export const RewardExchange: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
{t("rewardExchange.currentRewardPoints", { points: selectedStudent.reward_points })}
|
||||
</div>
|
||||
{affordableRewards.length === 0 ? (
|
||||
<div style={{ color: "var(--ss-text-secondary)" }}>{t("rewardExchange.noAffordableRewards")}</div>
|
||||
<div style={{ color: "var(--ss-text-secondary)" }}>
|
||||
{t("rewardExchange.noAffordableRewards")}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{affordableRewards
|
||||
.sort((a, b) => a.cost_points - b.cost_points || a.name.localeCompare(b.name, "zh-CN"))
|
||||
.sort(
|
||||
(a, b) => a.cost_points - b.cost_points || a.name.localeCompare(b.name, "zh-CN")
|
||||
)
|
||||
.map((reward) => (
|
||||
<div
|
||||
key={reward.id}
|
||||
|
||||
@@ -105,14 +105,19 @@ export const RewardSettings: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
: await (window as any).api.rewardSettingCreate(payload)
|
||||
|
||||
if (res.success) {
|
||||
messageApi.success(editing ? t("rewardSettings.updateSuccess") : t("rewardSettings.createSuccess"))
|
||||
messageApi.success(
|
||||
editing ? t("rewardSettings.updateSuccess") : t("rewardSettings.createSuccess")
|
||||
)
|
||||
setVisible(false)
|
||||
form.resetFields()
|
||||
setEditing(null)
|
||||
fetchRewards()
|
||||
emitDataUpdated()
|
||||
} else {
|
||||
messageApi.error(res.message || (editing ? t("rewardSettings.updateFailed") : t("rewardSettings.createFailed")))
|
||||
messageApi.error(
|
||||
res.message ||
|
||||
(editing ? t("rewardSettings.updateFailed") : t("rewardSettings.createFailed"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +201,11 @@ export const RewardSettings: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
name="cost_points"
|
||||
rules={[{ required: true, message: t("rewardSettings.costRequired") }]}
|
||||
>
|
||||
<InputNumber min={1} style={{ width: "100%" }} placeholder={t("rewardSettings.costPlaceholder")} />
|
||||
<InputNumber
|
||||
min={1}
|
||||
style={{ width: "100%" }}
|
||||
placeholder={t("rewardSettings.costPlaceholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -296,7 +296,9 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleClearSelectedStudents}
|
||||
disabled={!selectedStudentNames || selectedStudentNames.length === 0 || !canEdit}
|
||||
disabled={
|
||||
!selectedStudentNames || selectedStudentNames.length === 0 || !canEdit
|
||||
}
|
||||
>
|
||||
{t("score.clearSelectedStudents")}
|
||||
</Button>
|
||||
|
||||
@@ -27,7 +27,11 @@ type appSettings = {
|
||||
auto_score_enabled?: boolean
|
||||
}
|
||||
|
||||
const withTimeout = async (promise: Promise<any>, ms: number, timeoutMessage: string): Promise<any> => {
|
||||
const withTimeout = async (
|
||||
promise: Promise<any>,
|
||||
ms: number,
|
||||
timeoutMessage: string
|
||||
): Promise<any> => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
return await Promise.race<any>([
|
||||
@@ -397,7 +401,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
if (!(window as any).api) return
|
||||
try {
|
||||
const statusRes = await (window as any).api.dbGetStatus()
|
||||
if (!statusRes.success || !statusRes.data?.connected || statusRes.data?.type !== "postgresql") {
|
||||
if (
|
||||
!statusRes.success ||
|
||||
!statusRes.data?.connected ||
|
||||
statusRes.data?.type !== "postgresql"
|
||||
) {
|
||||
messageApi.warning(t("settings.database.uploadNeedRemote"))
|
||||
return
|
||||
}
|
||||
@@ -408,7 +416,9 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
"同步预检查超时,请稍后重试"
|
||||
)
|
||||
if (!previewRes.success || !previewRes.data?.can_sync) {
|
||||
messageApi.error(previewRes.message || previewRes.data?.message || t("settings.database.uploadFailed"))
|
||||
messageApi.error(
|
||||
previewRes.message || previewRes.data?.message || t("settings.database.uploadFailed")
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!previewRes.data.need_sync) {
|
||||
@@ -434,9 +444,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
"执行同步超时,请稍后重试"
|
||||
)
|
||||
if (applyRes.success && applyRes.data?.success) {
|
||||
messageApi.success(
|
||||
applyRes.data?.message || t("settings.database.uploadSuccess")
|
||||
)
|
||||
messageApi.success(applyRes.data?.message || t("settings.database.uploadSuccess"))
|
||||
emitDataUpdated("all")
|
||||
await loadAll()
|
||||
} else {
|
||||
@@ -1060,7 +1068,9 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
|
||||
{t("settings.app.title")}
|
||||
</div>
|
||||
<div style={{ color: "var(--ss-text-secondary)", marginBottom: "12px", fontSize: "12px" }}>
|
||||
<div
|
||||
style={{ color: "var(--ss-text-secondary)", marginBottom: "12px", fontSize: "12px" }}
|
||||
>
|
||||
{t("settings.app.description")}
|
||||
</div>
|
||||
<div style={{ marginTop: "8px" }}>
|
||||
|
||||
@@ -124,7 +124,6 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
useEffect(() => {
|
||||
const api = (window as any).api
|
||||
if (!api) return
|
||||
|
||||
;(async () => {
|
||||
await loadThemes()
|
||||
await loadCurrentTheme()
|
||||
|
||||
@@ -103,7 +103,8 @@ const api = {
|
||||
deleteReason: (id: number): Promise<{ success: boolean }> => invoke("reason_delete", { id }),
|
||||
|
||||
// DB - Reward
|
||||
rewardSettingQuery: (): Promise<{ success: boolean; data: any[] }> => invoke("reward_setting_query"),
|
||||
rewardSettingQuery: (): Promise<{ success: boolean; data: any[] }> =>
|
||||
invoke("reward_setting_query"),
|
||||
rewardSettingCreate: (data: {
|
||||
name: string
|
||||
cost_points: number
|
||||
@@ -121,8 +122,9 @@ const api = {
|
||||
data?: { redemption_id: number; remaining_reward_points: number }
|
||||
message?: string
|
||||
}> => invoke("reward_redeem", { data }),
|
||||
rewardRedemptionQuery: (params?: { limit?: number }): Promise<{ success: boolean; data: any[] }> =>
|
||||
invoke("reward_redemption_query", { params }),
|
||||
rewardRedemptionQuery: (params?: {
|
||||
limit?: number
|
||||
}): Promise<{ success: boolean; data: any[] }> => invoke("reward_redemption_query", { params }),
|
||||
|
||||
// DB - Event
|
||||
queryEvents: (params?: { limit?: number }): Promise<{ success: boolean; data: any[] }> =>
|
||||
@@ -273,7 +275,9 @@ const api = {
|
||||
message?: string
|
||||
}
|
||||
}> => invoke("db_sync_preview"),
|
||||
dbSyncApply: (strategy: "keep_local" | "keep_remote"): Promise<{
|
||||
dbSyncApply: (
|
||||
strategy: "keep_local" | "keep_remote"
|
||||
): Promise<{
|
||||
success: boolean
|
||||
data: { success: boolean; synced_records: number; resolved_conflicts: number; message?: string }
|
||||
}> => invoke("db_sync_apply", { strategy }),
|
||||
|
||||
Reference in New Issue
Block a user