refactor(web): align resource APIs and forms with the starter conventions
Centralize typed resource functions under common/apis and return complete JSON envelopes while preserving raw responses, bearer session isolation, and single-owner errors. Keep settings state and queues inside the setup store. Use Element Plus forms and dialogs for login, settings, imports, moves, and images; preserve editor interactions and themes. Update callers, tests, generated declarations, and request import constraints together.
This commit is contained in:
1 parent
cef0298c34
commit
6b537c03e0
114 files changed
+2876
-2217
No files matched your search
@@ -0,0 +1,62 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { getCaptchaApi, loginApi } from '@@/apis/auth'
|
||||
import { importFontApi } from '@@/apis/fonts'
|
||||
import { searchApi } from '@@/apis/search'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { pinia } from '@/pinia'
|
||||
import { useUserStore } from '@/pinia/stores/user'
|
||||
import { apiSuccess } from '../../../../tests/api-response'
|
||||
import { exportFilesApi, importNotesApi, writeFileApi } from './index'
|
||||
|
||||
const user = useUserStore(pinia)
|
||||
beforeEach(() => user.setToken('rms_api_fixture'))
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks(); user.setToken('')
|
||||
})
|
||||
const json = (data: unknown) => new Response(JSON.stringify(apiSuccess(data)), { headers: { 'Content-Type': 'application/json' } })
|
||||
it('aPI 返回完整 JSON 响应,验证码和登录使用真实请求体且不发送现有身份', async () => {
|
||||
const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(json({ captchaId: 'challenge', imageDataUrl: 'data:image/png;base64,test', expiresAt: '2030-01-01' })).mockResolvedValueOnce(json({ token: 'rms_new', expiresAt: '2030-01-01' }))
|
||||
const captcha = await getCaptchaApi({ replaceId: 'old' })
|
||||
expect(captcha.code).toBe(0); expect(captcha.data.captchaId).toBe('challenge')
|
||||
const fields = { password: 'fixture-password', captchaId: captcha.data.captchaId, captchaCode: '123456' }
|
||||
expect((await loginApi(fields)).data.token).toBe('rms_new')
|
||||
const requests = fetch.mock.calls.map(call => call[0] as Request)
|
||||
expect(requests.every(request => !request.headers.has('Authorization'))).toBe(true)
|
||||
expect(await requests[0]!.json()).toEqual({ replaceId: 'old' })
|
||||
expect(await requests[1]!.json()).toEqual(fields)
|
||||
})
|
||||
it('gET 使用 params 编码查询,保留 Bearer 和完整搜索响应', async () => {
|
||||
const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(json({ results: [] }))
|
||||
expect(await searchApi({ q: '笔记 & 示例' })).toEqual(apiSuccess({ results: [] }))
|
||||
const request = fetch.mock.calls[0]![0] as Request
|
||||
expect(new URL(request.url).searchParams.get('q')).toBe('笔记 & 示例')
|
||||
expect(request.headers.get('Authorization')).toBe('Bearer rms_api_fixture')
|
||||
})
|
||||
it('字体与笔记使用真实 multipart 文件,不被 JSON 请求头覆盖', async () => {
|
||||
const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(json({ id: 'font' })).mockResolvedValueOnce(json({ notes: 1, assets: 0, paths: ['fixture.md'], warnings: [] }))
|
||||
await importFontApi(new File(['font'], 'fixture.woff2'))
|
||||
await importNotesApi({ dir: 'folder', folder: '', files: [new File(['# 正文'], 'fixture.md')] })
|
||||
for (const [input] of fetch.mock.calls) expect((input as Request).headers.get('Content-Type')).toMatch(/^multipart\/form-data; boundary=/)
|
||||
const body = await (fetch.mock.calls[1]![0] as Request).formData()
|
||||
expect(body.get('dir')).toBe('folder')
|
||||
expect(body.get('files')).toMatchObject({ name: 'fixture.md' })
|
||||
})
|
||||
it('版本写入保留 Markdown 原文、条件请求头与小文件卸载保存', async () => {
|
||||
const fetch = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => json({ rev: 'next' }))
|
||||
expect((await writeFileApi({ path: '目录/a.md', content: '# 原文', ifMatch: 'old', keepalive: true })).data.rev).toBe('next')
|
||||
const request = fetch.mock.calls[0]![0] as Request
|
||||
expect(request.url).toContain(encodeURIComponent('目录'))
|
||||
expect(request.headers.get('If-Match')).toBe('"old"')
|
||||
expect(request.headers.get('Content-Type')).toBe('text/markdown; charset=utf-8')
|
||||
expect(fetch.mock.calls[0]![1]).toMatchObject({ keepalive: true })
|
||||
expect(await request.text()).toBe('# 原文')
|
||||
await writeFileApi({ path: 'new.md', content: '', ifMatch: '' })
|
||||
expect((fetch.mock.calls[1]![0] as Request).headers.get('If-None-Match')).toBe('*')
|
||||
})
|
||||
it('导出直接返回 Blob 与下载响应头,不模拟 Fetch Response', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('ZIP', { headers: { 'Content-Type': 'application/zip', 'Content-Disposition': 'attachment; filename="fixture.zip"' } }))
|
||||
const response = await exportFilesApi({ paths: ['fixture.md'] })
|
||||
expect(response.data).toBeInstanceOf(Blob)
|
||||
expect(await response.data.text()).toBe('ZIP')
|
||||
expect(response.headers.get('Content-Disposition')).toBe('attachment; filename="fixture.zip"')
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
import type * as Files from './type'
|
||||
import { encodePath } from '@@/utils/paths'
|
||||
import { request } from '@/http/axios'
|
||||
|
||||
/** 读取 Markdown 原文与版本。 */
|
||||
export function getFileApi(path: string) {
|
||||
return request<string>({
|
||||
url: `files/${encodePath(path)}`,
|
||||
method: 'get',
|
||||
responseType: 'text',
|
||||
returnResponse: true,
|
||||
errorOwner: 'caller',
|
||||
})
|
||||
}
|
||||
|
||||
/** 按版本写入 Markdown。 */
|
||||
export function writeFileApi(data: Files.WriteFileRequestData) {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'text/markdown; charset=utf-8' }
|
||||
if (data.ifMatch === '') headers['If-None-Match'] = '*'
|
||||
else if (data.ifMatch !== undefined) headers['If-Match'] = `"${data.ifMatch}"`
|
||||
return request<Files.WriteFileResponseData>({
|
||||
url: `files/${encodePath(data.path)}`,
|
||||
method: 'put',
|
||||
data: data.content,
|
||||
headers,
|
||||
fetchOptions: { keepalive: !!data.keepalive && new Blob([data.content]).size < 60_000 },
|
||||
errorOwner: 'caller',
|
||||
})
|
||||
}
|
||||
|
||||
/** 移入回收站。 */
|
||||
export function deleteFileApi(path: string) {
|
||||
return request<Files.DeleteFileResponseData>({
|
||||
url: `files/${encodePath(path)}`,
|
||||
method: 'delete',
|
||||
errorOwner: 'caller',
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建文件夹。 */
|
||||
export function createFolderApi(data: Files.FileRequestData) {
|
||||
return request<Files.FileOperationResponseData>({
|
||||
url: 'folders',
|
||||
method: 'post',
|
||||
data,
|
||||
errorOwner: 'caller',
|
||||
})
|
||||
}
|
||||
|
||||
/** 移动文件或目录。 */
|
||||
export function moveFileApi(data: Files.MoveRequestData) {
|
||||
return request<Files.FileOperationResponseData>({
|
||||
url: 'move',
|
||||
method: 'post',
|
||||
data,
|
||||
errorOwner: 'caller',
|
||||
})
|
||||
}
|
||||
|
||||
/** 导入 Markdown 或 ZIP。 */
|
||||
export function importNotesApi(data: Files.ImportRequestData) {
|
||||
const body = new FormData()
|
||||
body.append('dir', data.dir)
|
||||
body.append('folder', data.folder)
|
||||
for (const file of data.files) body.append('files', file)
|
||||
return request<Files.ImportResponseData>({
|
||||
url: 'import',
|
||||
method: 'post',
|
||||
data: body,
|
||||
errorOwner: 'caller',
|
||||
})
|
||||
}
|
||||
|
||||
/** 导出选中内容为 Markdown ZIP。 */
|
||||
export function exportFilesApi(data: Files.ExportRequestData) {
|
||||
return request<Blob>({
|
||||
url: 'export',
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob',
|
||||
returnResponse: true,
|
||||
errorOwner: 'caller',
|
||||
})
|
||||
}
|
||||
|
||||
/** 下载原始文件。 */
|
||||
export function getRawFileApi(path: string) {
|
||||
return request<Blob>({
|
||||
url: `raw/${encodePath(path)}`,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
returnResponse: true,
|
||||
errorOwner: 'caller',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { TrashItem } from '../trash/type'
|
||||
|
||||
export interface FileRequestData { path: string }
|
||||
export interface WriteFileRequestData { path: string, content: string, ifMatch?: string, keepalive?: boolean }
|
||||
export interface MoveRequestData { from: string, to: string }
|
||||
export interface ImportRequestData { dir: string, folder: string, files: File[] }
|
||||
export interface ExportRequestData { paths: string[] }
|
||||
export interface ImportResult { paths: string[], notes: number, assets: number, warnings: string[] }
|
||||
export type WriteFileResponseData = ApiResponseData<{ rev: string }>
|
||||
export type DeleteFileResponseData = ApiResponseData<TrashItem>
|
||||
export type FileOperationResponseData = ApiResponseData<{ ok: boolean }>
|
||||
export type ImportResponseData = ApiResponseData<ImportResult>
|
||||
Reference in new issue
Block a user