Skip to content

Commit e8dc9b0

Browse files
committed
feat(note): expose slug in admin and fix type errors
1 parent c8479f1 commit e8dc9b0

7 files changed

Lines changed: 98 additions & 47 deletions

File tree

src/api/notes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface GetNotesParams {
1515
export interface CreateNoteData {
1616
title: string
1717
text: string
18+
slug?: string
1819
mood?: string
1920
weather?: string
2021
password?: string | null
@@ -34,6 +35,7 @@ export interface UpdateNoteData extends Partial<CreateNoteData> {}
3435
// 用于 patch 操作的数据类型,允许将某些字段设为 null
3536
export interface PatchNoteData {
3637
topicId?: string | null
38+
slug?: string
3739
[key: string]: unknown
3840
}
3941

src/components/editor/codemirror/use-auto-theme.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { watch } from 'vue'
2-
import type { EditorView } from '@codemirror/view/dist'
3-
import type { Ref } from 'vue'
4-
51
import { oneDark } from '@codemirror/theme-one-dark'
2+
import type { EditorView } from '@codemirror/view'
63
import { githubLight } from '@ddietr/codemirror-themes/theme/github-light'
4+
import type { Ref } from 'vue'
5+
import { watch } from 'vue'
76

87
import { useStoreRef } from '~/hooks/use-store-ref'
98
import { UIStore } from '~/stores/ui'

src/components/editor/rich/RichEditor.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
1-
import { createElement } from 'react'
2-
import { createRoot } from 'react-dom/client'
3-
import { defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue'
4-
import type { ImageUploadFn, RichEditorVariant } from '@haklex/rich-editor'
1+
import type { RichEditorVariant } from '@haklex/rich-editor'
2+
import { DialogStackProvider } from '@haklex/rich-editor-ui'
53
import type { NestedDocDialogEditorProps } from '@haklex/rich-ext-nested-doc'
4+
import {
5+
NestedDocDialogEditorProvider,
6+
NestedDocPlugin,
7+
nestedDocEditNodes,
8+
} from '@haklex/rich-ext-nested-doc'
69
import type { ShiroEditorProps } from '@haklex/rich-kit-shiro'
10+
import { ExcalidrawConfigProvider, ShiroEditor } from '@haklex/rich-kit-shiro'
11+
import { ToolbarPlugin } from '@haklex/rich-plugin-toolbar'
12+
import { $convertToMarkdownString, TRANSFORMERS } from '@lexical/markdown'
713
import type {
814
Klass,
915
LexicalEditor,
1016
LexicalNode,
1117
SerializedEditorState,
1218
} from 'lexical'
19+
import { createElement } from 'react'
1320
import type { Root } from 'react-dom/client'
21+
import { createRoot } from 'react-dom/client'
1422
import type { PropType } from 'vue'
15-
16-
import { DialogStackProvider } from '@haklex/rich-editor-ui'
17-
import {
18-
NestedDocDialogEditorProvider,
19-
nestedDocEditNodes,
20-
NestedDocPlugin,
21-
} from '@haklex/rich-ext-nested-doc'
22-
import { ExcalidrawConfigProvider, ShiroEditor } from '@haklex/rich-kit-shiro'
23-
import { ToolbarPlugin } from '@haklex/rich-plugin-toolbar'
24-
import { $convertToMarkdownString, TRANSFORMERS } from '@lexical/markdown'
23+
import { defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue'
2524

2625
import '@haklex/rich-kit-shiro/style.css'
2726
import '@haklex/rich-plugin-toolbar/style.css'
@@ -117,7 +116,7 @@ export const RichEditor = defineComponent({
117116
selfHostnames: Array as PropType<string[]>,
118117
extraNodes: Array as PropType<Array<Klass<LexicalNode>>>,
119118
editorStyle: Object as PropType<Record<string, string | number>>,
120-
imageUpload: Function as PropType<ImageUploadFn>,
119+
imageUpload: Function as PropType<ShiroEditorProps['imageUpload']>,
121120
},
122121
emits: {
123122
change: (_value: SerializedEditorState) => true,

src/components/update-detail-modal/index.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,7 @@ import { getReleaseDetails } from '../../external/api/github-check-update'
66

77
import './markdown-styles.css'
88

9-
interface ReleaseDetails {
10-
name: string
11-
body: string
12-
html_url: string
13-
published_at: string
14-
tag_name: string
15-
}
9+
type ReleaseDetails = Awaited<ReturnType<typeof getReleaseDetails>>
1610

1711
export const UpdateDetailModal = defineComponent({
1812
props: {
@@ -36,8 +30,8 @@ export const UpdateDetailModal = defineComponent({
3630
try {
3731
const details = await getReleaseDetails(props.repo, props.version)
3832
releaseDetails.value = details
39-
} catch (error) {
40-
console.error('获取发布详情失败:', error)
33+
} catch {
34+
// Ignore transient GitHub API failures and keep the modal in fallback state.
4135
} finally {
4236
loading.value = false
4337
}
@@ -59,7 +53,10 @@ export const UpdateDetailModal = defineComponent({
5953
}
6054
}
6155

62-
const formatDate = (dateString: string) => {
56+
const formatDate = (dateString: string | null) => {
57+
if (!dateString) {
58+
return '未知时间'
59+
}
6360
return new Date(dateString).toLocaleString('zh-CN')
6461
}
6562

@@ -118,8 +115,7 @@ export const UpdateDetailModal = defineComponent({
118115
const htmlString =
119116
typeof result === 'string' ? result : markdown.replace(/\n/g, '<br>')
120117
return sanitizeHtml(htmlString)
121-
} catch (error) {
122-
console.error('Markdown 渲染失败:', error)
118+
} catch {
123119
// 降级到简单的文本显示
124120
return markdown.replace(/\n/g, '<br>')
125121
}

src/models/note.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface NoteModel {
1919
publicAt?: Date
2020
password?: string | null
2121
nid: number
22+
slug?: string
2223
hide: boolean
2324

2425
location?: string

src/views/manage-notes/list.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useMutation, useQueryClient } from '@tanstack/vue-query'
2+
import { debouncedRef } from '@vueuse/core'
13
import {
24
Book as BookIcon,
35
Bookmark as BookmarkIcon,
@@ -11,15 +13,11 @@ import {
1113
Trash2,
1214
} from 'lucide-vue-next'
1315
import { NButton, NEllipsis, NInput, NPopconfirm, NSpace } from 'naive-ui'
16+
import type { TableColumns } from 'naive-ui/lib/data-table/src/interface'
17+
import type { PropType } from 'vue'
1418
import { computed, defineComponent, reactive, ref, watchEffect } from 'vue'
1519
import { RouterLink } from 'vue-router'
1620
import { toast } from 'vue-sonner'
17-
import type { NoteModel } from '~/models/note'
18-
import type { TableColumns } from 'naive-ui/lib/data-table/src/interface'
19-
import type { PropType } from 'vue'
20-
21-
import { useMutation, useQueryClient } from '@tanstack/vue-query'
22-
import { debouncedRef } from '@vueuse/core'
2321

2422
import { notesApi } from '~/api/notes'
2523
import { searchApi } from '~/api/search'
@@ -33,12 +31,24 @@ import { WEB_URL } from '~/constants/env'
3331
import { queryKeys } from '~/hooks/queries/keys'
3432
import { useDataTable } from '~/hooks/use-data-table'
3533
import { useStoreRef } from '~/hooks/use-store-ref'
34+
import type { NoteModel } from '~/models/note'
3635
import { UIStore } from '~/stores/ui'
3736
import { formatNumber } from '~/utils/number'
3837

3938
import { HeaderActionButton } from '../../components/button/header-action-button'
4039
import { useLayout } from '../../layouts/content'
4140

41+
const buildNotePublicPath = (
42+
note: Pick<NoteModel, 'nid' | 'slug' | 'created'>,
43+
) => {
44+
if (note.slug) {
45+
const date = new Date(note.created)
46+
return `/notes/${date.getUTCFullYear()}/${date.getUTCMonth() + 1}/${date.getUTCDate()}/${note.slug}`
47+
}
48+
49+
return `/notes/${note.nid}`
50+
}
51+
4252
const NoteItem = defineComponent({
4353
name: 'NoteItem',
4454
props: {
@@ -97,6 +107,9 @@ const NoteItem = defineComponent({
97107
{row.value.location}
98108
</span>
99109
)}
110+
<span class="font-mono text-xs text-neutral-400 dark:text-neutral-500">
111+
{row.value.slug || '—'}
112+
</span>
100113
<span class="flex items-center gap-0.5 text-xs text-neutral-400 dark:text-neutral-500">
101114
<BookIcon class="h-2.5 w-2.5" />
102115
{formatNumber(row.value.count?.read || 0)}
@@ -121,7 +134,7 @@ const NoteItem = defineComponent({
121134

122135
<div class="flex shrink-0 items-center">
123136
<a
124-
href={`${WEB_URL}/notes/${row.value.nid}`}
137+
href={`${WEB_URL}${buildNotePublicPath(row.value)}`}
125138
target="_blank"
126139
rel="noopener noreferrer"
127140
aria-label="在新窗口打开日记"
@@ -212,7 +225,7 @@ export const ManageNoteListView = defineComponent({
212225
page: params.page,
213226
size: params.size,
214227
select:
215-
'title _id nid id created modified mood weather publicAt bookmark coordinates location count meta isPublished',
228+
'title _id nid id slug created modified mood weather publicAt bookmark coordinates location count meta isPublished',
216229
sortBy: params.sortBy || undefined,
217230
sortOrder: params.sortOrder || undefined,
218231
db_query: params.filters?.dbQuery,
@@ -340,7 +353,7 @@ export const ManageNoteListView = defineComponent({
340353
<TableTitleLink
341354
inPageTo={`/notes/edit?id=${row.id}`}
342355
title={row.title}
343-
externalLinkTo={`/notes/${row.nid}`}
356+
externalLinkTo={buildNotePublicPath(row)}
344357
id={row.id}
345358
withToken={isUnpublished || isSecret}
346359
>
@@ -382,6 +395,14 @@ export const ManageNoteListView = defineComponent({
382395
)
383396
},
384397
},
398+
{
399+
title: 'Slug',
400+
key: 'slug',
401+
width: 220,
402+
render(row) {
403+
return <span class="font-mono text-xs">{row.slug || '—'}</span>
404+
},
405+
},
385406
{
386407
title: '天气',
387408
key: 'weather',

src/views/manage-notes/write.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import type { TopicModel } from '@mx-space/api-client'
12
import { add } from 'date-fns'
23
import { isString } from 'es-toolkit/compat'
4+
import type { LexicalEditor } from 'lexical'
35
import {
46
BookmarkIcon,
57
MapPinIcon,
@@ -21,13 +23,7 @@ import {
2123
} from 'vue'
2224
import { useRouter } from 'vue-router'
2325
import { toast } from 'vue-sonner'
24-
import type { TopicModel } from '@mx-space/api-client'
2526
import type { CreateNoteData } from '~/api/notes'
26-
import type { DraftModel } from '~/models/draft'
27-
import type { Coordinate, NoteModel } from '~/models/note'
28-
import type { ContentFormat, WriteBaseType } from '~/shared/types/base'
29-
import type { LexicalEditor } from 'lexical'
30-
3127
import { notesApi } from '~/api/notes'
3228
import { topicsApi } from '~/api/topics'
3329
import { AiHelperButton } from '~/components/ai/ai-helper'
@@ -44,6 +40,7 @@ import {
4440
TextBaseDrawer,
4541
} from '~/components/drawer/text-base-drawer'
4642
import { WriteEditor } from '~/components/editor/write-editor'
43+
import { SlugInput } from '~/components/editor/write-editor/slug-input'
4744
import { GetLocationButton } from '~/components/location/get-location-button'
4845
import { SearchLocationButton } from '~/components/location/search-button'
4946
import { ParseContentButton } from '~/components/special-button/parse-content'
@@ -55,11 +52,15 @@ import { usePreferredContentFormat } from '~/hooks/use-preferred-content-format'
5552
import { useStoreRef } from '~/hooks/use-store-ref'
5653
import { useWriteDraft } from '~/hooks/use-write-draft'
5754
import { useLayout } from '~/layouts/content'
55+
import type { DraftModel } from '~/models/draft'
5856
import { DraftRefType } from '~/models/draft'
57+
import type { Coordinate, NoteModel } from '~/models/note'
58+
import type { ContentFormat, WriteBaseType } from '~/shared/types/base'
5959
import { UIStore } from '~/stores/ui'
6060
import { getDayOfYear } from '~/utils/time'
6161

6262
type NoteReactiveType = {
63+
slug: string
6364
mood: string
6465
weather: string
6566
password: string | null
@@ -84,6 +85,17 @@ const useNoteTopic = () => {
8485
return { topics, fetchTopic }
8586
}
8687

88+
const buildNotePublicPath = (
89+
note: Pick<NoteReactiveType, 'slug' | 'created'> & { nid?: number },
90+
) => {
91+
if (note.slug) {
92+
const date = note.created ? new Date(note.created) : new Date()
93+
return `/notes/${date.getUTCFullYear()}/${date.getUTCMonth() + 1}/${date.getUTCDate()}/${note.slug}`
94+
}
95+
96+
return note.nid ? `/notes/${note.nid}` : ''
97+
}
98+
8799
const NoteWriteView = defineComponent(() => {
88100
const defaultTitle = ref('新建日记')
89101
const router = useRouter()
@@ -97,6 +109,7 @@ const NoteWriteView = defineComponent(() => {
97109
const resetReactive: () => NoteReactiveType = () => ({
98110
text: '',
99111
title: '',
112+
slug: '',
100113
bookmark: false,
101114
mood: '',
102115
password: null,
@@ -135,6 +148,7 @@ const NoteWriteView = defineComponent(() => {
135148
target.meta = draft.meta
136149
if (draft.typeSpecificData) {
137150
const specific = draft.typeSpecificData
151+
target.slug = specific.slug || (isPartial ? target.slug : '')
138152
target.mood = specific.mood || (isPartial ? target.mood : '')
139153
target.weather = specific.weather || (isPartial ? target.weather : '')
140154
target.password =
@@ -198,6 +212,7 @@ const NoteWriteView = defineComponent(() => {
198212
location: data.location,
199213
coordinates: data.coordinates,
200214
topicId: data.topicId,
215+
slug: data.slug,
201216
isPublished: data.isPublished,
202217
},
203218
}),
@@ -245,6 +260,7 @@ const NoteWriteView = defineComponent(() => {
245260
return {
246261
...toRaw(data),
247262
title: data.title?.trim() || defaultTitle.value,
263+
slug: data.slug.trim() || undefined,
248264
password:
249265
data.password && data.password.length > 0 ? data.password : null,
250266
publicAt: data.publicAt
@@ -375,7 +391,7 @@ const NoteWriteView = defineComponent(() => {
375391
variant="note"
376392
subtitleSlot={() => (
377393
<div class="flex items-center gap-2 text-sm text-neutral-500">
378-
<span>{`${WEB_URL}/notes/${nid.value ?? ''}`}</span>
394+
<span>{`${WEB_URL}${buildNotePublicPath({ ...data, nid: nid.value })}`}</span>
379395
{data.text.length > 0 && <AiHelperButton reactiveData={data} />}
380396
</div>
381397
)}
@@ -393,6 +409,23 @@ const NoteWriteView = defineComponent(() => {
393409
>
394410
<SectionTitle icon={BookmarkIcon}>日记信息</SectionTitle>
395411

412+
<FormField
413+
label="Slug"
414+
description="用于 SEO 路径,留空则使用旧 nid 路径"
415+
>
416+
<SlugInput
417+
prefix={`${WEB_URL}/notes/${(() => {
418+
const date = data.created ? new Date(data.created) : new Date()
419+
return `${date.getUTCFullYear()}/${date.getUTCMonth() + 1}/${date.getUTCDate()}/`
420+
})()}`}
421+
value={data.slug}
422+
onChange={(value) => {
423+
data.slug = value
424+
}}
425+
placeholder="note-slug"
426+
/>
427+
</FormField>
428+
396429
<div class="grid grid-cols-2 gap-3">
397430
<FormField label="心情" required>
398431
<NSelect

0 commit comments

Comments
 (0)