@tnotesjs/core 0.0.8 → 0.1.15
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/dist/{chunk-JUMVJKWV.js → chunk-FVKQPBPP.js} +106 -10
- package/dist/{chunk-NASIL5FY.js → chunk-SQVHJR2Z.js} +0 -4
- package/dist/cli/index.js +2 -2
- package/dist/index.js +1 -1
- package/dist/vitepress/config/index.js +48 -2
- package/package.json +2 -1
- package/vitepress/assets/icons/icon__setting.svg +1 -0
- package/vitepress/assets/icons/index.ts +1 -0
- package/vitepress/components/Layout/Layout.vue +22 -2
- package/vitepress/components/Layout/NavBarSettingsTrigger.vue +59 -0
- package/vitepress/components/Layout/SidebarNavBefore.vue +47 -31
- package/vitepress/components/Layout/composables/useNoteSave.ts +21 -17
- package/vitepress/components/Layout/composables/useRenameOverlay.ts +33 -0
- package/vitepress/components/Layout/composables/useRenameRedirect.ts +59 -0
- package/vitepress/components/LoadingPage/LoadingPage.vue +84 -192
- package/vitepress/components/Settings/SettingsDialog.vue +243 -0
- package/vitepress/components/Settings/composables/useSettingsDialog.ts +35 -0
- package/vitepress/components/SidebarCard/SidebarCard.vue +12 -12
- package/vitepress/config/index.ts +7 -2
- package/vitepress/plugins/fileWatcherBridgePlugin.ts +49 -0
- package/vitepress/plugins/renameNotePlugin.ts +8 -1
- package/vitepress/theme/index.ts +44 -4
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 重命名后跳转工具:显示遮罩 → 等 Vite HMR 重建路由 → 整页跳到新 URL。
|
|
3
|
+
*
|
|
4
|
+
* 关于面板保存与文件系统直接重命名两条入口共用此函数,确保行为一致。
|
|
5
|
+
*/
|
|
6
|
+
import { useRenameOverlay } from './useRenameOverlay'
|
|
7
|
+
|
|
8
|
+
export interface RedirectAfterRenameOptions {
|
|
9
|
+
/** 主标题,默认「正在跳转到新地址...」 */
|
|
10
|
+
message?: string
|
|
11
|
+
/** 副提示,默认「请稍候」 */
|
|
12
|
+
tip?: string
|
|
13
|
+
/** 等待 HMR 的毫秒数,默认 200ms(经验值,给 Vite 重算路由表留时间) */
|
|
14
|
+
delayMs?: number
|
|
15
|
+
/** 站点 base,例如 `/TNotes.introduction/`;不传则使用 `import.meta.env.BASE_URL` 或 `/` */
|
|
16
|
+
base?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 根据后端返回的 `newUrl`(形如 `/notes/xxx/README`)跳转到最终地址。
|
|
21
|
+
*
|
|
22
|
+
* 注意:使用 `window.location.replace` 整页跳,避免 SPA 路由表滞后导致 404。
|
|
23
|
+
*/
|
|
24
|
+
export async function redirectAfterRename(
|
|
25
|
+
newUrl: string,
|
|
26
|
+
opts: RedirectAfterRenameOptions = {},
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const { show } = useRenameOverlay()
|
|
29
|
+
|
|
30
|
+
show({
|
|
31
|
+
message: opts.message ?? '正在跳转到新地址...',
|
|
32
|
+
tip: opts.tip ?? '请稍候',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const base =
|
|
36
|
+
opts.base ??
|
|
37
|
+
(typeof import.meta !== 'undefined' ? import.meta.env?.BASE_URL : '/') ??
|
|
38
|
+
'/'
|
|
39
|
+
|
|
40
|
+
const normalizedBase = base.endsWith('/') ? base : `${base}/`
|
|
41
|
+
const normalizedPath = newUrl.replace(/^\/+/, '')
|
|
42
|
+
const finalUrl = normalizedBase + normalizedPath
|
|
43
|
+
|
|
44
|
+
// 关键:先用 replaceState 把地址栏同步切到新 URL。
|
|
45
|
+
// 文件系统重命名场景下,Vite chokidar 会几乎同时触发 full-reload,
|
|
46
|
+
// 如果只用 setTimeout + location.replace,会被 full-reload 抢先以旧 URL 重载页面。
|
|
47
|
+
// 先 replaceState 后,无论谁先重载,浏览器都从新 URL 拉页面。
|
|
48
|
+
try {
|
|
49
|
+
window.history.replaceState(null, '', finalUrl)
|
|
50
|
+
} catch {
|
|
51
|
+
// 跨域等异常下退回 location.replace
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, opts.delayMs ?? 200))
|
|
55
|
+
|
|
56
|
+
// 再做一次显式跳转,确保 SPA 状态完全重置。
|
|
57
|
+
// 此时 location.href 已是 finalUrl,等价于 reload。
|
|
58
|
+
window.location.replace(finalUrl)
|
|
59
|
+
}
|
|
@@ -1,192 +1,84 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<div
|
|
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
|
-
const base = site.value.base || '/'
|
|
87
|
-
targetUrl = base + 'README'
|
|
88
|
-
tip.value = '参数缺失,即将跳转到首页'
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 倒计时
|
|
92
|
-
countdownTimer = setInterval(() => {
|
|
93
|
-
countdown.value--
|
|
94
|
-
if (countdown.value <= 0 && countdownTimer) {
|
|
95
|
-
clearInterval(countdownTimer)
|
|
96
|
-
}
|
|
97
|
-
}, 1000)
|
|
98
|
-
|
|
99
|
-
// 等待2秒后跳转到目标页面
|
|
100
|
-
timer = setTimeout(() => {
|
|
101
|
-
window.location.href = targetUrl
|
|
102
|
-
}, 2000)
|
|
103
|
-
} catch (err) {
|
|
104
|
-
console.error('Loading page error:', err)
|
|
105
|
-
// 发生错误时,延迟跳转到首页
|
|
106
|
-
setTimeout(() => {
|
|
107
|
-
goHome()
|
|
108
|
-
}, 3000)
|
|
109
|
-
}
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
onUnmounted(() => {
|
|
113
|
-
if (timer) clearTimeout(timer)
|
|
114
|
-
if (countdownTimer) clearInterval(countdownTimer)
|
|
115
|
-
})
|
|
116
|
-
</script>
|
|
117
|
-
|
|
118
|
-
<style scoped>
|
|
119
|
-
.loading-page {
|
|
120
|
-
position: fixed;
|
|
121
|
-
top: 0;
|
|
122
|
-
left: 0;
|
|
123
|
-
right: 0;
|
|
124
|
-
bottom: 0;
|
|
125
|
-
background: var(--vp-c-bg);
|
|
126
|
-
display: flex;
|
|
127
|
-
align-items: center;
|
|
128
|
-
justify-content: center;
|
|
129
|
-
z-index: 9999;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.loading-container {
|
|
133
|
-
text-align: center;
|
|
134
|
-
padding: 2rem;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.spinner {
|
|
138
|
-
width: 60px;
|
|
139
|
-
height: 60px;
|
|
140
|
-
margin: 0 auto 2rem;
|
|
141
|
-
border: 4px solid var(--vp-c-divider);
|
|
142
|
-
border-top-color: var(--vp-c-brand-1);
|
|
143
|
-
border-radius: 50%;
|
|
144
|
-
animation: spin 1s linear infinite;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
@keyframes spin {
|
|
148
|
-
to {
|
|
149
|
-
transform: rotate(360deg);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
h2 {
|
|
154
|
-
font-size: 1.5rem;
|
|
155
|
-
color: var(--vp-c-text-1);
|
|
156
|
-
margin-bottom: 0.5rem;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.tip {
|
|
160
|
-
color: var(--vp-c-text-2);
|
|
161
|
-
font-size: 0.9rem;
|
|
162
|
-
margin-bottom: 1rem;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.countdown {
|
|
166
|
-
color: var(--vp-c-brand-1);
|
|
167
|
-
font-size: 1rem;
|
|
168
|
-
margin-bottom: 1.5rem;
|
|
169
|
-
font-weight: 500;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
.error-msg {
|
|
173
|
-
color: var(--vp-c-danger-1);
|
|
174
|
-
font-size: 0.9rem;
|
|
175
|
-
margin-bottom: 1.5rem;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.back-home-btn {
|
|
179
|
-
padding: 0.5rem 1.5rem;
|
|
180
|
-
background-color: var(--vp-c-brand-1);
|
|
181
|
-
color: white;
|
|
182
|
-
border: none;
|
|
183
|
-
border-radius: 4px;
|
|
184
|
-
cursor: pointer;
|
|
185
|
-
font-size: 0.9rem;
|
|
186
|
-
transition: background-color 0.2s;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.back-home-btn:hover {
|
|
190
|
-
background-color: var(--vp-c-brand-2);
|
|
191
|
-
}
|
|
192
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<ClientOnly>
|
|
3
|
+
<Teleport to="body">
|
|
4
|
+
<div
|
|
5
|
+
v-if="visible"
|
|
6
|
+
class="tnotes-loading-overlay"
|
|
7
|
+
role="alert"
|
|
8
|
+
aria-live="polite"
|
|
9
|
+
>
|
|
10
|
+
<div class="loading-container">
|
|
11
|
+
<div class="spinner"></div>
|
|
12
|
+
<h2>{{ message }}</h2>
|
|
13
|
+
<p v-if="tip" class="tip">{{ tip }}</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</Teleport>
|
|
17
|
+
</ClientOnly>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
withDefaults(
|
|
22
|
+
defineProps<{
|
|
23
|
+
/** 是否显示遮罩,默认显示 */
|
|
24
|
+
visible?: boolean
|
|
25
|
+
/** 主标题 */
|
|
26
|
+
message?: string
|
|
27
|
+
/** 副提示文本 */
|
|
28
|
+
tip?: string
|
|
29
|
+
}>(),
|
|
30
|
+
{
|
|
31
|
+
visible: true,
|
|
32
|
+
message: '正在处理...',
|
|
33
|
+
tip: '',
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<style scoped>
|
|
39
|
+
.tnotes-loading-overlay {
|
|
40
|
+
position: fixed;
|
|
41
|
+
top: 0;
|
|
42
|
+
left: 0;
|
|
43
|
+
right: 0;
|
|
44
|
+
bottom: 0;
|
|
45
|
+
background: var(--vp-c-bg);
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
z-index: 9999;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.loading-container {
|
|
53
|
+
text-align: center;
|
|
54
|
+
padding: 2rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.spinner {
|
|
58
|
+
width: 60px;
|
|
59
|
+
height: 60px;
|
|
60
|
+
margin: 0 auto 2rem;
|
|
61
|
+
border: 4px solid var(--vp-c-divider);
|
|
62
|
+
border-top-color: var(--vp-c-brand-1);
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
animation: tnotes-loading-spin 1s linear infinite;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@keyframes tnotes-loading-spin {
|
|
68
|
+
to {
|
|
69
|
+
transform: rotate(360deg);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
h2 {
|
|
74
|
+
font-size: 1.5rem;
|
|
75
|
+
color: var(--vp-c-text-1);
|
|
76
|
+
margin-bottom: 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.tip {
|
|
80
|
+
color: var(--vp-c-text-2);
|
|
81
|
+
font-size: 0.9rem;
|
|
82
|
+
margin-bottom: 0;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ClientOnly>
|
|
3
|
+
<Teleport to="body">
|
|
4
|
+
<Transition name="tnotes-settings-dialog">
|
|
5
|
+
<div
|
|
6
|
+
v-if="state.visible"
|
|
7
|
+
class="tnotes-settings-overlay"
|
|
8
|
+
role="dialog"
|
|
9
|
+
aria-modal="true"
|
|
10
|
+
aria-labelledby="tnotes-settings-title"
|
|
11
|
+
@click.self="close"
|
|
12
|
+
>
|
|
13
|
+
<div
|
|
14
|
+
class="tnotes-settings-dialog"
|
|
15
|
+
:class="{ 'is-fullscreen': state.fullscreen }"
|
|
16
|
+
>
|
|
17
|
+
<header class="tnotes-settings-header">
|
|
18
|
+
<h2 id="tnotes-settings-title" class="tnotes-settings-title">
|
|
19
|
+
⚙️ 设置
|
|
20
|
+
</h2>
|
|
21
|
+
<div class="tnotes-settings-actions">
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="tnotes-settings-icon-btn"
|
|
25
|
+
:title="state.fullscreen ? '退出全屏' : '全屏'"
|
|
26
|
+
:aria-label="state.fullscreen ? '退出全屏' : '全屏'"
|
|
27
|
+
@click="toggleFullscreen"
|
|
28
|
+
>
|
|
29
|
+
<svg
|
|
30
|
+
v-if="!state.fullscreen"
|
|
31
|
+
width="18"
|
|
32
|
+
height="18"
|
|
33
|
+
viewBox="0 0 24 24"
|
|
34
|
+
fill="none"
|
|
35
|
+
stroke="currentColor"
|
|
36
|
+
stroke-width="2"
|
|
37
|
+
stroke-linecap="round"
|
|
38
|
+
stroke-linejoin="round"
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
>
|
|
41
|
+
<path d="M4 9V4h5" />
|
|
42
|
+
<path d="M20 9V4h-5" />
|
|
43
|
+
<path d="M4 15v5h5" />
|
|
44
|
+
<path d="M20 15v5h-5" />
|
|
45
|
+
</svg>
|
|
46
|
+
<svg
|
|
47
|
+
v-else
|
|
48
|
+
width="18"
|
|
49
|
+
height="18"
|
|
50
|
+
viewBox="0 0 24 24"
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
stroke-width="2"
|
|
54
|
+
stroke-linecap="round"
|
|
55
|
+
stroke-linejoin="round"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
<path d="M9 4v5H4" />
|
|
59
|
+
<path d="M15 4v5h5" />
|
|
60
|
+
<path d="M9 20v-5H4" />
|
|
61
|
+
<path d="M15 20v-5h5" />
|
|
62
|
+
</svg>
|
|
63
|
+
</button>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
class="tnotes-settings-icon-btn"
|
|
67
|
+
title="关闭"
|
|
68
|
+
aria-label="关闭"
|
|
69
|
+
@click="close"
|
|
70
|
+
>
|
|
71
|
+
<svg
|
|
72
|
+
width="18"
|
|
73
|
+
height="18"
|
|
74
|
+
viewBox="0 0 24 24"
|
|
75
|
+
fill="none"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
stroke-width="2"
|
|
78
|
+
stroke-linecap="round"
|
|
79
|
+
stroke-linejoin="round"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
>
|
|
82
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
83
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
84
|
+
</svg>
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</header>
|
|
88
|
+
<div class="tnotes-settings-body">
|
|
89
|
+
<Settings />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</Transition>
|
|
94
|
+
</Teleport>
|
|
95
|
+
</ClientOnly>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<script setup lang="ts">
|
|
99
|
+
import { onBeforeUnmount, watch } from 'vue'
|
|
100
|
+
|
|
101
|
+
import { useSettingsDialog } from './composables/useSettingsDialog'
|
|
102
|
+
import Settings from './Settings.vue'
|
|
103
|
+
|
|
104
|
+
const { state, close, toggleFullscreen } = useSettingsDialog()
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 在 dialog 打开期间,捕获阶段拦截 Ctrl/Cmd+K,
|
|
108
|
+
* 阻止 VitePress 本地搜索弹窗在设置面板之上叠加;
|
|
109
|
+
* 同时在捕获阶段处理 Escape 关闭,避免被搜索栏等其它组件吃掉。
|
|
110
|
+
*/
|
|
111
|
+
function blockSearchHotkey(e: KeyboardEvent) {
|
|
112
|
+
if (e.key === 'Escape') {
|
|
113
|
+
e.stopImmediatePropagation()
|
|
114
|
+
e.preventDefault()
|
|
115
|
+
close()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
e.stopImmediatePropagation()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
watch(
|
|
125
|
+
() => state.visible,
|
|
126
|
+
(visible) => {
|
|
127
|
+
if (typeof document === 'undefined') return
|
|
128
|
+
if (visible) {
|
|
129
|
+
document.addEventListener('keydown', blockSearchHotkey, { capture: true })
|
|
130
|
+
document.body.style.overflow = 'hidden'
|
|
131
|
+
} else {
|
|
132
|
+
document.removeEventListener('keydown', blockSearchHotkey, {
|
|
133
|
+
capture: true,
|
|
134
|
+
})
|
|
135
|
+
document.body.style.overflow = ''
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
onBeforeUnmount(() => {
|
|
141
|
+
if (typeof document === 'undefined') return
|
|
142
|
+
document.removeEventListener('keydown', blockSearchHotkey, { capture: true })
|
|
143
|
+
document.body.style.overflow = ''
|
|
144
|
+
})
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<style scoped>
|
|
148
|
+
.tnotes-settings-overlay {
|
|
149
|
+
position: fixed;
|
|
150
|
+
inset: 0;
|
|
151
|
+
background: rgba(0, 0, 0, 0.5);
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
z-index: 10000;
|
|
156
|
+
padding: 1rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.tnotes-settings-dialog {
|
|
160
|
+
background: var(--vp-c-bg);
|
|
161
|
+
color: var(--vp-c-text-1);
|
|
162
|
+
border: 1px solid var(--vp-c-divider);
|
|
163
|
+
border-radius: 12px;
|
|
164
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.24);
|
|
165
|
+
width: min(720px, 100%);
|
|
166
|
+
max-height: min(80vh, 100%);
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
transition:
|
|
171
|
+
width 0.2s ease,
|
|
172
|
+
max-height 0.2s ease,
|
|
173
|
+
border-radius 0.2s ease;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.tnotes-settings-dialog.is-fullscreen {
|
|
177
|
+
width: 100vw;
|
|
178
|
+
max-width: 100vw;
|
|
179
|
+
height: 100vh;
|
|
180
|
+
max-height: 100vh;
|
|
181
|
+
border-radius: 0;
|
|
182
|
+
border: none;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.tnotes-settings-header {
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: space-between;
|
|
189
|
+
padding: 0.75rem 1rem;
|
|
190
|
+
border-bottom: 1px solid var(--vp-c-divider);
|
|
191
|
+
flex-shrink: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.tnotes-settings-title {
|
|
195
|
+
font-size: 1rem;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
margin: 0;
|
|
198
|
+
color: var(--vp-c-text-1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.tnotes-settings-actions {
|
|
202
|
+
display: flex;
|
|
203
|
+
gap: 0.25rem;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.tnotes-settings-icon-btn {
|
|
207
|
+
display: inline-flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
width: 32px;
|
|
211
|
+
height: 32px;
|
|
212
|
+
padding: 0;
|
|
213
|
+
border: none;
|
|
214
|
+
border-radius: 6px;
|
|
215
|
+
background: transparent;
|
|
216
|
+
color: var(--vp-c-text-2);
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
transition:
|
|
219
|
+
background 0.15s ease,
|
|
220
|
+
color 0.15s ease;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.tnotes-settings-icon-btn:hover {
|
|
224
|
+
background: var(--vp-c-default-soft);
|
|
225
|
+
color: var(--vp-c-text-1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.tnotes-settings-body {
|
|
229
|
+
flex: 1;
|
|
230
|
+
overflow: auto;
|
|
231
|
+
padding: 1rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* dialog 进入/离开过渡 */
|
|
235
|
+
.tnotes-settings-dialog-enter-active,
|
|
236
|
+
.tnotes-settings-dialog-leave-active {
|
|
237
|
+
transition: opacity 0.18s ease;
|
|
238
|
+
}
|
|
239
|
+
.tnotes-settings-dialog-enter-from,
|
|
240
|
+
.tnotes-settings-dialog-leave-to {
|
|
241
|
+
opacity: 0;
|
|
242
|
+
}
|
|
243
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局「设置 Dialog」状态。
|
|
3
|
+
*
|
|
4
|
+
* Layout 顶层渲染 `<SettingsDialog />`,本 store 控制 visible/fullscreen,
|
|
5
|
+
* 任何位置(导航入口、SidebarCard 提示等)调 `open()` 即可弹出,
|
|
6
|
+
* 关闭后停留在当前路由与滚动位置。
|
|
7
|
+
*/
|
|
8
|
+
import { reactive } from 'vue'
|
|
9
|
+
|
|
10
|
+
export interface SettingsDialogState {
|
|
11
|
+
visible: boolean
|
|
12
|
+
fullscreen: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const state = reactive<SettingsDialogState>({
|
|
16
|
+
visible: false,
|
|
17
|
+
fullscreen: false,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export function useSettingsDialog() {
|
|
21
|
+
function open() {
|
|
22
|
+
state.visible = true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function close() {
|
|
26
|
+
state.visible = false
|
|
27
|
+
state.fullscreen = false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toggleFullscreen() {
|
|
31
|
+
state.fullscreen = !state.fullscreen
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { state, open, close, toggleFullscreen }
|
|
35
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useData } from 'vitepress'
|
|
3
|
-
import { useRouter } from 'vitepress'
|
|
4
3
|
import { ref, computed, watch } from 'vue'
|
|
5
4
|
|
|
6
|
-
import { NOTES_DIR_KEY, REPO_NAME, AUTHOR, ROOT_ITEM } from '../constants.ts'
|
|
7
|
-
import { data as sidebarConfig } from '../sidebar.data'
|
|
8
|
-
import { formatDate } from '../utils.ts'
|
|
9
|
-
// @ts-expect-error - VitePress Data Loader
|
|
10
|
-
import MindMapView from './MindMapView.vue'
|
|
11
|
-
import NotesTrendChart from './NotesTrendChart.vue'
|
|
12
5
|
import {
|
|
13
6
|
icon__fold,
|
|
14
7
|
icon__github,
|
|
@@ -19,6 +12,13 @@ import {
|
|
|
19
12
|
icon__number_gray,
|
|
20
13
|
icon__number_purple,
|
|
21
14
|
} from '../../assets/icons'
|
|
15
|
+
import { NOTES_DIR_KEY, REPO_NAME, AUTHOR, ROOT_ITEM } from '../constants.ts'
|
|
16
|
+
import { useSettingsDialog } from '../Settings/composables/useSettingsDialog'
|
|
17
|
+
import { data as sidebarConfig } from '../sidebar.data'
|
|
18
|
+
import { formatDate } from '../utils.ts'
|
|
19
|
+
// @ts-expect-error - VitePress Data Loader
|
|
20
|
+
import MindMapView from './MindMapView.vue'
|
|
21
|
+
import NotesTrendChart from './NotesTrendChart.vue'
|
|
22
22
|
|
|
23
23
|
// #region props
|
|
24
24
|
const props = defineProps({
|
|
@@ -97,7 +97,7 @@ const folderViewData = computed(() => {
|
|
|
97
97
|
})
|
|
98
98
|
// #endregion
|
|
99
99
|
|
|
100
|
-
const
|
|
100
|
+
const { open: openSettings } = useSettingsDialog()
|
|
101
101
|
const { site } = useData()
|
|
102
102
|
const baseUrl = site.value.base.replace(/\/$/, '')
|
|
103
103
|
|
|
@@ -261,10 +261,10 @@ function openVSCodeRepo() {
|
|
|
261
261
|
|
|
262
262
|
if (!notesDir) {
|
|
263
263
|
const shouldRedirect = confirm(
|
|
264
|
-
'
|
|
264
|
+
'请先配置本地知识库所在位置,点击确定打开设置面板',
|
|
265
265
|
)
|
|
266
266
|
if (shouldRedirect) {
|
|
267
|
-
|
|
267
|
+
openSettings()
|
|
268
268
|
}
|
|
269
269
|
return
|
|
270
270
|
}
|
|
@@ -278,10 +278,10 @@ function openVSCodeArticle(article) {
|
|
|
278
278
|
|
|
279
279
|
if (!notesDir) {
|
|
280
280
|
const shouldRedirect = confirm(
|
|
281
|
-
'
|
|
281
|
+
'请先配置本地知识库所在位置,点击确定打开设置面板',
|
|
282
282
|
)
|
|
283
283
|
if (shouldRedirect) {
|
|
284
|
-
|
|
284
|
+
openSettings()
|
|
285
285
|
}
|
|
286
286
|
return
|
|
287
287
|
}
|