officialblock 1.0.1 → 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.
Files changed (38) hide show
  1. package/README.md +25 -1
  2. package/dist/official-block.cjs.js +186 -1
  3. package/dist/official-block.es.js +22619 -73
  4. package/dist/official-block.umd.js +186 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +12 -2
  7. package/src/App.vue +32 -82
  8. package/src/components/ArticleList/article.vue +73 -0
  9. package/src/components/ArticleList/contact.vue +95 -0
  10. package/src/components/ArticleList/index.vue +215 -48
  11. package/src/components/ArticleList/setting.vue +407 -0
  12. package/src/components/Button/index.vue +183 -0
  13. package/src/components/Media/index.vue +327 -0
  14. package/src/components/Operate/index.vue +74 -0
  15. package/src/components/RichTextEditor/RichTextEditor.vue +277 -0
  16. package/src/components/RichTextEditor/index.ts +7 -0
  17. package/src/components/ThemePreview/ThemePreview.vue +462 -0
  18. package/src/components/ThemePreview/index.ts +4 -0
  19. package/src/components/index.ts +3 -0
  20. package/src/composables/useTheme.ts +205 -0
  21. package/src/index.ts +15 -4
  22. package/src/main.ts +16 -1
  23. package/src/router/index.ts +84 -0
  24. package/src/style.css +0 -1
  25. package/src/styles/editor.scss +649 -0
  26. package/src/styles/test.scss +20 -0
  27. package/src/styles/variables.scss +639 -0
  28. package/src/utils/common.ts +13 -0
  29. package/src/utils/theme.ts +335 -0
  30. package/src/views/Layout.vue +247 -0
  31. package/src/views/NotFound.vue +114 -0
  32. package/src/views/components/ArticleListDemo.vue +167 -0
  33. package/src/views/components/HeroSlideDemo.vue +353 -0
  34. package/src/views/components/RichTextEditorDemo.vue +53 -0
  35. package/src/views/components/ThemeDemo.vue +477 -0
  36. package/src/views/guide/Installation.vue +234 -0
  37. package/src/views/guide/Introduction.vue +174 -0
  38. 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>