mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 20:29:03 +08:00
首页新增正方形网格布局并支持连续展示
This commit is contained in:
+200
-1
@@ -24,6 +24,7 @@ interface reason {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SortType = "alphabet" | "surname" | "score"
|
type SortType = "alphabet" | "surname" | "score"
|
||||||
|
type LayoutType = "grouped" | "squareGrid"
|
||||||
type SearchKeyboardLayout = "t9" | "qwerty26"
|
type SearchKeyboardLayout = "t9" | "qwerty26"
|
||||||
|
|
||||||
const T9_KEY_MAP: Record<string, string> = {
|
const T9_KEY_MAP: Record<string, string> = {
|
||||||
@@ -66,6 +67,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
const [reasons, setReasons] = useState<reason[]>([])
|
const [reasons, setReasons] = useState<reason[]>([])
|
||||||
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 [searchKeyword, setSearchKeyword] = useState("")
|
const [searchKeyword, setSearchKeyword] = useState("")
|
||||||
const [showPinyinKeyboard, setShowPinyinKeyboard] = useState(false)
|
const [showPinyinKeyboard, setShowPinyinKeyboard] = useState(false)
|
||||||
const [searchKeyboardLayout, setSearchKeyboardLayout] = useState<SearchKeyboardLayout>("qwerty26")
|
const [searchKeyboardLayout, setSearchKeyboardLayout] = useState<SearchKeyboardLayout>("qwerty26")
|
||||||
@@ -327,6 +329,10 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
}, [students, searchKeyword, sortType, matchStudentName])
|
}, [students, searchKeyword, sortType, matchStudentName])
|
||||||
|
|
||||||
const groupedStudents = useMemo(() => {
|
const groupedStudents = useMemo(() => {
|
||||||
|
if (layoutType === "squareGrid") {
|
||||||
|
return [{ key: "all", students: sortedStudents }]
|
||||||
|
}
|
||||||
|
|
||||||
if (sortType === "score" || (sortType === "alphabet" && searchKeyword)) {
|
if (sortType === "score" || (sortType === "alphabet" && searchKeyword)) {
|
||||||
return [{ key: "all", students: sortedStudents }]
|
return [{ key: "all", students: sortedStudents }]
|
||||||
}
|
}
|
||||||
@@ -341,7 +347,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
return Object.entries(groups)
|
return Object.entries(groups)
|
||||||
.sort(([a], [b]) => a.localeCompare(b, "zh-CN"))
|
.sort(([a], [b]) => a.localeCompare(b, "zh-CN"))
|
||||||
.map(([key, students]) => ({ key, students }))
|
.map(([key, students]) => ({ key, students }))
|
||||||
}, [sortedStudents, sortType, searchKeyword])
|
}, [sortedStudents, sortType, searchKeyword, layoutType])
|
||||||
|
|
||||||
const groupedReasons = useMemo(() => {
|
const groupedReasons = useMemo(() => {
|
||||||
const groups: Record<string, reason[]> = {}
|
const groups: Record<string, reason[]> = {}
|
||||||
@@ -820,7 +826,190 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderStudentSquareCard = (student: student, index: number) => {
|
||||||
|
const avatarText = getDisplayText(student.name)
|
||||||
|
const avatarColor = getAvatarColor(student.name)
|
||||||
|
const isQuickActionMode = quickActionStudentId === student.id
|
||||||
|
|
||||||
|
let rankBadge: string | null = null
|
||||||
|
if (sortType === "score" && !searchKeyword) {
|
||||||
|
if (index === 0) rankBadge = "🥇"
|
||||||
|
else if (index === 1) rankBadge = "🥈"
|
||||||
|
else if (index === 2) rankBadge = "🥉"
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={student.id}
|
||||||
|
data-student-quick-card="true"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (suppressClickRef.current) {
|
||||||
|
suppressClickRef.current = false
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openOperation(student)
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.button !== 0) return
|
||||||
|
startLongPress(student)
|
||||||
|
}}
|
||||||
|
onMouseUp={cancelLongPress}
|
||||||
|
onMouseLeave={cancelLongPress}
|
||||||
|
onTouchStart={() => startLongPress(student)}
|
||||||
|
onTouchEnd={cancelLongPress}
|
||||||
|
onTouchCancel={cancelLongPress}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
openQuickAction(student)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
position: "relative",
|
||||||
|
aspectRatio: "1 / 1",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "var(--ss-card-bg)",
|
||||||
|
transition: "all 0.2s cubic-bezier(0.38, 0, 0.24, 1)",
|
||||||
|
border: isQuickActionMode
|
||||||
|
? "1px solid var(--ant-color-primary, #1677ff)"
|
||||||
|
: "1px solid var(--ss-border-color)",
|
||||||
|
overflow: "visible",
|
||||||
|
boxShadow: isQuickActionMode ? "0 8px 18px rgba(22, 119, 255, 0.18)" : undefined,
|
||||||
|
}}
|
||||||
|
styles={{ body: { height: "100%", padding: "10px" } }}
|
||||||
|
>
|
||||||
|
{rankBadge && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-10px",
|
||||||
|
left: "-10px",
|
||||||
|
fontSize: "24px",
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rankBadge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "8px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{student.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={student.avatarUrl}
|
||||||
|
alt={student.name}
|
||||||
|
style={{
|
||||||
|
width: "52px",
|
||||||
|
height: "52px",
|
||||||
|
borderRadius: "14px",
|
||||||
|
objectFit: "cover",
|
||||||
|
flexShrink: 0,
|
||||||
|
boxShadow: `0 4px 10px ${avatarColor}40`,
|
||||||
|
border: "1px solid var(--ss-border-color)",
|
||||||
|
backgroundColor: "var(--ss-bg-color)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "52px",
|
||||||
|
height: "52px",
|
||||||
|
borderRadius: "14px",
|
||||||
|
backgroundColor: avatarColor,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "white",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: avatarText.length > 1 ? "16px" : "20px",
|
||||||
|
flexShrink: 0,
|
||||||
|
boxShadow: `0 4px 10px ${avatarColor}40`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{avatarText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "var(--ss-text-main)",
|
||||||
|
maxWidth: "100%",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{student.name}
|
||||||
|
</div>
|
||||||
|
{isQuickActionMode ? (
|
||||||
|
<Space size={6}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleQuickAdjust(student, 1)
|
||||||
|
}}
|
||||||
|
style={{ minWidth: "44px", height: "28px", borderRadius: "14px", paddingInline: "8px" }}
|
||||||
|
>
|
||||||
|
+1
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleQuickAdjust(student, -1)
|
||||||
|
}}
|
||||||
|
style={{ minWidth: "44px", height: "28px", borderRadius: "14px", paddingInline: "8px" }}
|
||||||
|
>
|
||||||
|
-1
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Tag
|
||||||
|
color={student.score > 0 ? "success" : student.score < 0 ? "error" : "default"}
|
||||||
|
style={{ fontWeight: "bold", marginInlineEnd: 0 }}
|
||||||
|
>
|
||||||
|
{student.score > 0 ? `+${student.score}` : student.score}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const renderGroupedCards = () => {
|
const renderGroupedCards = () => {
|
||||||
|
if (layoutType === "squareGrid") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(118px, 1fr))",
|
||||||
|
gap: isPortraitMode ? "10px" : "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sortedStudents.map((student, idx) => renderStudentSquareCard(student, idx))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return groupedStudents.map((group) => (
|
return groupedStudents.map((group) => (
|
||||||
<div
|
<div
|
||||||
key={group.key}
|
key={group.key}
|
||||||
@@ -1064,6 +1253,7 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
|
|
||||||
const shouldShowQuickNav =
|
const shouldShowQuickNav =
|
||||||
groupedStudents.length > 1 &&
|
groupedStudents.length > 1 &&
|
||||||
|
layoutType !== "squareGrid" &&
|
||||||
sortType !== "score" &&
|
sortType !== "score" &&
|
||||||
!(sortType === "alphabet" && searchKeyword)
|
!(sortType === "alphabet" && searchKeyword)
|
||||||
|
|
||||||
@@ -1616,6 +1806,15 @@ export const Home: React.FC<HomeProps> = ({ canEdit, isPortraitMode = false }) =
|
|||||||
{ value: "score", label: t("home.sortBy.score") },
|
{ value: "score", label: t("home.sortBy.score") },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
value={layoutType}
|
||||||
|
onChange={(v) => setLayoutType(v as LayoutType)}
|
||||||
|
style={{ width: "140px" }}
|
||||||
|
options={[
|
||||||
|
{ value: "grouped", label: t("home.layoutBy.grouped") },
|
||||||
|
{ value: "squareGrid", label: t("home.layoutBy.squareGrid") },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,10 @@
|
|||||||
"surname": "Surname Group",
|
"surname": "Surname Group",
|
||||||
"score": "Points Rank"
|
"score": "Points Rank"
|
||||||
},
|
},
|
||||||
|
"layoutBy": {
|
||||||
|
"grouped": "Grouped List",
|
||||||
|
"squareGrid": "Square Grid"
|
||||||
|
},
|
||||||
"noStudents": "No student data, please add in Student Management",
|
"noStudents": "No student data, please add in Student Management",
|
||||||
"noMatch": "No matching students found",
|
"noMatch": "No matching students found",
|
||||||
"clearSearch": "Clear search",
|
"clearSearch": "Clear search",
|
||||||
|
|||||||
@@ -335,6 +335,10 @@
|
|||||||
"surname": "姓氏分组",
|
"surname": "姓氏分组",
|
||||||
"score": "积分排行"
|
"score": "积分排行"
|
||||||
},
|
},
|
||||||
|
"layoutBy": {
|
||||||
|
"grouped": "分组列表",
|
||||||
|
"squareGrid": "正方形网格"
|
||||||
|
},
|
||||||
"noStudents": "暂无学生数据,请前往学生管理添加",
|
"noStudents": "暂无学生数据,请前往学生管理添加",
|
||||||
"noMatch": "未找到匹配的学生",
|
"noMatch": "未找到匹配的学生",
|
||||||
"clearSearch": "清除搜索",
|
"clearSearch": "清除搜索",
|
||||||
|
|||||||
Reference in New Issue
Block a user