@tnotesjs/core 0.0.8 → 0.1.14

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ConfigManager,
3
3
  getConfigManager
4
- } from "./chunk-NASIL5FY.js";
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() {
@@ -45,10 +45,6 @@ function getDefaultConfig(repoName) {
45
45
  text: "\u{1F3E0} Home",
46
46
  link: "/"
47
47
  },
48
- {
49
- text: "\u2699\uFE0F Settings",
50
- link: "/Settings"
51
- },
52
48
  {
53
49
  text: "\u{1F4D2} TNotes",
54
50
  link: "https://tnotesjs.github.io/notes"
package/dist/cli/index.js CHANGED
@@ -22,10 +22,10 @@ import {
22
22
  parseArgs,
23
23
  parseReadmeCompletedNotes,
24
24
  runCommand
25
- } from "../chunk-JUMVJKWV.js";
25
+ } from "../chunk-FVKQPBPP.js";
26
26
  import {
27
27
  ConfigManager
28
- } from "../chunk-NASIL5FY.js";
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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ConfigManager,
3
3
  getConfigManager
4
- } from "./chunk-NASIL5FY.js";
4
+ } from "./chunk-SQVHJR2Z.js";
5
5
  export {
6
6
  ConfigManager,
7
7
  getConfigManager
@@ -4,8 +4,8 @@ import {
4
4
  UpdateNoteConfigCommand,
5
5
  generateAnchor,
6
6
  logger
7
- } from "../../chunk-JUMVJKWV.js";
8
- import "../../chunk-NASIL5FY.js";
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.0.8",
3
+ "version": "0.1.14",
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",
@@ -156,6 +156,7 @@
156
156
  @toggle-expand="toggleSidebarSections"
157
157
  @toggle-note-id="toggleNoteId"
158
158
  @focus-current="focusCurrentNote"
159
+ @open-settings="openSettings"
159
160
  />
160
161
  </template>
161
162
 
@@ -180,6 +181,16 @@
180
181
  <!-- <template #nav-screen-content-before>nav-screen-content-before</template> -->
181
182
  <!-- <template #nav-screen-content-after>nav-screen-content-after</template> -->
182
183
  </Layout>
184
+
185
+ <!-- 全局重命名遮罩:关于面板保存或文件系统重命名时由 useRenameOverlay 控制 -->
186
+ <LoadingPage
187
+ :visible="renameOverlayState.visible"
188
+ :message="renameOverlayState.message"
189
+ :tip="renameOverlayState.tip"
190
+ />
191
+
192
+ <!-- 全局设置 Dialog:由 useSettingsDialog 控制显隐 -->
193
+ <SettingsDialog />
183
194
  </template>
184
195
 
185
196
  <script setup>
@@ -189,15 +200,17 @@ import { computed, onMounted, ref, watch } from "vue";
189
200
 
190
201
  import { useCodeBlockFullscreen } from "../CodeBlockFullscreen";
191
202
  import { SIDEBAR_SHOW_NOTE_ID_KEY } from "../constants";
192
- import Discussions from "../Discussions/Discussions.vue";
193
203
  import { data as allNotesConfig } from "../notesConfig.data.ts";
194
204
  import AboutModal from "./AboutModal.vue";
195
205
  import AboutPanel from "./AboutPanel.vue";
206
+ import Discussions from "../Discussions/Discussions.vue";
207
+ import LoadingPage from "../LoadingPage/LoadingPage.vue";
196
208
  import { useCollapseControl } from "./composables/useCollapseControl";
197
209
  import { useNoteConfig } from "./composables/useNoteConfig";
198
210
  import { useNoteSave } from "./composables/useNoteSave";
199
211
  import { useNoteValidation } from "./composables/useNoteValidation";
200
212
  import { useRedirect } from "./composables/useRedirect";
213
+ import { useRenameOverlay } from "./composables/useRenameOverlay";
201
214
  import { useVSCodeIntegration } from "./composables/useVSCodeIntegration";
202
215
  import ContentCollapse from "./ContentCollapse.vue";
203
216
  import CustomSidebar from "./CustomSidebar.vue";
@@ -208,6 +221,8 @@ import ImagePreview from "./ImagePreview.vue";
208
221
  import NoteStatus from "./NoteStatus.vue";
209
222
  import SidebarNavBefore from "./SidebarNavBefore.vue";
210
223
  import Swiper from "./Swiper.vue";
224
+ import { useSettingsDialog } from "../Settings/composables/useSettingsDialog";
225
+ import SettingsDialog from "../Settings/SettingsDialog.vue";
211
226
 
212
227
  const { Layout } = DefaultTheme;
213
228
  const vpData = useData();
@@ -217,6 +232,12 @@ const route = useRoute();
217
232
  // 启用代码块全屏功能
218
233
  useCodeBlockFullscreen();
219
234
 
235
+ // 全局重命名遮罩状态(由 useRenameRedirect 控制 show/hide)
236
+ const { state: renameOverlayState } = useRenameOverlay();
237
+
238
+ // 全局设置 Dialog 控制
239
+ const { open: openSettings } = useSettingsDialog();
240
+
220
241
  // 自定义侧边栏引用
221
242
  const customSidebarRef = ref(null);
222
243
  const showNoteId = ref(false);
@@ -30,6 +30,16 @@
30
30
  >
31
31
  <img :src="icon__focus" class="toggle-icon" alt="聚焦当前笔记" />
32
32
  </button>
33
+
34
+ <!-- 设置按钮 -->
35
+ <button
36
+ class="toggle-btn settings-btn"
37
+ @click="$emit('open-settings')"
38
+ title="打开设置"
39
+ aria-label="打开设置"
40
+ >
41
+ <span class="settings-icon" aria-hidden="true">⚙️</span>
42
+ </button>
33
43
  </div>
34
44
  </template>
35
45
 
@@ -45,6 +55,7 @@ defineEmits<{
45
55
  'toggle-expand': []
46
56
  'toggle-note-id': []
47
57
  'focus-current': []
58
+ 'open-settings': []
48
59
  }>()
49
60
  </script>
50
61
 
@@ -89,4 +100,15 @@ defineEmits<{
89
100
  .toggle-btn:hover .toggle-icon {
90
101
  transform: scale(1.1);
91
102
  }
103
+
104
+ .settings-icon {
105
+ font-size: 18px;
106
+ line-height: 1;
107
+ display: inline-block;
108
+ transition: transform 0.2s ease;
109
+ }
110
+
111
+ .toggle-btn:hover .settings-icon {
112
+ transform: scale(1.1);
113
+ }
92
114
  </style>
@@ -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
- console.log('重命名完成:', result)
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
- // 如果标题改变了,先跳转到loading页,再由loading页根据configId查询目标URL
136
- if (titleChanged) {
137
- // 获取当前笔记的 configId (UUID)
138
- const configId = allNotesConfig[currentNoteId.value]?.id
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
- const loadingUrl = `${base}loading?configId=${encodeURIComponent(
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
+ }
@@ -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
+ }