fix(练习): 优化移动端布局和样式

- 调整底部固定区域布局,输入框和辅助信息分层显示
- 压缩矩阵格子间距和内边距,适配小屏幕
- 辅助信息区域限高并可滚动,只显示前2个裁剪因素
- 减小字体大小和组件尺寸,提升移动端体验
- 修复表头吸顶位置偏移

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
ittoview
2026-03-01 14:37:01 +00:00
parent cc8dd1e751
commit da04583703
5 changed files with 80 additions and 92 deletions

View File

@@ -1,4 +1,3 @@
import { motion } from 'framer-motion'
import type { CellInfo } from '@/utils/practice' import type { CellInfo } from '@/utils/practice'
import { knowledgeAreaMap, processMap } from '@/data' import { knowledgeAreaMap, processMap } from '@/data'
@@ -13,72 +12,53 @@ export function HintInfo({ currentCell }: HintInfoProps) {
const ka = knowledgeAreaMap.get(currentCell.knowledgeAreaId) const ka = knowledgeAreaMap.get(currentCell.knowledgeAreaId)
if (!ka?.tailoringFactors || ka.tailoringFactors.length === 0) { if (!ka?.tailoringFactors || ka.tailoringFactors.length === 0) {
return ( return (
<motion.div <div className="text-sm text-gray-500 dark:text-gray-400">
id="hint-info"
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 max-w-3xl mx-auto" </div>
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
</h3>
<p className="text-gray-500 dark:text-gray-400"></p>
</motion.div>
) )
} }
return ( return (
<motion.div <div className="space-y-2">
id="hint-info" <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 max-w-3xl mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
</h3> </h3>
<div className="space-y-3"> <div className="space-y-1.5 text-xs">
{ka.tailoringFactors.map((factor, index) => ( {ka.tailoringFactors.slice(0, 2).map((factor, index) => (
<div <div key={index} className="flex gap-2">
key={index} <span className="font-medium text-gray-700 dark:text-gray-300 shrink-0">
className="border-l-4 border-blue-500 pl-4 py-2" {factor.title}
> </span>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1"> <span className="text-gray-600 dark:text-gray-400 line-clamp-1">
{factor.title}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">
{factor.description} {factor.description}
</p> </span>
</div> </div>
))} ))}
{ka.tailoringFactors.length > 2 && (
<div className="text-gray-500 dark:text-gray-500">
+{ka.tailoringFactors.length - 2} ...
</div>
)}
</div> </div>
</motion.div> </div>
) )
} else { } else {
const process = processMap.get(currentCell.processId!) const process = processMap.get(currentCell.processId!)
const purpose = process?.purpose const purpose = process?.purpose
return ( return (
<motion.div <div>
id="hint-info" <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 max-w-3xl mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
</h3> </h3>
{purpose ? ( {purpose ? (
<p className="text-gray-700 dark:text-gray-300 leading-relaxed"> <p className="text-xs text-gray-700 dark:text-gray-300 line-clamp-2">
{purpose} {purpose}
</p> </p>
) : ( ) : (
<p className="text-gray-500 dark:text-gray-400"></p> <p className="text-xs text-gray-500 dark:text-gray-400"></p>
)} )}
</motion.div> </div>
) )
} }
} }

View File

@@ -38,13 +38,13 @@ export function KnowledgeAreaCell({
<motion.div <motion.div
data-cell-id={cellId} data-cell-id={cellId}
className={clsx( className={clsx(
'relative rounded-lg p-4 transition-all duration-200 cursor-pointer', 'relative rounded-lg p-2 transition-all duration-200 cursor-pointer',
'border-2 focus:outline-none', 'border-2 focus:outline-none',
isAnswered isAnswered
? 'bg-white dark:bg-gray-800 border-solid border-gray-300 dark:border-gray-600' ? 'bg-white dark:bg-gray-800 border-solid border-gray-300 dark:border-gray-600'
: 'border-dashed border-gray-300 dark:border-gray-600', : 'border-dashed border-gray-300 dark:border-gray-600',
isFocused && 'ring-2 ring-blue-500 border-blue-500', isFocused && 'ring-2 ring-blue-500 border-blue-500',
!isAnswered && 'min-h-[60px]' !isAnswered && 'min-h-[40px]'
)} )}
onClick={() => onClick(cellId)} onClick={() => onClick(cellId)}
tabIndex={tabIndex} tabIndex={tabIndex}
@@ -58,14 +58,14 @@ export function KnowledgeAreaCell({
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
{isAnswered && cellInfo && ( {isAnswered && cellInfo && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
<span <span
className="inline-block px-2 py-1 rounded text-white font-medium text-sm" className="inline-block px-1.5 py-0.5 rounded text-white font-medium text-xs"
style={{ backgroundColor: ka.color }} style={{ backgroundColor: ka.color }}
> >
{ka.order} {ka.order}
</span> </span>
<span className="text-gray-900 dark:text-gray-100 font-medium"> <span className="text-gray-900 dark:text-gray-100 font-medium text-sm">
{cellInfo.answer} {cellInfo.answer}
</span> </span>
</div> </div>

View File

@@ -28,19 +28,19 @@ export function PracticeMatrix({
const sortedPGs = [...processGroups].sort((a, b) => a.order - b.order) const sortedPGs = [...processGroups].sort((a, b) => a.order - b.order)
return ( return (
<div className="practice-matrix p-4"> <div className="practice-matrix px-2">
{/* 表头 */} {/* 表头 */}
<div <div
className="grid gap-3 mb-4 sticky bg-gray-50 dark:bg-gray-900 py-2 z-10" className="grid gap-2 mb-3 sticky bg-gray-50 dark:bg-gray-900 py-2 z-10"
style={{ style={{
gridTemplateColumns: 'repeat(5, 1fr)', gridTemplateColumns: 'repeat(5, 1fr)',
top: '60px', // 避开顶部进度条 top: '56px', // 避开顶部进度条
}} }}
> >
{sortedPGs.map((pg) => ( {sortedPGs.map((pg) => (
<div <div
key={pg.id} key={pg.id}
className="text-center font-semibold text-sm text-gray-700 dark:text-gray-300" className="text-center font-semibold text-xs text-gray-700 dark:text-gray-300"
> >
{pg.name} {pg.name}
</div> </div>
@@ -55,11 +55,11 @@ export function PracticeMatrix({
return ( return (
<div <div
key={ka.id} key={ka.id}
className="ka-section mb-6" className="ka-section mb-4"
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(5, 1fr)', gridTemplateColumns: 'repeat(5, 1fr)',
gap: '0.75rem', gap: '0.5rem',
}} }}
> >
{/* 知识领域格子横跨5列 */} {/* 知识领域格子横跨5列 */}
@@ -91,14 +91,14 @@ export function PracticeMatrix({
<div <div
key={pg.id} key={pg.id}
className="empty-cell bg-gray-100 dark:bg-gray-800 border border-dashed border-gray-300 dark:border-gray-600 rounded-lg" className="empty-cell bg-gray-100 dark:bg-gray-800 border border-dashed border-gray-300 dark:border-gray-600 rounded-lg"
style={{ minHeight: '80px' }} style={{ minHeight: '50px' }}
aria-hidden="true" aria-hidden="true"
/> />
) )
} }
return ( return (
<div key={pg.id} className="process-column flex flex-col gap-2"> <div key={pg.id} className="process-column flex flex-col gap-1.5">
{processes.map((p) => { {processes.map((p) => {
const processCellId = `process-${p.id}` const processCellId = `process-${p.id}`

View File

@@ -37,13 +37,13 @@ export function ProcessCell({
<motion.div <motion.div
data-cell-id={cellId} data-cell-id={cellId}
className={clsx( className={clsx(
'relative rounded-lg p-3 transition-all duration-200 cursor-pointer', 'relative rounded-lg p-2 transition-all duration-200 cursor-pointer',
'border-2 focus:outline-none', 'border-2 focus:outline-none',
isAnswered isAnswered
? 'bg-white dark:bg-gray-800 border-solid border-gray-300 dark:border-gray-600' ? 'bg-white dark:bg-gray-800 border-solid border-gray-300 dark:border-gray-600'
: 'border-dashed border-gray-300 dark:border-gray-600', : 'border-dashed border-gray-300 dark:border-gray-600',
isFocused && 'ring-2 ring-blue-500 border-blue-500', isFocused && 'ring-2 ring-blue-500 border-blue-500',
!isAnswered && 'min-h-[60px]' !isAnswered && 'min-h-[40px]'
)} )}
onClick={() => onClick(cellId)} onClick={() => onClick(cellId)}
tabIndex={tabIndex} tabIndex={tabIndex}
@@ -57,14 +57,14 @@ export function ProcessCell({
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
> >
{isAnswered && ( {isAnswered && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
<span <span
className="inline-block px-1.5 py-0.5 rounded text-white font-medium shrink-0" className="inline-block px-1 py-0.5 rounded text-white font-medium shrink-0"
style={{ backgroundColor: ka?.color, fontSize: '10px' }} style={{ backgroundColor: ka?.color, fontSize: '9px' }}
> >
{process.code} {process.code}
</span> </span>
<span className="text-gray-900 dark:text-gray-100 text-sm"> <span className="text-gray-900 dark:text-gray-100 text-xs">
{process.name} {process.name}
</span> </span>
</div> </div>

View File

@@ -297,38 +297,46 @@ export default function ProcessPracticePage() {
</div> </div>
</div> </div>
{/* 矩阵区域 */} {/* 主内容区域 */}
<div className="max-w-7xl mx-auto py-8"> <div className="pb-80">
<PracticeMatrix {/* 矩阵区域 */}
cellSequence={cellSequence} <div className="max-w-7xl mx-auto py-4">
answeredCells={answeredCells} <PracticeMatrix
currentCellId={currentCellId} cellSequence={cellSequence}
showAnswerForCell={showAnswerForCell} answeredCells={answeredCells}
onLongPress={handleLongPress} currentCellId={currentCellId}
onLongPressEnd={handleLongPressEnd} showAnswerForCell={showAnswerForCell}
onCellClick={handleCellClick} onLongPress={handleLongPress}
getCellTabIndex={getCellTabIndex} onLongPressEnd={handleLongPressEnd}
/> onCellClick={handleCellClick}
getCellTabIndex={getCellTabIndex}
/>
</div>
</div> </div>
{/* 输入区域(固定在屏幕中下部) */} {/* 底部固定区域 */}
<div className="fixed bottom-32 left-0 right-0 z-10"> <div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 z-10">
<InputArea <div className="max-w-7xl mx-auto">
userInput={userInput} {/* 输入区域 */}
charStatuses={charStatuses} <div className="py-4 border-b border-gray-200 dark:border-gray-700">
isComposing={isComposing} <InputArea
inputLocked={inputLocked} userInput={userInput}
lastErrorTimestamp={lastErrorTimestamp} charStatuses={charStatuses}
onInputChange={handleInputChange} isComposing={isComposing}
onCompositionStart={handleCompositionStart} inputLocked={inputLocked}
onCompositionEnd={handleCompositionEnd} lastErrorTimestamp={lastErrorTimestamp}
onPaste={handlePaste} onInputChange={handleInputChange}
/> onCompositionStart={handleCompositionStart}
</div> onCompositionEnd={handleCompositionEnd}
onPaste={handlePaste}
/>
</div>
{/* 辅助信息区域(固定在底部) */} {/* 辅助信息区域 */}
<div className="fixed bottom-0 left-0 right-0 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 py-4 z-10"> <div className="py-3 px-4 max-h-32 overflow-y-auto">
<HintInfo currentCell={currentCell} /> <HintInfo currentCell={currentCell} />
</div>
</div>
</div> </div>
{/* 无障碍通告区域 */} {/* 无障碍通告区域 */}