feat: 优化积分操作面板

This commit is contained in:
JSR
2026-07-17 21:41:31 +08:00
parent 123243261b
commit e998978133
12 changed files with 326 additions and 186 deletions
+50 -2
View File
@@ -268,6 +268,55 @@ html.platform-macos body {
will-change: transform, opacity; will-change: transform, opacity;
} }
.ss-home-operation-morph-modal .ss-operation-panel-section,
.ss-operation-drawer .ss-operation-panel-section {
min-width: 0;
}
.ss-custom-score-buttons,
.ss-no-reason-quick-buttons,
.ss-reason-preset-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.ss-custom-score-buttons .ant-btn {
min-width: 48px;
height: 38px;
padding: 0 14px;
font-size: 15px;
}
.ss-reason-preset-buttons .ant-btn {
min-height: 42px;
padding: 6px 14px;
font-size: 15px;
}
.ss-no-reason-quick-buttons .ant-btn {
min-width: 62px;
height: 50px;
padding: 0 18px;
font-size: 18px;
font-weight: 700;
}
@media (max-width: 768px) {
.ss-operation-panel-grid {
grid-template-columns: 1fr !important;
}
.ss-no-reason-quick-buttons .ant-btn {
min-width: 64px;
height: 52px;
}
.ss-reason-preset-buttons .ant-btn {
min-height: 46px;
}
}
.ss-operation-mask-fade-enter, .ss-operation-mask-fade-enter,
.ss-operation-mask-fade-appear { .ss-operation-mask-fade-appear {
opacity: 0 !important; opacity: 0 !important;
@@ -385,8 +434,7 @@ html.platform-macos body {
line-height: 52px; line-height: 52px;
} }
.ss-immersive-toolbar-segment .ss-immersive-toolbar-segment .ss-immersive-toolbar-search.ant-input-affix-wrapper-focused,
.ss-immersive-toolbar-search.ant-input-affix-wrapper-focused,
.ss-immersive-toolbar-segment .ss-immersive-toolbar-search.ant-input-affix-wrapper:focus, .ss-immersive-toolbar-segment .ss-immersive-toolbar-search.ant-input-affix-wrapper:focus,
.ss-immersive-toolbar-segment .ss-immersive-toolbar-search.ant-input-affix-wrapper:hover { .ss-immersive-toolbar-segment .ss-immersive-toolbar-search.ant-input-affix-wrapper:hover {
border: none !important; border: none !important;
+246 -184
View File
@@ -1098,6 +1098,15 @@ export const Home: React.FC<HomeProps> = ({
performSubmit(targets, reason.delta, reason.content) performSubmit(targets, reason.delta, reason.content)
} }
const handleNoReasonQuickSelect = (delta: number) => {
const targets = batchMode ? selectedStudents : selectedStudent ? [selectedStudent] : []
if (targets.length === 0) {
messageApi.warning(t("home.selectStudentFirst"))
return
}
performSubmit(targets, delta, "")
}
const cancelLongPress = () => { const cancelLongPress = () => {
if (longPressTimerRef.current !== null) { if (longPressTimerRef.current !== null) {
window.clearTimeout(longPressTimerRef.current) window.clearTimeout(longPressTimerRef.current)
@@ -2690,16 +2699,160 @@ export const Home: React.FC<HomeProps> = ({
)} )}
</div> </div>
{groupedReasons.length > 0 && ( <div
<div> className="ss-operation-panel-grid"
<div style={{
style={{ display: "grid",
marginBottom: "12px", gridTemplateColumns: isPortraitMode ? "1fr" : "minmax(0, 0.95fr) minmax(0, 1.25fr)",
display: "flex", gap: "20px",
alignItems: "center", alignItems: "start",
gap: "8px", }}
}} >
> <div className="ss-operation-panel-section">
<div style={{ marginBottom: "12px", display: "flex", alignItems: "center", gap: "8px" }}>
<span
style={{
fontWeight: 600,
fontSize: "14px",
whiteSpace: "nowrap",
wordBreak: "keep-all",
flexShrink: 0,
}}
>
{t("home.customOperation")}
</span>
<Divider style={{ flex: 1, margin: 0 }} />
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "18px" }}>
<div>
<div
style={{
color: "var(--ss-text-secondary)",
fontSize: "13px",
marginBottom: "8px",
}}
>
{t("home.adjustPoints")}
</div>
<div className="ss-custom-score-buttons">
{[-5, -3, -2, -1, 1, 2, 3, 5, 10].map((num) => (
<Button
key={num}
type={customScore === num ? "primary" : "default"}
danger={num < 0}
onClick={() => setCustomScore(num)}
>
{num > 0 ? `+${num}` : num}
</Button>
))}
<Button onClick={() => setCustomScore(0)}>0</Button>
</div>
<div
style={{
display: "flex",
gap: "12px",
alignItems: "center",
flexWrap: "wrap",
marginTop: "12px",
}}
>
<InputNumber
value={customScore}
onChange={(v) => setCustomScore(v as number)}
min={-99}
max={99}
step={1}
style={{ width: "140px" }}
placeholder={t("home.customPoints")}
/>
<span style={{ fontSize: "13px", color: "var(--ss-text-secondary)" }}>
{t("home.customPointsHint")}
</span>
</div>
</div>
<div>
<div
style={{
color: "var(--ss-text-secondary)",
fontSize: "13px",
marginBottom: "8px",
}}
>
{t("home.reason")}
</div>
<Input
value={reasonContent}
onChange={(e) => setReasonContent(e.target.value)}
placeholder={t("home.reasonPlaceholder")}
suffix={
reasonContent ? (
<DeleteOutlined
onClick={() => setReasonContent("")}
style={{ cursor: "pointer" }}
/>
) : undefined
}
/>
</div>
{customScore !== undefined && (
<div
style={{
padding: "16px",
background:
customScore > 0
? "var(--ant-color-success-bg, #f6ffed)"
: customScore < 0
? "var(--ant-color-error-bg, #fff2f0)"
: "var(--ss-bg-color)",
borderRadius: "8px",
border: `1px solid ${customScore > 0 ? "var(--ant-color-success-border, #b7eb8f)" : customScore < 0 ? "var(--ant-color-error-border, #ffccc7)" : "var(--ss-border-color)"}`,
marginTop: "2px",
}}
>
<div
style={{
fontSize: "13px",
fontWeight: 600,
marginBottom: "4px",
color: "var(--ss-text-main)",
}}
>
{t("home.preview")}
</div>
<div style={{ fontSize: "15px" }}>
{isBatchOperation
? t("home.selectedCount", { count: operationTargets.length })
: selectedStudent?.name}{" "}
<span
style={{
fontWeight: "bold",
color:
customScore > 0
? "var(--ant-color-success, #52c41a)"
: customScore < 0
? "var(--ant-color-error, #ff4d4f)"
: "inherit",
}}
>
{customScore > 0 ? `+${customScore}` : customScore}
</span>{" "}
{t("home.points")}
<span style={{ color: "var(--ss-text-secondary)", marginLeft: "8px" }}>
{reasonContent
? `${t("home.reasonLabel")}${reasonContent}`
: t("home.noReason")}
</span>
</div>
</div>
)}
</div>
</div>
<div className="ss-operation-panel-section">
<div style={{ marginBottom: "12px", display: "flex", alignItems: "center", gap: "8px" }}>
<span <span
style={{ style={{
fontWeight: 600, fontWeight: 600,
@@ -2713,193 +2866,102 @@ export const Home: React.FC<HomeProps> = ({
</span> </span>
<Divider style={{ flex: 1, margin: 0 }} /> <Divider style={{ flex: 1, margin: 0 }} />
</div> </div>
<div
style={{ <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
display: "flex", <div>
flexDirection: "column", <div
gap: "12px", style={{
maxHeight: "240px", color: "var(--ss-text-secondary)",
overflowY: "auto", fontSize: "13px",
paddingRight: "4px", marginBottom: "8px",
}} }}
> >
{groupedReasons.map(([category, items]) => ( {t("home.noReasonQuickActions")}
<div key={category}> </div>
<div className="ss-no-reason-quick-buttons">
{[-3, -2, -1, 1, 2, 3, 4, 5].map((num) => (
<Button key={num} danger={num < 0} onClick={() => handleNoReasonQuickSelect(num)}>
{num > 0 ? `+${num}` : num}
</Button>
))}
</div>
</div>
{groupedReasons.length > 0 && (
<div>
<div <div
style={{ style={{
fontSize: "12px",
color: "var(--ss-text-secondary)", color: "var(--ss-text-secondary)",
marginBottom: "6px", fontSize: "13px",
paddingLeft: "2px", marginBottom: "8px",
}} }}
> >
{category} {t("home.reasonPresets")}
</div> </div>
<Space wrap size="small"> <div
{items.map((r) => ( style={{
<Button display: "flex",
key={r.id} flexDirection: "column",
size="small" gap: "12px",
onClick={() => handleReasonSelect(r)} maxHeight: isPortraitMode ? "none" : "320px",
style={{ overflowY: isPortraitMode ? "visible" : "auto",
whiteSpace: "nowrap", paddingRight: "4px",
wordBreak: "keep-all", }}
borderColor: >
r.delta > 0 {groupedReasons.map(([category, items]) => (
? "var(--ant-color-success, #52c41a)" <div key={category}>
: r.delta < 0 <div
? "var(--ant-color-error, #ff4d4f)"
: undefined,
}}
>
{r.content}{" "}
<span
style={{ style={{
marginLeft: "4px", fontSize: "12px",
color: color: "var(--ss-text-secondary)",
r.delta > 0 marginBottom: "6px",
? "var(--ant-color-success, #52c41a)" paddingLeft: "2px",
: r.delta < 0
? "var(--ant-color-error, #ff4d4f)"
: "inherit",
fontWeight: "bold",
}} }}
> >
{r.delta > 0 ? `+${r.delta}` : r.delta} {category}
</span> </div>
</Button> <div className="ss-reason-preset-buttons">
{items.map((r) => (
<Button
key={r.id}
onClick={() => handleReasonSelect(r)}
style={{
whiteSpace: "nowrap",
wordBreak: "keep-all",
borderColor:
r.delta > 0
? "var(--ant-color-success, #52c41a)"
: r.delta < 0
? "var(--ant-color-error, #ff4d4f)"
: undefined,
}}
>
{r.content}{" "}
<span
style={{
marginLeft: "4px",
color:
r.delta > 0
? "var(--ant-color-success, #52c41a)"
: r.delta < 0
? "var(--ant-color-error, #ff4d4f)"
: "inherit",
fontWeight: "bold",
}}
>
{r.delta > 0 ? `+${r.delta}` : r.delta}
</span>
</Button>
))}
</div>
</div>
))} ))}
</Space> </div>
</div> </div>
))} )}
</div> </div>
</div> </div>
)}
<div>
<div style={{ marginBottom: "12px", display: "flex", alignItems: "center", gap: "8px" }}>
<span
style={{
fontWeight: 600,
fontSize: "14px",
whiteSpace: "nowrap",
wordBreak: "keep-all",
flexShrink: 0,
}}
>
{t("home.adjustPoints")}
</span>
<Divider style={{ flex: 1, margin: 0 }} />
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px", marginBottom: "12px" }}>
{[-5, -3, -2, -1, 1, 2, 3, 5, 10].map((num) => (
<Button
key={num}
size="small"
type={customScore === num ? "primary" : "default"}
danger={num < 0}
onClick={() => setCustomScore(num)}
style={{ minWidth: "42px" }}
>
{num > 0 ? `+${num}` : num}
</Button>
))}
<Button size="small" onClick={() => setCustomScore(0)} style={{ minWidth: "42px" }}>
0
</Button>
</div>
<div style={{ display: "flex", gap: "12px", alignItems: "center", flexWrap: "wrap" }}>
<InputNumber
value={customScore}
onChange={(v) => setCustomScore(v as number)}
min={-99}
max={99}
step={1}
style={{ width: "140px" }}
placeholder={t("home.customPoints")}
/>
<span style={{ fontSize: "13px", color: "var(--ss-text-secondary)" }}>
{t("home.customPointsHint")}
</span>
</div>
</div> </div>
<div>
<div style={{ marginBottom: "12px", display: "flex", alignItems: "center", gap: "8px" }}>
<span
style={{
fontWeight: 600,
fontSize: "14px",
whiteSpace: "nowrap",
wordBreak: "keep-all",
flexShrink: 0,
}}
>
{t("home.reason")}
</span>
<Divider style={{ flex: 1, margin: 0 }} />
</div>
<Input
value={reasonContent}
onChange={(e) => setReasonContent(e.target.value)}
placeholder={t("home.reasonPlaceholder")}
suffix={
reasonContent ? (
<DeleteOutlined onClick={() => setReasonContent("")} style={{ cursor: "pointer" }} />
) : undefined
}
/>
</div>
{customScore !== undefined && (
<div
style={{
padding: "16px",
background:
customScore > 0
? "var(--ant-color-success-bg, #f6ffed)"
: customScore < 0
? "var(--ant-color-error-bg, #fff2f0)"
: "var(--ss-bg-color)",
borderRadius: "8px",
border: `1px solid ${customScore > 0 ? "var(--ant-color-success-border, #b7eb8f)" : customScore < 0 ? "var(--ant-color-error-border, #ffccc7)" : "var(--ss-border-color)"}`,
marginTop: "4px",
}}
>
<div
style={{
fontSize: "13px",
fontWeight: 600,
marginBottom: "4px",
color: "var(--ss-text-main)",
}}
>
{t("home.preview")}
</div>
<div style={{ fontSize: "15px" }}>
{isBatchOperation
? t("home.selectedCount", { count: operationTargets.length })
: selectedStudent?.name}{" "}
<span
style={{
fontWeight: "bold",
color:
customScore > 0
? "var(--ant-color-success, #52c41a)"
: customScore < 0
? "var(--ant-color-error, #ff4d4f)"
: "inherit",
}}
>
{customScore > 0 ? `+${customScore}` : customScore}
</span>{" "}
{t("home.points")}
<span style={{ color: "var(--ss-text-secondary)", marginLeft: "8px" }}>
{reasonContent ? `${t("home.reasonLabel")}${reasonContent}` : t("home.noReason")}
</span>
</div>
</div>
)}
</div> </div>
) )
@@ -3730,7 +3792,7 @@ export const Home: React.FC<HomeProps> = ({
confirmLoading={submitLoading} confirmLoading={submitLoading}
okText={t("home.submitOperation")} okText={t("home.submitOperation")}
cancelText={t("common.cancel")} cancelText={t("common.cancel")}
width={560} width={820}
centered centered
forceRender forceRender
transitionName="" transitionName=""
+3
View File
@@ -511,8 +511,11 @@
"currentScore": "النقاط الحالية", "currentScore": "النقاط الحالية",
"quickOptions": "خيارات سريعة", "quickOptions": "خيارات سريعة",
"adjustPoints": "ضبط النقاط", "adjustPoints": "ضبط النقاط",
"customOperation": "مخصص",
"customPoints": "نقاط مخصصة", "customPoints": "نقاط مخصصة",
"customPointsHint": "أدخل أي قيمة في حقل الإدخال", "customPointsHint": "أدخل أي قيمة في حقل الإدخال",
"noReasonQuickActions": "إجراءات شائعة بدون سبب",
"reasonPresets": "إعدادات السبب المسبقة",
"reason": "السبب", "reason": "السبب",
"reasonPlaceholder": "أدخل سبب تغيير النقاط (اختياري)", "reasonPlaceholder": "أدخل سبب تغيير النقاط (اختياري)",
"preview": "معاينة التغيير", "preview": "معاينة التغيير",
+3
View File
@@ -511,8 +511,11 @@
"currentScore": "Aktuelle Punkte", "currentScore": "Aktuelle Punkte",
"quickOptions": "Schnelloptionen", "quickOptions": "Schnelloptionen",
"adjustPoints": "Punkte anpassen", "adjustPoints": "Punkte anpassen",
"customOperation": "Benutzerdefiniert",
"customPoints": "Benutzerdefinierte Punkte", "customPoints": "Benutzerdefinierte Punkte",
"customPointsHint": "Beliebigen Wert im Eingabefeld eingeben", "customPointsHint": "Beliebigen Wert im Eingabefeld eingeben",
"noReasonQuickActions": "Häufige Aktionen ohne Grund",
"reasonPresets": "Grund-Vorlagen",
"reason": "Grund", "reason": "Grund",
"reasonPlaceholder": "Grund für die Punkteänderung eingeben (optional)", "reasonPlaceholder": "Grund für die Punkteänderung eingeben (optional)",
"preview": "Änderungsvorschau", "preview": "Änderungsvorschau",
+3
View File
@@ -504,8 +504,11 @@
"currentScore": "Current Points", "currentScore": "Current Points",
"quickOptions": "Quick Options", "quickOptions": "Quick Options",
"adjustPoints": "Adjust Points", "adjustPoints": "Adjust Points",
"customOperation": "Custom",
"customPoints": "Custom Points", "customPoints": "Custom Points",
"customPointsHint": "Enter any value in the input box", "customPointsHint": "Enter any value in the input box",
"noReasonQuickActions": "Common No-Reason Actions",
"reasonPresets": "Reason Presets",
"reason": "Reason", "reason": "Reason",
"reasonPlaceholder": "Enter reason for points change (optional)", "reasonPlaceholder": "Enter reason for points change (optional)",
"preview": "Change Preview", "preview": "Change Preview",
+3
View File
@@ -511,8 +511,11 @@
"currentScore": "Puntos actuales", "currentScore": "Puntos actuales",
"quickOptions": "Opciones rápidas", "quickOptions": "Opciones rápidas",
"adjustPoints": "Ajustar puntos", "adjustPoints": "Ajustar puntos",
"customOperation": "Personalizado",
"customPoints": "Puntos personalizados", "customPoints": "Puntos personalizados",
"customPointsHint": "Ingrese cualquier valor en el campo de entrada", "customPointsHint": "Ingrese cualquier valor en el campo de entrada",
"noReasonQuickActions": "Acciones comunes sin razón",
"reasonPresets": "Preajustes de razón",
"reason": "Razón", "reason": "Razón",
"reasonPlaceholder": "Ingrese la razón de la modificación de puntos (opcional)", "reasonPlaceholder": "Ingrese la razón de la modificación de puntos (opcional)",
"preview": "Vista previa de la modificación", "preview": "Vista previa de la modificación",
+3
View File
@@ -511,8 +511,11 @@
"currentScore": "Points actuels", "currentScore": "Points actuels",
"quickOptions": "Options rapides", "quickOptions": "Options rapides",
"adjustPoints": "Ajuster les points", "adjustPoints": "Ajuster les points",
"customOperation": "Personnalise",
"customPoints": "Points personnalisés", "customPoints": "Points personnalisés",
"customPointsHint": "Entrez n'importe quelle valeur dans la zone de saisie", "customPointsHint": "Entrez n'importe quelle valeur dans la zone de saisie",
"noReasonQuickActions": "Actions courantes sans raison",
"reasonPresets": "Préréglages de raison",
"reason": "Raison", "reason": "Raison",
"reasonPlaceholder": "Entrez la raison de la modification des points (facultatif)", "reasonPlaceholder": "Entrez la raison de la modification des points (facultatif)",
"preview": "Aperçu de la modification", "preview": "Aperçu de la modification",
+3
View File
@@ -511,8 +511,11 @@
"currentScore": "現在のポイント", "currentScore": "現在のポイント",
"quickOptions": "クイックオプション", "quickOptions": "クイックオプション",
"adjustPoints": "ポイントを調整", "adjustPoints": "ポイントを調整",
"customOperation": "カスタム",
"customPoints": "カスタムポイント", "customPoints": "カスタムポイント",
"customPointsHint": "入力ボックスで任意の値を入力できます", "customPointsHint": "入力ボックスで任意の値を入力できます",
"noReasonQuickActions": "理由なしのよく使う操作",
"reasonPresets": "理由プリセット",
"reason": "理由", "reason": "理由",
"reasonPlaceholder": "ポイント変動の理由を入力(任意)", "reasonPlaceholder": "ポイント変動の理由を入力(任意)",
"preview": "変更プレビュー", "preview": "変更プレビュー",
+3
View File
@@ -511,8 +511,11 @@
"currentScore": "현재 포인트", "currentScore": "현재 포인트",
"quickOptions": "빠른 옵션", "quickOptions": "빠른 옵션",
"adjustPoints": "포인트 조정", "adjustPoints": "포인트 조정",
"customOperation": "사용자 정의",
"customPoints": "사용자 정의 포인트", "customPoints": "사용자 정의 포인트",
"customPointsHint": "입력란에 원하는 값을 입력하세요", "customPointsHint": "입력란에 원하는 값을 입력하세요",
"noReasonQuickActions": "이유 없는 자주 쓰는 작업",
"reasonPresets": "이유 프리셋",
"reason": "이유", "reason": "이유",
"reasonPlaceholder": "포인트 변경 이유 입력 (선택 사항)", "reasonPlaceholder": "포인트 변경 이유 입력 (선택 사항)",
"preview": "변경 미리보기", "preview": "변경 미리보기",
+3
View File
@@ -511,8 +511,11 @@
"currentScore": "Pontos atuais", "currentScore": "Pontos atuais",
"quickOptions": "Opções rápidas", "quickOptions": "Opções rápidas",
"adjustPoints": "Ajustar pontos", "adjustPoints": "Ajustar pontos",
"customOperation": "Personalizado",
"customPoints": "Pontos personalizados", "customPoints": "Pontos personalizados",
"customPointsHint": "Insira qualquer valor no campo de entrada", "customPointsHint": "Insira qualquer valor no campo de entrada",
"noReasonQuickActions": "Ações comuns sem motivo",
"reasonPresets": "Predefinições de motivo",
"reason": "Motivo", "reason": "Motivo",
"reasonPlaceholder": "Insira o motivo da alteração de pontos (opcional)", "reasonPlaceholder": "Insira o motivo da alteração de pontos (opcional)",
"preview": "Visualização da alteração", "preview": "Visualização da alteração",
+3
View File
@@ -511,8 +511,11 @@
"currentScore": "Текущие баллы", "currentScore": "Текущие баллы",
"quickOptions": "Быстрые опции", "quickOptions": "Быстрые опции",
"adjustPoints": "Настройка баллов", "adjustPoints": "Настройка баллов",
"customOperation": "Пользовательское",
"customPoints": "Пользовательские баллы", "customPoints": "Пользовательские баллы",
"customPointsHint": "Введите любое значение в поле ввода", "customPointsHint": "Введите любое значение в поле ввода",
"noReasonQuickActions": "Частые действия без причины",
"reasonPresets": "Предустановленные причины",
"reason": "Причина", "reason": "Причина",
"reasonPlaceholder": "Введите причину изменения баллов (необязательно)", "reasonPlaceholder": "Введите причину изменения баллов (необязательно)",
"preview": "Предпросмотр изменения", "preview": "Предпросмотр изменения",
+3
View File
@@ -504,8 +504,11 @@
"currentScore": "当前积分", "currentScore": "当前积分",
"quickOptions": "快捷选项", "quickOptions": "快捷选项",
"adjustPoints": "调整分值", "adjustPoints": "调整分值",
"customOperation": "自定义",
"customPoints": "自定义分值", "customPoints": "自定义分值",
"customPointsHint": "可在输入框微调特输入任意分值", "customPointsHint": "可在输入框微调特输入任意分值",
"noReasonQuickActions": "无理由常用操作",
"reasonPresets": "理由预设",
"reason": "操作理由", "reason": "操作理由",
"reasonPlaceholder": "输入加分/扣分的原因(可选)", "reasonPlaceholder": "输入加分/扣分的原因(可选)",
"preview": "变更预览", "preview": "变更预览",