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,39 +413,40 @@ 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"> <div className="flex items-center gap-3">
{isPracticeMode && ( {isPracticeMode && (
<>
<span className="text-sm text-gray-600 dark:text-gray-400">
{answeredCount} / {principles.length}
</span>
<button <button
type="button" type="button"
onClick={resetPractice} onClick={resetPractice}
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" 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> </button>
</>
)} )}
<div className="flex rounded-xl bg-slate-100 p-1 dark:bg-slate-800"> <div className="flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
<button <button
type="button" type="button"
onClick={() => setIsPracticeMode(false)} onClick={() => setIsPracticeMode(false)}
className={clsx( className={clsx(
'rounded-lg px-4 py-1.5 text-sm font-medium transition-colors', 'rounded-md px-3 py-1 text-sm font-medium transition-colors',
!isPracticeMode !isPracticeMode
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
)} )}
> >
@@ -454,10 +455,10 @@ export default function PrinciplesPage() {
type="button" type="button"
onClick={() => setIsPracticeMode(true)} onClick={() => setIsPracticeMode(true)}
className={clsx( className={clsx(
'rounded-lg px-4 py-1.5 text-sm font-medium transition-colors', 'rounded-md px-3 py-1 text-sm font-medium transition-colors',
isPracticeMode isPracticeMode
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
)} )}
> >
@@ -465,25 +466,22 @@ export default function PrinciplesPage() {
</div> </div>
</div> </div>
</div> </div>
{/* 进度条 */}
{isPracticeMode && ( {isPracticeMode && (
<div className="flex items-center gap-3"> <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<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}
</span>
</div>
)} )}
</div>
</div>
{/* 原则表格 */} {/* 中间可滚动区:原则表格 */}
<div className="overflow-x-auto rounded-2xl shadow-sm ring-1 ring-slate-200 dark:ring-slate-700"> <div className="flex-1 overflow-y-auto">
<table className="min-w-[840px] w-full border-collapse"> <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> <thead>
<tr> <tr>
{principleGroups.map((group) => ( {principleGroups.map((group) => (
@@ -498,14 +496,14 @@ export default function PrinciplesPage() {
</thead> </thead>
<tbody> <tbody>
{[0, 1, 2, 3].map((rowIdx) => ( {[0, 1, 2, 3].map((rowIdx) => (
<tr key={rowIdx} className="divide-x divide-slate-100 dark:divide-slate-700"> <tr key={rowIdx}>
{principleGroups.map((group) => { {principleGroups.map((group) => {
const principle = group.items[rowIdx] const principle = group.items[rowIdx]
if (!principle) { if (!principle) {
return ( return (
<td <td
key={`${group.id}-empty`} key={`${group.id}-empty`}
className="bg-white dark:bg-slate-800" className="border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"
/> />
) )
} }
@@ -517,8 +515,11 @@ export default function PrinciplesPage() {
showAnswerForCell?.principleId === principle.id showAnswerForCell?.principleId === principle.id
return ( return (
<td key={principle.id} className="p-0 align-top"> <td
<div className="flex min-h-[80px]"> key={principle.id}
className="border border-gray-200 dark:border-gray-700 p-0 align-top"
>
<div className="flex min-h-[72px]">
{/* 原则名称列 */} {/* 原则名称列 */}
{isPracticeMode ? ( {isPracticeMode ? (
<button <button
@@ -531,44 +532,40 @@ export default function PrinciplesPage() {
onPointerCancel={handlePointerUp} onPointerCancel={handlePointerUp}
tabIndex={isCurrent ? 0 : -1} tabIndex={isCurrent ? 0 : -1}
aria-current={isCurrent ? 'step' : undefined} aria-current={isCurrent ? 'step' : undefined}
aria-label={`原则:${principle.name}`}
className={clsx( 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', 'relative flex w-32 shrink-0 items-center justify-center',
'border-slate-100 dark:border-slate-700', 'border-r border-2 cursor-pointer transition-all duration-200 focus:outline-none',
isAnswered && 'border-r-gray-200 dark:border-r-gray-700',
'bg-green-600 text-white', isAnswered
!isAnswered && ? 'bg-white dark:bg-gray-800 border-solid border-gray-300 dark:border-gray-600'
!isCurrent && : 'border-dashed border-gray-300 dark:border-gray-600',
'bg-blue-700 text-blue-300 opacity-40 dark:bg-blue-800', isCurrent && 'ring-2 ring-blue-500 border-blue-500',
isCurrent && !isAnswered && 'min-h-[40px]'
!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 && (
{isAnswered <span className="px-2 text-xs font-semibold text-gray-900 dark:text-gray-100 text-center leading-snug">
? principle.name {principle.name}
: isCurrent
? '作答中…'
: '  '}
</span> </span>
)}
{isShowingAnswer && ( {isShowingAnswer && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/80 px-2 text-sm font-bold text-white"> <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} {showAnswerForCell.answer}
</span>
</div> </div>
)} )}
</button> </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"> <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} {principle.name}
</div> </div>
)} )}
{/* 描述列 */} {/* 描述列 */}
<div className="flex flex-1 items-center bg-white px-4 py-3 dark:bg-slate-800"> <div className="flex flex-1 items-center bg-white px-4 py-3 dark:bg-gray-800">
<p className="text-sm leading-relaxed text-slate-700 dark:text-slate-200"> <p className="text-sm leading-relaxed text-gray-700 dark:text-gray-200">
{principle.description} {principle.description}
</p> </p>
</div> </div>
@@ -581,20 +578,15 @@ export default function PrinciplesPage() {
</tbody> </tbody>
</table> </table>
</div> </div>
</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>
<div className="flex justify-center"> {/* 底部粘性区:练习输入(与 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 <InputArea
userInput={userInput} userInput={userInput}
charStatuses={charStatuses} charStatuses={charStatuses}
@@ -606,14 +598,27 @@ export default function PrinciplesPage() {
onCompositionEnd={handleCompositionEnd} onCompositionEnd={handleCompositionEnd}
onPaste={handlePaste} 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>
<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