2 次代码提交 866412f336 ... 24e0d014e4

作者 SHA1 备注 提交日期
  bob 24e0d014e4 ui: simplify course and management interfaces 1 月之前
  bob f47dbb3d35 docs: record production deployment 866412f 1 月之前
共有 7 个文件被更改,包括 124 次插入 和 122 次删除
  1. 26 0
      AGENTS.md
  2. 5 3
      src/pages/CourseWorkspace.tsx
  3. 13 71
      src/pages/Dashboard.tsx
  4. 7 5
      src/pages/DigitalInstructorsPage.tsx
  5. 24 21
      src/pages/OrganizationManage.tsx
  6. 14 22
      src/pages/admin/AdminLayout.tsx
  7. 35 0
      src/styles.css

+ 26 - 0
AGENTS.md

@@ -717,3 +717,29 @@
 - 未确认与风险:未在生产库或公网验证本次组合后的代码;课程内容和制作操作仍需真实平台管理员账号端到端验收。
 - 后续修改提示:修改课程编辑、快照文件或制作队列时,继续核对成员与管理员读取同一课程记录、文件鉴权以及编辑与结单并发状态。
 - 关联:C-20260924-04、C-20260924-06。
+
+### C-20260924-09|部署制作工作区编辑至生产服务器
+
+- 日期:2026-09-24
+- 类型:部署
+- 需求与原因:将已推送的课程制作工作区编辑功能部署到生产服务器。
+- 修改范围:部署提交 `866412f` 对应镜像到 `47.93.193.127:/opt/xinghen`,仅重建 `xinghen-app-1`;部署前数据库备份保存为 `/opt/xinghen/.deploy/pre-redeploy-866412f-20260924113445.dump`。未修改反向代理配置;PostgreSQL 与上传卷保留。
+- 保留/新增判断:遵守 D-007 至 D-010。生产数据库迁移由应用启动逻辑执行;数据库卷 `xinghen_postgres_data` 与上传卷 `xinghen_uploads_data` 不重建。
+- 功能确认状态:部分确认
+- 验证证据:`pnpm run check`、`pnpm run build`、`docker buildx build --platform linux/amd64 --pull --load` 通过;生产应用容器运行镜像 SHA `0ff8774dd3195b561027c6907fcfcee91d5cf296d384a7050b941332acd2c849`,状态为 running;PostgreSQL 健康,上传卷仍挂载到 `/app/uploads`。`https://xinghen.work/api/health` 返回 200 与 `{"ok":true}`,首页返回本次构建静态资源 `main-fVQOLHwP.js` 和 `main-CEdAkTL-.css`,HTTP 跳转 HTTPS 为 301,证书 SAN 包含 `xinghen.work`。
+- 未确认与风险:未使用真实平台管理员账号在公网完成课程编辑、分集与素材修改、成员端刷新回看等业务验收;新数据库迁移仅由应用启动执行,未在生产逐项检查新增数据结构。
+- 后续修改提示:后续继续验证平台编辑与成员端读取同一课程内容、鉴权文件访问以及编辑与结单竞态;数据库或上传卷仍按 D-007、D-008 保护。
+- 关联:C-20260924-06;部署提交 `866412f`。
+
+### C-20260924-10|简化课程、讲师及管理界面信息层级
+
+- 日期:2026-09-24
+- 类型:样式 / 功能
+- 需求与原因:用户确认按五项建议简化整个系统界面,减少课程列表、课程详情、数字讲师、机构管理和平台后台的首屏信息与并列入口。
+- 修改范围:`src/pages/Dashboard.tsx` 将课程列表收敛为名称、状态、最近更新和下一步,并按需展开排序;`src/pages/CourseWorkspace.tsx` 增加阶段提示及可展开课程资料;`src/pages/DigitalInstructorsPage.tsx` 将讲师备注、归属和创建时间放入“更多”;`src/pages/OrganizationManage.tsx` 按课程、成员、账单分区,成员新增入口展开后显示创建账号与邀请链接;`src/pages/admin/AdminLayout.tsx` 将平台入口按课程制作、人员与组织、运营分组;`src/styles.css` 补充布局与窄屏样式。
+- 保留/新增判断:保留 D-001、D-004、D-005、D-009。此次仅调整前端呈现与导航,原有接口、角色鉴权、积分流水和课程操作路径仍由服务端约束;被收起的信息保留可查入口。
+- 功能确认状态:部分确认
+- 验证证据:`pnpm run check`、`pnpm run build`、`git diff --check` 通过;逐项核对页面代码中的入口、分区、链接和状态条件。
+- 未确认与风险:本轮未使用真实个人、组织管理员及平台管理员账号进行浏览器端交互与窄屏目视验收;未部署公网。特别需要核对课程不同状态的提示、机构分区切换及平台导航下拉在实际浏览器中的呈现。
+- 后续修改提示:部署前用三类账号检查课程、讲师、成员和账单路径,并保留各角色原有可访问范围;若调整隐藏区域,不得让业务操作失去可发现入口。
+- 关联:C-20260924-05、C-20260924-07、C-20260924-08。

+ 5 - 3
src/pages/CourseWorkspace.tsx

@@ -310,12 +310,15 @@ export function CourseWorkspace() {
             </button>
           </div>
           <div style={{ display: 'flex', alignItems: 'center', gap: '14px', flexWrap: 'wrap', color: '#64748b', fontSize: '0.85rem', marginTop: '6px' }}>
-            <span title={`创建时间:${formatDateTime(course.createdAt)}`}>创建时间:{formatDateTime(course.createdAt)}</span>
-            <span>•</span>
             <span title={`最后更新时间:${formatDateTime(course.updatedAt || course.createdAt)}`} style={{ color: '#334155', fontWeight: 500 }}>
               最后更新:{formatDateTime(course.updatedAt || course.createdAt)}
             </span>
           </div>
+          <p className="workspace-next-step">{isCompleted ? '课程已完成,可以查看和下载交付成果。' : isProducing ? '平台正在制作,当前可以查看进度和已提交内容。' : isWaiting ? '课程已提交,正在等待平台接单。' : '继续完善分集、素材和数字讲师,准备好后提交制作。'}</p>
+          <details className="workspace-course-details"><summary>课程资料</summary><div>
+            {course.description && <p>{course.description}</p>}
+            <p>创建于 {formatDateTime(course.createdAt)} · {course.assets?.length || 0} 份课程素材</p>
+          </div></details>
         </div>
       </div>
 
@@ -344,7 +347,6 @@ export function CourseWorkspace() {
           onClick={() => setActiveWorkspaceTab('progress')}
         >
           制作进度
-          {!!course.progressEvents?.length && <span>{course.progressEvents.length}</span>}
         </button>
       </div>
 

+ 13 - 71
src/pages/Dashboard.tsx

@@ -1,4 +1,4 @@
-import { ArrowRight, BookOpen, CheckCircle2, Clock3, Loader2, Plus, Sparkles, Stars } from 'lucide-react'
+import { ArrowRight, Plus } from 'lucide-react'
 import { useEffect, useMemo, useState } from 'react'
 import { Link } from 'react-router-dom'
 import { api } from '../api'
@@ -24,9 +24,7 @@ export function Dashboard() {
       .finally(() => setLoading(false))
   }, [workspaceId])
 
-  const waitingCount = courses.filter((c) => c.status === 'WAITING_PRODUCTION').length
-  const inProductionCount = courses.filter((c) => c.status === 'IN_PRODUCTION').length
-  const completedCount = courses.filter((c) => c.status === 'COMPLETED').length
+  const [showSort, setShowSort] = useState(false)
 
   const sortedCourses = useMemo(() => {
     return [...courses].sort((a, b) => {
@@ -43,63 +41,20 @@ export function Dashboard() {
 
   return (
     <div className="page dashboard">
-      <section className="hero">
-        <div className="orbit orbit-one" />
-        <div className="orbit orbit-two" />
-        <div className="star-dot star-a" />
-        <div className="hero-copy">
-          <p className="eyebrow">
-            <Stars size={15} /> 智能课程工坊
-          </p>
-          <h1>
-            让每一门好课,
-            <br />
-            <em>留下清晰的星痕。</em>
-          </h1>
-          <p>归集课程素材,建立制作任务。简单几步,让创意从这里启程。</p>
-          <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
-            <Link className="primary-button" to="/courses/new" aria-disabled={currentOrganization?.isActive === false} onClick={(e) => { if (currentOrganization?.isActive === false) e.preventDefault() }}>
-              <Plus size={18} /> 创建新课程
-            </Link>
-          </div>
-        </div>
-        <div className="hero-note">
-          <span>LESS AND COSMOS</span>
-          <p>删去繁复,只留下课程制作真正需要的部分。</p>
-        </div>
-      </section>
-
       <section className="content-section">
         <div className="section-heading" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px' }}>
           <div>
-            <h2>{currentOrganization ? `${currentOrganization.name} · 我的课程` : '个人课程'}</h2>
+            <h1>{currentOrganization ? `${currentOrganization.name} · 课程` : '个人课程'}</h1>
           </div>
           <div style={{ display: 'flex', alignItems: 'center', gap: '16px', flexWrap: 'wrap' }}>
-            <div className="metrics">
-              <span>
-                <BookOpen size={16} /> {courses.length} 门课程
-              </span>
-              {waitingCount > 0 && (
-                <span>
-                  <Clock3 size={16} /> {waitingCount} 等待制作
-                </span>
-              )}
-              {inProductionCount > 0 && (
-                <span style={{ color: '#0875bd' }}>
-                  <Loader2 size={16} className="spin" /> {inProductionCount} 制作中
-                </span>
-              )}
-              {completedCount > 0 && (
-                <span style={{ color: '#2e7d32' }}>
-                  <CheckCircle2 size={16} /> {completedCount} 已完成
-                </span>
-              )}
-            </div>
+            <span className="dashboard-count">{courses.length} 门课程</span>
 
             {courses.length > 1 && (
-              <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
-                <span style={{ fontSize: '0.85rem', color: '#64748b' }}>排序:</span>
+              <div className="dashboard-sort">
+                <button type="button" className="dashboard-sort-toggle" aria-expanded={showSort} onClick={() => setShowSort(!showSort)}>排序{sortMode !== 'updated_desc' ? ' · 已调整' : ''}</button>
+                {showSort && <>
                 <select
+                  aria-label="课程排序"
                   style={{ padding: '4px 8px', borderRadius: '6px', border: '1px solid #cbd5e1', fontSize: '0.85rem', background: '#fff' }}
                   value={sortMode}
                   onChange={(e) => setSortMode(e.target.value as DashboardSortMode)}
@@ -109,8 +64,10 @@ export function Dashboard() {
                   <option value="created_desc">创建时间(最新优先)</option>
                   <option value="created_asc">创建时间(最早优先)</option>
                 </select>
+                </>}
               </div>
             )}
+            <Link className="primary-button" to="/courses/new" aria-disabled={currentOrganization?.isActive === false} onClick={(e) => { if (currentOrganization?.isActive === false) e.preventDefault() }}><Plus size={16} /> 创建课程</Link>
           </div>
         </div>
 
@@ -138,30 +95,15 @@ export function Dashboard() {
           <div className="course-grid">
             {sortedCourses.map((course, index) => (
               <Link to={`/courses/${course.id}`} className="course-card" key={course.id}>
-                <div className={`course-number tone-${index % 3}`}>
-                  {String(index + 1).padStart(2, '0')}
-                </div>
+                <div className={`course-number tone-${index % 3}`}>{String(index + 1).padStart(2, '0')}</div>
                 <div className="course-card-main">
                   <StatusBadge status={course.status} />
                   <h3>{course.name}</h3>
-                  <p>{course.description || '一门正在准备中的课程。'}</p>
                   <div className="course-meta">
-                    <span>
-                      {course.assets && course.assets.length > 0
-                        ? `${course.assets.length} 份课件资产`
-                        : '未上传课件'}
-                    </span>
-                    {course.status === 'COMPLETED' && (
-                      <span style={{ background: '#e8f5e9', color: '#2e7d32', fontWeight: 500 }}>
-                        <Sparkles size={12} style={{ display: 'inline', verticalAlign: '-1px' }} /> 成果可查收
-                      </span>
-                    )}
-                    <span title={`创建时间:${formatDateTime(course.createdAt)}`}>
-                      创建于 {formatDate(course.createdAt)}
-                    </span>
                     <span title={`最后更新时间:${formatDateTime(course.updatedAt || course.createdAt)}`} style={{ color: '#0f172a', fontWeight: 500 }}>
-                      更新于 {formatDateTime(course.updatedAt || course.createdAt)}
+                      更新于 {formatDate(course.updatedAt || course.createdAt)}
                     </span>
+                    <span>{course.status === 'COMPLETED' ? '查看成果' : course.status === 'IN_PRODUCTION' ? '查看进度' : course.status === 'WAITING_PRODUCTION' ? '查看提交状态' : '继续完善'}</span>
                   </div>
                 </div>
                 <ArrowRight className="card-arrow" size={19} />

+ 7 - 5
src/pages/DigitalInstructorsPage.tsx

@@ -74,13 +74,15 @@ export function DigitalInstructorsPage() {
         return <article key={item.id} className="digital-library-card digital-asset-card">
           <div className="digital-asset-photo">{item.images[0] ? <ProtectedImage src={item.images[0].url} alt={item.name} /> : <span>暂无照片</span>}</div>
           <div className="digital-asset-body"><div className="digital-asset-title"><h2>{item.name}</h2><span className={item.ready ? 'digital-state-ready' : 'digital-state-incomplete'}>{item.ready ? '素材齐备' : '待补齐'}</span></div>
-            <p className="digital-asset-note">{item.note || '暂无备注'}</p>
-            <p className="digital-asset-meta">所有者:{item.ownerName === '未知用户' ? '历史导入' : item.ownerName} · 创建者:{item.creatorName === '未知用户' ? '历史导入' : item.creatorName}</p>
-            <p className="digital-asset-meta">创建于 {new Date(item.createdAt).toLocaleString('zh-CN')}</p>
-            {item.usageCount > 0 && <button type="button" className="digital-usage-link" onClick={() => showUsage(item)}>用于 {item.usageCount} 门课程 · 查看</button>}
+            {item.usageCount > 0 && <p className="digital-asset-meta">用于 {item.usageCount} 门课程</p>}
             {usageId === item.id && <div className="digital-usage-list"><strong>正在使用这位讲师的课程</strong>{usedCourses.map(course => <Link key={course.id} to={`/courses/${course.id}`}>{course.name}</Link>)}</div>}
             <div className="digital-asset-actions">{canEdit && <Link className="secondary-button" to={`/digital-instructors/${item.id}/edit`}>编辑</Link>}
-              <details className="digital-more-menu"><summary className="secondary-button">更多操作</summary><div><button type="button" onClick={() => duplicate(item)} disabled={busy}>复制讲师</button>
+              <details className="digital-more-menu"><summary className="secondary-button">更多</summary><div>
+                {item.note && <p className="digital-asset-note">{item.note}</p>}
+                <p className="digital-asset-meta">所有者:{item.ownerName === '未知用户' ? '历史导入' : item.ownerName} · 创建者:{item.creatorName === '未知用户' ? '历史导入' : item.creatorName}</p>
+                <p className="digital-asset-meta">创建于 {new Date(item.createdAt).toLocaleString('zh-CN')}</p>
+                {item.usageCount > 0 && <button type="button" className="digital-usage-link" onClick={() => showUsage(item)}>查看使用课程</button>}
+                <button type="button" onClick={() => duplicate(item)} disabled={busy}>复制讲师</button>
                 {workspaceId === 'personal' && organizations.some(org => org.isActive) && <div className="digital-copy-org"><label>加入组织资产库<select value={targetOrg} onChange={event => setTargetOrg(event.target.value)}><option value="">选择组织</option>{organizations.filter(org => org.isActive).map(org => <option key={org.id} value={org.id}>{org.name}</option>)}</select></label><button type="button" onClick={() => copyToOrganization(item)} disabled={busy || !targetOrg}>复制到组织</button></div>}
                 {canDelete && <button type="button" className="digital-danger-action" disabled={busy || item.usageCount > 0} title={item.usageCount > 0 ? '已有课程使用,不能删除' : undefined} onClick={() => setDeleteTarget(item)}>{item.usageCount > 0 ? '已有课程使用,不能删除' : '删除讲师'}</button>}</div></details></div>
           </div>

+ 24 - 21
src/pages/OrganizationManage.tsx

@@ -29,6 +29,8 @@ export function OrganizationManage() {
   const [editingMemberName, setEditingMemberName] = useState('')
   const [savingMemberName, setSavingMemberName] = useState(false)
   const [showAudit, setShowAudit] = useState(false)
+  const [activeSection, setActiveSection] = useState<'courses' | 'members' | 'billing'>(() => window.location.hash === '#members' ? 'members' : 'courses')
+  const [showAddMember, setShowAddMember] = useState(false)
   const [courseSortField, setCourseSortField] = useState<CourseSortField>('updated_at')
   const [courseSortOrder, setCourseSortOrder] = useState<SortOrder>('desc')
 
@@ -117,10 +119,7 @@ export function OrganizationManage() {
   }
 
   useEffect(() => {
-    if (manage && window.location.hash) {
-      const el = document.querySelector(window.location.hash)
-      if (el) el.scrollIntoView({ behavior: 'smooth' })
-    }
+    if (manage && window.location.hash === '#members') setActiveSection('members')
   }, [manage])
 
   function toggleCourseSort(field: CourseSortField) {
@@ -147,6 +146,14 @@ export function OrganizationManage() {
       <div className="stats-grid"><div className="stat-card"><div className="stat-content"><small>组织成员</small><strong>{manage.members.length}</strong></div></div>
         <div className="stat-card"><div className="stat-content"><small>组织课程</small><strong>{content.courses.length}</strong></div></div>
         <div className="stat-card"><div className="stat-content"><small>组织积分余额</small><strong>{content.organization.pointsBalance.toLocaleString('zh-CN')}</strong></div></div></div>
+      <nav className="org-manage-sections" aria-label="机构管理分区">
+        <button type="button" className={activeSection === 'courses' ? 'active' : ''} onClick={() => setActiveSection('courses')}>课程</button>
+        <button type="button" className={activeSection === 'members' ? 'active' : ''} onClick={() => setActiveSection('members')}>成员</button>
+        <button type="button" className={activeSection === 'billing' ? 'active' : ''} onClick={() => setActiveSection('billing')}>账单</button>
+      </nav>
+      {activeSection === 'members' && <>
+      <div className="org-manage-section-heading"><h2>成员管理</h2><button type="button" className="primary-button small-button" aria-expanded={showAddMember} onClick={() => setShowAddMember(!showAddMember)}>添加成员</button></div>
+      {showAddMember && <>
       <section className="admin-section-block" id="create-teacher"><h2>现场创建老师</h2><form onSubmit={createUser} className="org-inline-form">
         <label>手机号<input name="phone" required pattern="1[0-9]{10}" inputMode="numeric" /></label>
         <label>姓名<input name="contactName" required maxLength={80} /></label>
@@ -164,12 +171,22 @@ export function OrganizationManage() {
         </div>}
         {manage.inviteActive && !activeInvite && <p className="org-invite-legacy-note">当前有效链接生成于此前版本,出于安全原因无法还原。请重新生成链接后,即可展示并复制二维码。</p>}
       </section>
+      </>}
       <section className="admin-section-block"><h2>待审批申请</h2>
         {manage.applications.filter((a: any) => a.status === 'PENDING').length === 0 ? <p>暂无待审批申请</p> :
           manage.applications.filter((a: any) => a.status === 'PENDING').map((a: any) => <div className="org-list-row" key={a.id}>
             <span>{a.contact_name || a.phone} · {a.phone}</span><div><button className="primary-button small-button" onClick={() => review(a.id, true)}>同意</button>
               <button className="secondary-button small-button" onClick={() => review(a.id, false)}>拒绝</button></div></div>)}
       </section>
+      <section className="admin-section-block" id="members">
+        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
+          <h2 style={{ margin: 0 }}>人员名单</h2>
+        </div>
+        <div className="admin-table-wrapper"><table className="admin-table"><thead><tr><th>姓名</th><th>手机号</th><th>组织身份</th><th>操作</th></tr></thead><tbody>
+        {manage.members.map((m: any) => <tr key={m.id}><td>{editingMemberId === m.id ? <input aria-label={`${m.phone} 的姓名`} value={editingMemberName} maxLength={80} autoFocus onChange={(e) => setEditingMemberName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') saveMemberName(m.id); if (e.key === 'Escape') setEditingMemberId(null) }} /> : (m.contact_name || '未填写')}</td><td>{m.phone}</td><td>{m.role === 'MANAGER' ? '组织管理员' : '成员'}</td><td>{editingMemberId === m.id ? <div className="action-button-group"><button className="primary-button small-button" disabled={savingMemberName} onClick={() => saveMemberName(m.id)}>{savingMemberName ? '保存中…' : '保存'}</button><button className="secondary-button small-button" disabled={savingMemberName} onClick={() => setEditingMemberId(null)}>取消</button></div> : <div className="action-button-group"><button className="secondary-button small-button" onClick={() => { setEditingMemberId(m.id); setEditingMemberName(m.contact_name || '') }}>修改姓名</button>{m.id !== user?.id && <button className="secondary-button small-button" style={{ color: '#ef4444' }} onClick={() => removeMember(m)}>移出组织</button>}</div>}</td></tr>)}
+      </tbody></table></div></section>
+      </>}
+      {activeSection === 'courses' && <>
       <section className="admin-section-block">
         <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px', marginBottom: '12px' }}>
           <h2 style={{ margin: 0 }}>课程管理</h2>
@@ -254,23 +271,8 @@ export function OrganizationManage() {
           </table>
         </div>
       </section>
-      <section className="admin-section-block" id="members">
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
-          <h2 style={{ margin: 0 }}>人员名单</h2>
-          <a
-            href="#create-teacher"
-            className="secondary-button small-button"
-            onClick={(e) => {
-              e.preventDefault()
-              document.getElementById('create-teacher')?.scrollIntoView({ behavior: 'smooth' })
-            }}
-          >
-            + 现场创建老师
-          </a>
-        </div>
-        <div className="admin-table-wrapper"><table className="admin-table"><thead><tr><th>姓名</th><th>手机号</th><th>组织身份</th><th>操作</th></tr></thead><tbody>
-        {manage.members.map((m: any) => <tr key={m.id}><td>{editingMemberId === m.id ? <input aria-label={`${m.phone} 的姓名`} value={editingMemberName} maxLength={80} autoFocus onChange={(e) => setEditingMemberName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') saveMemberName(m.id); if (e.key === 'Escape') setEditingMemberId(null) }} /> : (m.contact_name || '未填写')}</td><td>{m.phone}</td><td>{m.role === 'MANAGER' ? '组织管理员' : '成员'}</td><td>{editingMemberId === m.id ? <div className="action-button-group"><button className="primary-button small-button" disabled={savingMemberName} onClick={() => saveMemberName(m.id)}>{savingMemberName ? '保存中…' : '保存'}</button><button className="secondary-button small-button" disabled={savingMemberName} onClick={() => setEditingMemberId(null)}>取消</button></div> : <div className="action-button-group"><button className="secondary-button small-button" onClick={() => { setEditingMemberId(m.id); setEditingMemberName(m.contact_name || '') }}>修改姓名</button>{m.id !== user?.id && <button className="secondary-button small-button" style={{ color: '#ef4444' }} onClick={() => removeMember(m)}>移出组织</button>}</div>}</td></tr>)}
-      </tbody></table></div></section>
+      </>}
+      {activeSection === 'billing' && <>
       <section className="admin-section-block"><h2>组织积分流水</h2><div className="admin-table-wrapper"><table className="admin-table"><thead><tr><th>时间</th><th>事项</th><th>老师</th><th>积分变化</th></tr></thead><tbody>
         {manage.ledger.map((l: any) => <tr key={l.id}><td>{new Date(l.created_at).toLocaleString('zh-CN')}</td><td>{l.reason}</td><td>{l.beneficiary_name || '组织账户'}</td><td>{l.amount}</td></tr>)}
       </tbody></table></div></section>
@@ -306,6 +308,7 @@ export function OrganizationManage() {
           </div>
         )}
       </section>
+      </>}
     </>}
     </main>
   </div>

+ 14 - 22
src/pages/admin/AdminLayout.tsx

@@ -41,28 +41,20 @@ export function AdminLayout() {
           </div>
         </div>
 
-        <nav className="admin-nav">
-          <NavLink to="/admin" end className={({ isActive }) => (isActive ? 'active' : '')}>
-            <LayoutDashboard size={16} />
-            <span>概览看板</span>
-          </NavLink>
-          <NavLink to="/admin/courses" className={({ isActive }) => (isActive ? 'active' : '')}>
-            <BookOpen size={16} />
-            <span>全部课程</span>
-          </NavLink>
-          <NavLink to="/admin/production-queue" className={({ isActive }) => (isActive ? 'active' : '')}>
-            <Clock3 size={16} /><span>课程制作</span>
-          </NavLink>
-          <NavLink to="/admin/users" className={({ isActive }) => (isActive ? 'active' : '')}>
-            <Users size={16} />
-            <span>用户管理</span>
-          </NavLink>
-          <NavLink to="/admin/organizations" className={({ isActive }) => (isActive ? 'active' : '')}>组织管理</NavLink>
-          <NavLink to="/admin/digital-instructors" className={({ isActive }) => (isActive ? 'active' : '')}>数字讲师</NavLink>
-          <NavLink to="/admin/leads" className={({ isActive }) => (isActive ? 'active' : '')}>
-            <Megaphone size={16} />
-            <span>落地页线索</span>
-          </NavLink>
+        <nav className="admin-nav admin-nav-grouped" aria-label="平台管理">
+          <NavLink to="/admin" end className={({ isActive }) => (isActive ? 'active' : '')}><LayoutDashboard size={16} /><span>待办概览</span></NavLink>
+          <details className="admin-nav-group"><summary><Clock3 size={16} />课程制作</summary><div>
+            <NavLink to="/admin/production-queue">制作队列</NavLink>
+            <NavLink to="/admin/courses"><BookOpen size={15} />全部课程</NavLink>
+            <NavLink to="/admin/digital-instructors">数字讲师</NavLink>
+          </div></details>
+          <details className="admin-nav-group"><summary><Users size={16} />人员与组织</summary><div>
+            <NavLink to="/admin/users">用户管理</NavLink>
+            <NavLink to="/admin/organizations">组织管理</NavLink>
+          </div></details>
+          <details className="admin-nav-group"><summary><Megaphone size={16} />运营</summary><div>
+            <NavLink to="/admin/leads">落地页线索</NavLink>
+          </div></details>
         </nav>
 
         <div className="admin-top-actions">

+ 35 - 0
src/styles.css

@@ -5082,6 +5082,41 @@ button { cursor:pointer; }
 .digital-more-menu button:hover:not(:disabled) { background: #eef7ff; }
 .digital-more-menu button:disabled { color: #9aa7b4; cursor: not-allowed; }
 .digital-more-menu .digital-danger-action { color: #b33e3b; }
+
+/* 课程与管理界面的简洁层级 */
+.dashboard .content-section { padding-top: 42px; }
+.dashboard .section-heading h1 { margin: 0; font-size: clamp(25px, 3vw, 34px); }
+.dashboard-count { color: #64748b; font-size: 14px; white-space: nowrap; }
+.dashboard-sort { display: flex; align-items: center; gap: 8px; }
+.dashboard-sort-toggle { border: 0; background: transparent; color: #475569; cursor: pointer; padding: 8px; }
+.dashboard-sort-toggle:hover { color: var(--blue-action); }
+.dashboard .course-card { min-height: 126px; }
+.dashboard .course-card h3 { margin-bottom: 14px; }
+.dashboard .course-meta { display: flex; flex-wrap: wrap; gap: 12px; }
+.workspace-next-step { margin: 12px 0 0; color: #475569; font-size: 14px; }
+.workspace-course-details { margin-top: 9px; color: #64748b; font-size: 13px; }
+.workspace-course-details summary { cursor: pointer; width: max-content; }
+.workspace-course-details p { margin: 7px 0 0; }
+.org-manage-sections { display: flex; gap: 8px; border-bottom: 1px solid #e2e8f0; margin: 24px 0; overflow-x: auto; }
+.org-manage-sections button { border: 0; border-bottom: 2px solid transparent; background: transparent; color: #64748b; cursor: pointer; padding: 12px 20px; font: inherit; white-space: nowrap; }
+.org-manage-sections button.active { border-color: var(--blue-action); color: var(--blue-action); font-weight: 600; }
+.org-manage-section-heading { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 16px; }
+.org-manage-section-heading h2 { margin: 0; }
+.admin-nav-grouped { gap: 22px; }
+.admin-nav-group { position: relative; height: 100%; display: flex; align-items: center; }
+.admin-nav-group summary { display: flex; align-items: center; gap: 6px; color: #64748b; cursor: pointer; list-style: none; font-size: 14px; }
+.admin-nav-group summary::-webkit-details-marker { display: none; }
+.admin-nav-group[open] summary, .admin-nav-group summary:hover { color: var(--blue-action); }
+.admin-nav-group > div { position: absolute; top: calc(100% - 6px); left: -12px; z-index: 20; width: 160px; padding: 8px; border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; box-shadow: 0 12px 24px rgba(15, 23, 42, .12); }
+.admin-nav-group > div a { height: auto; display: flex; padding: 10px; border-radius: 5px; }
+.admin-nav-group > div a:hover, .admin-nav-group > div a.active { background: #eef7ff; }
+.admin-nav-group > div a.active:after { display: none; }
+@media (max-width: 900px) { .admin-nav-grouped { gap: 12px; } .admin-nav-group summary { white-space: nowrap; } }
+@media (max-width: 960px) {
+  .admin-topbar:has(.admin-nav-grouped) { height: auto; min-height: 70px; flex-wrap: wrap; padding-top: 10px; }
+  .admin-topbar .admin-nav-grouped { display: flex; order: 3; width: 100%; height: 48px; margin: 0; overflow-x: auto; gap: 24px; }
+  .admin-nav-group > div { position: fixed; top: 108px; left: 16px; }
+}
 .digital-copy-org { padding: 8px 5px; border-top: 1px solid #e9eef3; border-bottom: 1px solid #e9eef3; font-size: 12px; }
 .digital-copy-org label { display: grid; gap: 5px; }
 .digital-copy-org select { width: 100%; }