@@ -147,3 +126,44 @@ onBeforeUnmount(() => {
+
+
diff --git a/app/components/post-body-markdown-editor-vditor-config.test.ts b/app/components/post-body-markdown-editor-vditor-config.test.ts
index 697d5c0..87020e7 100644
--- a/app/components/post-body-markdown-editor-vditor-config.test.ts
+++ b/app/components/post-body-markdown-editor-vditor-config.test.ts
@@ -5,15 +5,21 @@ import {
} from './post-body-markdown-editor-vditor-config'
describe('PostBodyMarkdownEditor Vditor config', () => {
- test('桌面端使用完整工具栏与可预览模式', () => {
+ test('桌面端使用完整工具栏与即时渲染模式', () => {
const options = buildPostBodyMarkdownEditorVditorOptions({
value: 'hello',
isMobile: false,
onInput: () => undefined,
- uploadHandler: async () => '',
+ onUploadError: () => undefined,
})
- expect(options.mode).toBe('sv')
+ expect(options.mode).toBe('ir')
+ expect(options.lang).toBe('zh_CN')
+ expect(options.cdn).toBe('/vditor')
+ expect(options.preview).toEqual({
+ mode: 'editor',
+ actions: [],
+ })
expect(options.toolbar).toEqual(postBodyMarkdownEditorToolbarPresets.desktop)
expect(postBodyMarkdownEditorToolbarPresets.desktop.includes('preview')).toBe(false)
expect(postBodyMarkdownEditorToolbarPresets.desktop.length).toBeGreaterThan(postBodyMarkdownEditorToolbarPresets.mobile.length)
@@ -24,7 +30,7 @@ describe('PostBodyMarkdownEditor Vditor config', () => {
value: 'hello',
isMobile: true,
onInput: () => undefined,
- uploadHandler: async () => '',
+ onUploadError: () => undefined,
})
expect(options.mode).toBe('ir')
@@ -45,17 +51,49 @@ describe('PostBodyMarkdownEditor Vditor config', () => {
])
})
- test('上传处理器透传到 upload.handler', async () => {
- const uploadHandler = async () => 'ok'
+ test('上传配置会将服务端响应转换为 succMap 结构', async () => {
+ const options = buildPostBodyMarkdownEditorVditorOptions({
+ value: '',
+ isMobile: false,
+ onInput: () => undefined,
+ onUploadError: () => undefined,
+ }) as { upload?: { format?: (files: File[], responseText: string) => string } }
+
+ const files = [{ name: 'image.webp' }] as File[]
+ const result = options.upload?.format?.(files, JSON.stringify({
+ code: 0,
+ data: {
+ files: [{ url: '/public/upload/abc.webp' }],
+ },
+ }))
+ expect(result).toBe(JSON.stringify({
+ msg: '',
+ code: 0,
+ data: {
+ errFiles: [],
+ succMap: {
+ 'image.webp': '/public/upload/abc.webp',
+ },
+ },
+ }))
+ })
+
+ test('上传配置解析失败时返回错误结构', () => {
const options = buildPostBodyMarkdownEditorVditorOptions({
value: '',
isMobile: false,
onInput: () => undefined,
- uploadHandler,
- }) as { upload?: { handler?: (files: File[]) => Promise
} }
+ onUploadError: () => undefined,
+ }) as { upload?: { format?: (files: File[], responseText: string) => string } }
- expect(options.upload?.handler).toBeDefined()
- const result = await options.upload?.handler?.([] as File[])
- expect(result).toBe('ok')
+ const result = options.upload?.format?.([] as File[], 'invalid json')
+ expect(result).toBe(JSON.stringify({
+ msg: 'upload response parse failed',
+ code: 1,
+ data: {
+ errFiles: [],
+ succMap: {},
+ },
+ }))
})
})
diff --git a/app/components/post-body-markdown-editor-vditor-config.ts b/app/components/post-body-markdown-editor-vditor-config.ts
index b409b68..e500575 100644
--- a/app/components/post-body-markdown-editor-vditor-config.ts
+++ b/app/components/post-body-markdown-editor-vditor-config.ts
@@ -2,7 +2,7 @@ interface BuildVditorOptionsInput {
value: string
isMobile: boolean
onInput: (value: string) => void
- uploadHandler: (files: File[]) => Promise
+ onUploadError: () => void
}
const DESKTOP_TOOLBAR: ReadonlyArray = [
@@ -52,12 +52,64 @@ const MOBILE_TOOLBAR: ReadonlyArray = [
export function buildPostBodyMarkdownEditorVditorOptions(input: BuildVditorOptionsInput): Record {
return {
value: input.value,
+ lang: 'zh_CN',
+ cdn: '/vditor',
cache: { enable: false },
- mode: input.isMobile ? 'ir' : 'sv',
+ mode: 'ir',
+ preview: {
+ mode: 'editor',
+ actions: [] as string[],
+ },
toolbar: input.isMobile ? MOBILE_TOOLBAR : DESKTOP_TOOLBAR,
upload: {
+ url: '/api/file/upload',
+ fieldName: 'file',
+ multiple: true,
accept: 'image/*',
- handler: input.uploadHandler,
+ format(files: File[], responseText: string) {
+ try {
+ const parsed = JSON.parse(responseText) as {
+ files?: Array<{ url?: string }>
+ data?: { files?: Array<{ url?: string }> }
+ }
+ const nestedFiles = parsed?.data?.files
+ const rootFiles = parsed?.files
+ const uploaded = Array.isArray(nestedFiles)
+ ? nestedFiles
+ : (Array.isArray(rootFiles) ? rootFiles : [])
+ const succMap: Record = {}
+ uploaded.forEach((item, index) => {
+ const url = item?.url?.trim()
+ if (!url) {
+ return
+ }
+ const fallbackName = `upload-${index + 1}`
+ const sourceName = files[index]?.name?.trim() || fallbackName
+ succMap[sourceName] = url
+ })
+ return JSON.stringify({
+ msg: '',
+ code: 0,
+ data: {
+ errFiles: [] as string[],
+ succMap,
+ },
+ })
+ }
+ catch {
+ return JSON.stringify({
+ msg: 'upload response parse failed',
+ code: 1,
+ data: {
+ errFiles: [] as string[],
+ succMap: {} as Record,
+ },
+ })
+ }
+ },
+ error() {
+ input.onUploadError()
+ },
},
input(value: string) {
input.onInput(value)
diff --git a/app/pages/me/posts/[id].vue b/app/pages/me/posts/[id].vue
index 6aaad97..dda32c0 100644
--- a/app/pages/me/posts/[id].vue
+++ b/app/pages/me/posts/[id].vue
@@ -1,6 +1,7 @@
@@ -170,10 +191,10 @@ async function copyShareUrl() {
-
+
操作
+
+ 预览(新窗口)
+
保存文章
diff --git a/app/pages/me/posts/new.vue b/app/pages/me/posts/new.vue
index 299ac05..e734977 100644
--- a/app/pages/me/posts/new.vue
+++ b/app/pages/me/posts/new.vue
@@ -1,5 +1,6 @@
@@ -79,10 +97,10 @@ async function submit() {
-
+
操作
+
+ 预览(新窗口)
+
创建文章
diff --git a/app/pages/me/posts/preview/draft.vue b/app/pages/me/posts/preview/draft.vue
new file mode 100644
index 0000000..fae64c3
--- /dev/null
+++ b/app/pages/me/posts/preview/draft.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+ 预览加载中…
+
+
+
+
+ {{ visibilityLabel }}
+
+
+ 草稿
+
+
+
+
+ {{ draft.title || '未命名文章' }}
+
+
+ {{ draft.excerpt }}
+
+
+
+
+
+
+
diff --git a/app/utils/post-preview-draft.ts b/app/utils/post-preview-draft.ts
new file mode 100644
index 0000000..78d420c
--- /dev/null
+++ b/app/utils/post-preview-draft.ts
@@ -0,0 +1,99 @@
+export type PostPreviewDraftPayload = {
+ title: string
+ excerpt: string
+ bodyMarkdown: string
+ visibility: 'private' | 'unlisted' | 'public'
+}
+
+const PREVIEW_DRAFT_KEY_PREFIX = 'post-preview-draft:'
+const PREVIEW_DRAFT_TTL_MS = 10 * 60 * 1000
+
+type StoredPostPreviewDraftPayload = PostPreviewDraftPayload & {
+ createdAt: number
+}
+
+function isPostPreviewDraftKey(key: string): boolean {
+ return key.startsWith(PREVIEW_DRAFT_KEY_PREFIX)
+}
+
+export function createPostPreviewDraft(payload: PostPreviewDraftPayload): string {
+ const uniqueId = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
+ ? crypto.randomUUID()
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`
+ const key = `${PREVIEW_DRAFT_KEY_PREFIX}${uniqueId}`
+ const storedPayload: StoredPostPreviewDraftPayload = {
+ ...payload,
+ createdAt: Date.now(),
+ }
+ localStorage.setItem(key, JSON.stringify(storedPayload))
+ return uniqueId
+}
+
+export function readPostPreviewDraft(uniqueId: string): PostPreviewDraftPayload | null {
+ const key = `${PREVIEW_DRAFT_KEY_PREFIX}${uniqueId}`
+ const raw = localStorage.getItem(key)
+ if (!raw) {
+ return null
+ }
+
+ try {
+ const parsed = JSON.parse(raw) as Partial
+ if (
+ typeof parsed.title !== 'string' ||
+ typeof parsed.excerpt !== 'string' ||
+ typeof parsed.bodyMarkdown !== 'string' ||
+ typeof parsed.createdAt !== 'number' ||
+ (parsed.visibility !== 'private' && parsed.visibility !== 'unlisted' && parsed.visibility !== 'public')
+ ) {
+ localStorage.removeItem(key)
+ return null
+ }
+
+ if (Date.now() - parsed.createdAt > PREVIEW_DRAFT_TTL_MS) {
+ localStorage.removeItem(key)
+ return null
+ }
+
+ return {
+ title: parsed.title,
+ excerpt: parsed.excerpt,
+ bodyMarkdown: parsed.bodyMarkdown,
+ visibility: parsed.visibility,
+ }
+ } catch {
+ localStorage.removeItem(key)
+ return null
+ }
+}
+
+export function clearPostPreviewDrafts(options?: { onlyExpired?: boolean }) {
+ const onlyExpired = options?.onlyExpired === true
+ const now = Date.now()
+
+ for (let index = localStorage.length - 1; index >= 0; index -= 1) {
+ const key = localStorage.key(index)
+ if (!key || !isPostPreviewDraftKey(key)) {
+ continue
+ }
+
+ if (!onlyExpired) {
+ localStorage.removeItem(key)
+ continue
+ }
+
+ const raw = localStorage.getItem(key)
+ if (!raw) {
+ localStorage.removeItem(key)
+ continue
+ }
+
+ try {
+ const parsed = JSON.parse(raw) as Partial
+ if (typeof parsed.createdAt !== 'number' || now - parsed.createdAt > PREVIEW_DRAFT_TTL_MS) {
+ localStorage.removeItem(key)
+ }
+ } catch {
+ localStorage.removeItem(key)
+ }
+ }
+}
diff --git a/package.json b/package.json
index 26f1de4..f724c4b 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,9 @@
],
"private": true,
"scripts": {
- "build": "nuxt build && bun run cp:db && bun --elide-lines=0 --filter drizzle-pkg build",
- "dev": "nuxt dev",
+ "build": "bun run sync:vditor && nuxt build && bun run cp:db && bun --elide-lines=0 --filter drizzle-pkg build",
+ "dev": "bun run sync:vditor && nuxt dev",
+ "sync:vditor": "sh scripts/sync-vditor-assets.sh",
"cp:db": "cp build-files/run.sh .output/run.sh && cp .env.example .output/.env && cp -r build-files/migrate/* .output/server/ && cp build-files/seed.js .output/server/seed.js",
"migrate:test": "sh scripts/migrate-test.sh",
"db:migrate": "bun --elide-lines=0 --filter drizzle-pkg migrate",
diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite
index 053f98e7447c7ef4d493645ef4e9743af514a5d7..1430fcb5d164b90cb424162dcf9f200442b868d9 100644
GIT binary patch
delta 2454
zcma)8du&r>6#u^7cHO;Q@3%P-%Er76vM}M&-dAt$qD;^j7AIperUoZ#*KSTXy0C#v
zLB_TOm?okZbBSiie8nhkgGL5Maetsu;{ypQijl-U9LzyNe1ng3`&jE{)PC)k)6;W)
z=XW0Wr0wlYdpol`4-Y(%e={C9SkvptWhar1J&efvtCejpXeUk
zFoL%_^mos&)}hj;9*tIhJwRP$A*T`f0rE+nccA3mgrTkUEL?wIeVsk}%EdCyI#l%S
zuEF03w(oX1I}znn5b1F$q}-{bPSl;RL%fV{Gv})MQFd@>s7seH4ehA
zOoDjcTO0$agxk}2wv0|OK~qdbK@~-fhe;AdRq^^`Aet*b38HK|0`5vMA0hlz%55xA^IbU=+N#&85K=cL`~uq
zuc8PNi1FyzWl8eFK^r*wW;*&6P4@eERg^`qR}+-DV@_q#(ewQQ7^@32^8M$UYtw5E
z)vlr->xi3zzNZoPX_Bn?cuDaA1D~iGyP@wlE_pB3OALr
zs%bt&R(WWbqDq=Ko^x)&FG=dF_Uwg{t=jpH(bJDZF7_2fv7Lx~z;=@7+0N8~!CuK4
z_Z`D|+X|@)oEXgM|k~>j<#(^AJgp2$NSZ|
zoie>^X=c!8)Ri=w_FSZyS;4G2VK&tZdwMhYSgus6Br^)02FDm5yp2^C`4Um&l)0Zy
z!G=1OI#KtkH^@?ROPR$hFiTTlChjt`y4vJSOhyc4nlKba(+oKeSR;sBCL`oW_>2tZ
zoWLhZp^d_rHa#4frc0LHR513~26Tb~QyqxvfSt3M%HI2Me9;Zi0?M(?t2HhR<9Yhh
z6U$57YvcWFUSD6m(9^sz*w|3(+1T6|34}cHviw`RMSA;*LUV7@*g3YRf9$dCTvbC8
zBQDVcpK{E(-lN|=)5i%6uRs3jL<+~jdwjg7B$rXH~Q97fS2=`OJDriwTqZ7Txn~#
zi8EhYxT-+o#xMw=<-4bV`!2UV6r(
zx1AALX6?Ookx1+MNNczn!Y#gt75&vSb8p+YzOkXDH5`)Fs@fHi)$7-(;il3%mp2B>
zw65xhCwon1Isc?BP!
z2EQ4`n=(!LwcH$X62|B_BFD*3WCHHvFp7uB$}F2TkH*a#B6SUoVRI&37&k{+S~JYi
zoRg64h_+j2ff@Y3WM+U!$3*f8U=AS|%X4JNJg`Fg$ST$bkW-lEh|W!dnv5jpZnTU7XKV~MEw$*&{sNZY
BvvB|b
delta 2608
zcmcIme@qis9DlEO2V9|jq7p(F84YgHkkh+s@2-tHe~{_ek5CtMDmqtMkOD#xm{S*v
z;LLDl6U%mNu~VaSM3G-*LAFS@WoDamb7p@?m|;ss3uF?1PUqsx-n$miL>6LV?)tsY
zyZgTH@Av(-rbb^=qu-Yb%Nw4^g5@2r9iEp60HDzjnVdd{N;dkn+S
z6bl2FHwZa&XG>{|&(tOm(CAy$c8q|7o$b@p^=cMGfSr?ivcfn2=RIm*5Viz?S|FXGRu~drPcge+nQ3oYO}j|8JAsXFL}OXLy@I)
z4cS3_FXP^-Uj$e5(oes9{ozwjfg0cIO
za3mq}1LQYej9#))#;n5$=+zhlV0S{34uE6;7~ik0DQe&*oayD9o69|(0@vIv1^FKL
zCdbBtc?8WI++5&rZH4~BUG7)%nFPRkwY>~r1Vxc0(Wb~eFH4*v$YxQph&+b_LT&{U
zi&^0WKHuq_M7kY}Nnceul!29${+5S2!3bM!qOgGsLzFn;u-0{iR7Bots@KF~M^=on
zrD(>OEs8Ail9jXBEH*Q?i&w9mgh^g-?(&$Po)xE||H!Sbi&$BYxT?^#5#MMDczVLG
z#7P`0VX(%i_Y5-`*F)U*PU`=2{bP1%?l&Lu)JKBgsKhCp!)_n*4-lJVi=tRX(JXL+
z!dn%=jH7FJzhQTm5v4N6TkwR2tsr9Hv#BxuATc9rq#hlTrNQz
zN>UOy;+iT+;U!U)%seOavOqGdXrVO4G)65x(trCfj^y9oI!ycjT<(s;(ZLB){cX?{
zslcZ91YhDfP7p8=@sA)$fr3rl12^F7aVmEi8UkH??^34f{8G1*oShXN
zxQn;j8x8S&fM$1qgxmy%y~FOn8_QVl))?F@Gc$7p+$>W7!-QHt4%ho4bf{JJlvh@9
z6)sCrv8~wQkwWh^3^g7M2j0gZbf7)_LC;9t>2OoqP)p0;sd~H|db?xr)ShtXhnOKk
zNn;J3^JaUZ24VaHesliN)w*>I@&Z;!-u)C8)gG>YC$y(|q^V{cI4WI^%FvM$TCYAa
z4hB!}#%hP_4up<=G~9JC)ZQ@uX&g+2$2%3K$H?P-Y@SWcXz!zhF+IG0e^@<6YM_1B
zP;HNv;_8eq$5ULL@ik5{rnpYI{~g77_s`;&?i<{s7D=z1^57m*9+}0#cJW$w?p=6t
z2GY*fEr3tu5W0je1Wfj?&?1(mlUPWqn`Nm9s2hX&1W2dGLA+!MQ{;eZLayUKEZ4D+
zUbHlx!7?I3==(4=f%Q5AlghG~FCxcas0DPwJz#Vv+%u*ce#B>RdpxS!29xoSe{v_Y
zGc7qKS#4OTTcEx&
C4bE!-
diff --git a/public/upload/1777047996547-912573909-image.webp b/public/upload/1777047996547-912573909-image.webp
new file mode 100644
index 0000000000000000000000000000000000000000..25b6d7dddb43f30ca81ff2151d31b7468cd2ab33
GIT binary patch
literal 7928
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
i$e_@}kRcVcm`S`skAzt}QDG3SY8