mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
新增奖励兑换模式与奖励设置,并持久化兑换记录
This commit is contained in:
@@ -73,6 +73,8 @@ function MainContent(): React.JSX.Element {
|
||||
if (p.startsWith("/settlements")) return "settlements"
|
||||
if (p.startsWith("/reasons")) return "reasons"
|
||||
if (p.startsWith("/auto-score")) return "auto-score"
|
||||
if (p.startsWith("/reward-exchange")) return "reward-exchange"
|
||||
if (p.startsWith("/reward-settings")) return "reward-settings"
|
||||
if (p.startsWith("/settings")) return "settings"
|
||||
return "home"
|
||||
}, [location.pathname])
|
||||
@@ -272,6 +274,8 @@ function MainContent(): React.JSX.Element {
|
||||
if (key === "settlements") navigate("/settlements")
|
||||
if (key === "reasons") navigate("/reasons")
|
||||
if (key === "auto-score") navigate("/auto-score")
|
||||
if (key === "reward-exchange") navigate("/reward-exchange")
|
||||
if (key === "reward-settings") navigate("/reward-settings")
|
||||
if (key === "settings") navigate("/settings")
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ const loadScoreManager = () => import("./ScoreManager")
|
||||
const loadLeaderboard = () => import("./Leaderboard")
|
||||
const loadSettlementHistory = () => import("./SettlementHistory")
|
||||
const loadAutoScoreManager = () => import("./AutoScoreManager")
|
||||
const loadRewardExchange = () => import("./RewardExchange")
|
||||
const loadRewardSettings = () => import("./RewardSettings")
|
||||
|
||||
const Home = lazy(() => loadHome().then((m) => ({ default: m.Home })))
|
||||
const StudentManager = lazy(() => loadStudentManager().then((m) => ({ default: m.StudentManager })))
|
||||
@@ -26,6 +28,12 @@ const SettlementHistory = lazy(() =>
|
||||
const AutoScoreManager = lazy(() =>
|
||||
loadAutoScoreManager().then((m) => ({ default: m.AutoScoreManager }))
|
||||
)
|
||||
const RewardExchange = lazy(() =>
|
||||
loadRewardExchange().then((m) => ({ default: m.RewardExchange }))
|
||||
)
|
||||
const RewardSettings = lazy(() =>
|
||||
loadRewardSettings().then((m) => ({ default: m.RewardSettings }))
|
||||
)
|
||||
|
||||
const warmupRouteChunks = () =>
|
||||
Promise.allSettled([
|
||||
@@ -37,6 +45,8 @@ const warmupRouteChunks = () =>
|
||||
loadLeaderboard(),
|
||||
loadSettlementHistory(),
|
||||
loadAutoScoreManager(),
|
||||
loadRewardExchange(),
|
||||
loadRewardSettings(),
|
||||
])
|
||||
|
||||
const { Content } = Layout
|
||||
@@ -235,6 +245,14 @@ export function ContentArea({
|
||||
<Route path="/settlements" element={<SettlementHistory />} />
|
||||
<Route path="/reasons" element={<ReasonManager canEdit={permission === "admin"} />} />
|
||||
<Route path="/auto-score" element={<AutoScoreManager />} />
|
||||
<Route
|
||||
path="/reward-exchange"
|
||||
element={<RewardExchange canEdit={permission === "admin" || permission === "points"} />}
|
||||
/>
|
||||
<Route
|
||||
path="/reward-settings"
|
||||
element={<RewardSettings canEdit={permission === "admin"} />}
|
||||
/>
|
||||
<Route path="/settings" element={<Settings permission={permission} />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { Button, Card, Modal, Table, Tag, message } from "antd"
|
||||
import type { ColumnsType } from "antd/es/table"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
interface StudentItem {
|
||||
id: number
|
||||
name: string
|
||||
score: number
|
||||
reward_points: number
|
||||
}
|
||||
|
||||
interface RewardItem {
|
||||
id: number
|
||||
name: string
|
||||
cost_points: number
|
||||
}
|
||||
|
||||
interface RedemptionItem {
|
||||
id: number
|
||||
uuid: string
|
||||
student_name: string
|
||||
reward_id: number
|
||||
reward_name: string
|
||||
cost_points: number
|
||||
redeemed_at: string
|
||||
}
|
||||
|
||||
export const RewardExchange: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
const { t } = useTranslation()
|
||||
const [students, setStudents] = useState<StudentItem[]>([])
|
||||
const [rewards, setRewards] = useState<RewardItem[]>([])
|
||||
const [records, setRecords] = useState<RedemptionItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [exchangeMode, setExchangeMode] = useState(false)
|
||||
const [selectedStudent, setSelectedStudent] = useState<StudentItem | null>(null)
|
||||
const [chooseRewardVisible, setChooseRewardVisible] = useState(false)
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const emitDataUpdated = () => {
|
||||
window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category: "all" } }))
|
||||
}
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!(window as any).api) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const [stuRes, rewardRes, recordRes] = await Promise.all([
|
||||
(window as any).api.queryStudents({}),
|
||||
(window as any).api.rewardSettingQuery(),
|
||||
(window as any).api.rewardRedemptionQuery({ limit: 100 }),
|
||||
])
|
||||
|
||||
if (stuRes.success && Array.isArray(stuRes.data)) {
|
||||
setStudents(stuRes.data)
|
||||
}
|
||||
if (rewardRes.success && Array.isArray(rewardRes.data)) {
|
||||
setRewards(rewardRes.data)
|
||||
}
|
||||
if (recordRes.success && Array.isArray(recordRes.data)) {
|
||||
setRecords(recordRes.data)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
const onDataUpdated = (e: any) => {
|
||||
const category = e?.detail?.category
|
||||
if (category === "all" || category === "students" || category === "events") {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
window.addEventListener("ss:data-updated", onDataUpdated as any)
|
||||
return () => window.removeEventListener("ss:data-updated", onDataUpdated as any)
|
||||
}, [fetchData])
|
||||
|
||||
const sortedStudents = useMemo(
|
||||
() => [...students].sort((a, b) => b.reward_points - a.reward_points || a.name.localeCompare(b.name, "zh-CN")),
|
||||
[students]
|
||||
)
|
||||
|
||||
const affordableRewards = useMemo(() => {
|
||||
if (!selectedStudent) return []
|
||||
return rewards.filter((r) => r.cost_points <= selectedStudent.reward_points)
|
||||
}, [rewards, selectedStudent])
|
||||
|
||||
const handleStudentClick = (student: StudentItem) => {
|
||||
if (!exchangeMode) return
|
||||
if (!canEdit) {
|
||||
messageApi.error(t("common.readOnly"))
|
||||
return
|
||||
}
|
||||
setSelectedStudent(student)
|
||||
setChooseRewardVisible(true)
|
||||
}
|
||||
|
||||
const handleRedeem = async (reward: RewardItem) => {
|
||||
if (!(window as any).api || !selectedStudent) return
|
||||
if (!canEdit) {
|
||||
messageApi.error(t("common.readOnly"))
|
||||
return
|
||||
}
|
||||
|
||||
const res = await (window as any).api.rewardRedeem({
|
||||
student_name: selectedStudent.name,
|
||||
reward_id: reward.id,
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
messageApi.success(
|
||||
t("rewardExchange.redeemSuccess", {
|
||||
student: selectedStudent.name,
|
||||
reward: reward.name,
|
||||
points: reward.cost_points,
|
||||
})
|
||||
)
|
||||
setChooseRewardVisible(false)
|
||||
setSelectedStudent(null)
|
||||
setExchangeMode(false)
|
||||
fetchData()
|
||||
emitDataUpdated()
|
||||
} else {
|
||||
messageApi.error(res.message || t("rewardExchange.redeemFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<RedemptionItem> = [
|
||||
{ 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"),
|
||||
dataIndex: "cost_points",
|
||||
key: "cost_points",
|
||||
width: 120,
|
||||
render: (v: number) => <Tag color="gold">-{v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: t("rewardExchange.recordTime"),
|
||||
dataIndex: "redeemed_at",
|
||||
key: "redeemed_at",
|
||||
width: 180,
|
||||
render: (time: string) => new Date(time).toLocaleString(),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
{contextHolder}
|
||||
<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"}
|
||||
disabled={!canEdit}
|
||||
onClick={() => {
|
||||
if (!canEdit) {
|
||||
messageApi.error(t("common.readOnly"))
|
||||
return
|
||||
}
|
||||
setExchangeMode((prev) => !prev)
|
||||
}}
|
||||
>
|
||||
{exchangeMode ? t("rewardExchange.exitMode") : t("rewardExchange.enterMode")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "12px", color: "var(--ss-text-secondary)", fontSize: 13 }}>
|
||||
{exchangeMode ? t("rewardExchange.modeHint") : t("rewardExchange.normalHint")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
|
||||
gap: "12px",
|
||||
marginBottom: "24px",
|
||||
}}
|
||||
>
|
||||
{sortedStudents.map((student) => {
|
||||
const points = exchangeMode ? student.reward_points : student.score
|
||||
return (
|
||||
<Card
|
||||
key={student.id}
|
||||
hoverable={exchangeMode}
|
||||
onClick={() => handleStudentClick(student)}
|
||||
style={{
|
||||
cursor: exchangeMode ? "pointer" : "default",
|
||||
border: exchangeMode ? "1px solid var(--ant-color-primary, #1677ff)" : "1px solid var(--ss-border-color)",
|
||||
}}
|
||||
bodyStyle={{ padding: "12px" }}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>{student.name}</div>
|
||||
<Tag color={points > 0 ? "success" : points < 0 ? "error" : "default"}>
|
||||
{points > 0 ? `+${points}` : points}
|
||||
</Tag>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: "var(--ss-text-secondary)" }}>
|
||||
{exchangeMode ? t("rewardExchange.remainingRewardPoints") : t("rewardExchange.currentScore")}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Card title={t("rewardExchange.recordsTitle")} loading={loading}>
|
||||
<Table
|
||||
rowKey="uuid"
|
||||
dataSource={records}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 10, total: records.length, defaultCurrent: 1 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={t("rewardExchange.chooseRewardTitle", { name: selectedStudent?.name || "" })}
|
||||
open={chooseRewardVisible}
|
||||
onCancel={() => {
|
||||
setChooseRewardVisible(false)
|
||||
setSelectedStudent(null)
|
||||
}}
|
||||
footer={null}
|
||||
destroyOnHidden
|
||||
>
|
||||
{!selectedStudent ? null : (
|
||||
<>
|
||||
<div style={{ marginBottom: "12px", color: "var(--ss-text-secondary)", fontSize: 13 }}>
|
||||
{t("rewardExchange.currentRewardPoints", { points: selectedStudent.reward_points })}
|
||||
</div>
|
||||
{affordableRewards.length === 0 ? (
|
||||
<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"))
|
||||
.map((reward) => (
|
||||
<div
|
||||
key={reward.id}
|
||||
style={{
|
||||
border: "1px solid var(--ss-border-color)",
|
||||
borderRadius: 8,
|
||||
padding: "10px 12px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{reward.name}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--ss-text-secondary)" }}>
|
||||
{t("rewardExchange.costLabel", { points: reward.cost_points })}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" onClick={() => handleRedeem(reward)}>
|
||||
{t("rewardExchange.redeemNow")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import { Button, Form, Input, InputNumber, Modal, Popconfirm, Table, message } from "antd"
|
||||
import type { ColumnsType } from "antd/es/table"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
interface RewardItem {
|
||||
id: number
|
||||
name: string
|
||||
cost_points: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export const RewardSettings: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
const { t } = useTranslation()
|
||||
const [data, setData] = useState<RewardItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [editing, setEditing] = useState<RewardItem | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const emitDataUpdated = () => {
|
||||
window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category: "all" } }))
|
||||
}
|
||||
|
||||
const fetchRewards = useCallback(async () => {
|
||||
if (!(window as any).api) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await (window as any).api.rewardSettingQuery()
|
||||
if (res.success && Array.isArray(res.data)) {
|
||||
setData(res.data)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchRewards()
|
||||
}, [fetchRewards])
|
||||
|
||||
const openCreate = () => {
|
||||
if (!canEdit) {
|
||||
messageApi.error(t("common.readOnly"))
|
||||
return
|
||||
}
|
||||
setEditing(null)
|
||||
form.setFieldsValue({ name: "", cost_points: 1 })
|
||||
setVisible(true)
|
||||
}
|
||||
|
||||
const openEdit = (item: RewardItem) => {
|
||||
if (!canEdit) {
|
||||
messageApi.error(t("common.readOnly"))
|
||||
return
|
||||
}
|
||||
setEditing(item)
|
||||
form.setFieldsValue({ name: item.name, cost_points: item.cost_points })
|
||||
setVisible(true)
|
||||
}
|
||||
|
||||
const handleDelete = async (item: RewardItem) => {
|
||||
if (!(window as any).api) return
|
||||
if (!canEdit) {
|
||||
messageApi.error(t("common.readOnly"))
|
||||
return
|
||||
}
|
||||
|
||||
const res = await (window as any).api.rewardSettingDelete(item.id)
|
||||
if (res.success) {
|
||||
messageApi.success(t("rewardSettings.deleteSuccess"))
|
||||
fetchRewards()
|
||||
emitDataUpdated()
|
||||
} else {
|
||||
messageApi.error(res.message || t("rewardSettings.deleteFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!(window as any).api) return
|
||||
if (!canEdit) {
|
||||
messageApi.error(t("common.readOnly"))
|
||||
return
|
||||
}
|
||||
|
||||
const values = await form.validateFields()
|
||||
const payload = {
|
||||
name: String(values.name || "").trim(),
|
||||
cost_points: Number(values.cost_points),
|
||||
}
|
||||
|
||||
if (!payload.name) {
|
||||
messageApi.warning(t("rewardSettings.nameRequired"))
|
||||
return
|
||||
}
|
||||
if (!Number.isFinite(payload.cost_points) || payload.cost_points <= 0) {
|
||||
messageApi.warning(t("rewardSettings.costRequired"))
|
||||
return
|
||||
}
|
||||
|
||||
const res = editing
|
||||
? await (window as any).api.rewardSettingUpdate(editing.id, payload)
|
||||
: await (window as any).api.rewardSettingCreate(payload)
|
||||
|
||||
if (res.success) {
|
||||
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")))
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<RewardItem> = [
|
||||
{
|
||||
title: t("rewardSettings.rewardName"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: t("rewardSettings.costPoints"),
|
||||
dataIndex: "cost_points",
|
||||
key: "cost_points",
|
||||
width: 140,
|
||||
render: (v: number) => <b>{v}</b>,
|
||||
},
|
||||
{
|
||||
title: t("common.operation"),
|
||||
key: "operation",
|
||||
width: 180,
|
||||
render: (_, row) => (
|
||||
<>
|
||||
<Button type="link" disabled={!canEdit} onClick={() => openEdit(row)}>
|
||||
{t("common.edit")}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t("rewardSettings.deleteConfirm", { name: row.name })}
|
||||
onConfirm={() => handleDelete(row)}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<Button type="link" danger disabled={!canEdit}>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
{contextHolder}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "16px" }}>
|
||||
<h2 style={{ margin: 0, color: "var(--ss-text-main)" }}>{t("rewardSettings.title")}</h2>
|
||||
<Button type="primary" disabled={!canEdit} onClick={openCreate}>
|
||||
{t("rewardSettings.addReward")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
dataSource={data}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
bordered
|
||||
pagination={{ pageSize: 50, total: data.length, defaultCurrent: 1 }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editing ? t("rewardSettings.editTitle") : t("rewardSettings.addTitle")}
|
||||
open={visible}
|
||||
onOk={handleSave}
|
||||
onCancel={() => {
|
||||
setVisible(false)
|
||||
setEditing(null)
|
||||
}}
|
||||
okText={t("common.save")}
|
||||
cancelText={t("common.cancel")}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={{ cost_points: 1 }}>
|
||||
<Form.Item
|
||||
label={t("rewardSettings.rewardName")}
|
||||
name="name"
|
||||
rules={[{ required: true, message: t("rewardSettings.nameRequired") }]}
|
||||
>
|
||||
<Input placeholder={t("rewardSettings.namePlaceholder")} maxLength={64} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("rewardSettings.costPoints")}
|
||||
name="cost_points"
|
||||
rules={[{ required: true, message: t("rewardSettings.costRequired") }]}
|
||||
>
|
||||
<InputNumber min={1} style={{ width: "100%" }} placeholder={t("rewardSettings.costPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
FileTextOutlined,
|
||||
CloudOutlined,
|
||||
UploadOutlined,
|
||||
GiftOutlined,
|
||||
AppstoreAddOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@@ -166,6 +168,17 @@ export function Sidebar({
|
||||
icon: <SyncOutlined />,
|
||||
label: t("sidebar.autoScore"),
|
||||
},
|
||||
{
|
||||
key: "reward-exchange",
|
||||
icon: <GiftOutlined />,
|
||||
label: t("sidebar.rewardExchange"),
|
||||
},
|
||||
{
|
||||
key: "reward-settings",
|
||||
icon: <AppstoreAddOutlined />,
|
||||
label: t("sidebar.rewardSettings"),
|
||||
disabled: permission !== "admin",
|
||||
},
|
||||
{
|
||||
key: "leaderboard",
|
||||
icon: <UnorderedListOutlined />,
|
||||
|
||||
@@ -310,6 +310,8 @@
|
||||
"settlements": "Settlements",
|
||||
"reasons": "Reasons",
|
||||
"autoScore": "Auto Score",
|
||||
"rewardExchange": "Reward Exchange",
|
||||
"rewardSettings": "Reward Settings",
|
||||
"settings": "Settings",
|
||||
"remoteDb": "Remote Database",
|
||||
"refreshStatus": "Refresh Status",
|
||||
@@ -516,6 +518,46 @@
|
||||
"deleteFailed": "Delete failed",
|
||||
"others": "Others"
|
||||
},
|
||||
"rewardExchange": {
|
||||
"title": "Reward Exchange",
|
||||
"enterMode": "Enter Exchange Mode",
|
||||
"exitMode": "Exit Exchange Mode",
|
||||
"modeHint": "Reward exchange mode is active. Click a student card to choose an available reward.",
|
||||
"normalHint": "Current cards show original score. Enter mode to display redeemable reward points.",
|
||||
"remainingRewardPoints": "Remaining Reward Points",
|
||||
"currentScore": "Current Score",
|
||||
"chooseRewardTitle": "Choose Reward for {{name}}",
|
||||
"currentRewardPoints": "Current reward points: {{points}}",
|
||||
"costLabel": "Cost points: {{points}}",
|
||||
"redeemNow": "Redeem",
|
||||
"noAffordableRewards": "No rewards can be redeemed now. Earn more points first.",
|
||||
"redeemSuccess": "{{student}} redeemed \"{{reward}}\" and spent {{points}} points",
|
||||
"redeemFailed": "Redeem failed",
|
||||
"recordsTitle": "Redemption Records",
|
||||
"recordStudent": "Student",
|
||||
"recordReward": "Reward",
|
||||
"recordCost": "Cost",
|
||||
"recordTime": "Redeemed At"
|
||||
},
|
||||
"rewardSettings": {
|
||||
"title": "Reward Settings",
|
||||
"addReward": "Add Reward",
|
||||
"addTitle": "Add Reward",
|
||||
"editTitle": "Edit Reward",
|
||||
"rewardName": "Reward Name",
|
||||
"costPoints": "Cost Points",
|
||||
"namePlaceholder": "e.g. Homework-Free Card",
|
||||
"costPlaceholder": "Enter cost points",
|
||||
"nameRequired": "Please enter reward name",
|
||||
"costRequired": "Please enter positive integer points",
|
||||
"createSuccess": "Reward created",
|
||||
"createFailed": "Create reward failed",
|
||||
"updateSuccess": "Reward updated",
|
||||
"updateFailed": "Update reward failed",
|
||||
"deleteSuccess": "Reward deleted",
|
||||
"deleteFailed": "Delete reward failed",
|
||||
"deleteConfirm": "Delete reward \"{{name}}\"?"
|
||||
},
|
||||
"leaderboard": {
|
||||
"title": "Points Leaderboard",
|
||||
"rank": "Rank",
|
||||
|
||||
@@ -310,6 +310,8 @@
|
||||
"settlements": "结算历史",
|
||||
"reasons": "理由管理",
|
||||
"autoScore": "自动加分",
|
||||
"rewardExchange": "奖励兑换",
|
||||
"rewardSettings": "奖励设置",
|
||||
"settings": "系统设置",
|
||||
"remoteDb": "远程数据库",
|
||||
"refreshStatus": "刷新状态",
|
||||
@@ -516,6 +518,46 @@
|
||||
"deleteFailed": "删除失败",
|
||||
"others": "其他"
|
||||
},
|
||||
"rewardExchange": {
|
||||
"title": "奖励兑换",
|
||||
"enterMode": "进入奖励兑换模式",
|
||||
"exitMode": "退出奖励兑换模式",
|
||||
"modeHint": "当前为奖励兑换模式,点击学生卡可选择可兑换奖励。",
|
||||
"normalHint": "当前显示学生原始积分,点击按钮后将显示可兑换积分。",
|
||||
"remainingRewardPoints": "剩余兑换积分",
|
||||
"currentScore": "当前积分",
|
||||
"chooseRewardTitle": "为 {{name}} 选择奖励",
|
||||
"currentRewardPoints": "当前可兑换积分:{{points}}",
|
||||
"costLabel": "消耗积分:{{points}}",
|
||||
"redeemNow": "立即兑换",
|
||||
"noAffordableRewards": "当前没有可兑换的奖励,请先积累积分。",
|
||||
"redeemSuccess": "{{student}} 成功兑换「{{reward}}」,消耗 {{points}} 分",
|
||||
"redeemFailed": "兑换失败",
|
||||
"recordsTitle": "兑换记录",
|
||||
"recordStudent": "学生",
|
||||
"recordReward": "奖励",
|
||||
"recordCost": "消耗积分",
|
||||
"recordTime": "兑换时间"
|
||||
},
|
||||
"rewardSettings": {
|
||||
"title": "奖励设置",
|
||||
"addReward": "添加奖励",
|
||||
"addTitle": "添加奖励",
|
||||
"editTitle": "编辑奖励",
|
||||
"rewardName": "奖励名称",
|
||||
"costPoints": "消耗积分",
|
||||
"namePlaceholder": "例如:免作业券",
|
||||
"costPlaceholder": "请输入消耗积分",
|
||||
"nameRequired": "请输入奖励名称",
|
||||
"costRequired": "请输入正整数积分",
|
||||
"createSuccess": "奖励创建成功",
|
||||
"createFailed": "奖励创建失败",
|
||||
"updateSuccess": "奖励更新成功",
|
||||
"updateFailed": "奖励更新失败",
|
||||
"deleteSuccess": "奖励删除成功",
|
||||
"deleteFailed": "奖励删除失败",
|
||||
"deleteConfirm": "确认删除奖励「{{name}}」?"
|
||||
},
|
||||
"leaderboard": {
|
||||
"title": "积分排行榜",
|
||||
"rank": "排名",
|
||||
|
||||
@@ -100,6 +100,28 @@ const api = {
|
||||
invoke("reason_update", { id, data }),
|
||||
deleteReason: (id: number): Promise<{ success: boolean }> => invoke("reason_delete", { id }),
|
||||
|
||||
// DB - Reward
|
||||
rewardSettingQuery: (): Promise<{ success: boolean; data: any[] }> => invoke("reward_setting_query"),
|
||||
rewardSettingCreate: (data: {
|
||||
name: string
|
||||
cost_points: number
|
||||
}): Promise<{ success: boolean; data?: number; message?: string }> =>
|
||||
invoke("reward_setting_create", { data }),
|
||||
rewardSettingUpdate: (id: number, data: any): Promise<{ success: boolean; message?: string }> =>
|
||||
invoke("reward_setting_update", { id, data }),
|
||||
rewardSettingDelete: (id: number): Promise<{ success: boolean; message?: string }> =>
|
||||
invoke("reward_setting_delete", { id }),
|
||||
rewardRedeem: (data: {
|
||||
student_name: string
|
||||
reward_id: number
|
||||
}): Promise<{
|
||||
success: boolean
|
||||
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 }),
|
||||
|
||||
// DB - Event
|
||||
queryEvents: (params?: { limit?: number }): Promise<{ success: boolean; data: any[] }> =>
|
||||
invoke("event_query", { params }),
|
||||
|
||||
Reference in New Issue
Block a user