@tnotesjs/ui 0.1.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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +68 -0
- package/src/components/BilibiliVideo/BilibiliVideo.vue +55 -0
- package/src/components/Footprints/Footprints.vue +339 -0
- package/src/components/Footprints/parse.ts +122 -0
- package/src/components/Mermaid/Mermaid.vue +553 -0
- package/src/components/Mermaid/icons/icon__center_off.svg +1 -0
- package/src/components/Mermaid/icons/icon__center_on.svg +1 -0
- package/src/components/Mermaid/icons/icon__check.svg +3 -0
- package/src/components/Mermaid/icons/icon__clipboard.svg +8 -0
- package/src/components/Mermaid/icons/icon__fullscreen.svg +1 -0
- package/src/components/Mermaid/icons/icon__fullscreen_exit.svg +1 -0
- package/src/components/Mindmap/FocusBreadcrumbs.test.ts +265 -0
- package/src/components/Mindmap/FocusBreadcrumbs.vue +436 -0
- package/src/components/Mindmap/InlineRuns.ts +25 -0
- package/src/components/Mindmap/Mindmap.vue +1210 -0
- package/src/components/Mindmap/MindmapOutlineNode.vue +62 -0
- package/src/components/Mindmap/MindmapViewIcon.vue +41 -0
- package/src/components/Mindmap/editor/AppIcon.vue +107 -0
- package/src/components/Mindmap/editor/CanvasContextMenu.vue +157 -0
- package/src/components/Mindmap/editor/LinkPopover.vue +83 -0
- package/src/components/Mindmap/editor/MarkdownView.vue +163 -0
- package/src/components/Mindmap/editor/MindmapView.vue +253 -0
- package/src/components/Mindmap/editor/OutlineView.vue +2494 -0
- package/src/components/Mindmap/editor/RichInlineEditor.vue +393 -0
- package/src/components/Mindmap/editor/SelectionToolbar.vue +191 -0
- package/src/components/Mindmap/editor/canvasClipboard.ts +6 -0
- package/src/components/Mindmap/editor/imagePaste.ts +32 -0
- package/src/components/Mindmap/editor/mindmapClipboard.ts +93 -0
- package/src/components/Mindmap/editor/outlineDrag.ts +29 -0
- package/src/components/Mindmap/editor/platform.ts +18 -0
- package/src/components/Mindmap/expandLevel.ts +28 -0
- package/src/components/Mindmap/icons/icon__fullscreen.svg +1 -0
- package/src/components/Mindmap/icons/icon__fullscreen_exit.svg +1 -0
- package/src/components/Mindmap/icons/icon__zoom_fit.svg +1 -0
- package/src/components/Mindmap/markdown.ts +83 -0
- package/src/components/Mindmap/wheelInteraction.ts +7 -0
- package/src/components/NotesTable/NotesTable.vue +119 -0
- package/src/components/NotesTable/types.ts +6 -0
- package/src/components/WordList/RightClickMenu.vue +106 -0
- package/src/components/WordList/WordList.vue +692 -0
- package/src/components/WordList/wordListFeatures.ts +38 -0
- package/src/index.ts +30 -0
- package/src/styles/tokens.css +35 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { cloneSubtree, parseMarkdown } from '@tnotesjs/mindmap-core'
|
|
2
|
+
import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
3
|
+
|
|
4
|
+
const LIST_LINE_RE = /^\s*[-*+]\s+/
|
|
5
|
+
|
|
6
|
+
/** In-app fallback when Electron denies async Clipboard API (common in Desk). */
|
|
7
|
+
let mindmapClipboardBuffer = ''
|
|
8
|
+
|
|
9
|
+
function clipboardNodes(text: string): MindmapNode[] {
|
|
10
|
+
const fragment = text
|
|
11
|
+
.split(/\r?\n/)
|
|
12
|
+
.filter((line) => line.trim() !== '')
|
|
13
|
+
.map((line) => {
|
|
14
|
+
if (LIST_LINE_RE.test(line)) return line
|
|
15
|
+
const match = /^(\s*)(.*)$/.exec(line)!
|
|
16
|
+
return `${match[1]}- ${match[2]}`
|
|
17
|
+
})
|
|
18
|
+
.join('\n')
|
|
19
|
+
if (!fragment) return []
|
|
20
|
+
const parsed = parseMarkdown(`# _\n\n${fragment}\n`)
|
|
21
|
+
if (!parsed.valid) return []
|
|
22
|
+
return parsed.doc.root.children.map((node) => cloneSubtree(node))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Sync write that survives Electron's missing clipboard permission. */
|
|
26
|
+
export function writeMindmapClipboard(text: string, event?: ClipboardEvent | null): void {
|
|
27
|
+
mindmapClipboardBuffer = text
|
|
28
|
+
if (event?.clipboardData) {
|
|
29
|
+
event.clipboardData.setData('text/plain', text)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const textarea = document.createElement('textarea')
|
|
34
|
+
textarea.value = text
|
|
35
|
+
textarea.setAttribute('readonly', '')
|
|
36
|
+
textarea.style.position = 'fixed'
|
|
37
|
+
textarea.style.left = '-9999px'
|
|
38
|
+
textarea.style.top = '0'
|
|
39
|
+
textarea.style.opacity = '0'
|
|
40
|
+
document.body.append(textarea)
|
|
41
|
+
textarea.focus()
|
|
42
|
+
textarea.select()
|
|
43
|
+
document.execCommand('copy')
|
|
44
|
+
textarea.remove()
|
|
45
|
+
} catch {
|
|
46
|
+
// fall through to async API
|
|
47
|
+
}
|
|
48
|
+
void navigator.clipboard?.writeText(text).catch(() => {
|
|
49
|
+
// Buffer above still enables in-app Cmd+V.
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function readMindmapClipboard(event?: ClipboardEvent | null): Promise<string> {
|
|
54
|
+
const fromEvent = event?.clipboardData?.getData('text/plain') ?? ''
|
|
55
|
+
if (fromEvent.trim()) {
|
|
56
|
+
mindmapClipboardBuffer = fromEvent
|
|
57
|
+
return fromEvent
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const fromApi = (await navigator.clipboard?.readText?.()) ?? ''
|
|
61
|
+
if (fromApi.trim()) {
|
|
62
|
+
mindmapClipboardBuffer = fromApi
|
|
63
|
+
return fromApi
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Electron often denies readText without an explicit permission grant.
|
|
67
|
+
}
|
|
68
|
+
return mindmapClipboardBuffer
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 把剪贴板中的普通文字 / Markdown 列表作为当前主题后的同级子树插入。 */
|
|
72
|
+
export function pasteCanvasOutline(session: MindmapSession, anchorId: string, text: string): string[] {
|
|
73
|
+
const nodes = clipboardNodes(text)
|
|
74
|
+
const anchor = session.document.find(anchorId)
|
|
75
|
+
if (!anchor || nodes.length === 0) return []
|
|
76
|
+
const insertedIds: string[] = []
|
|
77
|
+
session.transact((doc) => {
|
|
78
|
+
const parent = anchor === session.focusRootNode ? anchor : (anchor.parent ?? session.focusRootNode)
|
|
79
|
+
let index = anchor === session.focusRootNode ? parent.children.length : parent.children.indexOf(anchor) + 1
|
|
80
|
+
for (const source of nodes) {
|
|
81
|
+
const inserted = doc.addNode(
|
|
82
|
+
parent,
|
|
83
|
+
{ ...source.content, image: source.content.image ? { ...source.content.image } : null },
|
|
84
|
+
index++,
|
|
85
|
+
)
|
|
86
|
+
inserted.collapsed = source.collapsed
|
|
87
|
+
for (const child of [...source.children]) doc.move(child, inserted, inserted.children.length)
|
|
88
|
+
insertedIds.push(inserted.id)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
session.selectMany(insertedIds, insertedIds[insertedIds.length - 1] ?? null, insertedIds[0] ?? null)
|
|
92
|
+
return insertedIds
|
|
93
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MindmapNode } from '@tnotesjs/mindmap-core'
|
|
2
|
+
|
|
3
|
+
export interface DropLevel {
|
|
4
|
+
node: MindmapNode
|
|
5
|
+
depth: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 根据横向指针位置解析“插到哪个祖先之后”。
|
|
10
|
+
* 每越过一条缩进线就提升一级;是否为末子不影响提升,否则普通兄弟无法拖回顶层。
|
|
11
|
+
*/
|
|
12
|
+
export function resolveAfterDropLevel(
|
|
13
|
+
anchor: MindmapNode,
|
|
14
|
+
focusRoot: MindmapNode,
|
|
15
|
+
clientX: number,
|
|
16
|
+
indentEdge: number,
|
|
17
|
+
depth: number,
|
|
18
|
+
indent: number,
|
|
19
|
+
): DropLevel {
|
|
20
|
+
let node = anchor
|
|
21
|
+
let nextDepth = depth
|
|
22
|
+
let edge = indentEdge
|
|
23
|
+
while (node.parent && node.parent !== focusRoot && clientX <= edge && nextDepth > 0) {
|
|
24
|
+
node = node.parent
|
|
25
|
+
nextDepth -= 1
|
|
26
|
+
edge -= indent
|
|
27
|
+
}
|
|
28
|
+
return { node, depth: nextDepth }
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function platformName(): string {
|
|
2
|
+
if (typeof navigator === 'undefined') return ''
|
|
3
|
+
const data = navigator as Navigator & { userAgentData?: { platform?: string } }
|
|
4
|
+
return `${data.userAgentData?.platform ?? ''} ${navigator.platform ?? ''} ${navigator.userAgent ?? ''}`.toLowerCase()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isApplePlatform(): boolean {
|
|
8
|
+
return /mac|iphone|ipad|ipod/.test(platformName())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function primaryShortcut(key: string, options: { shift?: boolean } = {}): string {
|
|
12
|
+
if (isApplePlatform()) return `${options.shift ? '⇧' : ''}⌘${key}`
|
|
13
|
+
return `Ctrl+${options.shift ? 'Shift+' : ''}${key}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function altShortcut(key: string): string {
|
|
17
|
+
return isApplePlatform() ? `⌥${key}` : `Alt+${key}`
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
2
|
+
|
|
3
|
+
export function normalizeExpandLevel(value: number): number {
|
|
4
|
+
return Math.max(1, Math.trunc(Number(value) || 1))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function childLevel(node: MindmapNode, root: MindmapNode): number {
|
|
8
|
+
let level = 0
|
|
9
|
+
let current: MindmapNode | null = node
|
|
10
|
+
while (current && current !== root) {
|
|
11
|
+
level += 1
|
|
12
|
+
current = current.parent
|
|
13
|
+
}
|
|
14
|
+
return level
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Level 1 renders root + direct children; level 2 additionally renders
|
|
19
|
+
* grandchildren. Nodes at the last visible level are collapsed.
|
|
20
|
+
*/
|
|
21
|
+
export function applyInitialExpandLevel(session: MindmapSession, value: number): void {
|
|
22
|
+
const visibleLevel = normalizeExpandLevel(value)
|
|
23
|
+
const root = session.document.root
|
|
24
|
+
session.document.traverse((node) => {
|
|
25
|
+
if (node === root || node.children.length === 0) return
|
|
26
|
+
session.setCollapsed(node.id, childLevel(node, root) >= visibleLevel)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none"><path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="#646cff" d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586zM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3z"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#646cff" d="m10 15.4l-5.9 5.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7L8.6 14H5q-.425 0-.712-.288T4 13t.288-.712T5 12h6q.425 0 .713.288T12 13v6q0 .425-.288.713T11 20t-.712-.288T10 19zm5.4-5.4H19q.425 0 .713.288T20 11t-.288.713T19 12h-6q-.425 0-.712-.288T12 11V5q0-.425.288-.712T13 4t.713.288T14 5v3.6l5.9-5.9q.275-.275.7-.275t.7.275t.275.7t-.275.7z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="#646cff" d="M17 4h3c1.1 0 2 .9 2 2v2h-2V6h-3zM4 8V6h3V4H4c-1.1 0-2 .9-2 2v2zm16 8v2h-3v2h3c1.1 0 2-.9 2-2v-2zM7 18H4v-2H2v2c0 1.1.9 2 2 2h3zm9-8v4H8v-4zm2-2H6v8h12z"/></svg>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface MindmapFenceOptions {
|
|
2
|
+
title?: string
|
|
3
|
+
initialExpandLevel?: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface MindmapReference {
|
|
7
|
+
path: string
|
|
8
|
+
title?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function cleanHeadingText(value: string): string {
|
|
12
|
+
return value.trim().replace(/\s+#+\s*$/, '').trim()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Parse the canonical `mindmap [title] 2` fence metadata. */
|
|
16
|
+
export function parseMindmapFence(openLine: string): MindmapFenceOptions | null {
|
|
17
|
+
const fenceBody = openLine.trim().replace(/^`+\s*/, '')
|
|
18
|
+
const nameMatch = fenceBody.match(/^mindmap(?=\s|\[|$)/)
|
|
19
|
+
if (!nameMatch) return null
|
|
20
|
+
|
|
21
|
+
let rest = fenceBody.slice(nameMatch[0].length).trim()
|
|
22
|
+
const options: MindmapFenceOptions = {}
|
|
23
|
+
const titleMatch = rest.match(/\[([^\]]+)\]/)
|
|
24
|
+
if (titleMatch) {
|
|
25
|
+
options.title = cleanHeadingText(titleMatch[1]) || undefined
|
|
26
|
+
rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (rest && !/^\d+$/.test(rest)) return null
|
|
30
|
+
if (rest) {
|
|
31
|
+
options.initialExpandLevel = Math.max(1, Number(rest))
|
|
32
|
+
}
|
|
33
|
+
return options
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @deprecated Mindmap fences no longer resolve `<<<` includes; kept for body-include parsers / tests. */
|
|
37
|
+
export function parseMindmapReference(line: string): MindmapReference | null {
|
|
38
|
+
const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
|
|
39
|
+
if (!match) return null
|
|
40
|
+
|
|
41
|
+
let rest = match[1].trim()
|
|
42
|
+
let title: string | undefined
|
|
43
|
+
const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/)
|
|
44
|
+
if (titleMatch) {
|
|
45
|
+
title = cleanHeadingText(titleMatch[1]) || undefined
|
|
46
|
+
rest = rest.slice(0, titleMatch.index).trim()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim()
|
|
50
|
+
return path ? { path, title } : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface NormalizeMindmapOptions {
|
|
54
|
+
title?: string
|
|
55
|
+
defaultTitle?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Ensure canonical mindmap Markdown has exactly one H1 root title. */
|
|
59
|
+
export function normalizeMindmapMarkdown(
|
|
60
|
+
source: string,
|
|
61
|
+
options: NormalizeMindmapOptions = {},
|
|
62
|
+
): string {
|
|
63
|
+
const lines = source.replace(/\r\n?/g, '\n').split('\n')
|
|
64
|
+
let existingTitle = ''
|
|
65
|
+
let rootIndex = -1
|
|
66
|
+
|
|
67
|
+
for (let index = 0; index < lines.length; index++) {
|
|
68
|
+
const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/)
|
|
69
|
+
if (!match) continue
|
|
70
|
+
existingTitle = cleanHeadingText(match[1])
|
|
71
|
+
rootIndex = index
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
|
|
76
|
+
const body = lines.filter((_, index) => index !== rootIndex)
|
|
77
|
+
while (body[0]?.trim() === '') body.shift()
|
|
78
|
+
while (body[body.length - 1]?.trim() === '') body.pop()
|
|
79
|
+
|
|
80
|
+
return body.length > 0
|
|
81
|
+
? `# ${rootTitle}\n\n${body.join('\n')}\n`
|
|
82
|
+
: `# ${rootTitle}\n`
|
|
83
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NotesTableRow } from './types'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
notes?: NotesTableRow[]
|
|
6
|
+
missingIds?: string[]
|
|
7
|
+
error?: string | null
|
|
8
|
+
}>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div v-if="error" class="tn-notes-table__error">
|
|
13
|
+
{{ error }}
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div v-else-if="(missingIds?.length ?? 0) > 0" class="tn-notes-table__warning">
|
|
17
|
+
以下笔记 ID 未找到配置: {{ missingIds!.join(', ') }}
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<table v-if="(notes?.length ?? 0) > 0" class="tn-notes-table">
|
|
21
|
+
<thead>
|
|
22
|
+
<tr>
|
|
23
|
+
<th>笔记</th>
|
|
24
|
+
<th>简介</th>
|
|
25
|
+
</tr>
|
|
26
|
+
</thead>
|
|
27
|
+
<tbody>
|
|
28
|
+
<tr v-for="note in notes" :key="note.id">
|
|
29
|
+
<td>
|
|
30
|
+
<a :href="note.url" class="tn-notes-table__link">
|
|
31
|
+
<span class="tn-notes-table__id">{{ note.id }}.</span>
|
|
32
|
+
<span>{{ note.title }}</span>
|
|
33
|
+
</a>
|
|
34
|
+
</td>
|
|
35
|
+
<td>
|
|
36
|
+
<span class="tn-notes-table__desc" :class="{ 'is-empty': !note.description }">
|
|
37
|
+
{{ note.description || '暂无简介' }}
|
|
38
|
+
</span>
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
41
|
+
</tbody>
|
|
42
|
+
</table>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<style scoped lang="scss">
|
|
46
|
+
.tn-notes-table {
|
|
47
|
+
width: 100%;
|
|
48
|
+
margin: 1.5rem 0;
|
|
49
|
+
border-collapse: collapse;
|
|
50
|
+
font-size: 0.95rem;
|
|
51
|
+
|
|
52
|
+
th,
|
|
53
|
+
td {
|
|
54
|
+
padding: 0.75rem 1rem;
|
|
55
|
+
text-align: left;
|
|
56
|
+
border: 1px solid var(--tn-c-divider);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
th {
|
|
60
|
+
background-color: var(--tn-c-bg-soft);
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
color: var(--tn-c-text);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
tbody tr {
|
|
66
|
+
transition: background-color 0.2s;
|
|
67
|
+
|
|
68
|
+
&:hover {
|
|
69
|
+
background-color: var(--tn-c-bg-soft);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
td {
|
|
74
|
+
color: var(--tn-c-text-2);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.tn-notes-table__link {
|
|
79
|
+
color: var(--tn-c-brand);
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
|
|
83
|
+
&:hover {
|
|
84
|
+
text-decoration: underline;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.tn-notes-table__id {
|
|
89
|
+
margin-right: 0.5rem;
|
|
90
|
+
font-family: var(--tn-font-mono);
|
|
91
|
+
font-size: 0.9em;
|
|
92
|
+
color: var(--tn-c-text-2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.tn-notes-table__desc {
|
|
96
|
+
line-height: 1.6;
|
|
97
|
+
|
|
98
|
+
&.is-empty {
|
|
99
|
+
color: var(--tn-c-text-2);
|
|
100
|
+
font-style: italic;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.tn-notes-table__error,
|
|
105
|
+
.tn-notes-table__warning {
|
|
106
|
+
margin: 1rem 0;
|
|
107
|
+
padding: 1rem;
|
|
108
|
+
border-radius: 4px;
|
|
109
|
+
border-left: 4px solid var(--tn-c-danger);
|
|
110
|
+
background-color: var(--tn-c-danger-soft);
|
|
111
|
+
color: var(--tn-c-danger);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.tn-notes-table__warning {
|
|
115
|
+
border-left-color: #f9b44e;
|
|
116
|
+
background-color: color-mix(in srgb, #f9b44e 16%, transparent);
|
|
117
|
+
color: #f9b44e;
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="show"
|
|
4
|
+
class="rightClickMenu"
|
|
5
|
+
:style="{ left: x + 'px', top: y + 'px' }"
|
|
6
|
+
>
|
|
7
|
+
<div v-if="showPin" class="menuItem" @click="handlePin">📌 Pin</div>
|
|
8
|
+
<div class="menuItem" @click="(e) => handlePronounce(e, 'en-GB')">
|
|
9
|
+
📢 Pronounce(英)
|
|
10
|
+
</div>
|
|
11
|
+
<div class="menuItem" @click="(e) => handlePronounce(e, 'en-US')">
|
|
12
|
+
📢 Pronounce(美)
|
|
13
|
+
</div>
|
|
14
|
+
<div
|
|
15
|
+
class="menuItem"
|
|
16
|
+
@click="(e) => handlePronounceAll(e, 'en-GB')"
|
|
17
|
+
>
|
|
18
|
+
📢 Pronounce All(英)
|
|
19
|
+
</div>
|
|
20
|
+
<div
|
|
21
|
+
class="menuItem"
|
|
22
|
+
@click="(e) => handlePronounceAll(e, 'en-US')"
|
|
23
|
+
>
|
|
24
|
+
📢 Pronounce All(美)
|
|
25
|
+
</div>
|
|
26
|
+
<div
|
|
27
|
+
v-if="showAutoShowCard"
|
|
28
|
+
class="menuItem"
|
|
29
|
+
@click="handleAutoShowCard"
|
|
30
|
+
>
|
|
31
|
+
🔍 Auto Show Card({{ isAutoShowCard ? '关' : '开' }})
|
|
32
|
+
</div>
|
|
33
|
+
<div class="menuItem" @click="handleCheckAll">✅ Check All</div>
|
|
34
|
+
<div class="menuItem" @click="handleReset">❌ Reset</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup>
|
|
39
|
+
defineProps({
|
|
40
|
+
show: Boolean,
|
|
41
|
+
isAutoShowCard: Boolean,
|
|
42
|
+
x: Number,
|
|
43
|
+
y: Number,
|
|
44
|
+
showPin: {
|
|
45
|
+
type: Boolean,
|
|
46
|
+
default: true,
|
|
47
|
+
},
|
|
48
|
+
showAutoShowCard: {
|
|
49
|
+
type: Boolean,
|
|
50
|
+
default: true,
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const emit = defineEmits([
|
|
55
|
+
'pin',
|
|
56
|
+
'pronounce',
|
|
57
|
+
'pronounceAll',
|
|
58
|
+
'autoShowCard',
|
|
59
|
+
'checkAll',
|
|
60
|
+
'reset',
|
|
61
|
+
])
|
|
62
|
+
const handlePin = (e) => {
|
|
63
|
+
emit('pin')
|
|
64
|
+
e.preventDefault()
|
|
65
|
+
}
|
|
66
|
+
const handleAutoShowCard = () => {
|
|
67
|
+
emit('autoShowCard')
|
|
68
|
+
}
|
|
69
|
+
const handlePronounce = (e, lang) => {
|
|
70
|
+
emit('pronounce', lang)
|
|
71
|
+
e.preventDefault()
|
|
72
|
+
}
|
|
73
|
+
const handlePronounceAll = (e, lang) => {
|
|
74
|
+
emit('pronounceAll', lang)
|
|
75
|
+
e.preventDefault()
|
|
76
|
+
}
|
|
77
|
+
const handleCheckAll = () => {
|
|
78
|
+
emit('checkAll')
|
|
79
|
+
}
|
|
80
|
+
const handleReset = () => {
|
|
81
|
+
emit('reset')
|
|
82
|
+
}
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<style scoped lang="scss">
|
|
86
|
+
.rightClickMenu {
|
|
87
|
+
position: fixed;
|
|
88
|
+
z-index: 99999;
|
|
89
|
+
background: #2c2c2c;
|
|
90
|
+
border: 1px solid #444;
|
|
91
|
+
border-radius: 6px;
|
|
92
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
color: #eee;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
user-select: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.menuItem {
|
|
100
|
+
padding: 8px 16px;
|
|
101
|
+
|
|
102
|
+
&:hover {
|
|
103
|
+
background-color: #444;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</style>
|