@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ConfigManager,
|
|
3
3
|
getConfigManager
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SQVHJR2Z.js";
|
|
5
5
|
|
|
6
6
|
// utils/errorHandler.ts
|
|
7
7
|
var TNotesError = class _TNotesError extends Error {
|
|
@@ -2053,6 +2053,9 @@ var ProcessManager = class {
|
|
|
2053
2053
|
}
|
|
2054
2054
|
};
|
|
2055
2055
|
|
|
2056
|
+
// services/file-watcher/service.ts
|
|
2057
|
+
import { request as httpRequest } from "http";
|
|
2058
|
+
|
|
2056
2059
|
// services/file-watcher/configChangeHandler.ts
|
|
2057
2060
|
var ConfigChangeHandler = class {
|
|
2058
2061
|
constructor(config2) {
|
|
@@ -2227,6 +2230,13 @@ async function safeExecute(label, fn, logger2) {
|
|
|
2227
2230
|
}
|
|
2228
2231
|
}
|
|
2229
2232
|
|
|
2233
|
+
// config/templates.ts
|
|
2234
|
+
function generateNoteTitle(noteIndex, title, repoUrl) {
|
|
2235
|
+
const dirName = `${noteIndex}. ${title}`;
|
|
2236
|
+
const encodedDirName = encodeURIComponent(dirName);
|
|
2237
|
+
return `# [${dirName}](${repoUrl}/${encodedDirName})`;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2230
2240
|
// services/file-watcher/folderChangeHandler.ts
|
|
2231
2241
|
var RENAME_REVERT_DELAY_MS = 2e3;
|
|
2232
2242
|
var DELETE_REINIT_DELAY_MS = 1e3;
|
|
@@ -2305,6 +2315,11 @@ var FolderChangeHandler = class {
|
|
|
2305
2315
|
logger2
|
|
2306
2316
|
);
|
|
2307
2317
|
}
|
|
2318
|
+
this.config.onRenameSuccess?.({
|
|
2319
|
+
oldFolder: oldName,
|
|
2320
|
+
newFolder: newName,
|
|
2321
|
+
noteIndex: newNoteIndex
|
|
2322
|
+
});
|
|
2308
2323
|
} finally {
|
|
2309
2324
|
this.scheduleTimer(() => {
|
|
2310
2325
|
scheduler.setUpdating(false);
|
|
@@ -2333,9 +2348,14 @@ var FolderChangeHandler = class {
|
|
|
2333
2348
|
return { oldNoteIndex, newNoteIndex };
|
|
2334
2349
|
}
|
|
2335
2350
|
async handleTitleOnlyRename(noteIndex, newName) {
|
|
2336
|
-
const { noteIndexCache, readmeService, logger: logger2 } = this.config;
|
|
2351
|
+
const { noteIndexCache, readmeService, logger: logger2, notesDir } = this.config;
|
|
2337
2352
|
logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u672A\u53D8 (${noteIndex})\uFF0C\u53EA\u66F4\u65B0\u6807\u9898`);
|
|
2338
2353
|
noteIndexCache.updateFolderName(noteIndex, newName);
|
|
2354
|
+
await safeExecute(
|
|
2355
|
+
`\u66F4\u65B0 README \u4E00\u7EA7\u6807\u9898 ${noteIndex}`,
|
|
2356
|
+
() => this.rewriteNoteReadmeH1(notesDir, newName, noteIndex),
|
|
2357
|
+
logger2
|
|
2358
|
+
);
|
|
2339
2359
|
const item = noteIndexCache.getByNoteIndex(noteIndex);
|
|
2340
2360
|
if (item) {
|
|
2341
2361
|
await readmeService.updateNoteInReadme(noteIndex, item.noteConfig);
|
|
@@ -2343,6 +2363,37 @@ var FolderChangeHandler = class {
|
|
|
2343
2363
|
await readmeService.regenerateSidebar();
|
|
2344
2364
|
logger2.success(`\u6807\u9898\u66F4\u65B0\u5B8C\u6210`);
|
|
2345
2365
|
}
|
|
2366
|
+
/**
|
|
2367
|
+
* 重写指定笔记目录下 README.md 的一级标题。
|
|
2368
|
+
*
|
|
2369
|
+
* 输入的 `folderName` 形如 `0003. 评论功能的技术实现`,
|
|
2370
|
+
* 据此推导出新标题文本(去掉 `${noteIndex}. ` 前缀)。
|
|
2371
|
+
*/
|
|
2372
|
+
async rewriteNoteReadmeH1(notesDir, folderName, noteIndex) {
|
|
2373
|
+
const readmePath = join3(notesDir, folderName, "README.md");
|
|
2374
|
+
if (!existsSync3(readmePath)) return;
|
|
2375
|
+
const newTitle = folderName.replace(/^\d{4}\.\s*/, "").trim();
|
|
2376
|
+
if (!newTitle) return;
|
|
2377
|
+
const content = await fsPromises.readFile(readmePath, "utf-8");
|
|
2378
|
+
const lines = content.split("\n");
|
|
2379
|
+
let h1Index = -1;
|
|
2380
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2381
|
+
if (lines[i].trim().startsWith("# ")) {
|
|
2382
|
+
h1Index = i;
|
|
2383
|
+
break;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (h1Index === -1) {
|
|
2387
|
+
this.config.logger.warn(
|
|
2388
|
+
`\u672A\u5728 ${readmePath} \u627E\u5230\u4E00\u7EA7\u6807\u9898\uFF0C\u8DF3\u8FC7 README H1 \u66F4\u65B0`
|
|
2389
|
+
);
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const newH1 = generateNoteTitle(noteIndex, newTitle, REPO_NOTES_URL);
|
|
2393
|
+
if (lines[h1Index] === newH1) return;
|
|
2394
|
+
lines[h1Index] = newH1;
|
|
2395
|
+
await fsPromises.writeFile(readmePath, lines.join("\n"), "utf-8");
|
|
2396
|
+
}
|
|
2346
2397
|
async handleIndexChangedRename(oldNoteIndex, newNoteIndex) {
|
|
2347
2398
|
const { noteService, noteIndexCache, readmeService, logger: logger2 } = this.config;
|
|
2348
2399
|
logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4: ${oldNoteIndex} \u2192 ${newNoteIndex}`);
|
|
@@ -2741,13 +2792,6 @@ import { writeFileSync as writeFileSync4, readFileSync as readFileSync5 } from "
|
|
|
2741
2792
|
import { join as join7 } from "path";
|
|
2742
2793
|
import { v4 as uuidv4 } from "uuid";
|
|
2743
2794
|
|
|
2744
|
-
// config/templates.ts
|
|
2745
|
-
function generateNoteTitle(noteIndex, title, repoUrl) {
|
|
2746
|
-
const dirName = `${noteIndex}. ${title}`;
|
|
2747
|
-
const encodedDirName = encodeURIComponent(dirName);
|
|
2748
|
-
return `# [${dirName}](${repoUrl}/${encodedDirName})`;
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
2795
|
// services/readme/service.ts
|
|
2752
2796
|
import {
|
|
2753
2797
|
existsSync as existsSync6,
|
|
@@ -3321,6 +3365,7 @@ var NoteService = class _NoteService {
|
|
|
3321
3365
|
// services/file-watcher/service.ts
|
|
3322
3366
|
var NOTES_DIR_NOT_SET_ERROR = "NOTES_DIR_PATH \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u542F\u52A8\u6587\u4EF6\u76D1\u542C";
|
|
3323
3367
|
var UPDATE_UNLOCK_DELAY_MS2 = 500;
|
|
3368
|
+
var RENAME_BROADCAST_PATH = "/__tnotes_broadcast_rename";
|
|
3324
3369
|
var FileWatcherService = class _FileWatcherService {
|
|
3325
3370
|
constructor(notesDir = NOTES_DIR_PATH) {
|
|
3326
3371
|
this.notesDir = notesDir;
|
|
@@ -3380,7 +3425,58 @@ var FileWatcherService = class _FileWatcherService {
|
|
|
3380
3425
|
noteService: this.noteService,
|
|
3381
3426
|
readmeService: this.readmeService,
|
|
3382
3427
|
noteIndexCache: this.noteIndexCache,
|
|
3383
|
-
logger
|
|
3428
|
+
logger,
|
|
3429
|
+
onRenameSuccess: (payload) => {
|
|
3430
|
+
void this.broadcastRename(payload);
|
|
3431
|
+
}
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
async broadcastRename(payload) {
|
|
3435
|
+
const path2 = `/${repoName}${RENAME_BROADCAST_PATH}`;
|
|
3436
|
+
const body = JSON.stringify(payload);
|
|
3437
|
+
const hosts = ["127.0.0.1", "::1"];
|
|
3438
|
+
let lastError = null;
|
|
3439
|
+
for (const host of hosts) {
|
|
3440
|
+
try {
|
|
3441
|
+
await this.postOnce(host, path2, body);
|
|
3442
|
+
return;
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
lastError = error;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
logger.warn(
|
|
3448
|
+
`\u5E7F\u64AD\u91CD\u547D\u540D\u4E8B\u4EF6\u5931\u8D25 (POST http://[127.0.0.1|::1]:${port}${path2}): ${String(lastError)}`
|
|
3449
|
+
);
|
|
3450
|
+
}
|
|
3451
|
+
postOnce(host, path2, body) {
|
|
3452
|
+
return new Promise((resolve2, reject) => {
|
|
3453
|
+
const req = httpRequest(
|
|
3454
|
+
{
|
|
3455
|
+
host,
|
|
3456
|
+
port,
|
|
3457
|
+
path: path2,
|
|
3458
|
+
method: "POST",
|
|
3459
|
+
family: host.includes(":") ? 6 : 4,
|
|
3460
|
+
headers: {
|
|
3461
|
+
"Content-Type": "application/json",
|
|
3462
|
+
"Content-Length": Buffer.byteLength(body)
|
|
3463
|
+
}
|
|
3464
|
+
},
|
|
3465
|
+
(res) => {
|
|
3466
|
+
res.on("data", () => {
|
|
3467
|
+
});
|
|
3468
|
+
res.on("end", () => {
|
|
3469
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
3470
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
resolve2();
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
);
|
|
3477
|
+
req.on("error", reject);
|
|
3478
|
+
req.write(body);
|
|
3479
|
+
req.end();
|
|
3384
3480
|
});
|
|
3385
3481
|
}
|
|
3386
3482
|
initRenameDetector() {
|
package/dist/cli/index.js
CHANGED
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
parseArgs,
|
|
23
23
|
parseReadmeCompletedNotes,
|
|
24
24
|
runCommand
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-FVKQPBPP.js";
|
|
26
26
|
import {
|
|
27
27
|
ConfigManager
|
|
28
|
-
} from "../chunk-
|
|
28
|
+
} from "../chunk-SQVHJR2Z.js";
|
|
29
29
|
|
|
30
30
|
// commands/update/UpdateCommand.ts
|
|
31
31
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
UpdateNoteConfigCommand,
|
|
5
5
|
generateAnchor,
|
|
6
6
|
logger
|
|
7
|
-
} from "../../chunk-
|
|
8
|
-
import "../../chunk-
|
|
7
|
+
} from "../../chunk-FVKQPBPP.js";
|
|
8
|
+
import "../../chunk-SQVHJR2Z.js";
|
|
9
9
|
|
|
10
10
|
// vitepress/config/index.ts
|
|
11
11
|
import fs2 from "fs";
|
|
@@ -639,6 +639,42 @@ function buildProgressPlugin(options = {}) {
|
|
|
639
639
|
};
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
// vitepress/plugins/fileWatcherBridgePlugin.ts
|
|
643
|
+
var RENAME_EVENT = "tnotes:note-renamed";
|
|
644
|
+
var BROADCAST_PATH = "/__tnotes_broadcast_rename";
|
|
645
|
+
function fileWatcherBridgePlugin() {
|
|
646
|
+
return {
|
|
647
|
+
name: "tnotes-file-watcher-bridge",
|
|
648
|
+
apply: "serve",
|
|
649
|
+
configureServer(server) {
|
|
650
|
+
server.middlewares.use((req, res, next) => {
|
|
651
|
+
const url = req.url ?? "";
|
|
652
|
+
const isBroadcast = url === BROADCAST_PATH || url.endsWith(BROADCAST_PATH);
|
|
653
|
+
if (!isBroadcast || req.method !== "POST") {
|
|
654
|
+
return next();
|
|
655
|
+
}
|
|
656
|
+
let body = "";
|
|
657
|
+
req.on("data", (chunk) => {
|
|
658
|
+
body += chunk.toString();
|
|
659
|
+
});
|
|
660
|
+
req.on("end", () => {
|
|
661
|
+
try {
|
|
662
|
+
const data = JSON.parse(body || "{}");
|
|
663
|
+
server.ws.send({ type: "custom", event: RENAME_EVENT, data });
|
|
664
|
+
res.statusCode = 204;
|
|
665
|
+
res.end();
|
|
666
|
+
} catch (error) {
|
|
667
|
+
res.statusCode = 400;
|
|
668
|
+
res.end(
|
|
669
|
+
error instanceof Error ? error.message : "Bad rename payload"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
642
678
|
// vitepress/plugins/getNoteByConfigIdPlugin.ts
|
|
643
679
|
function getNoteByConfigIdPlugin() {
|
|
644
680
|
return {
|
|
@@ -746,6 +782,10 @@ function renameNotePlugin() {
|
|
|
746
782
|
const startTime = Date.now();
|
|
747
783
|
await renameCommand.renameNote({ noteIndex, newTitle });
|
|
748
784
|
const duration = Date.now() - startTime;
|
|
785
|
+
const newFolderName = `${noteIndex}. ${newTitle.trim()}`;
|
|
786
|
+
const newUrl = `/notes/${encodeURIComponent(
|
|
787
|
+
newFolderName
|
|
788
|
+
)}/README`;
|
|
749
789
|
res.statusCode = 200;
|
|
750
790
|
res.setHeader("Content-Type", "application/json");
|
|
751
791
|
res.end(
|
|
@@ -753,6 +793,8 @@ function renameNotePlugin() {
|
|
|
753
793
|
success: true,
|
|
754
794
|
duration,
|
|
755
795
|
newTitle,
|
|
796
|
+
newFolderName,
|
|
797
|
+
newUrl,
|
|
756
798
|
message: "\u91CD\u547D\u540D\u5B8C\u6210"
|
|
757
799
|
})
|
|
758
800
|
);
|
|
@@ -856,9 +898,13 @@ function defineNotesConfig(overrides = {}) {
|
|
|
856
898
|
updateConfigPlugin(),
|
|
857
899
|
renameNotePlugin(),
|
|
858
900
|
getNoteByConfigIdPlugin(),
|
|
901
|
+
fileWatcherBridgePlugin(),
|
|
859
902
|
...overrideVite?.plugins || []
|
|
860
903
|
],
|
|
861
904
|
server: {
|
|
905
|
+
// 显式同时监听 IPv4 / IPv6,避免 Node 18+ 在 Windows 下偶发只绑定 ::1
|
|
906
|
+
// 导致 Chrome 解析 localhost → 127.0.0.1 时连接被拒、页面一直 pending。
|
|
907
|
+
// host: true,
|
|
862
908
|
watch: {
|
|
863
909
|
ignored: IGNORE_LIST
|
|
864
910
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tnotesjs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.17.1",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsup",
|
|
31
31
|
"build:check": "tsc --noEmit -p tsconfig.build.json",
|
|
32
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.build.json",
|
|
32
33
|
"dev": "tsup --watch",
|
|
33
34
|
"release": "node scripts/release.mjs",
|
|
34
35
|
"lint": "eslint . --max-warnings=0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#67676c" d="M19.9 12.66a1 1 0 0 1 0-1.32l1.28-1.44a1 1 0 0 0 .12-1.17l-2-3.46a1 1 0 0 0-1.07-.48l-1.88.38a1 1 0 0 1-1.15-.66l-.61-1.83a1 1 0 0 0-.95-.68h-4a1 1 0 0 0-1 .68l-.56 1.83a1 1 0 0 1-1.15.66L5 4.79a1 1 0 0 0-1 .48L2 8.73a1 1 0 0 0 .1 1.17l1.27 1.44a1 1 0 0 1 0 1.32L2.1 14.1a1 1 0 0 0-.1 1.17l2 3.46a1 1 0 0 0 1.07.48l1.88-.38a1 1 0 0 1 1.15.66l.61 1.83a1 1 0 0 0 1 .68h4a1 1 0 0 0 .95-.68l.61-1.83a1 1 0 0 1 1.15-.66l1.88.38a1 1 0 0 0 1.07-.48l2-3.46a1 1 0 0 0-.12-1.17ZM18.41 14l.8.9l-1.28 2.22l-1.18-.24a3 3 0 0 0-3.45 2L12.92 20h-2.56L10 18.86a3 3 0 0 0-3.45-2l-1.18.24l-1.3-2.21l.8-.9a3 3 0 0 0 0-4l-.8-.9l1.28-2.2l1.18.24a3 3 0 0 0 3.45-2L10.36 4h2.56l.38 1.14a3 3 0 0 0 3.45 2l1.18-.24l1.28 2.22l-.8.9a3 3 0 0 0 0 3.98m-6.77-6a4 4 0 1 0 4 4a4 4 0 0 0-4-4m0 6a2 2 0 1 1 2-2a2 2 0 0 1-2 2"/></svg>
|
|
@@ -28,6 +28,7 @@ export { default as icon__prev } from './icon__prev.svg'
|
|
|
28
28
|
export { default as icon__restore } from './icon__restore.svg'
|
|
29
29
|
export { default as icon__rotate } from './icon__rotate.svg'
|
|
30
30
|
export { default as icon__search } from './icon__search.svg'
|
|
31
|
+
export { default as icon__setting } from './icon__setting.svg'
|
|
31
32
|
export { default as icon__sidebar_collapsed } from './icon__sidebar_collapsed.svg'
|
|
32
33
|
export { default as icon__sidebar_opened } from './icon__sidebar_opened.svg'
|
|
33
34
|
export { default as icon__totop } from './icon__totop.svg'
|
|
@@ -174,12 +174,25 @@
|
|
|
174
174
|
<!-- <template #nav-bar-title-before>nav-bar-title-before</template> -->
|
|
175
175
|
<!-- <template #nav-bar-title-after>nav-bar-title-after</template> -->
|
|
176
176
|
<!-- <template #nav-bar-content-before>nav-bar-content-before</template> -->
|
|
177
|
-
<!--
|
|
177
|
+
<!-- 设置入口:固定在 VPNav 右侧(social-links / appearance 之后,extra 之前) -->
|
|
178
|
+
<template #nav-bar-content-after>
|
|
179
|
+
<NavBarSettingsTrigger />
|
|
180
|
+
</template>
|
|
178
181
|
|
|
179
182
|
<!-- !NOTE 不清楚下面的插槽所对应的位置 -->
|
|
180
183
|
<!-- <template #nav-screen-content-before>nav-screen-content-before</template> -->
|
|
181
184
|
<!-- <template #nav-screen-content-after>nav-screen-content-after</template> -->
|
|
182
185
|
</Layout>
|
|
186
|
+
|
|
187
|
+
<!-- 全局重命名遮罩:关于面板保存或文件系统重命名时由 useRenameOverlay 控制 -->
|
|
188
|
+
<LoadingPage
|
|
189
|
+
:visible="renameOverlayState.visible"
|
|
190
|
+
:message="renameOverlayState.message"
|
|
191
|
+
:tip="renameOverlayState.tip"
|
|
192
|
+
/>
|
|
193
|
+
|
|
194
|
+
<!-- 全局设置 Dialog:由 useSettingsDialog 控制显隐 -->
|
|
195
|
+
<SettingsDialog />
|
|
183
196
|
</template>
|
|
184
197
|
|
|
185
198
|
<script setup>
|
|
@@ -189,15 +202,17 @@ import { computed, onMounted, ref, watch } from "vue";
|
|
|
189
202
|
|
|
190
203
|
import { useCodeBlockFullscreen } from "../CodeBlockFullscreen";
|
|
191
204
|
import { SIDEBAR_SHOW_NOTE_ID_KEY } from "../constants";
|
|
192
|
-
import Discussions from "../Discussions/Discussions.vue";
|
|
193
205
|
import { data as allNotesConfig } from "../notesConfig.data.ts";
|
|
194
206
|
import AboutModal from "./AboutModal.vue";
|
|
195
207
|
import AboutPanel from "./AboutPanel.vue";
|
|
208
|
+
import Discussions from "../Discussions/Discussions.vue";
|
|
209
|
+
import LoadingPage from "../LoadingPage/LoadingPage.vue";
|
|
196
210
|
import { useCollapseControl } from "./composables/useCollapseControl";
|
|
197
211
|
import { useNoteConfig } from "./composables/useNoteConfig";
|
|
198
212
|
import { useNoteSave } from "./composables/useNoteSave";
|
|
199
213
|
import { useNoteValidation } from "./composables/useNoteValidation";
|
|
200
214
|
import { useRedirect } from "./composables/useRedirect";
|
|
215
|
+
import { useRenameOverlay } from "./composables/useRenameOverlay";
|
|
201
216
|
import { useVSCodeIntegration } from "./composables/useVSCodeIntegration";
|
|
202
217
|
import ContentCollapse from "./ContentCollapse.vue";
|
|
203
218
|
import CustomSidebar from "./CustomSidebar.vue";
|
|
@@ -205,9 +220,11 @@ import DocBeforeControls from "./DocBeforeControls.vue";
|
|
|
205
220
|
import DocFooter from "./DocFooter.vue";
|
|
206
221
|
import { data as readmeData } from "./homeReadme.data.ts";
|
|
207
222
|
import ImagePreview from "./ImagePreview.vue";
|
|
223
|
+
import NavBarSettingsTrigger from "./NavBarSettingsTrigger.vue";
|
|
208
224
|
import NoteStatus from "./NoteStatus.vue";
|
|
209
225
|
import SidebarNavBefore from "./SidebarNavBefore.vue";
|
|
210
226
|
import Swiper from "./Swiper.vue";
|
|
227
|
+
import SettingsDialog from "../Settings/SettingsDialog.vue";
|
|
211
228
|
|
|
212
229
|
const { Layout } = DefaultTheme;
|
|
213
230
|
const vpData = useData();
|
|
@@ -217,6 +234,9 @@ const route = useRoute();
|
|
|
217
234
|
// 启用代码块全屏功能
|
|
218
235
|
useCodeBlockFullscreen();
|
|
219
236
|
|
|
237
|
+
// 全局重命名遮罩状态(由 useRenameRedirect 控制 show/hide)
|
|
238
|
+
const { state: renameOverlayState } = useRenameOverlay();
|
|
239
|
+
|
|
220
240
|
// 自定义侧边栏引用
|
|
221
241
|
const customSidebarRef = ref(null);
|
|
222
242
|
const showNoteId = ref(false);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
class="tnotes-nav-settings-trigger"
|
|
5
|
+
title="打开设置"
|
|
6
|
+
aria-label="打开设置"
|
|
7
|
+
@click="open"
|
|
8
|
+
>
|
|
9
|
+
<img
|
|
10
|
+
:src="icon__setting"
|
|
11
|
+
class="tnotes-nav-settings-icon"
|
|
12
|
+
alt=""
|
|
13
|
+
aria-hidden="true"
|
|
14
|
+
/>
|
|
15
|
+
</button>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { icon__setting } from '../../assets/icons'
|
|
20
|
+
import { useSettingsDialog } from '../Settings/composables/useSettingsDialog'
|
|
21
|
+
|
|
22
|
+
const { open } = useSettingsDialog()
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<style scoped>
|
|
26
|
+
.tnotes-nav-settings-trigger {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
width: 36px;
|
|
31
|
+
height: 36px;
|
|
32
|
+
margin: 0 4px;
|
|
33
|
+
padding: 0;
|
|
34
|
+
background: transparent;
|
|
35
|
+
border: none;
|
|
36
|
+
border-radius: 6px;
|
|
37
|
+
color: var(--vp-c-text-2);
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
transition:
|
|
40
|
+
background 0.15s ease,
|
|
41
|
+
color 0.15s ease;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.tnotes-nav-settings-trigger:hover {
|
|
45
|
+
background: var(--vp-c-default-soft);
|
|
46
|
+
color: var(--vp-c-text-1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.tnotes-nav-settings-icon {
|
|
50
|
+
width: 20px;
|
|
51
|
+
height: 20px;
|
|
52
|
+
display: block;
|
|
53
|
+
transition: transform 0.2s ease;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.tnotes-nav-settings-trigger:hover .tnotes-nav-settings-icon {
|
|
57
|
+
transform: scale(1.1);
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -1,35 +1,40 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="sidebar-toggle-wrapper">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
:src="
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
</button>
|
|
3
|
+
<div class="group group-left">
|
|
4
|
+
<!-- 展开/折叠全部按钮 -->
|
|
5
|
+
<button
|
|
6
|
+
class="toggle-btn"
|
|
7
|
+
@click="$emit('toggle-expand')"
|
|
8
|
+
:title="isExpanded ? '全部折叠' : '全部展开'"
|
|
9
|
+
>
|
|
10
|
+
<img :src="icon__fold" class="toggle-icon" alt="切换展开折叠" />
|
|
11
|
+
</button>
|
|
12
|
+
<span class="group-label">目录</span>
|
|
13
|
+
</div>
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
<div class="group group-right">
|
|
16
|
+
<!-- 聚焦到当前笔记按钮 -->
|
|
17
|
+
<button
|
|
18
|
+
class="toggle-btn"
|
|
19
|
+
@click="$emit('focus-current')"
|
|
20
|
+
title="聚焦到当前笔记(点击切换多个位置)"
|
|
21
|
+
>
|
|
22
|
+
<img :src="icon__focus" class="toggle-icon" alt="聚焦当前笔记" />
|
|
23
|
+
</button>
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
<!-- 笔记编号显示切换按钮 -->
|
|
26
|
+
<button
|
|
27
|
+
class="toggle-btn"
|
|
28
|
+
@click="$emit('toggle-note-id')"
|
|
29
|
+
:title="showNoteId ? '隐藏笔记编号' : '显示笔记编号'"
|
|
30
|
+
>
|
|
31
|
+
<img
|
|
32
|
+
:src="showNoteId ? icon__number_purple : icon__number_gray"
|
|
33
|
+
class="toggle-icon"
|
|
34
|
+
alt="切换笔记编号"
|
|
35
|
+
/>
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
33
38
|
</div>
|
|
34
39
|
</template>
|
|
35
40
|
|
|
@@ -52,13 +57,24 @@ defineEmits<{
|
|
|
52
57
|
.sidebar-toggle-wrapper {
|
|
53
58
|
display: flex;
|
|
54
59
|
align-items: center;
|
|
55
|
-
justify-content:
|
|
56
|
-
|
|
57
|
-
padding: 12px 0 16px 0;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
padding: 12px 8px 16px;
|
|
58
62
|
border-bottom: 1px solid var(--vp-c-divider);
|
|
59
63
|
margin-bottom: 16px;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
.group {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 4px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.group-label {
|
|
73
|
+
font-size: 13px;
|
|
74
|
+
color: var(--vp-c-text-2);
|
|
75
|
+
user-select: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
.toggle-btn {
|
|
63
79
|
display: inline-flex;
|
|
64
80
|
align-items: center;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useData } from 'vitepress'
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { redirectAfterRename } from './useRenameRedirect'
|
|
3
5
|
|
|
4
6
|
import type { NoteConfig } from '../../../../types'
|
|
5
7
|
import type { Ref, ComputedRef } from 'vue'
|
|
@@ -54,6 +56,8 @@ export function useNoteSave(
|
|
|
54
56
|
isSaving.value = true
|
|
55
57
|
savingMessage.value = '正在保存配置...'
|
|
56
58
|
|
|
59
|
+
let renameNewUrl: string | null = null
|
|
60
|
+
|
|
57
61
|
try {
|
|
58
62
|
// 如果标题有变化,先重命名文件夹
|
|
59
63
|
if (titleChanged) {
|
|
@@ -75,10 +79,17 @@ export function useNoteSave(
|
|
|
75
79
|
throw new Error(`重命名失败: ${error}`)
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
//
|
|
79
|
-
const result = await renameResponse.json()
|
|
80
|
-
|
|
82
|
+
// 后端已经完成所有更新(含文件系统、根 README、sidebar)
|
|
83
|
+
const result = (await renameResponse.json()) as {
|
|
84
|
+
success: boolean
|
|
85
|
+
newUrl?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!result?.newUrl) {
|
|
89
|
+
throw new Error('重命名响应缺少 newUrl 字段')
|
|
90
|
+
}
|
|
81
91
|
|
|
92
|
+
renameNewUrl = result.newUrl
|
|
82
93
|
savingMessage.value = '文件已同步,准备跳转...'
|
|
83
94
|
}
|
|
84
95
|
|
|
@@ -132,21 +143,14 @@ export function useNoteSave(
|
|
|
132
143
|
showSuccessToast.value = false
|
|
133
144
|
}, 3000)
|
|
134
145
|
|
|
135
|
-
//
|
|
136
|
-
if (titleChanged) {
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (!configId) {
|
|
141
|
-
throw new Error('无法获取笔记的 configId')
|
|
142
|
-
}
|
|
146
|
+
// 如果标题改变了, 显示遮罩并整页跳到新 URL
|
|
147
|
+
if (titleChanged && renameNewUrl) {
|
|
148
|
+
// 提前结束「保存中」状态,避免遮罩退出后按钮文案残留
|
|
149
|
+
isSaving.value = false
|
|
150
|
+
savingMessage.value = ''
|
|
143
151
|
|
|
144
|
-
// 跳转到loading页,传递 configId 参数
|
|
145
152
|
const base = vpData.site.value.base || '/'
|
|
146
|
-
|
|
147
|
-
configId
|
|
148
|
-
)}`
|
|
149
|
-
window.location.href = loadingUrl
|
|
153
|
+
await redirectAfterRename(renameNewUrl, { base })
|
|
150
154
|
}
|
|
151
155
|
} catch (error) {
|
|
152
156
|
console.error('保存配置失败:', error)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局「重命名遮罩」状态。
|
|
3
|
+
*
|
|
4
|
+
* Layout 顶层渲染 `<LoadingPage />`,由本 store 控制 visible/message/tip,
|
|
5
|
+
* `useRenameRedirect` 调用 show/hide 即可。
|
|
6
|
+
*/
|
|
7
|
+
import { reactive } from 'vue'
|
|
8
|
+
|
|
9
|
+
export interface RenameOverlayState {
|
|
10
|
+
visible: boolean
|
|
11
|
+
message: string
|
|
12
|
+
tip: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const state = reactive<RenameOverlayState>({
|
|
16
|
+
visible: false,
|
|
17
|
+
message: '正在处理...',
|
|
18
|
+
tip: '',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export function useRenameOverlay() {
|
|
22
|
+
function show(payload: { message?: string; tip?: string } = {}) {
|
|
23
|
+
state.message = payload.message ?? '正在处理...'
|
|
24
|
+
state.tip = payload.tip ?? ''
|
|
25
|
+
state.visible = true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hide() {
|
|
29
|
+
state.visible = false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { state, show, hide }
|
|
33
|
+
}
|