mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 21:14:21 +08:00
将奖励兑换模式入口整合到积分主页并移除独立导航
This commit is contained in:
@@ -73,7 +73,6 @@ function MainContent(): React.JSX.Element {
|
|||||||
if (p.startsWith("/settlements")) return "settlements"
|
if (p.startsWith("/settlements")) return "settlements"
|
||||||
if (p.startsWith("/reasons")) return "reasons"
|
if (p.startsWith("/reasons")) return "reasons"
|
||||||
if (p.startsWith("/auto-score")) return "auto-score"
|
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("/reward-settings")) return "reward-settings"
|
||||||
if (p.startsWith("/settings")) return "settings"
|
if (p.startsWith("/settings")) return "settings"
|
||||||
return "home"
|
return "home"
|
||||||
@@ -274,7 +273,6 @@ function MainContent(): React.JSX.Element {
|
|||||||
if (key === "settlements") navigate("/settlements")
|
if (key === "settlements") navigate("/settlements")
|
||||||
if (key === "reasons") navigate("/reasons")
|
if (key === "reasons") navigate("/reasons")
|
||||||
if (key === "auto-score") navigate("/auto-score")
|
if (key === "auto-score") navigate("/auto-score")
|
||||||
if (key === "reward-exchange") navigate("/reward-exchange")
|
|
||||||
if (key === "reward-settings") navigate("/reward-settings")
|
if (key === "reward-settings") navigate("/reward-settings")
|
||||||
if (key === "settings") navigate("/settings")
|
if (key === "settings") navigate("/settings")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const loadScoreManager = () => import("./ScoreManager")
|
|||||||
const loadLeaderboard = () => import("./Leaderboard")
|
const loadLeaderboard = () => import("./Leaderboard")
|
||||||
const loadSettlementHistory = () => import("./SettlementHistory")
|
const loadSettlementHistory = () => import("./SettlementHistory")
|
||||||
const loadAutoScoreManager = () => import("./AutoScoreManager")
|
const loadAutoScoreManager = () => import("./AutoScoreManager")
|
||||||
const loadRewardExchange = () => import("./RewardExchange")
|
|
||||||
const loadRewardSettings = () => import("./RewardSettings")
|
const loadRewardSettings = () => import("./RewardSettings")
|
||||||
|
|
||||||
const Home = lazy(() => loadHome().then((m) => ({ default: m.Home })))
|
const Home = lazy(() => loadHome().then((m) => ({ default: m.Home })))
|
||||||
@@ -28,9 +27,6 @@ const SettlementHistory = lazy(() =>
|
|||||||
const AutoScoreManager = lazy(() =>
|
const AutoScoreManager = lazy(() =>
|
||||||
loadAutoScoreManager().then((m) => ({ default: m.AutoScoreManager }))
|
loadAutoScoreManager().then((m) => ({ default: m.AutoScoreManager }))
|
||||||
)
|
)
|
||||||
const RewardExchange = lazy(() =>
|
|
||||||
loadRewardExchange().then((m) => ({ default: m.RewardExchange }))
|
|
||||||
)
|
|
||||||
const RewardSettings = lazy(() =>
|
const RewardSettings = lazy(() =>
|
||||||
loadRewardSettings().then((m) => ({ default: m.RewardSettings }))
|
loadRewardSettings().then((m) => ({ default: m.RewardSettings }))
|
||||||
)
|
)
|
||||||
@@ -45,7 +41,6 @@ const warmupRouteChunks = () =>
|
|||||||
loadLeaderboard(),
|
loadLeaderboard(),
|
||||||
loadSettlementHistory(),
|
loadSettlementHistory(),
|
||||||
loadAutoScoreManager(),
|
loadAutoScoreManager(),
|
||||||
loadRewardExchange(),
|
|
||||||
loadRewardSettings(),
|
loadRewardSettings(),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -245,10 +240,6 @@ export function ContentArea({
|
|||||||
<Route path="/settlements" element={<SettlementHistory />} />
|
<Route path="/settlements" element={<SettlementHistory />} />
|
||||||
<Route path="/reasons" element={<ReasonManager canEdit={permission === "admin"} />} />
|
<Route path="/reasons" element={<ReasonManager canEdit={permission === "admin"} />} />
|
||||||
<Route path="/auto-score" element={<AutoScoreManager />} />
|
<Route path="/auto-score" element={<AutoScoreManager />} />
|
||||||
<Route
|
|
||||||
path="/reward-exchange"
|
|
||||||
element={<RewardExchange canEdit={permission === "admin" || permission === "points"} />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/reward-settings"
|
path="/reward-settings"
|
||||||
element={<RewardSettings canEdit={permission === "admin"} />}
|
element={<RewardSettings canEdit={permission === "admin"} />}
|
||||||
|
|||||||
+191
-35
@@ -9,6 +9,7 @@ interface student {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
score: number
|
score: number
|
||||||
|
reward_points: number
|
||||||
extra_json?: string | null
|
extra_json?: string | null
|
||||||
avatarUrl?: string | null
|
avatarUrl?: string | null
|
||||||
pinyinName?: string
|
pinyinName?: string
|
||||||
@@ -34,6 +35,12 @@ interface scoreEvent {
|
|||||||
event_time: string
|
event_time: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface rewardSetting {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
cost_points: number
|
||||||
|
}
|
||||||
|
|
||||||
type SortType = "alphabet" | "surname" | "score"
|
type SortType = "alphabet" | "surname" | "score"
|
||||||
type LayoutType = "grouped" | "squareGrid"
|
type LayoutType = "grouped" | "squareGrid"
|
||||||
type SearchKeyboardLayout = "t9" | "qwerty26"
|
type SearchKeyboardLayout = "t9" | "qwerty26"
|
||||||
@@ -76,6 +83,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [students, setStudents] = useState<student[]>([])
|
const [students, setStudents] = useState<student[]>([])
|
||||||
const [reasons, setReasons] = useState<reason[]>([])
|
const [reasons, setReasons] = useState<reason[]>([])
|
||||||
|
const [rewards, setRewards] = useState<rewardSetting[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [sortType, setSortType] = useState<SortType>("alphabet")
|
const [sortType, setSortType] = useState<SortType>("alphabet")
|
||||||
const [layoutType, setLayoutType] = useState<LayoutType>("grouped")
|
const [layoutType, setLayoutType] = useState<LayoutType>("grouped")
|
||||||
@@ -99,6 +107,10 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
const [latestEvent, setLatestEvent] = useState<scoreEvent | null>(null)
|
const [latestEvent, setLatestEvent] = useState<scoreEvent | null>(null)
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const [quickActionStudentId, setQuickActionStudentId] = useState<number | null>(null)
|
const [quickActionStudentId, setQuickActionStudentId] = useState<number | null>(null)
|
||||||
|
const [rewardMode, setRewardMode] = useState(false)
|
||||||
|
const [rewardStudent, setRewardStudent] = useState<student | null>(null)
|
||||||
|
const [rewardModalVisible, setRewardModalVisible] = useState(false)
|
||||||
|
const [redeemLoading, setRedeemLoading] = useState(false)
|
||||||
const longPressTimerRef = useRef<number | null>(null)
|
const longPressTimerRef = useRef<number | null>(null)
|
||||||
const suppressClickRef = useRef(false)
|
const suppressClickRef = useRef(false)
|
||||||
const fetchRequestIdRef = useRef(0)
|
const fetchRequestIdRef = useRef(0)
|
||||||
@@ -137,9 +149,10 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
const requestId = ++fetchRequestIdRef.current
|
const requestId = ++fetchRequestIdRef.current
|
||||||
logHome("fetchData:start", { requestId, silent })
|
logHome("fetchData:start", { requestId, silent })
|
||||||
if (!silent) setLoading(true)
|
if (!silent) setLoading(true)
|
||||||
const [stuRes, reaRes] = await Promise.all([
|
const [stuRes, reaRes, rewRes] = await Promise.all([
|
||||||
(window as any).api.queryStudents({}),
|
(window as any).api.queryStudents({}),
|
||||||
(window as any).api.queryReasons(),
|
(window as any).api.queryReasons(),
|
||||||
|
(window as any).api.rewardSettingQuery(),
|
||||||
])
|
])
|
||||||
if (requestId !== fetchRequestIdRef.current) return
|
if (requestId !== fetchRequestIdRef.current) return
|
||||||
|
|
||||||
@@ -163,6 +176,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
setStudents(enrichedStudents)
|
setStudents(enrichedStudents)
|
||||||
}
|
}
|
||||||
if (reaRes.success) setReasons(reaRes.data)
|
if (reaRes.success) setReasons(reaRes.data)
|
||||||
|
if (rewRes.success) setRewards(rewRes.data)
|
||||||
if (!silent) setLoading(false)
|
if (!silent) setLoading(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -373,6 +387,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
return false
|
return false
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const getDisplayPoints = useCallback(
|
||||||
|
(s: student) => (rewardMode ? Number(s.reward_points || 0) : Number(s.score || 0)),
|
||||||
|
[rewardMode]
|
||||||
|
)
|
||||||
|
|
||||||
const sortedStudents = useMemo(() => {
|
const sortedStudents = useMemo(() => {
|
||||||
const filtered = students.filter((s) => matchStudentName(s, searchKeyword))
|
const filtered = students.filter((s) => matchStudentName(s, searchKeyword))
|
||||||
|
|
||||||
@@ -393,11 +412,13 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
return surnameA.localeCompare(surnameB, "zh-CN")
|
return surnameA.localeCompare(surnameB, "zh-CN")
|
||||||
})
|
})
|
||||||
case "score":
|
case "score":
|
||||||
return filtered.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name, "zh-CN"))
|
return filtered.sort(
|
||||||
|
(a, b) => getDisplayPoints(b) - getDisplayPoints(a) || a.name.localeCompare(b.name, "zh-CN")
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
}, [students, searchKeyword, sortType, matchStudentName])
|
}, [students, searchKeyword, sortType, matchStudentName, getDisplayPoints])
|
||||||
|
|
||||||
const groupedStudents = useMemo(() => {
|
const groupedStudents = useMemo(() => {
|
||||||
if (sortType === "score" || (sortType === "alphabet" && searchKeyword)) {
|
if (sortType === "score" || (sortType === "alphabet" && searchKeyword)) {
|
||||||
@@ -479,6 +500,11 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
messageApi.error(t("common.readOnly"))
|
messageApi.error(t("common.readOnly"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (rewardMode) {
|
||||||
|
setRewardStudent(student)
|
||||||
|
setRewardModalVisible(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (batchMode) {
|
if (batchMode) {
|
||||||
setSelectedStudentIds((prev) =>
|
setSelectedStudentIds((prev) =>
|
||||||
prev.includes(student.id) ? prev.filter((id) => id !== student.id) : [...prev, student.id]
|
prev.includes(student.id) ? prev.filter((id) => id !== student.id) : [...prev, student.id]
|
||||||
@@ -491,6 +517,62 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
setOperationVisible(true)
|
setOperationVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleToggleRewardMode = () => {
|
||||||
|
if (!canEdit) {
|
||||||
|
messageApi.error(t("common.readOnly"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!rewardMode && rewards.length === 0) {
|
||||||
|
messageApi.warning(t("rewardExchange.noAffordableRewards"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setRewardMode((prev) => !prev)
|
||||||
|
setBatchMode(false)
|
||||||
|
setSelectedStudentIds([])
|
||||||
|
setOperationVisible(false)
|
||||||
|
setQuickActionStudentId(null)
|
||||||
|
setRewardStudent(null)
|
||||||
|
setRewardModalVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const affordableRewards = useMemo(() => {
|
||||||
|
if (!rewardStudent) return []
|
||||||
|
return rewards
|
||||||
|
.filter((r) => r.cost_points <= Number(rewardStudent.reward_points || 0))
|
||||||
|
.sort((a, b) => a.cost_points - b.cost_points || a.name.localeCompare(b.name, "zh-CN"))
|
||||||
|
}, [rewards, rewardStudent])
|
||||||
|
|
||||||
|
const handleRedeemReward = async (reward: rewardSetting) => {
|
||||||
|
if (!(window as any).api || !rewardStudent) return
|
||||||
|
if (!canEdit) {
|
||||||
|
messageApi.error(t("common.readOnly"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setRedeemLoading(true)
|
||||||
|
const res = await (window as any).api.rewardRedeem({
|
||||||
|
student_name: rewardStudent.name,
|
||||||
|
reward_id: reward.id,
|
||||||
|
})
|
||||||
|
setRedeemLoading(false)
|
||||||
|
if (res.success) {
|
||||||
|
messageApi.success(
|
||||||
|
t("rewardExchange.redeemSuccess", {
|
||||||
|
student: rewardStudent.name,
|
||||||
|
reward: reward.name,
|
||||||
|
points: reward.cost_points,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setRewardModalVisible(false)
|
||||||
|
setRewardStudent(null)
|
||||||
|
setRewardMode(false)
|
||||||
|
fetchData(true)
|
||||||
|
fetchLatestEvent()
|
||||||
|
emitDataUpdated("students")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messageApi.error(res.message || t("rewardExchange.redeemFailed"))
|
||||||
|
}
|
||||||
|
|
||||||
const performSubmit = async (targetStudents: student[], delta: number, content: string) => {
|
const performSubmit = async (targetStudents: student[], delta: number, content: string) => {
|
||||||
if (!(window as any).api) return
|
if (!(window as any).api) return
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
@@ -621,6 +703,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startLongPress = (student: student) => {
|
const startLongPress = (student: student) => {
|
||||||
|
if (rewardMode) return
|
||||||
cancelLongPress()
|
cancelLongPress()
|
||||||
longPressTimerRef.current = window.setTimeout(() => {
|
longPressTimerRef.current = window.setTimeout(() => {
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
@@ -634,6 +717,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openQuickAction = (student: student) => {
|
const openQuickAction = (student: student) => {
|
||||||
|
if (rewardMode) return
|
||||||
cancelLongPress()
|
cancelLongPress()
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
messageApi.error(t("common.readOnly"))
|
messageApi.error(t("common.readOnly"))
|
||||||
@@ -644,6 +728,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleQuickAdjust = (student: student, delta: number) => {
|
const handleQuickAdjust = (student: student, delta: number) => {
|
||||||
|
if (rewardMode) return
|
||||||
const content = delta > 0 ? t("home.addPoints") : t("home.deductPoints")
|
const content = delta > 0 ? t("home.addPoints") : t("home.deductPoints")
|
||||||
performSubmit([student], delta, content)
|
performSubmit([student], delta, content)
|
||||||
setQuickActionStudentId(null)
|
setQuickActionStudentId(null)
|
||||||
@@ -890,10 +975,16 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
<Tag
|
<Tag
|
||||||
color={student.score > 0 ? "success" : student.score < 0 ? "error" : "default"}
|
color={
|
||||||
|
getDisplayPoints(student) > 0
|
||||||
|
? "success"
|
||||||
|
: getDisplayPoints(student) < 0
|
||||||
|
? "error"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
style={{ fontWeight: "bold" }}
|
style={{ fontWeight: "bold" }}
|
||||||
>
|
>
|
||||||
{student.score > 0 ? `+${student.score}` : student.score}
|
{getDisplayPoints(student) > 0 ? `+${getDisplayPoints(student)}` : getDisplayPoints(student)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1035,10 +1126,12 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
<Tag
|
<Tag
|
||||||
color={student.score > 0 ? "success" : student.score < 0 ? "error" : "default"}
|
color={
|
||||||
|
getDisplayPoints(student) > 0 ? "success" : getDisplayPoints(student) < 0 ? "error" : "default"
|
||||||
|
}
|
||||||
style={{ fontWeight: "bold", marginInlineEnd: 0 }}
|
style={{ fontWeight: "bold", marginInlineEnd: 0 }}
|
||||||
>
|
>
|
||||||
{student.score > 0 ? `+${student.score}` : student.score}
|
{getDisplayPoints(student) > 0 ? `+${getDisplayPoints(student)}` : getDisplayPoints(student)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
@@ -1231,10 +1324,16 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
{student.name}
|
{student.name}
|
||||||
</div>
|
</div>
|
||||||
<Tag
|
<Tag
|
||||||
color={student.score > 0 ? "success" : student.score < 0 ? "error" : "default"}
|
color={
|
||||||
|
getDisplayPoints(student) > 0
|
||||||
|
? "success"
|
||||||
|
: getDisplayPoints(student) < 0
|
||||||
|
? "error"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
style={{ fontWeight: "bold", marginInlineEnd: 0 }}
|
style={{ fontWeight: "bold", marginInlineEnd: 0 }}
|
||||||
>
|
>
|
||||||
{student.score > 0 ? `+${student.score}` : student.score}
|
{getDisplayPoints(student) > 0 ? `+${getDisplayPoints(student)}` : getDisplayPoints(student)}
|
||||||
</Tag>
|
</Tag>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
|
<Tag color="processing" style={{ marginInlineEnd: 0 }}>
|
||||||
@@ -1952,6 +2051,36 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const batchToolbar = rewardMode
|
||||||
|
? null
|
||||||
|
: !batchMode
|
||||||
|
? (
|
||||||
|
<Button onClick={handleEnterBatchMode} disabled={!canEdit}>
|
||||||
|
{t("home.multiSelect")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Space size={8} wrap>
|
||||||
|
<Button onClick={handleSelectAllStudents} disabled={!canEdit || students.length === 0}>
|
||||||
|
{t("home.selectAll")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleClearSelectedStudents}
|
||||||
|
disabled={!canEdit || selectedStudentIds.length === 0}
|
||||||
|
>
|
||||||
|
{t("home.clearSelected")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleOpenBatchOperation}
|
||||||
|
disabled={!canEdit || selectedStudentIds.length === 0}
|
||||||
|
>
|
||||||
|
{t("home.batchOperate")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleExitBatchMode}>{t("common.cancel")}</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -2106,7 +2235,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
icon={<UndoOutlined />}
|
icon={<UndoOutlined />}
|
||||||
onClick={handleUndoLastEvent}
|
onClick={handleUndoLastEvent}
|
||||||
loading={undoLoading}
|
loading={undoLoading}
|
||||||
disabled={!canEdit || !latestEvent}
|
disabled={!canEdit || !latestEvent || rewardMode}
|
||||||
title={
|
title={
|
||||||
latestEvent
|
latestEvent
|
||||||
? t("home.undoLastHint", {
|
? t("home.undoLastHint", {
|
||||||
@@ -2118,31 +2247,10 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
>
|
>
|
||||||
{t("home.undoLastAction")}
|
{t("home.undoLastAction")}
|
||||||
</Button>
|
</Button>
|
||||||
{!batchMode ? (
|
<Button type={rewardMode ? "default" : "primary"} onClick={handleToggleRewardMode} disabled={!canEdit}>
|
||||||
<Button onClick={handleEnterBatchMode} disabled={!canEdit}>
|
{rewardMode ? t("rewardExchange.exitMode") : t("rewardExchange.enterMode")}
|
||||||
{t("home.multiSelect")}
|
</Button>
|
||||||
</Button>
|
{batchToolbar}
|
||||||
) : (
|
|
||||||
<Space size={8} wrap>
|
|
||||||
<Button onClick={handleSelectAllStudents} disabled={!canEdit || students.length === 0}>
|
|
||||||
{t("home.selectAll")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleClearSelectedStudents}
|
|
||||||
disabled={!canEdit || selectedStudentIds.length === 0}
|
|
||||||
>
|
|
||||||
{t("home.clearSelected")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleOpenBatchOperation}
|
|
||||||
disabled={!canEdit || selectedStudentIds.length === 0}
|
|
||||||
>
|
|
||||||
{t("home.batchOperate")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleExitBatchMode}>{t("common.cancel")}</Button>
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2183,6 +2291,54 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={t("rewardExchange.chooseRewardTitle", { name: rewardStudent?.name || "" })}
|
||||||
|
open={rewardModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setRewardModalVisible(false)
|
||||||
|
setRewardStudent(null)
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
{!rewardStudent ? null : (
|
||||||
|
<>
|
||||||
|
<div style={{ marginBottom: "12px", color: "var(--ss-text-secondary)", fontSize: 13 }}>
|
||||||
|
{t("rewardExchange.currentRewardPoints", { points: rewardStudent.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.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" loading={redeemLoading} onClick={() => handleRedeemReward(reward)}>
|
||||||
|
{t("rewardExchange.redeemNow")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{isPortraitMode ? (
|
{isPortraitMode ? (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
CloudOutlined,
|
CloudOutlined,
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
GiftOutlined,
|
|
||||||
AppstoreAddOutlined,
|
AppstoreAddOutlined,
|
||||||
} from "@ant-design/icons"
|
} from "@ant-design/icons"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
@@ -168,11 +167,6 @@ export function Sidebar({
|
|||||||
icon: <SyncOutlined />,
|
icon: <SyncOutlined />,
|
||||||
label: t("sidebar.autoScore"),
|
label: t("sidebar.autoScore"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "reward-exchange",
|
|
||||||
icon: <GiftOutlined />,
|
|
||||||
label: t("sidebar.rewardExchange"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "reward-settings",
|
key: "reward-settings",
|
||||||
icon: <AppstoreAddOutlined />,
|
icon: <AppstoreAddOutlined />,
|
||||||
|
|||||||
Reference in New Issue
Block a user