fix(练习): 重构布局和修复需求问题
- 修复知识领域显示完整名称(如"项目整合管理") - 改用 table 布局,参考 process-matrix 样式 - 输入区域添加半透明背景(bg-white/80 + backdrop-blur-md) - 辅助信息不再省略,显示完整内容 - 删除不需要的 KnowledgeAreaCell 组件 - 知识领域显示在左侧列,过程显示在单元格内 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -23,22 +23,17 @@ export function HintInfo({ currentCell }: HintInfoProps) {
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
敏捷裁剪因素
|
||||
</h3>
|
||||
<div className="space-y-1.5 text-xs">
|
||||
{ka.tailoringFactors.slice(0, 2).map((factor, index) => (
|
||||
<div className="space-y-2 text-xs">
|
||||
{ka.tailoringFactors.map((factor, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300 shrink-0">
|
||||
{factor.title}:
|
||||
</span>
|
||||
<span className="text-gray-600 dark:text-gray-400 line-clamp-1">
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
{factor.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{ka.tailoringFactors.length > 2 && (
|
||||
<div className="text-gray-500 dark:text-gray-500">
|
||||
+{ka.tailoringFactors.length - 2} 个因素...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -52,7 +47,7 @@ export function HintInfo({ currentCell }: HintInfoProps) {
|
||||
主要作用
|
||||
</h3>
|
||||
{purpose ? (
|
||||
<p className="text-xs text-gray-700 dark:text-gray-300 line-clamp-2">
|
||||
<p className="text-xs text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{purpose}
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import clsx from 'clsx'
|
||||
import type { KnowledgeArea } from '@/types/itto'
|
||||
import type { CellInfo } from '@/utils/practice'
|
||||
import { useLongPress } from '@/hooks/useLongPress'
|
||||
|
||||
interface KnowledgeAreaCellProps {
|
||||
ka: KnowledgeArea
|
||||
cellInfo?: CellInfo
|
||||
isAnswered: boolean
|
||||
isFocused: boolean
|
||||
showAnswer?: string | null
|
||||
onLongPress: (cellId: string) => void
|
||||
onLongPressEnd: () => void
|
||||
onClick: (cellId: string) => void
|
||||
tabIndex: number
|
||||
}
|
||||
|
||||
export function KnowledgeAreaCell({
|
||||
ka,
|
||||
cellInfo,
|
||||
isAnswered,
|
||||
isFocused,
|
||||
showAnswer,
|
||||
onLongPress,
|
||||
onLongPressEnd,
|
||||
onClick,
|
||||
tabIndex,
|
||||
}: KnowledgeAreaCellProps) {
|
||||
const cellId = `ka-${ka.id}`
|
||||
|
||||
const longPressHandlers = useLongPress(cellId, {
|
||||
onLongPress,
|
||||
onLongPressEnd,
|
||||
})
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
data-cell-id={cellId}
|
||||
className={clsx(
|
||||
'relative rounded-lg p-2 transition-all duration-200 cursor-pointer',
|
||||
'border-2 focus:outline-none',
|
||||
isAnswered
|
||||
? 'bg-white dark:bg-gray-800 border-solid 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',
|
||||
!isAnswered && 'min-h-[40px]'
|
||||
)}
|
||||
onClick={() => onClick(cellId)}
|
||||
tabIndex={tabIndex}
|
||||
role="button"
|
||||
aria-label={`知识领域:${ka.name}`}
|
||||
aria-describedby="hint-info"
|
||||
aria-current={isFocused ? 'true' : undefined}
|
||||
{...longPressHandlers}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{isAnswered && cellInfo && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="inline-block px-1.5 py-0.5 rounded text-white font-medium text-xs"
|
||||
style={{ backgroundColor: ka.color }}
|
||||
>
|
||||
{ka.order}
|
||||
</span>
|
||||
<span className="text-gray-900 dark:text-gray-100 font-medium text-sm">
|
||||
{cellInfo.answer}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 长按显示答案 */}
|
||||
{showAnswer && (
|
||||
<motion.div
|
||||
className="absolute inset-0 flex items-center justify-center bg-black/80 rounded-lg z-10"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<span className="text-white text-lg font-medium">{showAnswer}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { knowledgeAreas, processGroups } from '@/data'
|
||||
import { getProcessesByKaAndPg, type CellInfo } from '@/utils/practice'
|
||||
import { KnowledgeAreaCell } from './KnowledgeAreaCell'
|
||||
import { getProcessesByKaAndPg } from '@/utils/practice'
|
||||
import { ProcessCell } from './ProcessCell'
|
||||
|
||||
interface PracticeMatrixProps {
|
||||
cellSequence: CellInfo[]
|
||||
answeredCells: Map<string, boolean>
|
||||
currentCellId: string | null
|
||||
showAnswerForCell: { cellId: string; answer: string; expiresAt: number } | null
|
||||
@@ -15,7 +13,6 @@ interface PracticeMatrixProps {
|
||||
}
|
||||
|
||||
export function PracticeMatrix({
|
||||
cellSequence,
|
||||
answeredCells,
|
||||
currentCellId,
|
||||
showAnswerForCell,
|
||||
@@ -28,104 +25,90 @@ export function PracticeMatrix({
|
||||
const sortedPGs = [...processGroups].sort((a, b) => a.order - b.order)
|
||||
|
||||
return (
|
||||
<div className="practice-matrix px-2">
|
||||
{/* 表头 */}
|
||||
<div
|
||||
className="grid gap-2 mb-3 sticky bg-gray-50 dark:bg-gray-900 py-2 z-10"
|
||||
style={{
|
||||
gridTemplateColumns: 'repeat(5, 1fr)',
|
||||
top: '56px', // 避开顶部进度条
|
||||
}}
|
||||
>
|
||||
{sortedPGs.map((pg) => (
|
||||
<div
|
||||
key={pg.id}
|
||||
className="text-center font-semibold text-xs text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{pg.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse min-w-[900px]">
|
||||
{/* 表头:过程组 */}
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="sticky left-0 z-10 bg-gray-100 dark:bg-gray-800 p-2 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 min-w-[120px]">
|
||||
知识领域 / 过程组
|
||||
</th>
|
||||
{sortedPGs.map((pg) => (
|
||||
<th
|
||||
key={pg.id}
|
||||
className="p-2 text-center text-xs font-semibold text-white border border-gray-200 dark:border-gray-700 min-w-[120px]"
|
||||
style={{ backgroundColor: pg.color }}
|
||||
>
|
||||
{pg.name}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* 矩阵内容 */}
|
||||
{sortedKAs.map((ka) => {
|
||||
const kaCellId = `ka-${ka.id}`
|
||||
const kaCellInfo = cellSequence.find((c) => c.id === kaCellId)
|
||||
{/* 表体:知识领域 × 过程 */}
|
||||
<tbody>
|
||||
{sortedKAs.map((ka) => {
|
||||
return (
|
||||
<tr key={ka.id}>
|
||||
{/* 知识领域名称 */}
|
||||
<td
|
||||
className="sticky left-0 z-10 p-2 border border-gray-200 dark:border-gray-700 font-medium"
|
||||
style={{
|
||||
backgroundColor: `${ka.color}15`,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: ka.color,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-xs" style={{ color: ka.color }}>
|
||||
{ka.order}
|
||||
</span>
|
||||
<span className="text-xs text-gray-900 dark:text-white">
|
||||
{ka.name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ka.id}
|
||||
className="ka-section mb-4"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(5, 1fr)',
|
||||
gap: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{/* 知识领域格子(横跨5列) */}
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<KnowledgeAreaCell
|
||||
ka={ka}
|
||||
cellInfo={kaCellInfo}
|
||||
isAnswered={answeredCells.get(kaCellId) || false}
|
||||
isFocused={currentCellId === kaCellId}
|
||||
showAnswer={
|
||||
showAnswerForCell?.cellId === kaCellId
|
||||
? showAnswerForCell.answer
|
||||
: null
|
||||
}
|
||||
onLongPress={onLongPress}
|
||||
onLongPressEnd={onLongPressEnd}
|
||||
onClick={onCellClick}
|
||||
tabIndex={getCellTabIndex(kaCellId)}
|
||||
/>
|
||||
</div>
|
||||
{/* 每个过程组的单元格 */}
|
||||
{sortedPGs.map((pg) => {
|
||||
const processes = getProcessesByKaAndPg(ka.id, pg.id)
|
||||
|
||||
{/* 过程格子(5个过程组列) */}
|
||||
{sortedPGs.map((pg) => {
|
||||
const processes = getProcessesByKaAndPg(ka.id, pg.id)
|
||||
return (
|
||||
<td
|
||||
key={pg.id}
|
||||
className="p-2 border border-gray-200 dark:border-gray-700 align-top bg-white dark:bg-gray-800"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{processes.map((p) => {
|
||||
const processCellId = `process-${p.id}`
|
||||
|
||||
if (processes.length === 0) {
|
||||
// 空单元格:置灰,不可聚焦
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
style={{ minHeight: '50px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={pg.id} className="process-column flex flex-col gap-1.5">
|
||||
{processes.map((p) => {
|
||||
const processCellId = `process-${p.id}`
|
||||
|
||||
return (
|
||||
<ProcessCell
|
||||
key={p.id}
|
||||
process={p}
|
||||
isAnswered={answeredCells.get(processCellId) || false}
|
||||
isFocused={currentCellId === processCellId}
|
||||
showAnswer={
|
||||
showAnswerForCell?.cellId === processCellId
|
||||
? showAnswerForCell.answer
|
||||
: null
|
||||
}
|
||||
onLongPress={onLongPress}
|
||||
onLongPressEnd={onLongPressEnd}
|
||||
onClick={onCellClick}
|
||||
tabIndex={getCellTabIndex(processCellId)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
return (
|
||||
<ProcessCell
|
||||
key={p.id}
|
||||
process={p}
|
||||
isAnswered={answeredCells.get(processCellId) || false}
|
||||
isFocused={currentCellId === processCellId}
|
||||
showAnswer={
|
||||
showAnswerForCell?.cellId === processCellId
|
||||
? showAnswerForCell.answer
|
||||
: null
|
||||
}
|
||||
onLongPress={onLongPress}
|
||||
onLongPressEnd={onLongPressEnd}
|
||||
onClick={onCellClick}
|
||||
tabIndex={getCellTabIndex(processCellId)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -302,7 +302,6 @@ export default function ProcessPracticePage() {
|
||||
{/* 矩阵区域 */}
|
||||
<div className="max-w-7xl mx-auto py-4">
|
||||
<PracticeMatrix
|
||||
cellSequence={cellSequence}
|
||||
answeredCells={answeredCells}
|
||||
currentCellId={currentCellId}
|
||||
showAnswerForCell={showAnswerForCell}
|
||||
@@ -315,10 +314,10 @@ export default function ProcessPracticePage() {
|
||||
</div>
|
||||
|
||||
{/* 底部固定区域 */}
|
||||
<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">
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-md border-t border-gray-200 dark:border-gray-700 z-10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* 输入区域 */}
|
||||
<div className="py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="py-4 border-b border-gray-200/50 dark:border-gray-700/50">
|
||||
<InputArea
|
||||
userInput={userInput}
|
||||
charStatuses={charStatuses}
|
||||
@@ -333,7 +332,7 @@ export default function ProcessPracticePage() {
|
||||
</div>
|
||||
|
||||
{/* 辅助信息区域 */}
|
||||
<div className="py-3 px-4 max-h-32 overflow-y-auto">
|
||||
<div className="py-3 px-4 max-h-40 overflow-y-auto">
|
||||
<HintInfo currentCell={currentCell} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,8 +56,8 @@ export function generateCellSequence(): CellInfo[] {
|
||||
id: `ka-${ka.id}`,
|
||||
type: 'knowledge-area',
|
||||
knowledgeAreaId: ka.id,
|
||||
answer: ka.name.replace('项目', ''), // "项目整合管理" -> "整合管理"
|
||||
normalizedAnswer: normalizeAnswer(ka.name, true), // 知识领域去除"项目"
|
||||
answer: ka.name, // 保留完整名称 "项目整合管理"
|
||||
normalizedAnswer: normalizeAnswer(ka.name, true), // 标准化时去除"项目"
|
||||
order: order++,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user