C_Code 代码高亮组件
🎨 基于 Naive UI 的高效代码高亮组件,让代码展示变得专业而优雅
✨ 特性
- 🌈 多语言支持: 支持 JavaScript、TypeScript、Python、Java 等 30+ 种编程语言
- 📋 一键复制: 智能复制功能,支持悬浮按钮和标题栏双重交互
- 🖥️ 全屏查看: 内置全屏模态框,完美展示长代码片段
- 🎯 智能加载: 动态语言包加载,按需引入提升性能
- 🎨 语言图标: 内置丰富的编程语言图标映射
- 📏 灵活布局: 支持行号显示、自动换行、高度限制等配置
- ⌨️ 快捷操作: ESC 键退出全屏,提升用户体验
- 🎭 加载状态: 优雅的语言包加载提示
- 💪 TypeScript: 完整的类型定义和类型安全
- ⚡ 高性能: 基于 highlight.js 的优化渲染引擎
- 🎛️ 高度定制: 支持自定义标题、样式和事件处理
📦 安装
bash
# 确保已安装 highlight.js 相关依赖
npm install highlight.js
# 或者
bun add highlight.js
1
2
3
4
2
3
4
🎯 快速开始
基础用法
vue
<template>
<!-- 最简单的代码高亮 -->
<C_Code
:code="jsCode"
language="javascript"
title="JavaScript 示例"
@copy="handleCopy"
/>
</template>
<script setup>
const jsCode = `
function greet(name) {
console.log(\`Hello, \${name}!\`)
return \`Welcome, \${name}\`
}
greet('Vue Developer')
`
const handleCopy = (code) => {
console.log('代码已复制:', code.length, '个字符')
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
多语言展示
vue
<template>
<div class="code-examples">
<n-tabs type="line" animated>
<n-tab-pane
v-for="(example, lang) in codeExamples"
:key="lang"
:name="lang"
:tab="getLanguageLabel(lang)"
>
<C_Code
:code="example.code"
:language="lang"
:title="example.title"
:show-fullscreen="true"
@copy="handleCopy"
@fullscreen="handleFullscreen"
/>
</n-tab-pane>
</n-tabs>
</div>
</template>
<script setup>
const codeExamples = {
javascript: {
title: 'JavaScript ES6+ 示例',
code: `
// 现代 JavaScript 示例
const fetchUserData = async (userId) => {
try {
const response = await fetch(\`/api/users/\${userId}\`)
const userData = await response.json()
return {
...userData,
isActive: userData.status === 'active',
fullName: \`\${userData.firstName} \${userData.lastName}\`
}
} catch (error) {
console.error('获取用户数据失败:', error)
throw new Error('用户数据加载失败')
}
}
// 使用示例
fetchUserData(123)
.then(user => console.log('用户信息:', user))
.catch(error => console.error(error))
`
},
typescript: {
title: 'TypeScript 接口定义',
code: `
// TypeScript 类型定义示例
interface User {
id: number
name: string
email: string
avatar?: string
roles: Role[]
createdAt: Date
updatedAt: Date
}
interface Role {
id: number
name: string
permissions: Permission[]
}
interface Permission {
id: number
action: string
resource: string
}
// 泛型函数示例
function createApiResponse<T>(
data: T,
success: boolean = true,
message?: string
): ApiResponse<T> {
return {
data,
success,
message: message || (success ? '操作成功' : '操作失败'),
timestamp: new Date().toISOString()
}
}
// 使用示例
const userResponse = createApiResponse<User>({
id: 1,
name: 'John Doe',
email: 'john@example.com',
roles: [],
createdAt: new Date(),
updatedAt: new Date()
})
`
},
python: {
title: 'Python 数据处理',
code: `
# Python 数据处理示例
import pandas as pd
import numpy as np
from typing import List, Dict, Optional
class DataAnalyzer:
"""数据分析器类"""
def __init__(self, data: pd.DataFrame):
self.data = data
self.results: Dict[str, any] = {}
def analyze_user_behavior(self) -> Dict[str, float]:
"""分析用户行为数据"""
# 计算基础统计信息
stats = {
'total_users': len(self.data),
'avg_session_duration': self.data['session_duration'].mean(),
'bounce_rate': (self.data['page_views'] == 1).sum() / len(self.data),
'conversion_rate': self.data['converted'].sum() / len(self.data)
}
# 用户分群分析
high_value_users = self.data[
(self.data['revenue'] > self.data['revenue'].quantile(0.8)) &
(self.data['session_duration'] > 300)
]
stats['high_value_user_ratio'] = len(high_value_users) / len(self.data)
self.results.update(stats)
return stats
def generate_insights(self) -> List[str]:
"""生成数据洞察"""
insights = []
if self.results.get('bounce_rate', 0) > 0.7:
insights.append("跳出率较高,需要优化页面内容")
if self.results.get('conversion_rate', 0) < 0.05:
insights.append("转化率偏低,建议优化转化流程")
return insights
# 使用示例
data = pd.read_csv('user_behavior.csv')
analyzer = DataAnalyzer(data)
results = analyzer.analyze_user_behavior()
insights = analyzer.generate_insights()
print("分析结果:", results)
print("改进建议:", insights)
`
},
vue: {
title: 'Vue 3 组件示例',
code: `
<template>
<div class="user-profile">
<div class="profile-header">
<img :src="user.avatar" :alt="user.name" class="avatar" />
<div class="user-info">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<n-tag :type="user.isActive ? 'success' : 'default'">
{{ user.isActive ? '在线' : '离线' }}
</n-tag>
</div>
</div>
<div class="profile-stats">
<div v-for="stat in userStats" :key="stat.label" class="stat-item">
<span class="stat-value">{{ stat.value }}</span>
<span class="stat-label">{{ stat.label }}</span>
</div>
</div>
<div class="profile-actions">
<n-button type="primary" @click="sendMessage">
发送消息
</n-button>
<n-button @click="viewProfile">
查看详情
</n-button>
</div>
</div>
</template>
<script setup lang="ts">
interface User {
id: number
name: string
email: string
avatar: string
isActive: boolean
}
interface UserStat {
label: string
value: string | number
}
const props = defineProps<{
userId: number
}>()
const emit = defineEmits<{
message: [userId: number]
viewProfile: [userId: number]
}>()
const user = ref<User | null>(null)
const loading = ref(false)
const userStats = computed((): UserStat[] => [
{ label: '关注者', value: '1.2K' },
{ label: '关注中', value: '234' },
{ label: '帖子', value: '89' },
{ label: '点赞', value: '3.4K' }
])
const fetchUser = async () => {
loading.value = true
try {
const response = await userApi.getUser(props.userId)
user.value = response.data
} finally {
loading.value = false
}
}
const sendMessage = () => {
emit('message', props.userId)
}
const viewProfile = () => {
emit('viewProfile', props.userId)
}
onMounted(() => {
fetchUser()
})
</script>
<style scoped>
.user-profile {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.profile-header {
display: flex;
align-items: center;
margin-bottom: 24px;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
margin-right: 16px;
}
.profile-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-item {
text-align: center;
padding: 12px;
background: #f5f5f5;
border-radius: 6px;
}
.stat-value {
display: block;
font-size: 18px;
font-weight: 600;
color: #333;
}
.stat-label {
font-size: 12px;
color: #666;
}
</style>
`
}
}
const getLanguageLabel = (lang) => {
const labels = {
javascript: 'JavaScript',
typescript: 'TypeScript',
python: 'Python',
vue: 'Vue 3'
}
return labels[lang] || lang.toUpperCase()
}
const handleCopy = (code) => {
message.success(`已复制 ${code.length} 个字符`)
}
const handleFullscreen = (isFullscreen) => {
message.info(isFullscreen ? '已进入全屏模式' : '已退出全屏模式')
}
</script>
<style scoped>
.code-examples {
padding: 20px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
📖 API 文档
Props
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
code | string | '' | 要显示的代码内容 |
language | string | 'text' | 编程语言标识符 |
title | string | - | 代码块标题 |
showHeader | boolean | true | 是否显示标题栏 |
showLineNumbers | boolean | true | 是否显示行号 |
wordWrap | boolean | false | 是否启用自动换行 |
trim | boolean | true | 是否移除首尾空白 |
showFullscreen | boolean | false | 是否显示全屏按钮 |
maxHeight | string | number | - | 最大高度限制 |
autoLoadLanguage | boolean | true | 是否自动加载语言包 |
Events
事件名 | 参数 | 说明 |
---|---|---|
copy | (code: string) | 复制代码时触发 |
click | (event: MouseEvent) | 点击代码区域时触发 |
fullscreen | (isFullscreen: boolean) | 全屏状态切换时触发 |
暴露方法
方法名 | 参数 | 返回值 | 说明 |
---|---|---|---|
copyCode | - | Promise<void> | 手动触发复制代码 |
toggleFullscreen | - | void | 手动切换全屏状态 |
支持的编程语言
语言 | 标识符 | 图标 |
---|---|---|
JavaScript | javascript | 🟨 |
TypeScript | typescript | 🔷 |
Python | python | 🐍 |
Java | java | ☕ |
C++ | cpp | ⚡ |
Go | go | 🐹 |
Rust | rust | 🦀 |
PHP | php | 🐘 |
C# | csharp | 💙 |
HTML | html | 🌐 |
CSS | css | 🎨 |
Vue | vue | 💚 |
React | react | ⚛️ |
JSON | json | 📄 |
SQL | sql | 🗃️ |
Bash | bash | 💻 |
YAML | yaml | 📋 |
XML | xml | 📑 |
Markdown | markdown | 📝 |
🎨 使用示例
场景 1: 技术文档展示
vue
<template>
<div class="documentation">
<n-card title="API 使用文档" style="margin-bottom: 16px;">
<n-space>
<n-select
v-model:value="selectedApi"
:options="apiOptions"
placeholder="选择 API"
style="width: 200px"
/>
<n-button @click="generateExample" type="primary">
生成示例
</n-button>
</n-space>
</n-card>
<div class="api-sections">
<!-- 请求示例 -->
<n-card title="请求示例" class="mb-16px">
<n-tabs type="line">
<n-tab-pane name="curl" tab="cURL">
<C_Code
:code="apiExamples.curl"
language="bash"
title="cURL 请求"
@copy="handleCopy"
/>
</n-tab-pane>
<n-tab-pane name="javascript" tab="JavaScript">
<C_Code
:code="apiExamples.javascript"
language="javascript"
title="JavaScript Fetch"
@copy="handleCopy"
/>
</n-tab-pane>
<n-tab-pane name="python" tab="Python">
<C_Code
:code="apiExamples.python"
language="python"
title="Python Requests"
@copy="handleCopy"
/>
</n-tab-pane>
</n-tabs>
</n-card>
<!-- 响应示例 -->
<n-card title="响应示例" class="mb-16px">
<C_Code
:code="apiExamples.response"
language="json"
title="JSON 响应"
:max-height="400"
@copy="handleCopy"
/>
</n-card>
<!-- SDK 示例 -->
<n-card title="SDK 使用示例">
<n-tabs type="line">
<n-tab-pane name="vue" tab="Vue 3">
<C_Code
:code="sdkExamples.vue"
language="vue"
title="Vue 3 Composition API"
:show-fullscreen="true"
@copy="handleCopy"
@fullscreen="handleFullscreen"
/>
</n-tab-pane>
<n-tab-pane name="react" tab="React">
<C_Code
:code="sdkExamples.react"
language="react"
title="React Hooks"
:show-fullscreen="true"
@copy="handleCopy"
/>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</div>
</template>
<script setup>
const message = useMessage()
const selectedApi = ref('users')
const apiOptions = [
{ label: '用户管理', value: 'users' },
{ label: '文章管理', value: 'articles' },
{ label: '评论系统', value: 'comments' }
]
const apiExamples = reactive({
curl: `# 获取用户列表
curl -X GET "https://api.example.com/v1/users" \\
-H "Authorization: Bearer YOUR_TOKEN" \\
-H "Content-Type: application/json" \\
-H "Accept: application/json"
# 创建新用户
curl -X POST "https://api.example.com/v1/users" \\
-H "Authorization: Bearer YOUR_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"name": "John Doe",
"email": "john@example.com",
"role": "user"
}'`,
javascript: `// 使用 Fetch API
const apiClient = {
baseURL: 'https://api.example.com/v1',
token: 'YOUR_TOKEN',
async request(endpoint, options = {}) {
const url = \`\${this.baseURL}\${endpoint}\`
const config = {
headers: {
'Authorization': \`Bearer \${this.token}\`,
'Content-Type': 'application/json',
...options.headers
},
...options
}
try {
const response = await fetch(url, config)
if (!response.ok) {
throw new Error(\`HTTP \${response.status}: \${response.statusText}\`)
}
return await response.json()
} catch (error) {
console.error('API 请求失败:', error)
throw error
}
},
// 获取用户列表
async getUsers(params = {}) {
const query = new URLSearchParams(params).toString()
return this.request(\`/users?\${query}\`)
},
// 创建用户
async createUser(userData) {
return this.request('/users', {
method: 'POST',
body: JSON.stringify(userData)
})
}
}
// 使用示例
apiClient.getUsers({ page: 1, limit: 10 })
.then(data => console.log('用户列表:', data))
.catch(error => console.error('获取失败:', error))`,
python: `import requests
import json
from typing import Dict, Optional, Any
class APIClient:
"""API 客户端封装"""
def __init__(self, base_url: str, token: str):
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
})
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
"""发送 HTTP 请求"""
url = f"{self.base_url}{endpoint}"
try:
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"API 请求失败: {e}")
raise
def get_users(self, page: int = 1, limit: int = 10) -> Dict[str, Any]:
"""获取用户列表"""
params = {'page': page, 'limit': limit}
return self._request('GET', '/users', params=params)
def create_user(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
"""创建新用户"""
return self._request('POST', '/users', json=user_data)
def update_user(self, user_id: int, user_data: Dict[str, Any]) -> Dict[str, Any]:
"""更新用户信息"""
return self._request('PUT', f'/users/{user_id}', json=user_data)
# 使用示例
client = APIClient('https://api.example.com/v1', 'YOUR_TOKEN')
# 获取用户列表
users = client.get_users(page=1, limit=20)
print(f"获取到 {len(users['data'])} 个用户")
# 创建新用户
new_user = {
'name': 'Jane Smith',
'email': 'jane@example.com',
'role': 'admin'
}
created_user = client.create_user(new_user)
print(f"用户创建成功,ID: {created_user['id']}")`,
response: `{
"success": true,
"data": {
"users": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"avatar": "https://example.com/avatars/john.jpg",
"role": "admin",
"status": "active",
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-07-19T10:30:00Z",
"profile": {
"firstName": "John",
"lastName": "Doe",
"bio": "Full-stack developer with 5+ years experience",
"location": "San Francisco, CA",
"website": "https://johndoe.dev",
"social": {
"github": "johndoe",
"twitter": "john_doe_dev",
"linkedin": "johndoe"
}
},
"preferences": {
"theme": "dark",
"language": "en",
"timezone": "America/Los_Angeles",
"notifications": {
"email": true,
"push": false,
"sms": false
}
},
"permissions": [
"users:read",
"users:write",
"articles:read",
"articles:write",
"admin:dashboard"
]
}
],
"pagination": {
"currentPage": 1,
"totalPages": 5,
"totalItems": 47,
"itemsPerPage": 10,
"hasNextPage": true,
"hasPrevPage": false
}
},
"message": "用户列表获取成功",
"timestamp": "2025-07-19T10:30:00Z",
"requestId": "req_1234567890abcdef"
}`
})
const sdkExamples = {
vue: `<template>
<div class="user-manager">
<div class="header">
<h2>用户管理</h2>
<n-button @click="refreshUsers" :loading="loading">
刷新
</n-button>
</div>
<n-data-table
:columns="columns"
:data="users"
:loading="loading"
:pagination="pagination"
@update:page="handlePageChange"
/>
<!-- 用户详情模态框 -->
<n-modal v-model:show="showUserModal" preset="dialog">
<template #header>
<span>{{ editingUser ? '编辑用户' : '新增用户' }}</span>
</template>
<UserForm
v-model:user="currentUser"
:loading="submitting"
@submit="handleSubmit"
@cancel="closeModal"
/>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { useUsers } from '@/composables/useUsers'
import type { User, UserFilters } from '@/types/user'
const {
users,
loading,
pagination,
filters,
refreshUsers,
createUser,
updateUser,
deleteUser
} = useUsers()
const showUserModal = ref(false)
const editingUser = ref<User | null>(null)
const currentUser = ref<Partial<User>>({})
const submitting = ref(false)
const columns = [
{
title: '用户名',
key: 'name',
render: (row: User) => h('span', { class: 'font-medium' }, row.name)
},
{
title: '邮箱',
key: 'email'
},
{
title: '角色',
key: 'role',
render: (row: User) => h(NTag, {
type: row.role === 'admin' ? 'error' : 'default'
}, row.role)
},
{
title: '状态',
key: 'status',
render: (row: User) => h(NTag, {
type: row.status === 'active' ? 'success' : 'warning'
}, row.status === 'active' ? '活跃' : '禁用')
},
{
title: '操作',
key: 'actions',
render: (row: User) => h(NSpace, [
h(NButton, {
size: 'small',
onClick: () => editUser(row)
}, '编辑'),
h(NButton, {
size: 'small',
type: 'error',
onClick: () => handleDelete(row)
}, '删除')
])
}
]
const editUser = (user: User) => {
editingUser.value = user
currentUser.value = { ...user }
showUserModal.value = true
}
const handleSubmit = async (userData: Partial<User>) => {
submitting.value = true
try {
if (editingUser.value) {
await updateUser(editingUser.value.id, userData)
} else {
await createUser(userData as Omit<User, 'id'>)
}
closeModal()
await refreshUsers()
} finally {
submitting.value = false
}
}
const handleDelete = async (user: User) => {
// 确认删除逻辑
await deleteUser(user.id)
await refreshUsers()
}
const closeModal = () => {
showUserModal.value = false
editingUser.value = null
currentUser.value = {}
}
const handlePageChange = (page: number) => {
pagination.value.current = page
refreshUsers()
}
// 初始化加载
onMounted(() => {
refreshUsers()
})
</script>`,
react: `import React, { useState, useEffect, useCallback } from 'react'
import { Button, Table, Modal, Form, Input, Select, Tag, Space, message } from 'antd'
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
import { useUsers } from '../hooks/useUsers'
import type { User, UserFormData } from '../types/user'
const { Option } = Select
export const UserManager: React.FC = () => {
const {
users,
loading,
pagination,
fetchUsers,
createUser,
updateUser,
deleteUser
} = useUsers()
const [isModalOpen, setIsModalOpen] = useState(false)
const [editingUser, setEditingUser] = useState<User | null>(null)
const [form] = Form.useForm()
const columns = [
{
title: '用户名',
dataIndex: 'name',
key: 'name',
render: (text: string) => <span className="font-medium">{text}</span>
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email'
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
render: (role: string) => (
<Tag color={role === 'admin' ? 'red' : 'default'}>
{role}
</Tag>
)
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={status === 'active' ? 'green' : 'orange'}>
{status === 'active' ? '活跃' : '禁用'}
</Tag>
)
},
{
title: '操作',
key: 'actions',
render: (_: any, record: User) => (
<Space>
<Button
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
编辑
</Button>
<Button
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>
删除
</Button>
</Space>
)
}
]
const handleEdit = (user: User) => {
setEditingUser(user)
form.setFieldsValue(user)
setIsModalOpen(true)
}
const handleAdd = () => {
setEditingUser(null)
form.resetFields()
setIsModalOpen(true)
}
const handleSubmit = async (values: UserFormData) => {
try {
if (editingUser) {
await updateUser(editingUser.id, values)
message.success('用户更新成功')
} else {
await createUser(values)
message.success('用户创建成功')
}
setIsModalOpen(false)
fetchUsers()
} catch (error) {
message.error('操作失败')
}
}
const handleDelete = async (user: User) => {
Modal.confirm({
title: '确认删除',
content: \`确定要删除用户 "\${user.name}" 吗?\`,
onOk: async () => {
try {
await deleteUser(user.id)
message.success('用户删除成功')
fetchUsers()
} catch (error) {
message.error('删除失败')
}
}
})
}
const handleTableChange = (pagination: any) => {
fetchUsers({
page: pagination.current,
pageSize: pagination.pageSize
})
}
const refreshUsers = useCallback(() => {
fetchUsers()
}, [fetchUsers])
useEffect(() => {
fetchUsers()
}, [])
return (
<div className="user-manager">
<div className="header mb-4 flex justify-between items-center">
<h2 className="text-xl font-semibold">用户管理</h2>
<Space>
<Button
icon={<ReloadOutlined />}
onClick={refreshUsers}
loading={loading}
>
刷新
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
>
新增用户
</Button>
</Space>
</div>
<Table
columns={columns}
dataSource={users}
loading={loading}
pagination={{
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
\`第 \${range[0]}-\${range[1]} 条,共 \${total} 条\`
}}
onChange={handleTableChange}
rowKey="id"
/>
<Modal
title={editingUser ? '编辑用户' : '新增用户'}
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
className="pt-4"
>
<Form.Item
name="name"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
name="email"
label="邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' }
]}
>
<Input placeholder="请输入邮箱" />
</Form.Item>
<Form.Item
name="role"
label="角色"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select placeholder="请选择角色">
<Option value="user">普通用户</Option>
<Option value="admin">管理员</Option>
</Select>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select placeholder="请选择状态">
<Option value="active">活跃</Option>
<Option value="inactive">禁用</Option>
</Select>
</Form.Item>
<Form.Item className="mb-0 text-right">
<Space>
<Button onClick={() => setIsModalOpen(false)}>
取消
</Button>
<Button type="primary" htmlType="submit">
{editingUser ? '更新' : '创建'}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
)
}`
}
const generateExample = () => {
// 根据选择的 API 生成对应示例
message.success(`已生成 ${selectedApi.value} API 示例`)
}
const handleCopy = (code) => {
message.success(`已复制 ${code.length} 个字符到剪贴板`)
}
const handleFullscreen = (isFullscreen) => {
message.info(isFullscreen ? '已进入全屏模式' : '已退出全屏模式')
}
</script>
<style scoped>
.documentation {
padding: 24px;
}
.api-sections .n-card {
margin-bottom: 16px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
场景 2: 代码教程展示
vue
<template>
<div class="tutorial-demo">
<n-card title="Vue 3 学习教程" style="margin-bottom: 16px;">
<n-steps :current="currentStep" size="small">
<n-step
v-for="(step, index) in tutorialSteps"
:key="index"
:title="step.title"
/>
</n-steps>
</n-card>
<n-grid cols="2" x-gap="16">
<!-- 教程内容 -->
<n-grid-item>
<n-card :title="currentTutorial.title" size="small">
<div class="tutorial-content">
<div class="description">
{{ currentTutorial.description }}
</div>
<div class="key-points">
<h4>关键要点:</h4>
<ul>
<li v-for="point in currentTutorial.keyPoints" :key="point">
{{ point }}
</li>
</ul>
</div>
<C_Code
:code="currentTutorial.code"
:language="currentTutorial.language"
:title="currentTutorial.codeTitle"
:show-fullscreen="true"
@copy="handleCopy"
/>
<div class="tutorial-actions">
<n-button-group>
<n-button
@click="prevStep"
:disabled="currentStep === 0"
>
上一步
</n-button>
<n-button
@click="nextStep"
:disabled="currentStep === tutorialSteps.length - 1"
type="primary"
>
下一步
</n-button>
</n-button-group>
</div>
</div>
</n-card>
</n-grid-item>
<!-- 实时预览 -->
<n-grid-item>
<n-card title="实时预览" size="small">
<div class="preview-container">
<div v-if="currentStep === 0" class="demo-basic">
<h3>{{ message }}</h3>
<n-button @click="updateMessage">更新消息</n-button>
</div>
<div v-if="currentStep === 1" class="demo-reactive">
<n-input v-model:value="inputValue" placeholder="输入一些文字" />
<p>你输入的是: {{ inputValue }}</p>
</div>
<div v-if="currentStep === 2" class="demo-computed">
<n-input-number v-model:value="num1" placeholder="第一个数字" />
<n-input-number v-model:value="num2" placeholder="第二个数字" />
<p>计算结果: {{ computedSum }}</p>
</div>
<div v-if="currentStep === 3" class="demo-lifecycle">
<p>组件已挂载 {{ mountedTime }} 秒</p>
<n-button @click="resetTimer">重置计时器</n-button>
</div>
</div>
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script setup>
const message = useMessage()
const currentStep = ref(0)
// 示例数据
const inputValue = ref('')
const num1 = ref(0)
const num2 = ref(0)
const mountedTime = ref(0)
const messageText = ref('Hello Vue 3!')
const computedSum = computed(() => {
return (num1.value || 0) + (num2.value || 0)
})
const tutorialSteps = [
{
title: '响应式基础',
description: '学习 Vue 3 的响应式系统基础,了解 ref 的使用方法。',
keyPoints: [
'ref() 用于创建响应式数据',
'.value 用于访问和修改响应式数据',
'模板中会自动解包,无需 .value'
],
language: 'vue',
codeTitle: 'ref 响应式示例',
code: `<template>
<div>
<h3>{{ message }}</h3>
<button @click="updateMessage">更新消息</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 创建响应式数据
const message = ref('Hello Vue 3!')
// 修改响应式数据的函数
const updateMessage = () => {
message.value = '消息已更新: ' + new Date().toLocaleTimeString()
}
</script>`
},
{
title: '双向绑定',
description: '学习 v-model 的使用,实现表单元素的双向数据绑定。',
keyPoints: [
'v-model 实现双向数据绑定',
'输入框的值会实时同步到数据',
'数据变化也会更新输入框'
],
language: 'vue',
codeTitle: 'v-model 双向绑定',
code: `<template>
<div>
<input v-model="inputValue" placeholder="输入一些文字" />
<p>你输入的是: {{ inputValue }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 创建响应式数据用于双向绑定
const inputValue = ref('')
// 监听数据变化
watch(inputValue, (newValue) => {
console.log('输入值变化:', newValue)
})
</script>`
},
{
title: '计算属性',
description: '学习 computed 计算属性,实现基于其他数据的派生数据。',
keyPoints: [
'computed 用于创建计算属性',
'计算属性会缓存结果',
'只有依赖的响应式数据发生变化时才会重新计算'
],
language: 'vue',
codeTitle: 'computed 计算属性',
code: `<template>
<div>
<input type="number" v-model.number="num1" placeholder="第一个数字" />
<input type="number" v-model.number="num2" placeholder="第二个数字" />
<p>计算结果: {{ computedSum }}</p>
<p>结果是否为偶数: {{ isEven }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const num1 = ref(0)
const num2 = ref(0)
// 计算属性 - 自动计算两数之和
const computedSum = computed(() => {
console.log('计算属性执行')
return (num1.value || 0) + (num2.value || 0)
})
// 另一个计算属性 - 判断结果是否为偶数
const isEven = computed(() => {
return computedSum.value % 2 === 0
})
</script>`
},
{
title: '生命周期',
description: '学习 Vue 3 组件的生命周期钩子函数。',
keyPoints: [
'onMounted 在组件挂载后执行',
'onUnmounted 在组件卸载前执行',
'生命周期钩子用于处理副作用'
],
language: 'vue',
codeTitle: '生命周期钩子',
code: `<template>
<div>
<p>组件已挂载 {{ mountedTime }} 秒</p>
<button @click="resetTimer">重置计时器</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const mountedTime = ref(0)
let timer = null
// 组件挂载后执行
onMounted(() => {
console.log('组件已挂载')
// 启动计时器
timer = setInterval(() => {
mountedTime.value++
}, 1000)
})
// 组件卸载前执行
onUnmounted(() => {
console.log('组件即将卸载')
// 清理计时器
if (timer) {
clearInterval(timer)
timer = null
}
})
// 重置计时器
const resetTimer = () => {
mountedTime.value = 0
}
</script>`
}
]
const currentTutorial = computed(() => tutorialSteps[currentStep.value])
const updateMessage = () => {
messageText.value = '消息已更新: ' + new Date().toLocaleTimeString()
}
const nextStep = () => {
if (currentStep.value < tutorialSteps.length - 1) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
const resetTimer = () => {
mountedTime.value = 0
}
const handleCopy = (code) => {
message.success('代码已复制到剪贴板')
}
// 计时器逻辑
let timer = null
onMounted(() => {
timer = setInterval(() => {
mountedTime.value++
}, 1000)
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<style scoped>
.tutorial-demo {
padding: 24px;
}
.tutorial-content {
padding: 16px 0;
}
.description {
font-size: 16px;
line-height: 1.6;
margin-bottom: 16px;
color: #555;
}
.key-points {
background: #f8f9fa;
padding: 16px;
border-radius: 6px;
margin-bottom: 20px;
border-left: 4px solid #1890ff;
}
.key-points h4 {
margin: 0 0 8px 0;
color: #333;
}
.key-points ul {
margin: 0;
padding-left: 20px;
}
.key-points li {
margin-bottom: 4px;
color: #555;
}
.tutorial-actions {
margin-top: 20px;
text-align: center;
}
.preview-container {
padding: 20px;
background: #fafafa;
border-radius: 6px;
min-height: 200px;
}
.demo-basic, .demo-reactive, .demo-computed, .demo-lifecycle {
padding: 16px;
}
.demo-basic h3 {
color: #1890ff;
margin-bottom: 16px;
}
.demo-reactive input, .demo-computed input {
margin-bottom: 12px;
width: 100%;
}
.demo-computed input {
margin-right: 8px;
width: calc(50% - 4px);
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
场景 3: 代码对比工具
vue
<template>
<div class="code-comparison">
<n-card title="代码对比工具" style="margin-bottom: 16px;">
<n-space>
<n-select
v-model:value="selectedComparison"
:options="comparisonOptions"
placeholder="选择对比场景"
style="width: 200px"
/>
<n-button @click="swapCodeBlocks" type="primary">
交换对比
</n-button>
<n-button @click="copyDifferences">
复制差异
</n-button>
</n-space>
</n-card>
<n-grid cols="2" x-gap="16">
<!-- 左侧代码 -->
<n-grid-item>
<n-card :title="currentComparison.leftTitle" size="small">
<template #header-extra>
<n-tag type="error" size="small">
- {{ getDifferenceCount().removed }} 行
</n-tag>
</template>
<C_Code
:code="currentComparison.leftCode"
:language="currentComparison.language"
:title="currentComparison.leftTitle"
:show-fullscreen="true"
:max-height="500"
@copy="handleCopy"
/>
</n-card>
</n-grid-item>
<!-- 右侧代码 -->
<n-grid-item>
<n-card :title="currentComparison.rightTitle" size="small">
<template #header-extra>
<n-tag type="success" size="small">
+ {{ getDifferenceCount().added }} 行
</n-tag>
</template>
<C_Code
:code="currentComparison.rightCode"
:language="currentComparison.language"
:title="currentComparison.rightTitle"
:show-fullscreen="true"
:max-height="500"
@copy="handleCopy"
/>
</n-card>
</n-grid-item>
</n-grid>
<!-- 差异统计 -->
<n-card class="mt-16px" title="差异统计">
<n-grid cols="4" x-gap="16">
<n-grid-item>
<n-statistic label="总行数变化" :value="getDifferenceCount().total" />
</n-grid-item>
<n-grid-item>
<n-statistic
label="新增行数"
:value="getDifferenceCount().added"
value-style="color: #52c41a;"
/>
</n-grid-item>
<n-grid-item>
<n-statistic
label="删除行数"
:value="getDifferenceCount().removed"
value-style="color: #ff4d4f;"
/>
</n-grid-item>
<n-grid-item>
<n-statistic
label="修改百分比"
:value="`${getDifferenceCount().percentage}%`"
/>
</n-grid-item>
</n-grid>
</n-card>
<!-- 差异详情 -->
<n-card class="mt-16px" title="详细差异">
<n-collapse>
<n-collapse-item
v-for="(diff, index) in detailedDifferences"
:key="index"
:title="diff.title"
>
<div class="diff-content">
<div class="diff-description">{{ diff.description }}</div>
<div v-if="diff.example" class="diff-example">
<h5>示例代码:</h5>
<C_Code
:code="diff.example"
:language="currentComparison.language"
:show-header="false"
@copy="handleCopy"
/>
</div>
</div>
</n-collapse-item>
</n-collapse>
</n-card>
</div>
</template>
<script setup>
const message = useMessage()
const selectedComparison = ref('vue-options-composition')
const comparisonOptions = [
{ label: 'Vue Options API vs Composition API', value: 'vue-options-composition' },
{ label: 'JavaScript vs TypeScript', value: 'js-ts' },
{ label: 'React Class vs Hooks', value: 'react-class-hooks' },
{ label: 'CSS vs Tailwind', value: 'css-tailwind' }
]
const comparisons = {
'vue-options-composition': {
leftTitle: 'Vue 2 Options API',
rightTitle: 'Vue 3 Composition API',
language: 'vue',
leftCode: `<template>
<div class="user-list">
<h2>用户列表</h2>
<div class="filters">
<input v-model="searchTerm" placeholder="搜索用户..." />
<select v-model="selectedRole">
<option value="">所有角色</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
</select>
</div>
<div class="user-grid">
<div
v-for="user in filteredUsers"
:key="user.id"
class="user-card"
@click="selectUser(user)"
>
<img :src="user.avatar" :alt="user.name" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<span class="role">{{ user.role }}</span>
</div>
</div>
<div v-if="loading" class="loading">
加载中...
</div>
</div>
</template>
<script>
export default {
name: 'UserList',
data() {
return {
users: [],
searchTerm: '',
selectedRole: '',
loading: false,
selectedUser: null
}
},
computed: {
filteredUsers() {
let filtered = this.users
if (this.searchTerm) {
filtered = filtered.filter(user =>
user.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(this.searchTerm.toLowerCase())
)
}
if (this.selectedRole) {
filtered = filtered.filter(user => user.role === this.selectedRole)
}
return filtered
}
},
watch: {
searchTerm(newTerm) {
this.debouncedSearch(newTerm)
}
},
methods: {
async fetchUsers() {
this.loading = true
try {
const response = await this.$http.get('/api/users')
this.users = response.data
} catch (error) {
this.$message.error('获取用户列表失败')
} finally {
this.loading = false
}
},
selectUser(user) {
this.selectedUser = user
this.$emit('user-selected', user)
},
debouncedSearch: debounce(function(term) {
console.log('搜索用户:', term)
}, 300)
},
async mounted() {
await this.fetchUsers()
}
}
</script>`,
rightCode: `<template>
<div class="user-list">
<h2>用户列表</h2>
<div class="filters">
<input v-model="searchTerm" placeholder="搜索用户..." />
<select v-model="selectedRole">
<option value="">所有角色</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
</select>
</div>
<div class="user-grid">
<div
v-for="user in filteredUsers"
:key="user.id"
class="user-card"
@click="selectUser(user)"
>
<img :src="user.avatar" :alt="user.name" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<span class="role">{{ user.role }}</span>
</div>
</div>
<div v-if="loading" class="loading">
加载中...
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { debounce } from 'lodash-es'
import { useMessage } from 'naive-ui'
import { useUsers } from '@/composables/useUsers'
// 响应式数据
const searchTerm = ref('')
const selectedRole = ref('')
const selectedUser = ref(null)
// 使用组合式函数
const { users, loading, fetchUsers } = useUsers()
const message = useMessage()
// 计算属性
const filteredUsers = computed(() => {
let filtered = users.value
if (searchTerm.value) {
filtered = filtered.filter(user =>
user.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.value.toLowerCase())
)
}
if (selectedRole.value) {
filtered = filtered.filter(user => user.role === selectedRole.value)
}
return filtered
})
// 事件定义
const emit = defineEmits(['user-selected'])
// 监听器
const debouncedSearch = debounce((term) => {
console.log('搜索用户:', term)
}, 300)
watch(searchTerm, (newTerm) => {
debouncedSearch(newTerm)
})
// 方法
const selectUser = (user) => {
selectedUser.value = user
emit('user-selected', user)
}
// 生命周期
onMounted(async () => {
try {
await fetchUsers()
} catch (error) {
message.error('获取用户列表失败')
}
})
</script>`
},
'js-ts': {
leftTitle: 'JavaScript (ES6+)',
rightTitle: 'TypeScript',
language: 'typescript',
leftCode: `// JavaScript 用户管理类
export class UserManager {
constructor(apiClient) {
this.apiClient = apiClient
this.users = []
this.cache = new Map()
}
async fetchUsers(filters = {}) {
const cacheKey = JSON.stringify(filters)
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)
}
try {
const response = await this.apiClient.get('/users', { params: filters })
const users = response.data
this.cache.set(cacheKey, users)
this.users = users
return users
} catch (error) {
console.error('获取用户失败:', error)
throw new Error('用户数据加载失败')
}
}
async createUser(userData) {
if (!userData.name || !userData.email) {
throw new Error('用户名和邮箱是必需的')
}
const response = await this.apiClient.post('/users', userData)
const newUser = response.data
this.users.push(newUser)
this.clearCache()
return newUser
}
async updateUser(id, userData) {
const userIndex = this.users.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error('用户不存在')
}
const response = await this.apiClient.put(\`/users/\${id}\`, userData)
const updatedUser = response.data
this.users[userIndex] = updatedUser
this.clearCache()
return updatedUser
}
findUsersByRole(role) {
return this.users.filter(user => user.role === role)
}
clearCache() {
this.cache.clear()
}
}`,
rightCode: `// TypeScript 用户管理类
interface User {
id: number
name: string
email: string
role: 'admin' | 'user' | 'moderator'
avatar?: string
createdAt: Date
updatedAt: Date
}
interface UserFilters {
role?: string
status?: 'active' | 'inactive'
search?: string
page?: number
limit?: number
}
interface ApiResponse<T> {
data: T
success: boolean
message?: string
}
interface ApiClient {
get<T>(url: string, config?: { params?: Record<string, any> }): Promise<ApiResponse<T>>
post<T>(url: string, data: any): Promise<ApiResponse<T>>
put<T>(url: string, data: any): Promise<ApiResponse<T>>
delete(url: string): Promise<ApiResponse<void>>
}
export class UserManager {
private users: User[] = []
private cache = new Map<string, User[]>()
constructor(private apiClient: ApiClient) {}
async fetchUsers(filters: UserFilters = {}): Promise<User[]> {
const cacheKey = JSON.stringify(filters)
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!
}
try {
const response = await this.apiClient.get<User[]>('/users', { params: filters })
const users = response.data
this.cache.set(cacheKey, users)
this.users = users
return users
} catch (error) {
console.error('获取用户失败:', error)
throw new Error('用户数据加载失败')
}
}
async createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
if (!userData.name || !userData.email) {
throw new Error('用户名和邮箱是必需的')
}
const response = await this.apiClient.post<User>('/users', userData)
const newUser = response.data
this.users.push(newUser)
this.clearCache()
return newUser
}
async updateUser(id: number, userData: Partial<Omit<User, 'id'>>): Promise<User> {
const userIndex = this.users.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error('用户不存在')
}
const response = await this.apiClient.put<User>(\`/users/\${id}\`, userData)
const updatedUser = response.data
this.users[userIndex] = updatedUser
this.clearCache()
return updatedUser
}
findUsersByRole(role: User['role']): User[] {
return this.users.filter(user => user.role === role)
}
getUserById(id: number): User | undefined {
return this.users.find(user => user.id === id)
}
private clearCache(): void {
this.cache.clear()
}
}`
}
}
const currentComparison = computed(() =>
comparisons[selectedComparison.value] || comparisons['vue-options-composition']
)
const detailedDifferences = computed(() => {
const key = selectedComparison.value
const differences = {
'vue-options-composition': [
{
title: '导入方式变化',
description: 'Composition API 需要显式导入所需的函数,而 Options API 通过 this 访问。',
example: `// Composition API
import { ref, computed, watch, onMounted } from 'vue'
// Options API
// 通过 this.xxx 直接访问`
},
{
title: '数据定义方式',
description: 'Composition API 使用 ref/reactive 创建响应式数据,Options API 在 data 函数中返回。',
example: `// Composition API
const searchTerm = ref('')
const users = ref([])
// Options API
data() {
return {
searchTerm: '',
users: []
}
}`
},
{
title: '逻辑组织方式',
description: 'Composition API 可以将相关逻辑组织在一起,Options API 按照选项类型分组。',
example: `// Composition API - 用户相关逻辑集中
const { users, loading, fetchUsers } = useUsers()
// Options API - 按选项类型分散
data() { /* 数据 */ },
methods: { /* 方法 */ },
computed: { /* 计算属性 */ }`
}
],
'js-ts': [
{
title: '类型定义',
description: 'TypeScript 提供完整的类型定义,JavaScript 依赖运行时检查。',
example: `// TypeScript
interface User {
id: number
name: string
email: string
}
// JavaScript
// 无编译时类型检查`
},
{
title: '函数参数类型',
description: 'TypeScript 明确指定参数和返回值类型,JavaScript 需要文档或注释说明。',
example: `// TypeScript
async createUser(userData: Omit<User, 'id'>): Promise<User>
// JavaScript
async createUser(userData) // 类型不明确`
}
]
}
return differences[key] || []
})
const getDifferenceCount = () => {
const leftLines = currentComparison.value.leftCode.split('\n').length
const rightLines = currentComparison.value.rightCode.split('\n').length
const diff = rightLines - leftLines
return {
total: Math.abs(diff),
added: Math.max(0, diff),
removed: Math.max(0, -diff),
percentage: Math.round((Math.abs(diff) / leftLines) * 100)
}
}
const swapCodeBlocks = () => {
const current = comparisons[selectedComparison.value]
const temp = current.leftCode
current.leftCode = current.rightCode
current.rightCode = temp
const tempTitle = current.leftTitle
current.leftTitle = current.rightTitle
current.rightTitle = tempTitle
message.success('代码块已交换')
}
const copyDifferences = () => {
const differences = detailedDifferences.value
.map(diff => `${diff.title}: ${diff.description}`)
.join('\n')
navigator.clipboard.writeText(differences)
message.success('差异详情已复制到剪贴板')
}
const handleCopy = (code) => {
message.success('代码已复制到剪贴板')
}
</script>
<style scoped>
.code-comparison {
padding: 24px;
}
.diff-content {
padding: 12px 0;
}
.diff-description {
font-size: 14px;
line-height: 1.6;
color: #555;
margin-bottom: 12px;
}
.diff-example h5 {
margin: 0 0 8px 0;
font-size: 13px;
color: #333;
font-weight: 600;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
🛠️ 高级用法
动态语言加载
vue
<template>
<div class="dynamic-language-demo">
<n-space class="mb-16px">
<n-select
v-model:value="selectedLanguage"
:options="availableLanguages"
placeholder="选择编程语言"
style="width: 200px"
/>
<n-button
@click="loadLanguageAndDisplay"
:loading="loading"
type="primary"
>
加载语言包
</n-button>
</n-space>
<C_Code
v-if="dynamicCode"
:code="dynamicCode"
:language="selectedLanguage"
:title="`${selectedLanguage} 示例代码`"
:auto-load-language="false"
@copy="handleCopy"
/>
</div>
</template>
<script setup>
import { useHighlight } from '@/plugins/highlight'
const highlight = useHighlight()
const message = useMessage()
const selectedLanguage = ref('')
const dynamicCode = ref('')
const loading = ref(false)
const availableLanguages = [
{ label: 'Go', value: 'go' },
{ label: 'Rust', value: 'rust' },
{ label: 'Swift', value: 'swift' },
{ label: 'Kotlin', value: 'kotlin' },
{ label: 'Ruby', value: 'ruby' },
]
const codeExamples = {
go: `package main
import (
"fmt"
"net/http"
"encoding/json"
)
type User struct {
ID int \`json:"id"\`
Name string \`json:"name"\`
Email string \`json:"email"\`
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
user := User{
ID: 1,
Name: "John Doe",
Email: "john@example.com",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/user", getUserHandler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}`,
rust: `use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Debug)]
struct UserManager {
users: HashMap<u32, User>,
next_id: u32,
}
impl UserManager {
fn new() -> Self {
UserManager {
users: HashMap::new(),
next_id: 1,
}
}
fn create_user(&mut self, name: String, email: String) -> &User {
let user = User {
id: self.next_id,
name,
email,
};
self.users.insert(self.next_id, user);
let created_user = self.users.get(&self.next_id).unwrap();
self.next_id += 1;
created_user
}
fn get_user(&self, id: u32) -> Option<&User> {
self.users.get(&id)
}
fn list_users(&self) -> Vec<&User> {
self.users.values().collect()
}
}
fn main() {
let mut manager = UserManager::new();
let user1 = manager.create_user(
"Alice".to_string(),
"alice@example.com".to_string()
);
println!("Created user: {:?}", user1);
let users = manager.list_users();
println!("All users: {:?}", users);
}`,
}
const loadLanguageAndDisplay = async () => {
if (!selectedLanguage.value) {
message.warning('请先选择一种编程语言')
return
}
loading.value = true
try {
// 检查语言包是否已加载
const loadedLanguages = highlight.getLoadedLanguages()
if (!loadedLanguages.includes(selectedLanguage.value)) {
message.info(`正在加载 ${selectedLanguage.value} 语言包...`)
await highlight.loadLanguage(selectedLanguage.value)
}
// 显示示例代码
dynamicCode.value =
codeExamples[selectedLanguage.value] || '// 暂无示例代码'
message.success(`${selectedLanguage.value} 语言包加载成功`)
} catch (error) {
message.error(`语言包加载失败: ${error.message}`)
console.error('Language loading error:', error)
} finally {
loading.value = false
}
}
const handleCopy = (code) => {
message.success('代码已复制到剪贴板')
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
自定义主题和样式
vue
<template>
<div class="custom-theme-demo">
<n-space class="mb-16px">
<n-select
v-model:value="selectedTheme"
:options="themeOptions"
placeholder="选择主题"
style="width: 150px"
/>
<n-color-picker v-model:value="customAccentColor" />
<n-button @click="applyCustomTheme"> 应用自定义主题 </n-button>
</n-space>
<C_Code
:code="themeCode"
language="css"
title="自定义主题样式"
:style="customCodeStyle"
class="custom-themed-code"
@copy="handleCopy"
/>
</div>
</template>
<script setup>
const selectedTheme = ref('dark')
const customAccentColor = ref('#00d4aa')
const themeOptions = [
{ label: '深色主题', value: 'dark' },
{ label: '浅色主题', value: 'light' },
{ label: '护眼主题', value: 'eye-care' },
{ label: '高对比度', value: 'high-contrast' },
]
const themeCode = `/* 自定义代码高亮主题 */
.custom-themed-code {
--code-bg: #1e1e1e;
--code-text: #d4d4d4;
--code-keyword: #569cd6;
--code-string: #ce9178;
--code-comment: #6a9955;
--code-number: #b5cea8;
--code-function: #dcdcaa;
--border-radius: 8px;
--header-bg: #2d2d30;
}
.c-code-wrapper {
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.c-code-header {
background: var(--header-bg);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.c-code-content .n-code {
background: var(--code-bg) !important;
color: var(--code-text) !important;
}
/* 语法高亮颜色 */
.hljs-keyword { color: var(--code-keyword); }
.hljs-string { color: var(--code-string); }
.hljs-comment { color: var(--code-comment); }
.hljs-number { color: var(--code-number); }
.hljs-function { color: var(--code-function); }`
const customCodeStyle = computed(() => {
const themes = {
dark: {
'--code-bg': '#1e1e1e',
'--code-text': '#d4d4d4',
'--header-bg': '#2d2d30',
},
light: {
'--code-bg': '#ffffff',
'--code-text': '#333333',
'--header-bg': '#f5f5f5',
},
'eye-care': {
'--code-bg': '#f7f3e9',
'--code-text': '#5c4b37',
'--header-bg': '#ede3d3',
},
'high-contrast': {
'--code-bg': '#000000',
'--code-text': '#ffffff',
'--header-bg': '#333333',
},
}
return {
...themes[selectedTheme.value],
'--accent-color': customAccentColor.value,
}
})
const applyCustomTheme = () => {
message.success(
`已应用 ${
themeOptions.find((t) => t.value === selectedTheme.value)?.label
} 主题`
)
}
const handleCopy = (code) => {
message.success('主题代码已复制')
}
</script>
<style scoped>
.custom-theme-demo {
padding: 20px;
}
.custom-themed-code {
transition: all 0.3s ease;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
代码片段管理器
vue
<template>
<div class="snippet-manager">
<n-card title="代码片段管理器" style="margin-bottom: 16px;">
<n-space>
<n-button @click="showAddModal = true" type="primary">
添加片段
</n-button>
<n-button @click="importSnippets"> 导入片段 </n-button>
<n-button @click="exportSnippets"> 导出片段 </n-button>
<n-input
v-model:value="searchTerm"
placeholder="搜索代码片段..."
style="width: 200px"
>
<template #prefix>
<div class="i-mdi:magnify"></div>
</template>
</n-input>
</n-space>
</n-card>
<n-grid cols="1 600:2 900:3" x-gap="16" y-gap="16">
<n-grid-item v-for="snippet in filteredSnippets" :key="snippet.id">
<n-card size="small">
<template #header>
<div class="snippet-header">
<span class="snippet-title">{{ snippet.title }}</span>
<div class="snippet-meta">
<n-tag
size="small"
:color="{ color: getLanguageColor(snippet.language) }"
>
{{ snippet.language }}
</n-tag>
<span class="snippet-date">{{
formatDate(snippet.createdAt)
}}</span>
</div>
</div>
</template>
<template #header-extra>
<n-dropdown
:options="getSnippetMenuOptions(snippet)"
@select="handleSnippetAction"
>
<n-button size="tiny" quaternary>
<template #icon>
<div class="i-mdi:dots-vertical"></div>
</template>
</n-button>
</n-dropdown>
</template>
<div class="snippet-content">
<p class="snippet-description">{{ snippet.description }}</p>
<C_Code
:code="snippet.code"
:language="snippet.language"
:show-header="false"
:max-height="200"
@copy="() => handleSnippetCopy(snippet)"
/>
<div class="snippet-tags" v-if="snippet.tags?.length">
<n-tag
v-for="tag in snippet.tags"
:key="tag"
size="small"
style="margin-right: 4px;"
>
{{ tag }}
</n-tag>
</div>
</div>
</n-card>
</n-grid-item>
</n-grid>
<!-- 添加/编辑片段模态框 -->
<n-modal v-model:show="showAddModal" preset="dialog" style="width: 800px;">
<template #header>
<span>{{ editingSnippet ? '编辑片段' : '添加片段' }}</span>
</template>
<div class="add-snippet-form">
<n-form :model="snippetForm" label-placement="top">
<n-grid cols="2" x-gap="16">
<n-grid-item>
<n-form-item label="标题" path="title">
<n-input
v-model:value="snippetForm.title"
placeholder="片段标题"
/>
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="语言" path="language">
<n-select
v-model:value="snippetForm.language"
:options="languageOptions"
placeholder="选择语言"
/>
</n-form-item>
</n-grid-item>
</n-grid>
<n-form-item label="描述" path="description">
<n-input
v-model:value="snippetForm.description"
type="textarea"
placeholder="片段描述"
:rows="2"
/>
</n-form-item>
<n-form-item label="代码" path="code">
<n-input
v-model:value="snippetForm.code"
type="textarea"
placeholder="在这里输入代码..."
:rows="10"
style="font-family: 'Fira Code', 'Consolas', monospace;"
/>
</n-form-item>
<n-form-item label="标签" path="tags">
<n-dynamic-tags v-model:value="snippetForm.tags" />
</n-form-item>
</n-form>
<div class="form-actions">
<n-space>
<n-button @click="closeAddModal">取消</n-button>
<n-button @click="saveSnippet" type="primary">
{{ editingSnippet ? '更新' : '保存' }}
</n-button>
</n-space>
</div>
</div>
</n-modal>
</div>
</template>
<script setup>
const message = useMessage()
const dialog = useDialog()
const searchTerm = ref('')
const showAddModal = ref(false)
const editingSnippet = ref(null)
const snippetForm = reactive({
title: '',
language: 'javascript',
description: '',
code: '',
tags: [],
})
const languageOptions = [
{ label: 'JavaScript', value: 'javascript' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'Python', value: 'python' },
{ label: 'Java', value: 'java' },
{ label: 'Go', value: 'go' },
{ label: 'Rust', value: 'rust' },
{ label: 'HTML', value: 'html' },
{ label: 'CSS', value: 'css' },
{ label: 'SQL', value: 'sql' },
{ label: 'Bash', value: 'bash' },
]
const snippets = ref([
{
id: 1,
title: 'Vue 3 响应式数据',
description: 'Vue 3 Composition API 中创建响应式数据的基本模式',
language: 'javascript',
code: `import { ref, reactive, computed } from 'vue'
// 基本响应式数据
const count = ref(0)
const message = ref('Hello Vue 3!')
// 响应式对象
const state = reactive({
user: null,
loading: false,
error: null
})
// 计算属性
const doubleCount = computed(() => count.value * 2)`,
tags: ['vue', 'composition-api', 'reactive'],
createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
},
{
id: 2,
title: 'JavaScript 防抖函数',
description: '实现防抖功能的工具函数,用于优化高频事件处理',
language: 'javascript',
code: `function debounce(func, wait, immediate = false) {
let timeout
return function executedFunction(...args) {
const later = () => {
timeout = null
if (!immediate) func.apply(this, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(this, args)
}
}
// 使用示例
const debouncedSearch = debounce((query) => {
console.log('搜索:', query)
}, 300)`,
tags: ['javascript', 'utility', 'performance'],
createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000),
},
{
id: 3,
title: 'CSS Flexbox 居中',
description: '使用 Flexbox 实现各种居中布局的常用模式',
language: 'css',
code: `/* 水平和垂直居中 */
.flex-center {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* 水平居中,垂直顶部对齐 */
.flex-center-top {
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 2rem;
}
/* 垂直居中,水平分散 */
.flex-space-between {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}`,
tags: ['css', 'flexbox', 'layout'],
createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000),
},
])
const filteredSnippets = computed(() => {
if (!searchTerm.value) {
return snippets.value
}
const term = searchTerm.value.toLowerCase()
return snippets.value.filter(
(snippet) =>
snippet.title.toLowerCase().includes(term) ||
snippet.description.toLowerCase().includes(term) ||
snippet.tags.some((tag) => tag.toLowerCase().includes(term)) ||
snippet.language.toLowerCase().includes(term)
)
})
const getLanguageColor = (language) => {
const colors = {
javascript: '#f7df1e',
typescript: '#3178c6',
python: '#3776ab',
java: '#ed8b00',
go: '#00add8',
rust: '#ce422b',
html: '#e34f26',
css: '#1572b6',
sql: '#336791',
bash: '#4eaa25',
}
return colors[language] || '#666666'
}
const formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric',
})
}
const getSnippetMenuOptions = (snippet) => {
return [
{ label: '编辑', key: 'edit', snippet },
{ label: '复制', key: 'copy', snippet },
{ label: '分享', key: 'share', snippet },
{ type: 'divider' },
{ label: '删除', key: 'delete', snippet },
]
}
const handleSnippetAction = (key, option) => {
const snippet = option.snippet
switch (key) {
case 'edit':
editSnippet(snippet)
break
case 'copy':
handleSnippetCopy(snippet)
break
case 'share':
shareSnippet(snippet)
break
case 'delete':
deleteSnippet(snippet)
break
}
}
const handleSnippetCopy = (snippet) => {
navigator.clipboard.writeText(snippet.code)
message.success(`"${snippet.title}" 代码已复制`)
}
const editSnippet = (snippet) => {
editingSnippet.value = snippet
Object.assign(snippetForm, {
title: snippet.title,
language: snippet.language,
description: snippet.description,
code: snippet.code,
tags: [...snippet.tags],
})
showAddModal.value = true
}
const shareSnippet = (snippet) => {
const shareData = {
title: snippet.title,
text: snippet.description,
url: `data:text/plain;charset=utf-8,${encodeURIComponent(snippet.code)}`,
}
if (navigator.share) {
navigator.share(shareData)
} else {
navigator.clipboard.writeText(snippet.code)
message.success('代码已复制到剪贴板')
}
}
const deleteSnippet = (snippet) => {
dialog.warning({
title: '确认删除',
content: `确定要删除代码片段 "${snippet.title}" 吗?`,
positiveText: '删除',
negativeText: '取消',
onPositiveClick: () => {
const index = snippets.value.findIndex((s) => s.id === snippet.id)
if (index > -1) {
snippets.value.splice(index, 1)
message.success('代码片段已删除')
}
},
})
}
const saveSnippet = () => {
if (!snippetForm.title || !snippetForm.code) {
message.error('标题和代码不能为空')
return
}
if (editingSnippet.value) {
// 更新现有片段
const index = snippets.value.findIndex(
(s) => s.id === editingSnippet.value.id
)
if (index > -1) {
snippets.value[index] = {
...editingSnippet.value,
...snippetForm,
updatedAt: new Date(),
}
message.success('代码片段已更新')
}
} else {
// 添加新片段
const newSnippet = {
id: Date.now(),
...snippetForm,
createdAt: new Date(),
updatedAt: new Date(),
}
snippets.value.push(newSnippet)
message.success('代码片段已保存')
}
closeAddModal()
}
const closeAddModal = () => {
showAddModal.value = false
editingSnippet.value = null
Object.assign(snippetForm, {
title: '',
language: 'javascript',
description: '',
code: '',
tags: [],
})
}
const exportSnippets = () => {
const dataStr = JSON.stringify(snippets.value, null, 2)
const blob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `code-snippets-${new Date().toISOString().split('T')[0]}.json`
a.click()
URL.revokeObjectURL(url)
message.success('代码片段已导出')
}
const importSnippets = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
try {
const importedSnippets = JSON.parse(e.target.result)
if (Array.isArray(importedSnippets)) {
snippets.value.push(...importedSnippets)
message.success(`成功导入 ${importedSnippets.length} 个代码片段`)
} else {
message.error('文件格式不正确')
}
} catch (error) {
message.error('导入失败: ' + error.message)
}
}
reader.readAsText(file)
}
input.click()
}
</script>
<style scoped>
.snippet-manager {
padding: 24px;
}
.snippet-header {
width: 100%;
}
.snippet-title {
font-weight: 600;
font-size: 14px;
}
.snippet-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.snippet-date {
font-size: 12px;
color: #999;
}
.snippet-content {
padding: 12px 0;
}
.snippet-description {
font-size: 13px;
color: #666;
line-height: 1.4;
margin-bottom: 12px;
}
.snippet-tags {
margin-top: 12px;
}
.add-snippet-form {
padding: 20px 0;
}
.form-actions {
text-align: right;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
⚠️ 注意事项
1. 语言包大小
vue
<!-- ✅ 推荐:按需加载语言包 -->
<script setup>
const loadLanguageOnDemand = async (language) => {
try {
await highlight.loadLanguage(language)
// 语言包加载成功
} catch (error) {
console.warn(`语言包加载失败: ${language}`)
}
}
</script>
<!-- ❌ 不推荐:一次性加载所有语言包 -->
<script setup>
import 'highlight.js/lib/languages/javascript'
import 'highlight.js/lib/languages/typescript'
// ... 加载太多语言包会增加包体积
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2. 代码内容安全
javascript
// ✅ 推荐:对用户输入的代码进行清理
const sanitizeCode = (code) => {
// 移除潜在的恶意脚本标签
return code
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '')
}
// ❌ 不推荐:直接使用未清理的用户输入
const unsafeCode = userInput // 可能包含恶意代码
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
3. 大型代码块性能
vue
<!-- ✅ 推荐:限制代码块高度 -->
<C_Code
:code="largeCode"
language="javascript"
:max-height="400" <!-- 限制高度避免渲染过多内容 -->
/>
<!-- ❌ 不推荐:显示超大代码块而不限制高度 -->
<C_Code
:code="veryLargeCode" <!-- 可能导致性能问题 -->
language="javascript"
/>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
4. 复制功能兼容性
javascript
// ✅ 推荐:提供复制功能的降级方案
const copyWithFallback = async (text) => {
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(text)
return true
} else {
// 降级方案:使用 textarea
const textarea = document.createElement('textarea')
textarea.value = text
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
return true
}
} catch (error) {
console.error('复制失败:', error)
return false
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
🐛 故障排除
常见问题
Q1: 语言高亮不生效?
A1: 检查语言包加载:
javascript
// 确保语言包已正确加载
const checkLanguageLoaded = async (language) => {
const highlight = useHighlight()
const loadedLanguages = highlight.getLoadedLanguages()
if (!loadedLanguages.includes(language)) {
await highlight.loadLanguage(language)
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Q2: 复制功能在某些浏览器不工作?
A2: 检查 HTTPS 和权限:
javascript
// 检查复制 API 是否可用
const checkClipboardAPI = () => {
if (!navigator.clipboard) {
console.warn('Clipboard API 不可用,可能需要 HTTPS')
return false
}
return true
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Q3: 代码块样式异常?
A3: 检查 CSS 冲突:
scss
// 确保代码块样式不被全局样式覆盖
.c-code-wrapper {
// 使用更高的特异性
.n-code {
font-family: 'Fira Code', 'Consolas', monospace !important;
line-height: 1.5 !important;
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Q4: 全屏模式样式问题?
A4: 检查 z-index 层级:
scss
// 确保全屏模态框在最顶层
.fullscreen-content {
z-index: 9999;
position: relative;
}
1
2
3
4
5
2
3
4
5
Q5: 长代码加载慢?
A5: 使用虚拟滚动优化:
vue
<template>
<!-- 对于超长代码,考虑分页或虚拟滚动 -->
<C_Code
:code="longCode"
language="javascript"
:max-height="500" <!-- 限制可视区域 -->
/>
</template>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
🎯 最佳实践
1. 代码内容管理
javascript
// ✅ 推荐:标准化的代码片段结构
interface CodeSnippet {
id: string
title: string
description: string
language: string
code: string
tags: string[]
category: string
difficulty: 'beginner' | 'intermediate' | 'advanced'
createdAt: Date
updatedAt: Date
}
// 创建代码片段的工厂函数
const createCodeSnippet = (data: Partial<CodeSnippet>): CodeSnippet => {
return {
id: data.id || generateUniqueId(),
title: data.title || '未命名代码片段',
description: data.description || '',
language: data.language || 'text',
code: data.code || '',
tags: data.tags || [],
category: data.category || 'misc',
difficulty: data.difficulty || 'beginner',
createdAt: data.createdAt || new Date(),
updatedAt: new Date()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2. 性能优化策略
vue
<template>
<!-- 使用 v-memo 优化大量代码块渲染 -->
<div
v-for="snippet in codeSnippets"
:key="snippet.id"
v-memo="[snippet.code, snippet.language, snippet.title]"
>
<C_Code
:code="snippet.code"
:language="snippet.language"
:title="snippet.title"
/>
</div>
</template>
<script setup>
// 使用防抖优化搜索
const debouncedSearch = debounce((term) => {
searchCodeSnippets(term)
}, 300)
// 虚拟化长列表
const visibleSnippets = computed(() => {
if (allSnippets.value.length > 50) {
return allSnippets.value.slice(
virtualScrollState.startIndex,
virtualScrollState.endIndex
)
}
return allSnippets.value
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
3. 无障碍访问支持
vue
<template>
<C_Code
:code="code"
language="javascript"
role="region"
aria-label="代码示例"
aria-describedby="code-description"
/>
<div id="code-description" class="sr-only">
这是一个 JavaScript 代码示例,展示了函数的基本用法
</div>
</template>
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
4. 主题和样式定制
scss
// 创建可复用的主题变量
:root {
// 默认主题
--code-bg: #ffffff;
--code-text: #333333;
--code-keyword: #0000ff;
--code-string: #008000;
--code-comment: #808080;
--code-number: #ff6600;
}
[data-theme='dark'] {
// 深色主题
--code-bg: #1e1e1e;
--code-text: #d4d4d4;
--code-keyword: #569cd6;
--code-string: #ce9178;
--code-comment: #6a9955;
--code-number: #b5cea8;
}
// 组件样式
.c-code-wrapper {
background: var(--code-bg);
color: var(--code-text);
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
// 语法高亮样式
.hljs-keyword {
color: var(--code-keyword);
}
.hljs-string {
color: var(--code-string);
}
.hljs-comment {
color: var(--code-comment);
}
.hljs-number {
color: var(--code-number);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
5. 错误处理和用户体验
vue
<script setup>
const codeState = reactive({
loading: false,
error: null,
code: '',
language: 'javascript',
})
// 带错误处理的代码加载
const loadCodeWithErrorHandling = async (source) => {
codeState.loading = true
codeState.error = null
try {
const code = await fetchCodeFromSource(source)
codeState.code = code
// 验证代码内容
if (code.length > 50000) {
message.warning('代码内容较长,可能影响性能')
}
} catch (error) {
codeState.error = error.message
message.error('代码加载失败: ' + error.message)
// 提供降级方案
codeState.code = '// 代码加载失败,请稍后重试'
} finally {
codeState.loading = false
}
}
// 复制功能的完整错误处理
const handleCopyWithErrorHandling = async (code) => {
try {
await navigator.clipboard.writeText(code)
message.success('代码已复制到剪贴板')
// 分析复制的代码
analytics.track('code_copied', {
language: codeState.language,
lines: code.split('\n').length,
characters: code.length,
})
} catch (error) {
console.error('复制失败:', error)
// 提供手动复制提示
message.error('自动复制失败,请手动复制代码')
// 尝试选中代码
selectCodeText()
}
}
const selectCodeText = () => {
const codeElement = document.querySelector('.n-code pre')
if (codeElement) {
const range = document.createRange()
range.selectNodeContents(codeElement)
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
6. 代码片段版本管理
javascript
// 代码片段版本控制系统
class SnippetVersionManager {
constructor() {
this.versions = new Map()
this.maxVersions = 10
}
saveVersion(snippetId, code, comment = '') {
if (!this.versions.has(snippetId)) {
this.versions.set(snippetId, [])
}
const versions = this.versions.get(snippetId)
const newVersion = {
id: generateVersionId(),
code,
comment,
timestamp: new Date(),
hash: generateCodeHash(code),
}
versions.unshift(newVersion)
// 限制版本数量
if (versions.length > this.maxVersions) {
versions.splice(this.maxVersions)
}
return newVersion
}
getVersions(snippetId) {
return this.versions.get(snippetId) || []
}
restoreVersion(snippetId, versionId) {
const versions = this.getVersions(snippetId)
const version = versions.find((v) => v.id === versionId)
if (version) {
return version.code
}
throw new Error('版本不存在')
}
compareVersions(snippetId, version1Id, version2Id) {
const versions = this.getVersions(snippetId)
const v1 = versions.find((v) => v.id === version1Id)
const v2 = versions.find((v) => v.id === version2Id)
if (!v1 || !v2) {
throw new Error('版本不存在')
}
return {
version1: v1,
version2: v2,
differences: calculateDifferences(v1.code, v2.code),
}
}
}
// 使用示例
const versionManager = new SnippetVersionManager()
const saveSnippetVersion = (snippet, comment) => {
const version = versionManager.saveVersion(snippet.id, snippet.code, comment)
message.success(`版本 ${version.id} 已保存`)
return version
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
📝 更新日志
v1.0.0 (2025-07-19)
- ✨ 基于 Naive UI NCode 的完整组件封装
- ✨ 支持 30+ 种编程语言高亮显示
- ✨ 内置一键复制和全屏查看功能
- ✨ 动态语言包加载,按需引入
- ✨ 丰富的编程语言图标映射
- ✨ 悬浮复制按钮和智能交互
- ✨ ESC 键快捷操作支持
- ✨ 完整的 TypeScript 类型定义
- ✨ 自定义主题和样式配置
- ✨ 高性能代码渲染引擎
🤝 贡献指南
- Fork 项目
- 创建功能分支 (
git checkout -b feature/amazing-feature
) - 提交更改 (
git commit -m 'Add amazing feature'
) - 推送到分支 (
git push origin feature/amazing-feature
) - 创建 Pull Request
📄 许可证
Copyright (c) 2025 by ChenYu, All Rights Reserved.
💡 提示: 这个代码高亮组件基于强大的 highlight.js 库构建,提供了完整的代码展示和交互功能。支持多种编程语言、主题定制、一键复制等特性,适用于技术文档、代码教程、API 文档等各种代码展示场景。无论是简单的代码片段还是复杂的代码对比,都能提供专业级的代码高亮体验。结合 TypeScript 支持和高度可定制的配置,让代码展示既美观又实用。如果遇到问题请先查看文档,或者在团队群里讨论。让我们一起打造更优雅的代码展示体验! 🎨