officialblock 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -5
- package/dist/official-block.cjs.js +186 -1
- package/dist/official-block.es.js +22627 -70
- package/dist/official-block.umd.js +186 -1
- package/dist/style.css +1 -1
- package/package.json +12 -2
- package/src/App.vue +32 -82
- package/src/components/ArticleList/article.vue +73 -0
- package/src/components/ArticleList/contact.vue +95 -0
- package/src/components/ArticleList/index.vue +220 -48
- package/src/components/ArticleList/setting.vue +407 -0
- package/src/components/Button/index.vue +183 -0
- package/src/components/HeroSlide/index.ts +1 -0
- package/src/components/HeroSlide/index.vue +21 -3
- package/src/components/HeroSlide/type.ts +19 -0
- package/src/components/Media/index.vue +327 -0
- package/src/components/Operate/index.vue +74 -0
- package/src/components/RichTextEditor/RichTextEditor.vue +277 -0
- package/src/components/RichTextEditor/index.ts +7 -0
- package/src/components/ThemePreview/ThemePreview.vue +462 -0
- package/src/components/ThemePreview/index.ts +4 -0
- package/src/components/index.ts +14 -0
- package/src/composables/useTheme.ts +205 -0
- package/src/index.ts +26 -8
- package/src/main.ts +16 -1
- package/src/router/index.ts +84 -0
- package/src/style.css +0 -1
- package/src/styles/editor.scss +649 -0
- package/src/styles/test.scss +20 -0
- package/src/styles/variables.scss +639 -0
- package/src/types.ts +8 -0
- package/src/utils/common.ts +13 -0
- package/src/utils/theme.ts +335 -0
- package/src/views/Layout.vue +247 -0
- package/src/views/NotFound.vue +114 -0
- package/src/views/components/ArticleListDemo.vue +167 -0
- package/src/views/components/HeroSlideDemo.vue +353 -0
- package/src/views/components/RichTextEditorDemo.vue +53 -0
- package/src/views/components/ThemeDemo.vue +477 -0
- package/src/views/guide/Installation.vue +234 -0
- package/src/views/guide/Introduction.vue +174 -0
- package/src/views/guide/QuickStart.vue +265 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主题工具函数
|
|
3
|
+
* 提供便捷的主题相关工具方法
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 颜色工具函数
|
|
7
|
+
export class ThemeUtils {
|
|
8
|
+
/**
|
|
9
|
+
* 将十六进制颜色转换为RGB
|
|
10
|
+
*/
|
|
11
|
+
static hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
12
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
|
13
|
+
return result ? {
|
|
14
|
+
r: parseInt(result[1], 16),
|
|
15
|
+
g: parseInt(result[2], 16),
|
|
16
|
+
b: parseInt(result[3], 16)
|
|
17
|
+
} : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 将RGB颜色转换为十六进制
|
|
22
|
+
*/
|
|
23
|
+
static rgbToHex(r: number, g: number, b: number): string {
|
|
24
|
+
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 调整颜色亮度
|
|
29
|
+
*/
|
|
30
|
+
static adjustBrightness(hex: string, percent: number): string {
|
|
31
|
+
const rgb = this.hexToRgb(hex)
|
|
32
|
+
if (!rgb) return hex
|
|
33
|
+
|
|
34
|
+
const adjust = (color: number) => {
|
|
35
|
+
const adjusted = Math.round(color * (1 + percent / 100))
|
|
36
|
+
return Math.max(0, Math.min(255, adjusted))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return this.rgbToHex(
|
|
40
|
+
adjust(rgb.r),
|
|
41
|
+
adjust(rgb.g),
|
|
42
|
+
adjust(rgb.b)
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 生成颜色色阶
|
|
48
|
+
*/
|
|
49
|
+
static generateColorScale(baseColor: string): Record<string, string> {
|
|
50
|
+
const scales = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
51
|
+
const result: Record<string, string> = {}
|
|
52
|
+
|
|
53
|
+
scales.forEach((scale, index) => {
|
|
54
|
+
let brightness: number
|
|
55
|
+
if (scale <= 500) {
|
|
56
|
+
// 浅色:从 +80% 到 0%
|
|
57
|
+
brightness = 80 - (index * 16)
|
|
58
|
+
} else {
|
|
59
|
+
// 深色:从 -20% 到 -80%
|
|
60
|
+
brightness = -20 - ((index - 5) * 12)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
result[scale.toString()] = this.adjustBrightness(baseColor, brightness)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 检查颜色对比度是否符合可访问性标准
|
|
71
|
+
*/
|
|
72
|
+
static getContrastRatio(color1: string, color2: string): number {
|
|
73
|
+
const getLuminance = (hex: string): number => {
|
|
74
|
+
const rgb = this.hexToRgb(hex)
|
|
75
|
+
if (!rgb) return 0
|
|
76
|
+
|
|
77
|
+
const normalize = (color: number) => {
|
|
78
|
+
const c = color / 255
|
|
79
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return 0.2126 * normalize(rgb.r) + 0.7152 * normalize(rgb.g) + 0.0722 * normalize(rgb.b)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lum1 = getLuminance(color1)
|
|
86
|
+
const lum2 = getLuminance(color2)
|
|
87
|
+
const brightest = Math.max(lum1, lum2)
|
|
88
|
+
const darkest = Math.min(lum1, lum2)
|
|
89
|
+
|
|
90
|
+
return (brightest + 0.05) / (darkest + 0.05)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 判断颜色是否为深色
|
|
95
|
+
*/
|
|
96
|
+
static isDarkColor(hex: string): boolean {
|
|
97
|
+
const rgb = this.hexToRgb(hex)
|
|
98
|
+
if (!rgb) return false
|
|
99
|
+
|
|
100
|
+
// 使用相对亮度公式
|
|
101
|
+
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000
|
|
102
|
+
return brightness < 128
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 获取颜色的最佳文本颜色(黑色或白色)
|
|
107
|
+
*/
|
|
108
|
+
static getBestTextColor(backgroundColor: string): string {
|
|
109
|
+
return this.isDarkColor(backgroundColor) ? '#ffffff' : '#000000'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 混合两种颜色
|
|
114
|
+
*/
|
|
115
|
+
static mixColors(color1: string, color2: string, ratio: number = 0.5): string {
|
|
116
|
+
const rgb1 = this.hexToRgb(color1)
|
|
117
|
+
const rgb2 = this.hexToRgb(color2)
|
|
118
|
+
|
|
119
|
+
if (!rgb1 || !rgb2) return color1
|
|
120
|
+
|
|
121
|
+
const mix = (c1: number, c2: number) => Math.round(c1 * (1 - ratio) + c2 * ratio)
|
|
122
|
+
|
|
123
|
+
return this.rgbToHex(
|
|
124
|
+
mix(rgb1.r, rgb2.r),
|
|
125
|
+
mix(rgb1.g, rgb2.g),
|
|
126
|
+
mix(rgb1.b, rgb2.b)
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 响应式工具函数
|
|
132
|
+
export class ResponsiveUtils {
|
|
133
|
+
/**
|
|
134
|
+
* 获取当前屏幕断点
|
|
135
|
+
*/
|
|
136
|
+
static getCurrentBreakpoint(): string {
|
|
137
|
+
const width = window.innerWidth
|
|
138
|
+
|
|
139
|
+
if (width < 475) return 'xs'
|
|
140
|
+
if (width < 640) return 'sm'
|
|
141
|
+
if (width < 768) return 'md'
|
|
142
|
+
if (width < 1024) return 'lg'
|
|
143
|
+
if (width < 1280) return 'xl'
|
|
144
|
+
return '2xl'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 检查是否为移动设备
|
|
149
|
+
*/
|
|
150
|
+
static isMobile(): boolean {
|
|
151
|
+
return window.innerWidth < 768
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 检查是否为平板设备
|
|
156
|
+
*/
|
|
157
|
+
static isTablet(): boolean {
|
|
158
|
+
const width = window.innerWidth
|
|
159
|
+
return width >= 768 && width < 1024
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 检查是否为桌面设备
|
|
164
|
+
*/
|
|
165
|
+
static isDesktop(): boolean {
|
|
166
|
+
return window.innerWidth >= 1024
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 监听屏幕尺寸变化
|
|
171
|
+
*/
|
|
172
|
+
static onBreakpointChange(callback: (breakpoint: string) => void): () => void {
|
|
173
|
+
let currentBreakpoint = this.getCurrentBreakpoint()
|
|
174
|
+
|
|
175
|
+
const handleResize = () => {
|
|
176
|
+
const newBreakpoint = this.getCurrentBreakpoint()
|
|
177
|
+
if (newBreakpoint !== currentBreakpoint) {
|
|
178
|
+
currentBreakpoint = newBreakpoint
|
|
179
|
+
callback(newBreakpoint)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
window.addEventListener('resize', handleResize)
|
|
184
|
+
|
|
185
|
+
// 返回清理函数
|
|
186
|
+
return () => {
|
|
187
|
+
window.removeEventListener('resize', handleResize)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 动画工具函数
|
|
193
|
+
export class AnimationUtils {
|
|
194
|
+
/**
|
|
195
|
+
* 缓动函数
|
|
196
|
+
*/
|
|
197
|
+
static easing = {
|
|
198
|
+
linear: (t: number) => t,
|
|
199
|
+
easeInQuad: (t: number) => t * t,
|
|
200
|
+
easeOutQuad: (t: number) => t * (2 - t),
|
|
201
|
+
easeInOutQuad: (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
202
|
+
easeInCubic: (t: number) => t * t * t,
|
|
203
|
+
easeOutCubic: (t: number) => (--t) * t * t + 1,
|
|
204
|
+
easeInOutCubic: (t: number) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 数值动画
|
|
209
|
+
*/
|
|
210
|
+
static animate(
|
|
211
|
+
from: number,
|
|
212
|
+
to: number,
|
|
213
|
+
duration: number,
|
|
214
|
+
callback: (value: number) => void,
|
|
215
|
+
easing: (t: number) => number = this.easing.easeOutQuad
|
|
216
|
+
): () => void {
|
|
217
|
+
const startTime = performance.now()
|
|
218
|
+
let animationId: number
|
|
219
|
+
|
|
220
|
+
const step = (currentTime: number) => {
|
|
221
|
+
const elapsed = currentTime - startTime
|
|
222
|
+
const progress = Math.min(elapsed / duration, 1)
|
|
223
|
+
const easedProgress = easing(progress)
|
|
224
|
+
const currentValue = from + (to - from) * easedProgress
|
|
225
|
+
|
|
226
|
+
callback(currentValue)
|
|
227
|
+
|
|
228
|
+
if (progress < 1) {
|
|
229
|
+
animationId = requestAnimationFrame(step)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
animationId = requestAnimationFrame(step)
|
|
234
|
+
|
|
235
|
+
// 返回取消函数
|
|
236
|
+
return () => {
|
|
237
|
+
if (animationId) {
|
|
238
|
+
cancelAnimationFrame(animationId)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 滚动到指定位置
|
|
245
|
+
*/
|
|
246
|
+
static scrollTo(
|
|
247
|
+
element: HTMLElement | Window,
|
|
248
|
+
to: number,
|
|
249
|
+
duration: number = 300,
|
|
250
|
+
easing: (t: number) => number = this.easing.easeOutQuad
|
|
251
|
+
): Promise<void> {
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
const isWindow = element === window
|
|
254
|
+
const from = isWindow ? window.pageYOffset : (element as HTMLElement).scrollTop
|
|
255
|
+
|
|
256
|
+
this.animate(from, to, duration, (value) => {
|
|
257
|
+
if (isWindow) {
|
|
258
|
+
window.scrollTo(0, value)
|
|
259
|
+
} else {
|
|
260
|
+
(element as HTMLElement).scrollTop = value
|
|
261
|
+
}
|
|
262
|
+
}, easing)
|
|
263
|
+
|
|
264
|
+
setTimeout(resolve, duration)
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 存储工具函数
|
|
270
|
+
export class StorageUtils {
|
|
271
|
+
/**
|
|
272
|
+
* 安全的 localStorage 操作
|
|
273
|
+
*/
|
|
274
|
+
static setItem(key: string, value: any): boolean {
|
|
275
|
+
try {
|
|
276
|
+
localStorage.setItem(key, JSON.stringify(value))
|
|
277
|
+
return true
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.warn('Failed to save to localStorage:', error)
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
static getItem<T>(key: string, defaultValue?: T): T | null {
|
|
285
|
+
try {
|
|
286
|
+
const item = localStorage.getItem(key)
|
|
287
|
+
return item ? JSON.parse(item) : defaultValue || null
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.warn('Failed to read from localStorage:', error)
|
|
290
|
+
return defaultValue || null
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
static removeItem(key: string): boolean {
|
|
295
|
+
try {
|
|
296
|
+
localStorage.removeItem(key)
|
|
297
|
+
return true
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.warn('Failed to remove from localStorage:', error)
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 清理过期的存储项
|
|
306
|
+
*/
|
|
307
|
+
static cleanExpired(): void {
|
|
308
|
+
const now = Date.now()
|
|
309
|
+
const keysToRemove: string[] = []
|
|
310
|
+
|
|
311
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
312
|
+
const key = localStorage.key(i)
|
|
313
|
+
if (key && key.startsWith('officialblock-')) {
|
|
314
|
+
try {
|
|
315
|
+
const item = JSON.parse(localStorage.getItem(key) || '{}')
|
|
316
|
+
if (item.expires && item.expires < now) {
|
|
317
|
+
keysToRemove.push(key)
|
|
318
|
+
}
|
|
319
|
+
} catch (error) {
|
|
320
|
+
// 忽略解析错误
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
keysToRemove.forEach(key => localStorage.removeItem(key))
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 导出所有工具类
|
|
330
|
+
export default {
|
|
331
|
+
ThemeUtils,
|
|
332
|
+
ResponsiveUtils,
|
|
333
|
+
AnimationUtils,
|
|
334
|
+
StorageUtils
|
|
335
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<a-layout class="layout">
|
|
3
|
+
<!-- 顶部导航栏 -->
|
|
4
|
+
<a-layout-header class="header">
|
|
5
|
+
<div class="header-content">
|
|
6
|
+
<div class="logo">
|
|
7
|
+
<h1>OfficialBlock</h1>
|
|
8
|
+
<a-tag color="blue" size="small">v{{ version }}</a-tag>
|
|
9
|
+
</div>
|
|
10
|
+
<nav class="nav">
|
|
11
|
+
<router-link to="/guide/introduction" class="nav-link">指南</router-link>
|
|
12
|
+
<router-link to="/components/article-list" class="nav-link">组件</router-link>
|
|
13
|
+
<a-button
|
|
14
|
+
type="text"
|
|
15
|
+
class="mobile-menu-btn"
|
|
16
|
+
@click="toggleMobileMenu"
|
|
17
|
+
:icon="isMobileMenuOpen ? 'icon-close' : 'icon-menu'"
|
|
18
|
+
>
|
|
19
|
+
<template #icon>
|
|
20
|
+
<icon-menu v-if="!isMobileMenuOpen" />
|
|
21
|
+
<icon-close v-if="isMobileMenuOpen" />
|
|
22
|
+
</template>
|
|
23
|
+
</a-button>
|
|
24
|
+
</nav>
|
|
25
|
+
</div>
|
|
26
|
+
</a-layout-header>
|
|
27
|
+
|
|
28
|
+
<a-layout class="main-container">
|
|
29
|
+
<!-- 移动端遮罩层 -->
|
|
30
|
+
<div
|
|
31
|
+
v-if="isMobileMenuOpen"
|
|
32
|
+
class="mobile-overlay"
|
|
33
|
+
@click="toggleMobileMenu"
|
|
34
|
+
></div>
|
|
35
|
+
|
|
36
|
+
<!-- 左侧菜单栏 -->
|
|
37
|
+
<a-layout-sider
|
|
38
|
+
class="sidebar"
|
|
39
|
+
:class="{ 'mobile-open': isMobileMenuOpen }"
|
|
40
|
+
:width="280"
|
|
41
|
+
:collapsed="false"
|
|
42
|
+
:collapsible="false"
|
|
43
|
+
>
|
|
44
|
+
<a-menu
|
|
45
|
+
:selected-keys="selectedKeys"
|
|
46
|
+
:default-open-keys="['guide', 'components']"
|
|
47
|
+
:style="{ height: '100%', borderRight: 0 }"
|
|
48
|
+
@menu-item-click="handleMenuClick"
|
|
49
|
+
>
|
|
50
|
+
<a-sub-menu key="guide" title="开发指南">
|
|
51
|
+
<template #icon>
|
|
52
|
+
<icon-book />
|
|
53
|
+
</template>
|
|
54
|
+
<a-menu-item key="/guide/introduction">介绍</a-menu-item>
|
|
55
|
+
<a-menu-item key="/guide/installation">安装</a-menu-item>
|
|
56
|
+
<a-menu-item key="/guide/quickstart">快速开始</a-menu-item>
|
|
57
|
+
</a-sub-menu>
|
|
58
|
+
|
|
59
|
+
<a-sub-menu key="components" title="组件">
|
|
60
|
+
<template #icon>
|
|
61
|
+
<icon-apps />
|
|
62
|
+
</template>
|
|
63
|
+
<a-menu-item key="/components/article-list">ArticleList 文章列表</a-menu-item>
|
|
64
|
+
<a-menu-item key="/components/hero-slide">HeroSlide 轮播图</a-menu-item>
|
|
65
|
+
<a-menu-item key="/components/rich-text-editor">RichTextEditor 富文本编辑器</a-menu-item>
|
|
66
|
+
<a-menu-item key="/components/theme">Theme 主题系统</a-menu-item>
|
|
67
|
+
</a-sub-menu>
|
|
68
|
+
</a-menu>
|
|
69
|
+
</a-layout-sider>
|
|
70
|
+
|
|
71
|
+
<!-- 主内容区域 -->
|
|
72
|
+
<a-layout-content class="content">
|
|
73
|
+
<router-view />
|
|
74
|
+
</a-layout-content>
|
|
75
|
+
</a-layout>
|
|
76
|
+
</a-layout>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script setup lang="ts">
|
|
80
|
+
import { ref, watch } from 'vue'
|
|
81
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
82
|
+
import {
|
|
83
|
+
IconMenu,
|
|
84
|
+
IconClose,
|
|
85
|
+
IconBook,
|
|
86
|
+
IconApps
|
|
87
|
+
} from '@arco-design/web-vue/es/icon'
|
|
88
|
+
|
|
89
|
+
const route = useRoute()
|
|
90
|
+
const router = useRouter()
|
|
91
|
+
|
|
92
|
+
const version = ref('1.0.1')
|
|
93
|
+
const isMobileMenuOpen = ref(false)
|
|
94
|
+
const selectedKeys = ref([route.path])
|
|
95
|
+
|
|
96
|
+
// 监听路由变化,更新选中的菜单项
|
|
97
|
+
watch(() => route.path, (newPath) => {
|
|
98
|
+
selectedKeys.value = [newPath]
|
|
99
|
+
}, { immediate: true })
|
|
100
|
+
|
|
101
|
+
const toggleMobileMenu = () => {
|
|
102
|
+
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const handleMenuClick = (key: string) => {
|
|
106
|
+
router.push(key)
|
|
107
|
+
// 在移动端点击菜单项后关闭菜单
|
|
108
|
+
if (window.innerWidth <= 768) {
|
|
109
|
+
isMobileMenuOpen.value = false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<style scoped>
|
|
115
|
+
.layout {
|
|
116
|
+
min-height: 100vh;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.header {
|
|
120
|
+
background: #fff;
|
|
121
|
+
border-bottom: 1px solid var(--color-border-2);
|
|
122
|
+
position: fixed;
|
|
123
|
+
top: 0;
|
|
124
|
+
left: 0;
|
|
125
|
+
right: 0;
|
|
126
|
+
z-index: 1000;
|
|
127
|
+
height: 60px;
|
|
128
|
+
padding: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.header-content {
|
|
132
|
+
max-width: 1200px;
|
|
133
|
+
margin: 0 auto;
|
|
134
|
+
padding: 0 20px;
|
|
135
|
+
height: 100%;
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.logo {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 12px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.logo h1 {
|
|
148
|
+
margin: 0;
|
|
149
|
+
font-size: 24px;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
color: rgb(var(--primary-6));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.nav {
|
|
155
|
+
display: flex;
|
|
156
|
+
gap: 32px;
|
|
157
|
+
align-items: center;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.nav-link {
|
|
161
|
+
color: var(--color-text-2);
|
|
162
|
+
text-decoration: none;
|
|
163
|
+
font-weight: 500;
|
|
164
|
+
transition: color 0.3s;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.nav-link:hover,
|
|
168
|
+
.nav-link.router-link-active {
|
|
169
|
+
color: rgb(var(--primary-6));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.mobile-menu-btn {
|
|
173
|
+
display: none;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.main-container {
|
|
177
|
+
padding-top: 60px;
|
|
178
|
+
min-height: calc(100vh - 60px);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.sidebar {
|
|
182
|
+
background: #fff;
|
|
183
|
+
border-right: 1px solid var(--color-border-2);
|
|
184
|
+
position: fixed;
|
|
185
|
+
left: 0;
|
|
186
|
+
top: 60px;
|
|
187
|
+
bottom: 0;
|
|
188
|
+
overflow-y: auto;
|
|
189
|
+
z-index: 999;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.content {
|
|
193
|
+
margin-left: 280px;
|
|
194
|
+
padding: 32px;
|
|
195
|
+
background: var(--color-bg-1);
|
|
196
|
+
min-height: calc(100vh - 60px);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.mobile-overlay {
|
|
200
|
+
position: fixed;
|
|
201
|
+
top: 0;
|
|
202
|
+
left: 0;
|
|
203
|
+
right: 0;
|
|
204
|
+
bottom: 0;
|
|
205
|
+
background: rgba(0, 0, 0, 0.5);
|
|
206
|
+
z-index: 1000;
|
|
207
|
+
display: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* 响应式设计 */
|
|
211
|
+
@media (max-width: 768px) {
|
|
212
|
+
.mobile-menu-btn {
|
|
213
|
+
display: inline-flex;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.nav-link {
|
|
217
|
+
display: none;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.mobile-overlay {
|
|
221
|
+
display: block;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.sidebar {
|
|
225
|
+
transform: translateX(-100%);
|
|
226
|
+
transition: transform 0.3s;
|
|
227
|
+
z-index: 1001;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.sidebar.mobile-open {
|
|
231
|
+
transform: translateX(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.content {
|
|
235
|
+
margin-left: 0;
|
|
236
|
+
padding: 16px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.header-content {
|
|
240
|
+
padding: 0 16px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.nav {
|
|
244
|
+
gap: 16px;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="not-found">
|
|
3
|
+
<div class="content">
|
|
4
|
+
<h1>404</h1>
|
|
5
|
+
<h2>页面未找到</h2>
|
|
6
|
+
<p>抱歉,您访问的页面不存在。</p>
|
|
7
|
+
<div class="actions">
|
|
8
|
+
<router-link to="/guide/introduction" class="btn btn-primary">
|
|
9
|
+
返回首页
|
|
10
|
+
</router-link>
|
|
11
|
+
<router-link to="/components/article-list" class="btn btn-secondary">
|
|
12
|
+
查看组件
|
|
13
|
+
</router-link>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
// 404 页面
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<style scoped>
|
|
24
|
+
.not-found {
|
|
25
|
+
min-height: 100vh;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
30
|
+
color: white;
|
|
31
|
+
text-align: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.content h1 {
|
|
35
|
+
font-size: 120px;
|
|
36
|
+
font-weight: 700;
|
|
37
|
+
margin: 0;
|
|
38
|
+
line-height: 1;
|
|
39
|
+
opacity: 0.8;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.content h2 {
|
|
43
|
+
font-size: 32px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
margin: 16px 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.content p {
|
|
49
|
+
font-size: 18px;
|
|
50
|
+
margin: 24px 0 32px 0;
|
|
51
|
+
opacity: 0.9;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.actions {
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 16px;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
flex-wrap: wrap;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.btn {
|
|
62
|
+
padding: 12px 24px;
|
|
63
|
+
border-radius: 8px;
|
|
64
|
+
text-decoration: none;
|
|
65
|
+
font-weight: 500;
|
|
66
|
+
transition: all 0.3s;
|
|
67
|
+
display: inline-block;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.btn-primary {
|
|
71
|
+
background: rgba(255, 255, 255, 0.2);
|
|
72
|
+
color: white;
|
|
73
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.btn-primary:hover {
|
|
77
|
+
background: rgba(255, 255, 255, 0.3);
|
|
78
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.btn-secondary {
|
|
82
|
+
background: transparent;
|
|
83
|
+
color: white;
|
|
84
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.btn-secondary:hover {
|
|
88
|
+
background: rgba(255, 255, 255, 0.1);
|
|
89
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@media (max-width: 768px) {
|
|
93
|
+
.content h1 {
|
|
94
|
+
font-size: 80px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.content h2 {
|
|
98
|
+
font-size: 24px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.content p {
|
|
102
|
+
font-size: 16px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.actions {
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
align-items: center;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.btn {
|
|
111
|
+
width: 200px;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
</style>
|