fix(练习): 优化移动端布局和样式
- 调整底部固定区域布局,输入框和辅助信息分层显示 - 压缩矩阵格子间距和内边距,适配小屏幕 - 辅助信息区域限高并可滚动,只显示前2个裁剪因素 - 减小字体大小和组件尺寸,提升移动端体验 - 修复表头吸顶位置偏移 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
{/* 无障碍通告区域 */}
|
{/* 无障碍通告区域 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user