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:
eezd committed 2026-10-26 11:53:37 +08:00
1 parent cef0298c34
commit 6b537c03e0
114 files changed
+2876 -2217

No files matched your search

@@ -1,18 +1,26 @@
<script setup lang="ts">
import type { BackupKind, MaintenanceStatus } from '@/pages/settings/apis/type'
import { authApi } from '@@/apis/auth'
import type { BackupKind, MaintenanceStatus } from '@@/apis/maintenance/type'
import type { AxiosHeaders, AxiosResponse } from 'axios'
import { getAuthStateApi } from '@@/apis/auth'
import { cancelBackupUploadApi, clearTempApi, exportBackupApi, exportLogsApi, exportNotesApi, getMaintenanceStatusApi, importSettingsApi, maintainIndexApi, resetSettingsApi, restoreBackupApi, startBackupUploadApi, uploadBackupChunkApi, validateBackupApi } from '@@/apis/maintenance'
import { refreshPlugins } from '@@/plugins/host'
import { confirmAction } from '@@/utils/confirm'
import { isCancel } from 'axios'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { settingsApi } from '@/pages/settings/apis'
import { pinia } from '@/pinia'
import { useSettingsStore } from '@/pinia/stores/settings'
import { useUserStore } from '@/pinia/stores/user'
import { refreshPlugins } from '../../../common/plugins/host'
import { flushSettings, loadSettings, reloadTheme } from '../../../pinia/stores/settings'
import SettingRow from './SettingRow.vue'
const props = defineProps<{ prepareMaintenance: () => Promise<void> }>()
const emit = defineEmits<{ changed: [], importNotes: [] }>()
const { flushSettings, loadSettings, reloadTheme } = useSettingsStore(pinia)
const user = useUserStore()
const busy = ref(false)
@@ -28,7 +36,7 @@ onBeforeUnmount(() => {
})
onMounted(async () => {
try {
const status = await settingsApi.maintenanceStatus(); if (mounted) last.value = status
const status = (await getMaintenanceStatusApi()).data; if (mounted) last.value = status
} catch (cause) {
if (mounted) error.value = `读取维护状态失败:${String(cause)}`
}
@@ -45,7 +53,7 @@ async function perform(label: string, action: () => Promise<void>) {
user.assertSession(version)
await action()
if (mounted) {
emit('changed'); last.value = await settingsApi.maintenanceStatus()
emit('changed'); last.value = (await getMaintenanceStatusApi()).data
}
} catch (cause) {
if (mounted && !isCancel(cause)) error.value = String(cause)
@@ -55,11 +63,11 @@ async function perform(label: string, action: () => Promise<void>) {
}
}
}
async function download(response: Response, fallback: string) {
async function download(response: AxiosResponse<Blob, unknown, AxiosHeaders>, fallback: string) {
const version = user.getSessionVersion()
const blob = await response.blob()
const blob = response.data
user.assertSession(version)
const header = response.headers.get('Content-Disposition') ?? ''
const header = String(response.headers.get('content-disposition') ?? '')
const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header)?.[1]
const plain = /filename="([^"]+)"/i.exec(header)?.[1]
const name = encoded ? decodeURIComponent(encoded) : plain ?? fallback
@@ -69,21 +77,29 @@ async function download(response: Response, fallback: string) {
message.value = '导出文件已生成'
}
async function exportPackage(kind: BackupKind) {
await perform('正在导出…', async () => download(await settingsApi.exportBackup(kind), `rawmark-${kind}.zip`))
await perform('正在导出…', async () => download(await exportBackupApi(kind), `rawmark-${kind}.zip`))
}
async function exportMarkdown() {
await perform('正在导出笔记…', async () => download(await settingsApi.exportNotes(), 'rawmark-notes.zip'))
await perform('正在导出笔记…', async () => download(await exportNotesApi(), 'rawmark-notes.zip'))
}
async function exportSystemLogs() {
await perform('正在导出日志…', async () => download(await settingsApi.exportLogs(), 'rawmark-system-logs.zip'))
await perform('正在导出日志…', async () => download(await exportLogsApi(), 'rawmark-system-logs.zip'))
}
async function syncPreferences() {
await loadSettings(); await reloadTheme(); await refreshPlugins()
}
async function reset() {
if (!confirm('恢复全部 RawMark 偏好?快捷键恢复默认,回收站期限恢复为 30 天,插件停用。保留笔记、账号、已安装资源、插件存储和本机标签页/布局。操作前会备份设置。')) return
if (busy.value) return
busy.value = true
let confirmed = false
try {
confirmed = await confirmAction('恢复全部 RawMark 偏好?快捷键恢复默认,回收站期限恢复为 30 天,插件停用。保留笔记、账号、已安装资源、插件存储和本机标签页/布局。操作前会备份设置。')
} finally {
busy.value = false
}
if (!confirmed) return
await perform('正在重置设置…', async () => {
await settingsApi.resetSettings(); await syncPreferences(); message.value = '设置已重置,原设置已备份'
await resetSettingsApi(); await syncPreferences(); message.value = '设置已重置,原设置已备份'
})
}
async function restore(kind: BackupKind, event: Event) {
@@ -93,39 +109,39 @@ async function restore(kind: BackupKind, event: Event) {
await perform('正在上传…', async () => {
if (file.size <= 0 || file.size > 2 * 1024 ** 3) throw new Error('备份压缩大小须在 0–2 GiB 之间')
const version = user.getSessionVersion()
const { id } = await settingsApi.startBackupUpload(kind, file.size)
const { id } = (await startBackupUploadApi({ kind, size: file.size })).data
let submitted = false
try {
let offset = 0
while (offset < file.size) {
if (!mounted) throw new Error('已取消上传')
const chunk = file.slice(offset, offset + 8 * 1024 ** 2)
const result = await settingsApi.uploadBackupChunk(id, offset, chunk)
const result = (await uploadBackupChunkApi({ id, offset, body: chunk })).data
if (result.offset !== offset + chunk.size) throw new Error('服务端返回的上传进度不一致')
offset = result.offset; progress.value = `正在上传… ${Math.round(offset / file.size * 100)}%`
}
progress.value = '正在校验备份…'
const summary = await settingsApi.validateBackup(id)
const summary = (await validateBackupApi(id)).data
if (!mounted) throw new Error('已取消导入')
const description = kind === 'full'
? '将替换全部实例数据,恢复备份中的密码与 API 令牌,短暂维护后需要重新登录。'
: '将替换全部偏好和插件配置,合并字体、主题和插件,同 ID 资源以导入包为准。'
if (!confirm(`备份时间:${new Date(summary.createdAt).toLocaleString()}\n条目:${summary.entries},展开大小:${(summary.size / 1024 ** 2).toFixed(2)} MiB\n${description}\n操作前会备份当前内容。确认继续?`)) return
if (!await confirmAction(`备份时间:${new Date(summary.createdAt).toLocaleString()}\n条目:${summary.entries},展开大小:${(summary.size / 1024 ** 2).toFixed(2)} MiB\n${description}\n操作前会备份当前内容。确认继续?`)) return
await prepare()
user.assertSession(version)
progress.value = kind === 'full' ? '正在恢复实例,请勿关闭页面…' : '正在导入设置…'
if (kind === 'settings') {
await settingsApi.importSettings(id); submitted = true
await importSettingsApi(id); submitted = true
await syncPreferences(); message.value = '设置与资源已导入,原设置已备份'
} else {
await settingsApi.restoreBackup(id); submitted = true
await restoreBackupApi(id); submitted = true
user.invalidate(version)
const recoveryVersion = user.getSessionVersion()
for (let attempt = 0; attempt < 30; attempt++) {
await new Promise(resolve => setTimeout(resolve, 2000))
if (recoveryVersion !== user.getSessionVersion()) return
try {
if ((await authApi.state()).setUp) {
if (((await getAuthStateApi()).data).setUp) {
location.reload(); return
}
} catch { /* 服务关闭和重新监听期间继续等待,最终超时会明确提示。 */ }
@@ -135,7 +151,7 @@ async function restore(kind: BackupKind, event: Event) {
} finally {
if (!submitted && version === user.getSessionVersion()) {
try {
await settingsApi.cancelBackupUpload(id)
await cancelBackupUploadApi(id)
} catch (cause) {
if (mounted) error.value = `清理上传暂存失败:${String(cause)}`
}
@@ -145,12 +161,12 @@ async function restore(kind: BackupKind, event: Event) {
}
async function index(operation: 'optimize' | 'rebuild') {
await perform(operation === 'optimize' ? '正在优化索引…' : '正在重建索引…', async () => {
await settingsApi.maintainIndex(operation); message.value = operation === 'optimize' ? '索引已优化' : '索引已重建'
await maintainIndexApi(operation); message.value = operation === 'optimize' ? '索引已优化' : '索引已重建'
})
}
async function clearTemp() {
await perform('正在清理临时文件…', async () => {
const result = await settingsApi.clearTemp()
const result = (await clearTempApi()).data
message.value = `已清理 ${result.files} 个文件,释放 ${(result.bytes / 1024 ** 2).toFixed(2)} MiB`
if (result.failed.length) error.value = `部分清理失败:\n${result.failed.join('\n')}`
})