@tnotesjs/core 0.7.0 → 0.8.0

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 (55) hide show
  1. package/commands/BaseCommand.ts +58 -0
  2. package/commands/build/BuildCommand.ts +25 -0
  3. package/commands/build/PreviewCommand.ts +29 -0
  4. package/commands/build/index.ts +8 -0
  5. package/commands/dev/DevCommand.ts +75 -0
  6. package/commands/dev/index.ts +7 -0
  7. package/commands/git/PullCommand.ts +25 -0
  8. package/commands/git/PushCommand.ts +64 -0
  9. package/commands/git/index.ts +8 -0
  10. package/commands/index.ts +11 -0
  11. package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
  12. package/commands/init-sub-repo/index.ts +1 -0
  13. package/commands/misc/HelpCommand.ts +104 -0
  14. package/commands/misc/index.ts +7 -0
  15. package/commands/models.ts +87 -0
  16. package/commands/note/CreateNoteCommand.ts +160 -0
  17. package/commands/note/RenameNoteCommand.ts +147 -0
  18. package/commands/note/UpdateNoteConfigCommand.ts +78 -0
  19. package/commands/note/index.ts +9 -0
  20. package/commands/registry.ts +47 -0
  21. package/commands/update/UpdateCommand.ts +219 -0
  22. package/commands/update/index.ts +7 -0
  23. package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
  24. package/commands/update-completed-count/index.ts +5 -0
  25. package/dist/markdown/index.cjs +6 -9
  26. package/dist/markdown/index.js +6 -9
  27. package/dist/vitepress/config/index.cjs +214 -58
  28. package/dist/vitepress/config/index.js +208 -52
  29. package/markdown/components.ts +86 -0
  30. package/markdown/index.ts +17 -0
  31. package/markdown/noteFormatter.test.ts +44 -0
  32. package/markdown/noteFormatter.ts +237 -0
  33. package/package.json +7 -3
  34. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
  35. package/vitepress/components/EnWordList/EnWordList.vue +16 -662
  36. package/vitepress/components/Footprints/Footprints.vue +15 -537
  37. package/vitepress/components/Mermaid/Mermaid.vue +13 -588
  38. package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
  39. package/vitepress/components/MindmapPreview/markdown.ts +1 -1
  40. package/vitepress/components/NotesTable/NotesTable.vue +11 -130
  41. package/vitepress/configs/markdown.config.ts +170 -26
  42. package/vitepress/theme/index.ts +9 -13
  43. package/vitepress/theme/styles/base.scss +15 -0
  44. package/workspace/atomic.ts +113 -0
  45. package/workspace/errors.ts +27 -0
  46. package/workspace/index.ts +40 -0
  47. package/workspace/mutationQueue.ts +28 -0
  48. package/workspace/paths.ts +64 -0
  49. package/workspace/reconcile.test.ts +300 -0
  50. package/workspace/reconcile.ts +95 -0
  51. package/workspace/scanner.ts +292 -0
  52. package/workspace/types.ts +224 -0
  53. package/workspace/workspace.test.ts +333 -0
  54. package/workspace/workspace.ts +1020 -0
  55. package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
@@ -1,668 +1,22 @@
1
- <!--
2
- vitepress/components/EnWordList/EnWordList.vue
1
+ <!--
2
+ Deprecated local shell. Canonical implementation lives in @tnotesjs/ui as
3
+ WordList. Kept so deep path imports do not break during migration.
3
4
  -->
4
-
5
- <script setup>
6
- import { marked } from 'marked'
7
- import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
8
-
9
- import {
10
- EN_WORDS_REPO_BASE_URL,
11
- EN_WORDS_REPO_BASE_RAW_URL,
12
- EN_WORD_LIST_COMP_IS_AUTO_SHOW_CARD,
13
- } from '../constants.ts'
14
- import RightClickMenu from './RightClickMenu.vue'
15
-
16
- const props = defineProps({
17
- words: {
18
- type: Array,
19
- default: () => [],
20
- },
21
- needSort: {
22
- type: Boolean,
23
- default: false,
24
- },
25
- })
26
-
27
- const isMobile = computed(() => {
28
- if (typeof navigator === 'undefined') return false
29
- return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
30
- navigator.userAgent
31
- )
32
- })
33
-
34
- // checkbox ---------------------------------------------------
35
-
36
- const pathname = typeof window !== 'undefined' ? window.location.pathname : ''
37
- const sortedWords = props.needSort
38
- ? computed(() =>
39
- [...new Set(props.words)].sort(
40
- (a, b) =>
41
- a.toLowerCase()[0].charCodeAt() - b.toLowerCase()[0].charCodeAt()
42
- )
43
- )
44
- : computed(() => [...new Set(props.words)])
45
- const checkedStates = ref({})
46
-
47
- const updateCheckedState = (word, isChecked) => {
48
- const key = `${pathname}-${word}`
49
- checkedStates.value[word] = isChecked
50
- localStorage.setItem(key, isChecked)
51
- }
52
-
53
- const checkAll = () => {
54
- Object.keys(checkedStates.value).forEach((word) => {
55
- updateCheckedState(word, true)
56
- })
57
- hideContextMenu()
58
- }
59
-
60
- const reset = () => {
61
- sortedWords.value.forEach((word) => {
62
- const key = `${pathname}-${word}`
63
- localStorage.removeItem(key)
64
- checkedStates.value[word] = false
65
- })
66
- hideContextMenu()
67
- }
68
-
69
- // word card ---------------------------------------------------
70
-
71
- const topZIndex = ref(10000)
72
-
73
- const isAutoShowCard = ref(false)
74
-
75
- // 卡片状态
76
- const showCard = ref(false)
77
- const cardX = ref(0)
78
- const cardY = ref(0)
79
- const cardContent = ref('')
80
- const wordCache = ref({})
81
-
82
- // 加载失败的词汇(也就是词库中不存在的词汇)
83
- const failedWords = ref({})
84
-
85
- // pinnedCards: { id, word, x, y, isDragging }
86
- const pinnedCards = ref([])
87
- let draggingCard = null
88
- let offsetX = 0
89
- let offsetY = 0
90
-
91
- const CARD_DEFAULT_WIDTH = 400
92
- const CARD_DEFAULT_HEIGHT = 500
93
-
94
- let resizingCard = null
95
- let startX = 0
96
- let startY = 0
97
- let startWidth = 0
98
- let startHeight = 0
99
-
100
- // 右键菜单状态
101
- const contextMenuVisible = ref(false)
102
- const contextMenuX = ref(0)
103
- const contextMenuY = ref(0)
104
- let currentWordForContextMenu = null
105
-
106
- // 防抖计时器
107
- let hoverTimer = null
108
-
109
- /**
110
- * 显示单词卡片
111
- */
112
- const showWordCard = async (e, word) => {
113
- cardContent.value = '<em>加载中……</em>'
114
- return new Promise((resolve) => {
115
- clearTimeout(hoverTimer)
116
- hoverTimer = setTimeout(async () => {
117
- const { clientX, clientY } = e
118
- cardX.value = clientX + 10
119
- cardY.value = clientY + 10
120
- showCard.value = true
121
-
122
- if (wordCache.value[word]) {
123
- cardContent.value = wordCache.value[word]
124
- resolve()
125
- return
126
- }
127
-
128
- const url = `${EN_WORDS_REPO_BASE_RAW_URL}${encodeURIComponent(
129
- word.toLowerCase().replaceAll(/\s/g, '_')
130
- )}.md`
131
- try {
132
- const res = await fetch(url)
133
- if (res.ok) {
134
- let text = await res.text()
135
- text = marked.parse(text)
136
- wordCache.value[word] = text
137
- cardContent.value = text
138
- } else {
139
- cardContent.value = `<em>无法加载单词内容</em>`
140
- }
141
- } catch (err) {
142
- console.error(err)
143
- cardContent.value = `<em>加载失败</em>`
144
- }
145
- resolve()
146
- }, 300)
147
- })
148
- }
149
-
150
- // const convertMarkdownToHTML = (text) => {
151
- // const lines = text.trim().split('\n')
152
- // let stack = [{ level: -1, html: [] }]
153
-
154
- // for (let line of lines) {
155
- // const match = line.match(/^(\s*)-\s(.*)/)
156
- // if (!match) continue
157
-
158
- // const indent = match[1].length
159
- // const content = match[2]
160
- // const currentLevel = stack[stack.length - 1]
161
-
162
- // const imageMatch = content.match(/^!\$$(.+?)$$/)
163
- // const processedContent = imageMatch
164
- // ? `<img src="${imageMatch[1]}" alt="" />`
165
- // : content
166
-
167
- // // 1. 深了:如果缩进比上一级更深,开启新子列表
168
- // if (indent > currentLevel.level) {
169
- // stack.push({ level: indent, html: [] })
170
- // }
171
- // // 2. 浅了:如果缩进更浅,关闭之前的列表直到匹配层级
172
- // else if (indent < currentLevel.level) {
173
- // while (stack.length > 1 && stack[stack.length - 2].level >= indent) {
174
- // const closed = stack.pop()
175
- // const innerHTML = closed.html.join('')
176
- // stack[stack.length - 1].html.push(`<ul>${innerHTML}</ul>`)
177
- // }
178
- // }
179
- // // 3. 一致:stack[-1] 是与当前 indent 层级一致的节点,添加当前 li 内容。
180
- // stack[stack.length - 1].html.push(`<li>${processedContent}</li>`)
181
- // }
182
-
183
- // // 清理栈中剩余的 ul
184
- // while (stack.length > 1) {
185
- // const closed = stack.pop()
186
- // const innerHTML = closed.html.join('')
187
- // stack[stack.length - 1].html.push(`<ul>${innerHTML}</ul>`)
188
- // }
189
- // console.log(stack)
190
-
191
- // return stack[0].html.join('')
192
- // }
193
-
194
- const preloadWords = async () => {
195
- const wordsToPreload = sortedWords.value
196
- if (!wordsToPreload.length) return
197
-
198
- for (let i = 0; i < wordsToPreload.length; i++) {
199
- const word = wordsToPreload[i]
200
-
201
- // 如果已经缓存过,跳过
202
- if (wordCache.value[word]) continue
203
-
204
- const url = `${EN_WORDS_REPO_BASE_RAW_URL}${encodeURIComponent(
205
- word.toLowerCase().replaceAll(/\s/g, '_')
206
- )}.md`
207
- try {
208
- const res = await fetch(url)
209
- if (res.ok) {
210
- let text = await res.text()
211
- text = marked.parse(text)
212
- wordCache.value[word] = text
213
- console.log(`✅ 预加载完成: ${word}`)
214
- } else {
215
- wordCache.value[word] = `<em>无法加载单词内容</em>`
216
- failedWords.value[word] = true
217
- }
218
- } catch (err) {
219
- console.error(`❌ 加载失败: ${word}`, err)
220
- wordCache.value[word] = `<em>加载失败</em>`
221
- failedWords.value[word] = true
222
- }
223
-
224
- // 可选:加个延迟避免并发请求过多
225
- await new Promise((resolve) => setTimeout(resolve, 200))
226
- }
227
- }
228
-
229
- const pinCard = (word) => {
230
- // const key = `${pathname}-${word}`
231
- // const storedState = localStorage.getItem(key)
232
- // if (!storedState || storedState !== 'true') {
233
- // updateCheckedState(word, true)
234
- // }
235
-
236
- // 如果已存在该卡片则不再重复添加
237
- if (pinnedCards.value.some((card) => card.word === word)) return
238
-
239
- pinnedCards.value.push({
240
- id: Date.now(),
241
- word,
242
- x: cardX.value,
243
- y: cardY.value,
244
- content: cardContent.value,
245
- width: CARD_DEFAULT_WIDTH,
246
- height: CARD_DEFAULT_HEIGHT,
247
- zIndex: topZIndex.value++,
248
- })
249
- }
250
-
251
- const bringToFront = (card) => {
252
- const index = pinnedCards.value.indexOf(card)
253
- if (index > -1) {
254
- pinnedCards.value = [
255
- ...pinnedCards.value.slice(0, index),
256
- ...pinnedCards.value.slice(index + 1),
257
- { ...card, zIndex: topZIndex.value++ },
258
- ]
259
- }
260
- }
261
-
262
- const removeCard = (id) => {
263
- pinnedCards.value = pinnedCards.value.filter((card) => card.id !== id)
264
- }
265
-
266
- const startDrag = (card, e) => {
267
- draggingCard = card
268
- offsetX = e.clientX - card.x
269
- offsetY = e.clientY - card.y
270
- document.addEventListener('mousemove', onDragging)
271
- document.addEventListener('mouseup', stopDrag)
272
- }
273
-
274
- const onDragging = (e) => {
275
- if (!draggingCard) return
276
- draggingCard.x = e.clientX - offsetX
277
- draggingCard.y = e.clientY - offsetY
278
- }
279
-
280
- const stopDrag = () => {
281
- draggingCard = null
282
- document.removeEventListener('mousemove', onDragging)
283
- document.removeEventListener('mouseup', stopDrag)
284
- }
285
-
286
- const showContextMenu = (e, word) => {
287
- e.preventDefault()
288
- currentWordForContextMenu = word
289
- contextMenuX.value = e.clientX
290
- contextMenuY.value = e.clientY
291
- contextMenuVisible.value = true
292
- }
293
-
294
- const hideContextMenu = () => {
295
- contextMenuVisible.value = false
296
- }
297
-
298
- const handleContextMenuPin = () => {
299
- if (currentWordForContextMenu) {
300
- const word = currentWordForContextMenu
301
- // 提前加载内容
302
- showWordCard(
303
- { clientX: contextMenuX.value, clientY: contextMenuY.value },
304
- word
305
- ).then(() => {
306
- pinCard(word)
307
- showCard.value = false
308
- })
309
- hideContextMenu()
310
- }
311
- }
312
-
313
- const startResize = (card, e) => {
314
- resizingCard = card
315
- startX = e.clientX
316
- startY = e.clientY
317
- startWidth = card.width
318
- startHeight = card.height
319
-
320
- document.addEventListener('mousemove', onResizing)
321
- document.addEventListener('mouseup', stopResize)
322
- }
323
-
324
- const onResizing = (e) => {
325
- if (!resizingCard) return
326
-
327
- const newWidth = startWidth + (e.clientX - startX)
328
- const newHeight = startHeight + (e.clientY - startY)
329
-
330
- // 设置最小尺寸
331
- if (newWidth > 200) resizingCard.width = newWidth
332
- if (newHeight > 100) resizingCard.height = newHeight
333
- }
334
-
335
- const stopResize = () => {
336
- resizingCard = null
337
- document.removeEventListener('mousemove', onResizing)
338
- document.removeEventListener('mouseup', stopResize)
339
- }
340
-
341
- /**
342
- * 处理鼠标离开事件
343
- */
344
- const handleMouseLeave = () => {
345
- setTimeout(() => {
346
- hideWordCard()
347
- }, 100)
348
- }
349
-
350
- /**
351
- * 隐藏单词卡片
352
- */
353
- const hideWordCard = () => {
354
- showCard.value = false
355
- }
356
-
357
- // pronounce ----------------------------------------------------------
358
-
359
- let currentPronounceAllIndex = ref(0)
360
- let isPronouncingAll = ref(false)
361
- let pronounceAllInterval = null
362
-
363
- const handlePronounceAll = (lang) => {
364
- if (isPronouncingAll.value) {
365
- // 如果正在播放,就停止
366
- stopPronounceAll()
367
- return
368
- }
369
-
370
- const wordsToSpeak = sortedWords.value
371
- if (!wordsToSpeak.length) return
372
-
373
- currentPronounceAllIndex.value = 0
374
- isPronouncingAll.value = true
375
-
376
- const speakNext = async () => {
377
- if (
378
- !isPronouncingAll.value ||
379
- currentPronounceAllIndex.value >= wordsToSpeak.length
380
- ) {
381
- stopPronounceAll()
382
- return
383
- }
384
-
385
- const word = wordsToSpeak[currentPronounceAllIndex.value]
386
- const utterance = new SpeechSynthesisUtterance(word)
387
- utterance.lang = lang
388
- speechSynthesis.speak(utterance)
389
-
390
- await nextTick()
391
- currentPronounceAllIndex.value++
392
- }
393
-
394
- speakNext()
395
-
396
- // 每隔 1.5 秒读一个词
397
- pronounceAllInterval = setInterval(speakNext, 1500)
398
-
399
- hideContextMenu()
400
- }
401
-
402
- const stopPronounceAll = () => {
403
- isPronouncingAll.value = false
404
- if (pronounceAllInterval) {
405
- clearInterval(pronounceAllInterval)
406
- pronounceAllInterval = null
407
- }
408
- speechSynthesis.cancel() // 停止所有未完成的语音
409
- }
410
-
411
- const handlePronounce = (word, lang = 'en-GB') => {
412
- if ('speechSynthesis' in window) {
413
- stopPronounceAll()
414
-
415
- const utterance = new SpeechSynthesisUtterance(word)
416
- utterance.lang = lang
417
- speechSynthesis.speak(utterance)
418
- hideContextMenu()
419
- } else {
420
- alert('您的浏览器不支持语音功能,请尝试使用 Chrome 或 Edge 浏览器。')
421
- }
422
- }
423
-
424
- // hooks ----------------------------------------------------------
425
-
426
- onMounted(() => {
427
- sortedWords.value.forEach((word) => {
428
- const key = `${pathname}-${word}`
429
- const storedState = localStorage.getItem(key)
430
- checkedStates.value[word] = storedState === 'true'
431
- })
432
-
433
- if (!isMobile.value) preloadWords()
434
-
435
- // 添加点击事件监听以隐藏右键菜单
436
- if (typeof document !== 'undefined') {
437
- document.body.addEventListener('click', hideContextMenu)
438
- }
439
- })
440
-
441
- /**
442
- * 销毁时清理定时器
443
- */
444
- onUnmounted(() => {
445
- clearTimeout(hoverTimer)
446
- if (typeof document !== 'undefined') {
447
- document.removeEventListener('mousemove', onDragging)
448
- document.removeEventListener('mouseup', stopDrag)
449
- document.body.removeEventListener('click', hideContextMenu)
450
- }
451
- })
452
- </script>
453
-
454
5
  <template>
455
- <div class="enWordList">
456
- <ol>
457
- <li
458
- v-for="(word, index) in sortedWords"
459
- :key="word"
460
- :class="{
461
- pronounced: isPronouncingAll && currentPronounceAllIndex === index + 1,
462
- }"
463
- >
464
- <span class="index">{{ index + 1 }}.</span>
465
- <input
466
- type="checkbox"
467
- :id="word"
468
- :checked="checkedStates[word]"
469
- @change="(e) => updateCheckedState(word, e.target.checked)"
470
- />
471
- <label :for="word">
472
- <a
473
- :href="`${EN_WORDS_REPO_BASE_URL}${encodeURIComponent(
474
- word.toLowerCase().replaceAll(/\s/g, '_')
475
- )}.md`"
476
- :class="{
477
- lineThrough: checkedStates[word],
478
- textRed: failedWords[word],
479
- }"
480
- @mouseenter="(e) => isAutoShowCard && showWordCard(e, word)"
481
- @mouseleave="handleMouseLeave"
482
- @contextmenu="(e) => showContextMenu(e, word)"
483
- @click.ctrl.exact="(e) => handlePronounce(word)"
484
- >
485
- {{ word }}
486
- </a>
487
- </label>
488
- </li>
489
- </ol>
490
-
491
- <div
492
- class="wordCard"
493
- :style="{ left: cardX + 'px', top: cardY + 'px' }"
494
- v-if="showCard"
495
- >
496
- <div class="wordCardContent" v-html="cardContent"></div>
497
- </div>
498
-
499
- <!-- pinned cards -->
500
- <div
501
- v-for="card in pinnedCards"
502
- :key="card.id"
503
- class="wordCard"
504
- :style="{
505
- left: card.x + 'px',
506
- top: card.y + 'px',
507
- width: card.width + 'px',
508
- height: card.height + 'px',
509
- zIndex: card.zIndex,
510
- }"
511
- @mousedown="(e) => startDrag(card, e)"
512
- @click="bringToFront(card)"
513
- >
514
- <div class="wordCardContentWrapper">
515
- <div class="wordCardContent" v-html="card.content"></div>
516
- </div>
517
- <button class="closeBtn" @click.stop="removeCard(card.id)">
518
-
519
- </button>
520
- <div
521
- class="resizeHandle"
522
- @mousedown.stop="startResize(card, $event)"
523
- ></div>
524
- </div>
525
- </div>
526
-
527
- <RightClickMenu
528
- v-if="!isMobile"
529
- :show="contextMenuVisible"
530
- :x="contextMenuX"
531
- :y="contextMenuY"
532
- :isAutoShowCard="isAutoShowCard"
533
- @pin="handleContextMenuPin"
534
- @pronounce="(lang) => handlePronounce(currentWordForContextMenu, lang)"
535
- @pronounceAll="(lang) => handlePronounceAll(lang)"
536
- @autoShowCard="
537
- () => {
538
- isAutoShowCard = !isAutoShowCard
539
- hideContextMenu()
540
- }
541
- "
542
- @checkAll="checkAll"
543
- @reset="reset"
544
- />
6
+ <WordList :words="words" :need-sort="needSort" />
545
7
  </template>
546
8
 
547
- <style scoped lang="scss">
548
- /* EnWordList 组件样式 */
549
-
550
- .enWordList {
551
- // Checkbox 样式
552
- input[type='checkbox'] {
553
- margin: 8px;
554
- transform: scale(1.3);
555
- cursor: pointer;
556
- }
557
-
558
- // 链接样式
559
- a {
560
- text-decoration: none;
561
- color: #4fc3f7;
562
-
563
- &:hover {
564
- text-decoration: underline !important;
565
- }
566
-
567
- &.lineThrough {
568
- color: #999;
569
- text-decoration: line-through;
570
- }
571
-
572
- &.textRed {
573
- color: #f40 !important;
574
- }
575
- }
576
-
577
- // 单词列表样式
578
- ol {
579
- list-style-type: decimal;
580
- padding-left: 20px;
581
-
582
- li {
583
- display: flex;
584
- align-items: center;
585
- margin-bottom: 8px;
586
- transition: all 0.3s ease;
9
+ <script setup lang="ts">
10
+ import { WordList } from '@tnotesjs/ui'
587
11
 
588
- &.pronounced {
589
- background-color: rgba(255, 255, 0, 0.1);
590
- }
591
- }
12
+ withDefaults(
13
+ defineProps<{
14
+ words?: string[]
15
+ needSort?: boolean
16
+ }>(),
17
+ {
18
+ words: () => [],
19
+ needSort: false
592
20
  }
593
-
594
- // 序号样式
595
- .index {
596
- margin-right: 10px;
597
- color: #aaa;
598
- }
599
- }
600
-
601
- // 单词卡片样式(🌑 暗色悬浮卡片)
602
- .wordCard {
603
- position: fixed;
604
- z-index: 9999;
605
- background: #1e1e1e;
606
- border: 1px solid #333;
607
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
608
- padding: 12px 16px;
609
- max-width: 600px;
610
- min-width: 200px;
611
- min-height: 100px;
612
- font-size: 14px;
613
- line-height: 1.4;
614
- border-radius: 8px;
615
- color: #eee;
616
- pointer-events: auto;
617
- font-family: sans-serif;
618
- cursor: move;
619
-
620
- // 关闭按钮
621
- .closeBtn {
622
- position: absolute;
623
- right: 5px;
624
- top: 5px;
625
- background: none;
626
- border: none;
627
- font-size: 16px;
628
- cursor: pointer;
629
- color: #ccc;
630
-
631
- &:hover {
632
- color: white;
633
- }
634
- }
635
- }
636
-
637
- // 卡片内容包裹器
638
- .wordCardContentWrapper {
639
- width: 100%;
640
- height: 100%;
641
- overflow: auto;
642
- }
643
-
644
- // 卡片内容样式
645
- .wordCardContent {
646
- :deep(ul) {
647
- margin: 0.5rem 0;
648
- padding-left: 1rem;
649
- }
650
- }
651
-
652
- // 调整大小手柄
653
- .resizeHandle {
654
- position: absolute;
655
- right: 0;
656
- bottom: 0;
657
- width: 12px;
658
- height: 12px;
659
- background-color: #666;
660
- cursor: nwse-resize;
661
- z-index: 2;
662
- border-radius: 50%;
663
-
664
- &:hover {
665
- background-color: #aaa;
666
- }
667
- }
668
- </style>
21
+ )
22
+ </script>