feat: P3a 代码语法高亮 (highlight.js + CodeBlock组件)
This commit is contained in:
+18
-43
@@ -1,6 +1,6 @@
|
||||
# 项目进度追踪
|
||||
|
||||
## 当前阶段:P0 全部完成 ✅
|
||||
## 当前阶段:P3 — 体验增强
|
||||
|
||||
### Phase 1-3 — 全部完成 ✅
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
| **P0d** | 会员定价页重构(用量可视化) | ✅ |
|
||||
| **P0e** | 模型选择器升级(卡片式 + 能力标签) | ✅ |
|
||||
| **P0f** | 会话分享链接 — HMAC 签名 + 分享查看页 | ✅ |
|
||||
| **全栈验证** | 后端 13 suites / 92 tests / 前端 61 pages | ✅ |
|
||||
|
||||
### P1 — 学习技能包(全部完成 ✅)
|
||||
| 子项 | 说明 | 状态 |
|
||||
@@ -21,53 +20,29 @@
|
||||
| **后端** | SkillsModule + 8 技能定义 + 过滤/详情 API | ✅ |
|
||||
| **前端** | /skills 市场 + /skills/[id] 详情 + header 入口 | ✅ |
|
||||
| **沙盒集成** | 替代硬编码 SCENES,从 API 动态加载,?skill=xxx 支持 | ✅ |
|
||||
| **全栈验证** | 后端 92 tests / 前端 70 pages | ✅ |
|
||||
|
||||
### P2 — CSS 统一(全部完成 ✅)
|
||||
| 子项 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| **Mobile variables.css** | Design Token 变量定义(Web/Mobile 统一) | ✅ |
|
||||
| **Mobile 全站 18 页** | 批量替换 hardcoded color → CSS vars | ✅ |
|
||||
| **PC 响应式** | header/compare/sandbox/member/workshop 移动端修复 | ✅ |
|
||||
|
||||
### P3 — 体验增强(当前阶段)
|
||||
| 优先级 | 项目 | 说明 | 状态 |
|
||||
|--------|------|------|------|
|
||||
| **P3a** | 代码语法高亮 | 沙盒聊天中代码块用 Shiki/prism 渲染 | ⏳ |
|
||||
| **P3b** | 会话重命名 | 侧边栏对话标题可编辑 | ⏳ |
|
||||
| **P3c** | LLM 集成层增强 | 模型能力检测 / 用量统计 | ⏳ |
|
||||
| **P3d** | Mobile 样式复查 | 剩余页面 CSS 变量覆盖率检查 | ⏳ |
|
||||
| — | 多模态(文件上传/图片理解) | 远期规划 | 📋 |
|
||||
| — | SDK / 共享 UI 包 | 远期规划 | 📋 |
|
||||
|
||||
### 参考来源
|
||||
- opencode 源码克隆到 `/tmp/opencode-source/`(21 packages, 147MB)
|
||||
- 关键参考模块: `packages/plugin`(hooks + tool 系统)、`packages/ui/i18n/`(20+ 语言)、`packages/llm`(provider 接口)
|
||||
|
||||
### 新页面
|
||||
- `/learning/analytics` — 学情分析看板(对话分析→知识图谱→薄弱环节→推荐)
|
||||
- `/learning/path` — 分阶段学习路径(4 阶段 12 任务,自动追踪完成进度)
|
||||
- `/my` 首页添加了学情分析和学习路径入口链接
|
||||
|
||||
### 验证
|
||||
- 后端 13 suites / 92 tests passed ✅
|
||||
- 前端 60 页面构建无报错 ✅
|
||||
|
||||
### 今日修复项(2026-05-15)
|
||||
|
||||
#### 颜色约定修复(18 个文件)
|
||||
- `page.tsx`(首页)、`dashboard/page.tsx`、`my/*`(4 个)、`models/page.tsx`
|
||||
- `discover/*`(3 个)、`circles/*`(2 个)、`admin/*`(2 个)
|
||||
- `courses/[id]/client.tsx`、`contents/[id]/client.tsx`
|
||||
- `sandbox/compare/page.tsx`、`sandbox/code/page.tsx`
|
||||
- 全部替换 `text-gray-*`/`bg-gray-*`/`border-gray-*` → CSS 变量
|
||||
|
||||
#### 模型列表统一
|
||||
- 新建 `frontend/src/lib/models.ts` — 单数据源
|
||||
- `sandbox/page.tsx`、`sandbox/compare/page.tsx`、`prompts/workshop/page.tsx` 统一导入
|
||||
|
||||
#### 对比实验室增强
|
||||
- 修复硬编码颜色、更新 model ID 为后端支持的值
|
||||
- 新增参数调节面板(temperature/top-p/max-tokens)
|
||||
|
||||
#### 清理
|
||||
- 删除根目录 9 个残留文件(next-dev*.log、PROGRESS_FINAL* 等)
|
||||
- 删除 `backend/src/e2e.spec.ts` 占位符
|
||||
|
||||
### 关键决策
|
||||
- `frontend/src/lib/models.ts` 作为模型列表单数据源
|
||||
- 所有颜色使用 CSS 变量(`text-foreground`/`text-muted-foreground`/`bg-card`/`bg-muted`/`border-border`)
|
||||
- `nest build` 正常产出(`--tsc` 遗漏 prisma 模块)
|
||||
- 沙盒 JWT 认证保留
|
||||
|
||||
### 相关文件
|
||||
- `backend/src/modules/learning/` — 学习分析模块(service + controller + module)
|
||||
- `frontend/src/app/learning/analytics/page.tsx` — 学情分析看板页面
|
||||
- `frontend/src/app/learning/path/page.tsx` — 学习路径页面
|
||||
- `frontend/src/lib/models.ts` — 模型列表单数据源(新建)
|
||||
- `frontend/src/app/sandbox/compare/page.tsx` — 对比实验室(修复颜色+参数+model)
|
||||
- `frontend/src/app/page.tsx`, `dashboard/`, `my/`, `discover/`, `circles/`, `admin/`, `courses/`, `contents/` — 颜色修复
|
||||
- `docs/产品优化方向.md` — 完整优化方案
|
||||
|
||||
Generated
+9
@@ -27,6 +27,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lucide-react": "^1.14.0",
|
||||
"next": "^14.2.35",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -3769,6 +3770,14 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lucide-react": "^1.14.0",
|
||||
"next": "^14.2.35",
|
||||
"next-themes": "^0.4.6",
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
|
||||
|
||||
/* highlight.js for code syntax highlighting */
|
||||
@import 'highlight.js/styles/atom-one-dark.css';
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getToken, apiFetch } from '@/lib/auth';
|
||||
import { DEFAULT_MODEL } from '@/lib/models';
|
||||
import { ModelSelector } from '@/components/ui/model-selector';
|
||||
import { useT } from '@/i18n';
|
||||
import { CodeBlock } from '@/components/ui/code-block';
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1';
|
||||
|
||||
@@ -412,7 +413,18 @@ function SandboxPage() {
|
||||
<div className="w-8 h-8 bg-brand-600 rounded-xl flex items-center justify-center text-white text-sm font-bold shrink-0">Y</div>
|
||||
)}
|
||||
<div className={`max-w-[75%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap ${msg.role === 'user' ? 'bg-brand-600 text-white rounded-tr-none' : 'bg-muted text-foreground rounded-tl-none'}`}>
|
||||
{msg.content}
|
||||
{(() => {
|
||||
const parts = msg.content.split(/(```(?:\w+)?\n[\s\S]*?```)/g)
|
||||
return parts.map((part, pi) => {
|
||||
const match = part.match(/^```(\w*)\n([\s\S]*)```$/)
|
||||
if (match) {
|
||||
const [, lang, code] = match
|
||||
return <CodeBlock key={pi} code={code.trim()} language={lang || undefined} />
|
||||
}
|
||||
if (pi > 0 && parts[pi-1].startsWith('```')) return null
|
||||
return <span key={pi} className="whitespace-pre-wrap">{part}</span>
|
||||
})
|
||||
})()}
|
||||
</div>
|
||||
{msg.role === 'user' && (
|
||||
<div className="w-8 h-8 bg-muted-foreground/20 rounded-xl flex items-center justify-center text-xs font-bold shrink-0">我</div>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import hljs from 'highlight.js/lib/core'
|
||||
import javascript from 'highlight.js/lib/languages/javascript'
|
||||
import python from 'highlight.js/lib/languages/python'
|
||||
import typescript from 'highlight.js/lib/languages/typescript'
|
||||
import xml from 'highlight.js/lib/languages/xml'
|
||||
import css from 'highlight.js/lib/languages/css'
|
||||
import bash from 'highlight.js/lib/languages/bash'
|
||||
import json from 'highlight.js/lib/languages/json'
|
||||
|
||||
hljs.registerLanguage('javascript', javascript)
|
||||
hljs.registerLanguage('js', javascript)
|
||||
hljs.registerLanguage('python', python)
|
||||
hljs.registerLanguage('py', python)
|
||||
hljs.registerLanguage('typescript', typescript)
|
||||
hljs.registerLanguage('ts', typescript)
|
||||
hljs.registerLanguage('xml', xml)
|
||||
hljs.registerLanguage('html', xml)
|
||||
hljs.registerLanguage('css', css)
|
||||
hljs.registerLanguage('bash', bash)
|
||||
hljs.registerLanguage('sh', bash)
|
||||
hljs.registerLanguage('json', json)
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, language }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const codeRef = useRef<HTMLElement>(null)
|
||||
|
||||
let highlighted = ''
|
||||
try {
|
||||
if (language && hljs.getLanguage(language)) {
|
||||
highlighted = hljs.highlight(code, { language }).value
|
||||
} else {
|
||||
highlighted = hljs.highlightAuto(code).value
|
||||
}
|
||||
} catch {
|
||||
highlighted = code
|
||||
}
|
||||
|
||||
async function copyCode() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative group my-2 rounded-xl overflow-hidden border border-border">
|
||||
<div className="flex items-center justify-between px-4 py-1.5 bg-muted/80 border-b border-border">
|
||||
<span className="text-xs text-muted-foreground">{language || 'code'}</span>
|
||||
<button onClick={copyCode}
|
||||
className="text-xs px-2 py-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-accent transition-colors">
|
||||
{copied ? '已复制' : '复制'}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto p-4 text-sm leading-relaxed bg-[#1e1e1e]">
|
||||
<code ref={codeRef} className={`language-${language || ''}`} dangerouslySetInnerHTML={{ __html: highlighted }} />
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user