fix(整合): 修复十二项原则布局、格子配色及菜单位置

This commit is contained in:
ittoview
2026-03-18 16:34:49 +00:00
parent 2dbc2a5e0a
commit 2e271a295b
3 changed files with 197 additions and 185 deletions

View File

@@ -18,10 +18,10 @@ const navItems = [
{ path: '/', label: '首页', icon: Home }, { path: '/', label: '首页', icon: Home },
{ path: '/process-matrix', label: '49过程矩阵', icon: LayoutGrid }, { path: '/process-matrix', label: '49过程矩阵', icon: LayoutGrid },
{ path: '/process-practice', label: '过程背诵练习', icon: GraduationCap }, { path: '/process-practice', label: '过程背诵练习', icon: GraduationCap },
{ path: '/principles', label: '十二项原则', icon: BookMarked },
{ path: '/knowledge-areas', label: '知识领域', icon: BookOpen }, { path: '/knowledge-areas', label: '知识领域', icon: BookOpen },
{ path: '/process-groups', label: '过程组', icon: Layers }, { path: '/process-groups', label: '过程组', icon: Layers },
{ path: '/process-graph', label: '过程关系图', icon: Share2 }, { path: '/process-graph', label: '过程关系图', icon: Share2 },
{ path: '/principles', label: '十二项原则', icon: BookMarked },
{ path: '/settings', label: '设置', icon: Settings }, { path: '/settings', label: '设置', icon: Settings },
] ]

View File

@@ -1,5 +1,12 @@
{ {
"changelogEntries": [ "changelogEntries": [
{
"id": "2026-03-18-principles-layout-fix",
"date": "2026-03-18",
"type": "fix",
"title": "修复十二项原则布局sticky结构、格子配色及菜单位置",
"scope": "整合"
},
{ {
"id": "2026-03-18-principles-page", "id": "2026-03-18-principles-page",
"date": "2026-03-18", "date": "2026-03-18",

View File

@@ -413,208 +413,213 @@ export default function PrinciplesPage() {
// ─── 渲染 ───────────────────────────────────────────────────── // ─── 渲染 ─────────────────────────────────────────────────────
return ( return (
<div className="min-h-screen bg-slate-50 px-4 py-8 dark:bg-slate-900"> // 与 ProcessPracticePage 保持相同的 flex-col 布局结构
<div className="mx-auto max-w-7xl space-y-6"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col">
{/* 标题栏 */} {/* 顶部粘性区:标题 + 进度条 */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="sticky top-0 z-20 bg-white dark:bg-gray-800 shadow-sm">
<div> <div className="max-w-7xl mx-auto px-4 py-3">
<p className="text-xs font-semibold uppercase tracking-widest text-blue-600 dark:text-blue-400"> <div className="flex items-center justify-between mb-2">
PMBOK <h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">
</p>
<h1 className="mt-1 text-2xl font-bold text-slate-900 dark:text-white">
</h1> </h1>
</div> <div className="flex items-center gap-3">
{isPracticeMode && (
<div className="flex items-center gap-3"> <>
{isPracticeMode && ( <span className="text-sm text-gray-600 dark:text-gray-400">
<button {answeredCount} / {principles.length}
type="button" </span>
onClick={resetPractice} <button
className="rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-600 transition-colors hover:border-red-300 hover:text-red-600 dark:border-slate-600 dark:text-slate-300 dark:hover:text-red-400" type="button"
> onClick={resetPractice}
className="text-xs px-2 py-1 text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors"
</button> >
)}
<div className="flex rounded-xl bg-slate-100 p-1 dark:bg-slate-800"> </button>
<button </>
type="button" )}
onClick={() => setIsPracticeMode(false)} <div className="flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
className={clsx( <button
'rounded-lg px-4 py-1.5 text-sm font-medium transition-colors', type="button"
!isPracticeMode onClick={() => setIsPracticeMode(false)}
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white' className={clsx(
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200' 'rounded-md px-3 py-1 text-sm font-medium transition-colors',
)} !isPracticeMode
> ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
</button> )}
<button >
type="button"
onClick={() => setIsPracticeMode(true)} </button>
className={clsx( <button
'rounded-lg px-4 py-1.5 text-sm font-medium transition-colors', type="button"
isPracticeMode onClick={() => setIsPracticeMode(true)}
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white' className={clsx(
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200' 'rounded-md px-3 py-1 text-sm font-medium transition-colors',
)} isPracticeMode
> ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
</button> )}
>
</button>
</div>
</div> </div>
</div> </div>
</div> {isPracticeMode && (
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
{/* 进度条 */}
{isPracticeMode && (
<div className="flex items-center gap-3">
<div className="h-2 flex-1 rounded-full bg-slate-200 dark:bg-slate-700">
<div <div
className="h-2 rounded-full bg-blue-500 transition-all duration-300" className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${(answeredCount / principles.length) * 100}%` }} style={{ width: `${(answeredCount / principles.length) * 100}%` }}
/> />
</div> </div>
<span className="whitespace-nowrap text-sm text-slate-500 dark:text-slate-400"> )}
{answeredCount} / {principles.length} </div>
</span> </div>
</div>
)} {/* 中间可滚动区:原则表格 */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-7xl mx-auto py-4 px-4">
<div className="overflow-x-auto">
<table className="min-w-[720px] w-full border-collapse">
<thead>
<tr>
{principleGroups.map((group) => (
<th
key={group.id}
className="bg-blue-700 px-4 py-3 text-center text-base font-bold text-white dark:bg-blue-800"
>
{group.label}
</th>
))}
</tr>
</thead>
<tbody>
{[0, 1, 2, 3].map((rowIdx) => (
<tr key={rowIdx}>
{principleGroups.map((group) => {
const principle = group.items[rowIdx]
if (!principle) {
return (
<td
key={`${group.id}-empty`}
className="border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"
/>
)
}
const isAnswered = !!answeredCells.get(principle.id)
const isCurrent =
isPracticeMode && principle.id === currentPrincipleId
const isShowingAnswer =
showAnswerForCell?.principleId === principle.id
{/* 原则表格 */}
<div className="overflow-x-auto rounded-2xl shadow-sm ring-1 ring-slate-200 dark:ring-slate-700">
<table className="min-w-[840px] w-full border-collapse">
<thead>
<tr>
{principleGroups.map((group) => (
<th
key={group.id}
className="bg-blue-700 px-4 py-3 text-center text-base font-bold text-white dark:bg-blue-800"
>
{group.label}
</th>
))}
</tr>
</thead>
<tbody>
{[0, 1, 2, 3].map((rowIdx) => (
<tr key={rowIdx} className="divide-x divide-slate-100 dark:divide-slate-700">
{principleGroups.map((group) => {
const principle = group.items[rowIdx]
if (!principle) {
return ( return (
<td <td
key={`${group.id}-empty`} key={principle.id}
className="bg-white dark:bg-slate-800" className="border border-gray-200 dark:border-gray-700 p-0 align-top"
/> >
) <div className="flex min-h-[72px]">
} {/* 原则名称列 */}
{isPracticeMode ? (
<button
type="button"
data-principle-id={principle.id}
onClick={() => switchToPrinciple(principle)}
onPointerDown={() => handlePointerDown(principle.id)}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
onPointerCancel={handlePointerUp}
tabIndex={isCurrent ? 0 : -1}
aria-current={isCurrent ? 'step' : undefined}
aria-label={`原则:${principle.name}`}
className={clsx(
'relative flex w-32 shrink-0 items-center justify-center',
'border-r border-2 cursor-pointer transition-all duration-200 focus:outline-none',
'border-r-gray-200 dark:border-r-gray-700',
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',
isCurrent && 'ring-2 ring-blue-500 border-blue-500',
!isAnswered && 'min-h-[40px]'
)}
>
{isAnswered && (
<span className="px-2 text-xs font-semibold text-gray-900 dark:text-gray-100 text-center leading-snug">
{principle.name}
</span>
)}
{isShowingAnswer && (
<div className="absolute inset-0 flex items-center justify-center bg-black/80 rounded z-10">
<span className="text-white text-sm font-medium px-2 text-center">
{showAnswerForCell.answer}
</span>
</div>
)}
</button>
) : (
<div className="flex w-32 shrink-0 items-center justify-center bg-blue-700 px-2 py-3 text-center text-sm font-bold leading-snug text-white dark:bg-blue-800">
{principle.name}
</div>
)}
const isAnswered = !!answeredCells.get(principle.id) {/* 描述列 */}
const isCurrent = <div className="flex flex-1 items-center bg-white px-4 py-3 dark:bg-gray-800">
isPracticeMode && principle.id === currentPrincipleId <p className="text-sm leading-relaxed text-gray-700 dark:text-gray-200">
const isShowingAnswer = {principle.description}
showAnswerForCell?.principleId === principle.id </p>
return (
<td key={principle.id} className="p-0 align-top">
<div className="flex min-h-[80px]">
{/* 原则名称列 */}
{isPracticeMode ? (
<button
type="button"
data-principle-id={principle.id}
onClick={() => switchToPrinciple(principle)}
onPointerDown={() => handlePointerDown(principle.id)}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
onPointerCancel={handlePointerUp}
tabIndex={isCurrent ? 0 : -1}
aria-current={isCurrent ? 'step' : undefined}
className={clsx(
'relative flex w-36 shrink-0 items-center justify-center border-r px-2 py-3 text-center text-sm font-semibold transition-all focus:outline-none',
'border-slate-100 dark:border-slate-700',
isAnswered &&
'bg-green-600 text-white',
!isAnswered &&
!isCurrent &&
'bg-blue-700 text-blue-300 opacity-40 dark:bg-blue-800',
isCurrent &&
!isAnswered &&
'bg-amber-400 text-amber-900 ring-2 ring-inset ring-amber-500',
isCurrent &&
isAnswered &&
'bg-green-600 text-white ring-2 ring-inset ring-amber-500'
)}
>
<span className="leading-snug">
{isAnswered
? principle.name
: isCurrent
? '作答中…'
: '  '}
</span>
{isShowingAnswer && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/80 px-2 text-sm font-bold text-white">
{showAnswerForCell.answer}
</div>
)}
</button>
) : (
<div className="flex w-36 shrink-0 items-center justify-center bg-blue-700 px-2 py-3 text-center text-sm font-bold leading-snug text-white dark:bg-blue-800">
{principle.name}
</div> </div>
)}
{/* 描述列 */}
<div className="flex flex-1 items-center bg-white px-4 py-3 dark:bg-slate-800">
<p className="text-sm leading-relaxed text-slate-700 dark:text-slate-200">
{principle.description}
</p>
</div> </div>
</div> </td>
</td> )
) })}
})} </tr>
</tr> ))}
))} </tbody>
</tbody> </table>
</table>
</div>
{/* 练习输入区 */}
{isPracticeMode && currentPrinciple && (
<div className="rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
<div className="mb-5 border-b border-slate-100 pb-5 dark:border-slate-700">
<p className="text-xs font-semibold uppercase tracking-widest text-blue-600 dark:text-blue-400">
· {currentPrinciple.categoryId === 'people' ? '人' : currentPrinciple.categoryId === 'environment' ? '环境' : '事'}
</p>
<p className="mt-2 text-lg font-semibold text-slate-900 dark:text-white">
{currentPrinciple.description}
</p>
</div>
<div className="flex justify-center">
<InputArea
userInput={userInput}
charStatuses={charStatuses}
isComposing={isComposing}
inputLocked={inputLocked}
lastErrorTimestamp={lastErrorTimestamp}
onInputChange={handleInputChange}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onPaste={handlePaste}
/>
</div>
<p className="mt-5 text-center text-xs text-slate-400 dark:text-slate-500">
/ Ctrl+H · Tab / Shift+Tab · Esc
</p>
</div> </div>
)} </div>
</div> </div>
{/* 底部粘性区:练习输入(与 ProcessPracticePage 结构一致) */}
{isPracticeMode && currentPrinciple && (
<div className="sticky bottom-0 bg-white/60 dark:bg-gray-800/60 backdrop-blur-md border-t border-gray-200 dark:border-gray-700 z-10 pb-8">
<div className="max-w-7xl mx-auto px-6">
<div className="py-3 border-b border-gray-200/50 dark:border-gray-700/50">
<div className="flex items-center justify-center gap-3">
<InputArea
userInput={userInput}
charStatuses={charStatuses}
isComposing={isComposing}
inputLocked={inputLocked}
lastErrorTimestamp={lastErrorTimestamp}
onInputChange={handleInputChange}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onPaste={handlePaste}
/>
<button
type="button"
onClick={() => currentPrincipleId && handleLongPress(currentPrincipleId)}
className="p-2 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
title="查看答案(长按格子也可以)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
</div>
<div className="py-3 text-center text-xs text-gray-500 dark:text-gray-400">
{currentPrinciple.description}
<span className="ml-3 text-gray-400 dark:text-gray-500">
· / Ctrl+H · Tab
</span>
</div>
</div>
</div>
)}
{/* 无障碍播报区 */} {/* 无障碍播报区 */}
<div <div
id="aria-live-region" id="aria-live-region"