Browse Source

fix: migrate console API calls to useClientApi fetchData

- Replace request+unwrap with fetchData across /me admin, posts, timeline, rss, profile
- Inline form errors: profile uploads/save use notify:false + getApiErrorMessage
- Remove duplicate extractError/toast in media-storage, orphans, markdown editor
- PostComments and logout use fetchData for consistent error toasts

Made-with: Cursor
tags/邮箱功能前置
npmrun 4 weeks ago
parent
commit
f73d766565
  1. 4
      app/components/AppShell.vue
  2. 29
      app/components/PostBodyMarkdownEditor.vue
  3. 59
      app/components/PostComments.vue
  4. 4
      app/pages/index/index.vue
  5. 7
      app/pages/me/admin/config/index.vue
  6. 28
      app/pages/me/admin/media-storage.vue
  7. 10
      app/pages/me/admin/users/index.vue
  8. 6
      app/pages/me/index.vue
  9. 32
      app/pages/me/media/orphans.vue
  10. 9
      app/pages/me/posts/[id].vue
  11. 6
      app/pages/me/posts/index.vue
  12. 8
      app/pages/me/posts/new.vue
  13. 44
      app/pages/me/profile/index.vue
  14. 19
      app/pages/me/rss/index.vue
  15. 15
      app/pages/me/timeline/index.vue

4
app/components/AppShell.vue

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../utils/http/factory'
import { useAuthSession } from '../composables/useAuthSession' import { useAuthSession } from '../composables/useAuthSession'
withDefaults( withDefaults(
@ -12,6 +11,7 @@ withDefaults(
const route = useRoute() const route = useRoute()
const { loggedIn, user, refresh, clear, pending } = useAuthSession() const { loggedIn, user, refresh, clear, pending } = useAuthSession()
const { fetchData } = useClientApi()
const { allowRegister, siteName } = useGlobalConfig() const { allowRegister, siteName } = useGlobalConfig()
const logoutLoading = ref(false) const logoutLoading = ref(false)
@ -91,7 +91,7 @@ const accountMenuItems = computed(() => {
async function logout() { async function logout() {
logoutLoading.value = true logoutLoading.value = true
try { try {
unwrapApiBody(await request<ApiResponse<{ success: boolean }>>('/api/auth/logout', { method: 'POST' })) await fetchData<{ success: boolean }>('/api/auth/logout', { method: 'POST' })
clear() clear()
await navigateTo('/login') await navigateTo('/login')
} finally { } finally {

29
app/components/PostBodyMarkdownEditor.vue

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { MdEditor } from 'md-editor-v3' import { MdEditor } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css' import 'md-editor-v3/lib/style.css'
import { request, unwrapApiBody, type ApiResponse } from '~/utils/http/factory'
const props = defineProps<{ const props = defineProps<{
modelValue: string modelValue: string
@ -11,7 +10,7 @@ const emit = defineEmits<{
'update:modelValue': [string] 'update:modelValue': [string]
}>() }>()
const toast = useToast() const { fetchData } = useClientApi()
const editorId = `post-body-md-${useId()}` const editorId = `post-body-md-${useId()}`
const local = computed({ const local = computed({
@ -19,40 +18,18 @@ const local = computed({
set: (v: string) => emit('update:modelValue', v), set: (v: string) => emit('update:modelValue', v),
}) })
function extractUploadError(e: unknown): string {
if (e && typeof e === 'object') {
const fe = e as {
statusMessage?: string
message?: string
data?: { message?: string }
}
if (typeof fe.statusMessage === 'string' && fe.statusMessage.length) {
return fe.statusMessage
}
if (typeof fe.data?.message === 'string' && fe.data.message.length) {
return fe.data.message
}
if (typeof fe.message === 'string' && fe.message.length) {
return fe.message
}
}
return '图片上传失败'
}
async function onUploadImg(files: File[], callback: (urls: string[]) => void) { async function onUploadImg(files: File[], callback: (urls: string[]) => void) {
const form = new FormData() const form = new FormData()
for (const f of files) { for (const f of files) {
form.append('file', f) form.append('file', f)
} }
try { try {
const res = await request<ApiResponse<{ files: { url: string }[] }>>('/api/file/upload', { const { files: uploaded } = await fetchData<{ files: { url: string }[] }>('/api/file/upload', {
method: 'POST', method: 'POST',
body: form, body: form,
}) })
const { files: uploaded } = unwrapApiBody(res)
callback(uploaded.map((x) => x.url)) callback(uploaded.map((x) => x.url))
} catch (e: unknown) { } catch {
toast.add({ title: extractUploadError(e), color: 'error' })
callback([]) callback([])
} }
} }

59
app/components/PostComments.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { unwrapApiBody, request, type ApiResponse } from '~/utils/http/factory' import { unwrapApiBody, type ApiResponse } from '~/utils/http/factory'
import { useAuthSession } from '~/composables/useAuthSession' import { useAuthSession } from '~/composables/useAuthSession'
type CommentNode = { type CommentNode = {
@ -29,6 +29,7 @@ const props = defineProps<{
}>() }>()
const { loggedIn, refresh: refreshAuthSession } = useAuthSession() const { loggedIn, refresh: refreshAuthSession } = useAuthSession()
const { fetchData } = useClientApi()
const toast = useToast() const toast = useToast()
onMounted(() => { onMounted(() => {
@ -95,44 +96,17 @@ function authorLine(node: CommentNode): string {
return '访客' return '访客'
} }
function extractErrorMessage(e: unknown): string {
if (e && typeof e === 'object') {
const fe = e as {
statusMessage?: string
message?: string
data?: { message?: string }
}
if (typeof fe.statusMessage === 'string' && fe.statusMessage.length) {
return fe.statusMessage
}
if (typeof fe.data?.message === 'string' && fe.data.message.length) {
return fe.data.message
}
if (typeof fe.message === 'string' && fe.message.length) {
return fe.message
}
}
if (e instanceof Error) {
return e.message
}
return '操作失败'
}
async function deleteComment(node: CommentNode) { async function deleteComment(node: CommentNode) {
const postId = data.value?.postId const postId = data.value?.postId
if (postId == null) { if (postId == null) {
return return
} }
try { try {
const res = await request<ApiResponse<{ ok: boolean }>>( await fetchData(`/api/me/posts/${postId}/comments/${node.id}`, { method: 'DELETE' })
`/api/me/posts/${postId}/comments/${node.id}`,
{ method: 'DELETE' },
)
unwrapApiBody(res)
await refreshComments() await refreshComments()
} }
catch (e: unknown) { catch {
toast.add({ title: extractErrorMessage(e), color: 'error' }) /* fetchData 已 toast */
} }
} }
@ -164,29 +138,18 @@ async function submitComment() {
...(loggedIn.value ? {} : { guestDisplayName: guestName.value.trim() }), ...(loggedIn.value ? {} : { guestDisplayName: guestName.value.trim() }),
} }
if (loggedIn.value) { await fetchData<{ id: number }>(`${base}/comments`, {
const res = await request<ApiResponse<{ id: number }>>(`${base}/comments`, { method: 'POST',
method: 'POST', body: payload,
body: payload, })
})
unwrapApiBody(res)
}
else {
const res = await $fetch<ApiResponse<{ id: number }>>(`${base}/comments`, {
method: 'POST',
body: payload,
credentials: 'include',
})
unwrapApiBody(res)
}
draftBody.value = '' draftBody.value = ''
guestName.value = '' guestName.value = ''
replyToId.value = null replyToId.value = null
await refreshComments() await refreshComments()
} }
catch (e: unknown) { catch {
toast.add({ title: extractErrorMessage(e), color: 'error' }) /* fetchData 已 toast */
} }
finally { finally {
submitting.value = false submitting.value = false

4
app/pages/index/index.vue

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../utils/http/factory'
import { useAuthSession } from '../../composables/useAuthSession' import { useAuthSession } from '../../composables/useAuthSession'
const { loggedIn, user, clear } = useAuthSession() const { loggedIn, user, clear } = useAuthSession()
const { fetchData } = useClientApi()
const { allowRegister, siteName } = useGlobalConfig() const { allowRegister, siteName } = useGlobalConfig()
const logoutLoading = ref(false) const logoutLoading = ref(false)
@ -37,7 +37,7 @@ const features = [
async function logout() { async function logout() {
logoutLoading.value = true logoutLoading.value = true
try { try {
unwrapApiBody(await request<ApiResponse<{ success: boolean }>>('/api/auth/logout', { method: 'POST' })) await fetchData<{ success: boolean }>('/api/auth/logout', { method: 'POST' })
clear() clear()
await navigateTo('/login') await navigateTo('/login')
} finally { } finally {

7
app/pages/me/admin/config/index.vue

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../../utils/http/factory'
import { useAuthSession } from '../../../../composables/useAuthSession' import { useAuthSession } from '../../../../composables/useAuthSession'
definePageMeta({ title: '应用配置' }) definePageMeta({ title: '应用配置' })
@ -14,6 +13,7 @@ type GlobalConfigPayload = {
} }
const { user, refresh } = useAuthSession() const { user, refresh } = useAuthSession()
const { fetchData } = useClientApi()
const { refresh: refreshGlobalConfig } = useGlobalConfig() const { refresh: refreshGlobalConfig } = useGlobalConfig()
const loading = ref(true) const loading = ref(true)
@ -33,8 +33,7 @@ async function ensureAdmin() {
async function load() { async function load() {
loading.value = true loading.value = true
try { try {
const res = await request<ApiResponse<GlobalConfigPayload>>('/api/config/global') const { config: cfg } = await fetchData<GlobalConfigPayload>('/api/config/global')
const cfg = unwrapApiBody(res).config
siteName.value = cfg.siteName siteName.value = cfg.siteName
allowRegister.value = cfg.allowRegister allowRegister.value = cfg.allowRegister
mediaOrphanAutoSweepEnabled.value = cfg.mediaOrphanAutoSweepEnabled mediaOrphanAutoSweepEnabled.value = cfg.mediaOrphanAutoSweepEnabled
@ -50,7 +49,7 @@ onMounted(async () => {
}) })
async function putKey(key: string, value: unknown) { async function putKey(key: string, value: unknown) {
await request('/api/config/global', { await fetchData('/api/config/global', {
method: 'PUT', method: 'PUT',
body: { key, value }, body: { key, value },
}) })

28
app/pages/me/admin/media-storage.vue

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
import { useAuthSession } from '../../../composables/useAuthSession' import { useAuthSession } from '../../../composables/useAuthSession'
definePageMeta({ title: '媒体存储校验' }) definePageMeta({ title: '媒体存储校验' })
@ -14,6 +13,7 @@ type AuditReport = {
} }
const { user, refresh } = useAuthSession() const { user, refresh } = useAuthSession()
const { fetchData } = useClientApi()
const toast = useToast() const toast = useToast()
const loading = ref(false) const loading = ref(false)
@ -28,30 +28,11 @@ async function ensureAdmin() {
} }
} }
function extractError(e: unknown): string {
if (e && typeof e === 'object') {
const fe = e as { statusMessage?: string; message?: string; data?: { message?: string } }
if (typeof fe.statusMessage === 'string' && fe.statusMessage.length) {
return fe.statusMessage
}
if (typeof fe.data?.message === 'string' && fe.data.message.length) {
return fe.data.message
}
if (typeof fe.message === 'string' && fe.message.length) {
return fe.message
}
}
return '操作失败'
}
async function runAudit() { async function runAudit() {
loading.value = true loading.value = true
try { try {
const res = await request<ApiResponse<AuditReport>>('/api/admin/media/storage-audit') report.value = await fetchData<AuditReport>('/api/admin/media/storage-audit')
report.value = unwrapApiBody(res)
toast.add({ title: '校验完成', color: 'success' }) toast.add({ title: '校验完成', color: 'success' })
} catch (e: unknown) {
toast.add({ title: extractError(e), color: 'error' })
} finally { } finally {
loading.value = false loading.value = false
} }
@ -60,16 +41,13 @@ async function runAudit() {
async function runCleanup() { async function runCleanup() {
cleaning.value = true cleaning.value = true
try { try {
const res = await request<ApiResponse<{ removed: number; removedIds: number[] }>>( const { removed } = await fetchData<{ removed: number; removedIds: number[] }>(
'/api/admin/media/storage-audit-cleanup', '/api/admin/media/storage-audit-cleanup',
{ method: 'POST', body: {} }, { method: 'POST', body: {} },
) )
const { removed } = unwrapApiBody(res)
toast.add({ title: `已移除 ${removed} 条失效记录(无引用且磁盘无文件)`, color: 'success' }) toast.add({ title: `已移除 ${removed} 条失效记录(无引用且磁盘无文件)`, color: 'success' })
cleanupOpen.value = false cleanupOpen.value = false
await runAudit() await runAudit()
} catch (e: unknown) {
toast.add({ title: extractError(e), color: 'error' })
} finally { } finally {
cleaning.value = false cleaning.value = false
} }

10
app/pages/me/admin/users/index.vue

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../../utils/http/factory'
import { useAuthSession } from '../../../../composables/useAuthSession' import { useAuthSession } from '../../../../composables/useAuthSession'
import { buildPublicProfileAbsoluteUrl } from '../../../../utils/public-profile-url' import { buildPublicProfileAbsoluteUrl } from '../../../../utils/public-profile-url'
definePageMeta({ title: '用户管理' }) definePageMeta({ title: '用户管理' })
const { user, refresh } = useAuthSession() const { user, refresh } = useAuthSession()
const { fetchData } = useClientApi()
const toast = useToast() const toast = useToast()
const rows = ref< const rows = ref<
@ -44,8 +44,8 @@ async function ensureAdmin() {
async function load() { async function load() {
loading.value = true loading.value = true
try { try {
const res = await request<ApiResponse<{ users: typeof rows.value }>>('/api/admin/users') const { users } = await fetchData<{ users: typeof rows.value }>('/api/admin/users')
rows.value = unwrapApiBody(res).users rows.value = users
} finally { } finally {
loading.value = false loading.value = false
} }
@ -59,7 +59,7 @@ onMounted(async () => {
async function createUser() { async function createUser() {
creating.value = true creating.value = true
try { try {
await request('/api/admin/users', { await fetchData('/api/admin/users', {
method: 'POST', method: 'POST',
body: { body: {
username: form.username, username: form.username,
@ -77,7 +77,7 @@ async function createUser() {
} }
async function setStatus(id: number, status: 'active' | 'disabled') { async function setStatus(id: number, status: 'active' | 'disabled') {
await request(`/api/admin/users/${id}`, { method: 'PATCH', body: { status } }) await fetchData(`/api/admin/users/${id}`, { method: 'PATCH', body: { status } })
await load() await load()
} }
</script> </script>

6
app/pages/me/index.vue

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../utils/http/factory'
import { useAuthSession } from '../../composables/useAuthSession' import { useAuthSession } from '../../composables/useAuthSession'
definePageMeta({ definePageMeta({
@ -7,6 +6,7 @@ definePageMeta({
}) })
const { user } = useAuthSession() const { user } = useAuthSession()
const { fetchData } = useClientApi()
type ProfilePayload = { type ProfilePayload = {
profile: { publicSlug: string | null } profile: { publicSlug: string | null }
@ -16,8 +16,8 @@ const publicSlug = ref<string | null>(null)
onMounted(async () => { onMounted(async () => {
try { try {
const res = await request<ApiResponse<ProfilePayload>>('/api/me/profile') const { profile } = await fetchData<ProfilePayload>('/api/me/profile')
publicSlug.value = unwrapApiBody(res).profile.publicSlug publicSlug.value = profile.publicSlug
} catch { } catch {
publicSlug.value = null publicSlug.value = null
} }

32
app/pages/me/media/orphans.vue

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
import { useAuthSession } from '../../../composables/useAuthSession' import { useAuthSession } from '../../../composables/useAuthSession'
definePageMeta({ title: '图片孤儿审查' }) definePageMeta({ title: '图片孤儿审查' })
@ -20,6 +19,7 @@ type OrphanItem = {
type Filter = 'all' | 'deletable' | 'cooling' type Filter = 'all' | 'deletable' | 'cooling'
const toast = useToast() const toast = useToast()
const { fetchData } = useClientApi()
const { refresh: refreshAuth } = useAuthSession() const { refresh: refreshAuth } = useAuthSession()
const filter = ref<Filter>('all') const filter = ref<Filter>('all')
@ -57,26 +57,6 @@ const confirmDescription = computed(() => {
return n === 1 ? '确定删除该图片?此操作不可恢复。' : `确定删除选中的 ${n} 张图片?此操作不可恢复。` return n === 1 ? '确定删除该图片?此操作不可恢复。' : `确定删除选中的 ${n} 张图片?此操作不可恢复。`
}) })
function extractError(e: unknown): string {
if (e && typeof e === 'object') {
const fe = e as {
statusMessage?: string
message?: string
data?: { message?: string }
}
if (typeof fe.statusMessage === 'string' && fe.statusMessage.length) {
return fe.statusMessage
}
if (typeof fe.data?.message === 'string' && fe.data.message.length) {
return fe.data.message
}
if (typeof fe.message === 'string' && fe.message.length) {
return fe.message
}
}
return '操作失败'
}
function formatBytes(n: number): string { function formatBytes(n: number): string {
if (n < 1024) { if (n < 1024) {
return `${n} B` return `${n} B`
@ -103,16 +83,13 @@ async function load() {
page: String(page.value), page: String(page.value),
pageSize: String(pageSize.value), pageSize: String(pageSize.value),
}) })
const res = await request<ApiResponse<{ items: OrphanItem[]; total: number }>>( const body = await fetchData<{ items: OrphanItem[]; total: number }>(
`/api/me/media/orphans?${q.toString()}`, `/api/me/media/orphans?${q.toString()}`,
) )
const body = unwrapApiBody(res)
items.value = body.items items.value = body.items
total.value = body.total total.value = body.total
const allowed = new Set(body.items.filter((i) => i.state === 'deletable').map((i) => i.id)) const allowed = new Set(body.items.filter((i) => i.state === 'deletable').map((i) => i.id))
selectedIds.value = new Set([...selectedIds.value].filter((id) => allowed.has(id))) selectedIds.value = new Set([...selectedIds.value].filter((id) => allowed.has(id)))
} catch (e: unknown) {
toast.add({ title: extractError(e), color: 'error' })
} finally { } finally {
loading.value = false loading.value = false
} }
@ -170,17 +147,14 @@ function openConfirm(ids: number[]) {
async function executeDelete() { async function executeDelete() {
deleting.value = true deleting.value = true
try { try {
const res = await request<ApiResponse<{ deleted: number }>>('/api/me/media/orphans-delete', { const { deleted } = await fetchData<{ deleted: number }>('/api/me/media/orphans-delete', {
method: 'POST', method: 'POST',
body: { ids: confirmIds.value }, body: { ids: confirmIds.value },
}) })
const { deleted } = unwrapApiBody(res)
toast.add({ title: `已删除 ${deleted}`, color: 'success' }) toast.add({ title: `已删除 ${deleted}`, color: 'success' })
confirmOpen.value = false confirmOpen.value = false
selectedIds.value = new Set() selectedIds.value = new Set()
await load() await load()
} catch (e: unknown) {
toast.add({ title: extractError(e), color: 'error' })
} finally { } finally {
deleting.value = false deleting.value = false
} }

9
app/pages/me/posts/[id].vue

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
import { useAuthSession } from '../../../composables/useAuthSession' import { useAuthSession } from '../../../composables/useAuthSession'
definePageMeta({ title: '编辑文章' }) definePageMeta({ title: '编辑文章' })
@ -7,6 +6,7 @@ definePageMeta({ title: '编辑文章' })
const route = useRoute() const route = useRoute()
const id = computed(() => route.params.id as string) const id = computed(() => route.params.id as string)
const { user, refresh: refreshAuth } = useAuthSession() const { user, refresh: refreshAuth } = useAuthSession()
const { fetchData } = useClientApi()
const state = reactive({ const state = reactive({
title: '', title: '',
@ -30,8 +30,7 @@ const publicPostHref = computed(() => {
async function load() { async function load() {
loading.value = true loading.value = true
try { try {
const res = await request<ApiResponse<{ post: typeof state }>>(`/api/me/posts/${id.value}`) const { post: p } = await fetchData<{ post: typeof state }>(`/api/me/posts/${id.value}`)
const p = unwrapApiBody(res).post
Object.assign(state, { Object.assign(state, {
title: p.title, title: p.title,
slug: p.slug, slug: p.slug,
@ -57,7 +56,7 @@ watch(id, () => {
async function save() { async function save() {
saving.value = true saving.value = true
try { try {
await request(`/api/me/posts/${id.value}`, { await fetchData(`/api/me/posts/${id.value}`, {
method: 'PUT', method: 'PUT',
body: { body: {
title: state.title, title: state.title,
@ -74,7 +73,7 @@ async function save() {
} }
async function remove() { async function remove() {
await request(`/api/me/posts/${id.value}`, { method: 'DELETE' }) await fetchData(`/api/me/posts/${id.value}`, { method: 'DELETE' })
await navigateTo('/me/posts') await navigateTo('/me/posts')
} }

6
app/pages/me/posts/index.vue

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
import { useAuthSession } from '../../../composables/useAuthSession' import { useAuthSession } from '../../../composables/useAuthSession'
definePageMeta({ title: '文章' }) definePageMeta({ title: '文章' })
@ -9,12 +8,13 @@ type Row = { id: number; title: string; slug: string; visibility: string }
const posts = ref<Row[]>([]) const posts = ref<Row[]>([])
const loading = ref(true) const loading = ref(true)
const { user, refresh: refreshAuth } = useAuthSession() const { user, refresh: refreshAuth } = useAuthSession()
const { fetchData } = useClientApi()
async function load() { async function load() {
loading.value = true loading.value = true
try { try {
const res = await request<ApiResponse<{ posts: Row[] }>>('/api/me/posts') const { posts: list } = await fetchData<{ posts: Row[] }>('/api/me/posts')
posts.value = unwrapApiBody(res).posts posts.value = list
} finally { } finally {
loading.value = false loading.value = false
} }

8
app/pages/me/posts/new.vue

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
definePageMeta({ title: '新建文章' }) definePageMeta({ title: '新建文章' })
const { fetchData } = useClientApi()
const state = reactive({ const state = reactive({
title: '', title: '',
slug: '', slug: '',
@ -15,7 +15,7 @@ const loading = ref(false)
async function submit() { async function submit() {
loading.value = true loading.value = true
try { try {
const res = await request<ApiResponse<{ post: { id: number } }>>('/api/me/posts', { const { post } = await fetchData<{ post: { id: number } }>('/api/me/posts', {
method: 'POST', method: 'POST',
body: { body: {
title: state.title, title: state.title,
@ -25,7 +25,7 @@ async function submit() {
visibility: state.visibility, visibility: state.visibility,
}, },
}) })
const id = unwrapApiBody(res).post.id const id = post.id
await navigateTo(`/me/posts/${id}`) await navigateTo(`/me/posts/${id}`)
} finally { } finally {
loading.value = false loading.value = false

44
app/pages/me/profile/index.vue

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuthSession } from '../../../composables/useAuthSession' import { useAuthSession } from '../../../composables/useAuthSession'
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
definePageMeta({ title: '资料' }) definePageMeta({ title: '资料' })
const { refresh: refreshAuthSession } = useAuthSession() const { refresh: refreshAuthSession } = useAuthSession()
const { fetchData, getApiErrorMessage } = useClientApi()
type ProfileGet = { type ProfileGet = {
profile: { profile: {
@ -62,11 +62,11 @@ async function onAvatarFileChange(ev: Event) {
try { try {
const form = new FormData() const form = new FormData()
form.append('file', file) form.append('file', file)
const res = await request<ApiResponse<{ files: { url: string }[] }>>('/api/file/upload', { const { files } = await fetchData<{ files: { url: string }[] }>('/api/file/upload', {
method: 'POST', method: 'POST',
body: form, body: form,
notify: false,
}) })
const { files } = unwrapApiBody(res)
const url = files[0]?.url const url = files[0]?.url
if (!url) { if (!url) {
message.value = '上传未返回地址' message.value = '上传未返回地址'
@ -75,10 +75,7 @@ async function onAvatarFileChange(ev: Event) {
state.avatar = url state.avatar = url
message.value = '头像已上传,请点击下方「保存」写入资料' message.value = '头像已上传,请点击下方「保存」写入资料'
} catch (e: unknown) { } catch (e: unknown) {
message.value = message.value = getApiErrorMessage(e)
typeof e === 'object' && e !== null && 'statusMessage' in e
? String((e as { statusMessage: string }).statusMessage)
: '头像上传失败'
} finally { } finally {
uploadingAvatar.value = false uploadingAvatar.value = false
} }
@ -96,11 +93,11 @@ async function onHeaderIconFileChange(ev: Event) {
try { try {
const form = new FormData() const form = new FormData()
form.append('file', file) form.append('file', file)
const res = await request<ApiResponse<{ files: { url: string }[] }>>('/api/file/upload', { const { files } = await fetchData<{ files: { url: string }[] }>('/api/file/upload', {
method: 'POST', method: 'POST',
body: form, body: form,
notify: false,
}) })
const { files } = unwrapApiBody(res)
const url = files[0]?.url const url = files[0]?.url
if (!url) { if (!url) {
message.value = '上传未返回地址' message.value = '上传未返回地址'
@ -109,10 +106,7 @@ async function onHeaderIconFileChange(ev: Event) {
state.publicHomeHeaderIconUrl = url state.publicHomeHeaderIconUrl = url
message.value = '顶栏图标已上传,请点击下方「保存」写入' message.value = '顶栏图标已上传,请点击下方「保存」写入'
} catch (e: unknown) { } catch (e: unknown) {
message.value = message.value = getApiErrorMessage(e)
typeof e === 'object' && e !== null && 'statusMessage' in e
? String((e as { statusMessage: string }).statusMessage)
: '顶栏图标上传失败'
} finally { } finally {
uploadingHeaderIcon.value = false uploadingHeaderIcon.value = false
} }
@ -121,12 +115,12 @@ async function onHeaderIconFileChange(ev: Event) {
async function load() { async function load() {
loading.value = true loading.value = true
try { try {
const [profileRes, meCfgRes] = await Promise.all([ const [profilePayload, meCfgPayload] = await Promise.all([
request<ApiResponse<ProfileGet>>('/api/me/profile'), fetchData<ProfileGet>('/api/me/profile'),
request<ApiResponse<MeConfigGet>>('/api/config/me'), fetchData<MeConfigGet>('/api/config/me'),
]) ])
const p = unwrapApiBody(profileRes).profile const p = profilePayload.profile
const cfg = unwrapApiBody(meCfgRes).config const cfg = meCfgPayload.config
state.nickname = p.nickname ?? '' state.nickname = p.nickname ?? ''
state.avatar = p.avatar ?? '' state.avatar = p.avatar ?? ''
state.avatarVisibility = p.avatarVisibility state.avatarVisibility = p.avatarVisibility
@ -155,8 +149,9 @@ async function save() {
message.value = '社交链接 JSON 无效' message.value = '社交链接 JSON 无效'
return return
} }
await request('/api/me/profile', { await fetchData('/api/me/profile', {
method: 'PUT', method: 'PUT',
notify: false,
body: { body: {
nickname: state.nickname || null, nickname: state.nickname || null,
avatar: state.avatar || null, avatar: state.avatar || null,
@ -168,12 +163,14 @@ async function save() {
}, },
}) })
await Promise.all([ await Promise.all([
request('/api/config/me', { fetchData('/api/config/me', {
method: 'PUT', method: 'PUT',
notify: false,
body: { key: 'publicHomeHeaderTitle', value: state.publicHomeHeaderTitle }, body: { key: 'publicHomeHeaderTitle', value: state.publicHomeHeaderTitle },
}), }),
request('/api/config/me', { fetchData('/api/config/me', {
method: 'PUT', method: 'PUT',
notify: false,
body: { key: 'publicHomeHeaderIconUrl', value: state.publicHomeHeaderIconUrl }, body: { key: 'publicHomeHeaderIconUrl', value: state.publicHomeHeaderIconUrl },
}), }),
]) ])
@ -185,10 +182,7 @@ async function save() {
} }
await load() await load()
} catch (e: unknown) { } catch (e: unknown) {
message.value = message.value = getApiErrorMessage(e)
typeof e === 'object' && e !== null && 'statusMessage' in e
? String((e as { statusMessage: string }).statusMessage)
: '保存失败'
} finally { } finally {
saving.value = false saving.value = false
} }

19
app/pages/me/rss/index.vue

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
definePageMeta({ title: 'RSS' }) definePageMeta({ title: 'RSS' })
type Feed = { id: number; feedUrl: string; title: string | null; lastError: string | null } type Feed = { id: number; feedUrl: string; title: string | null; lastError: string | null }
@ -22,6 +20,7 @@ const copiedItemId = ref<number | null>(null)
let copyResetTimer: ReturnType<typeof setTimeout> | undefined let copyResetTimer: ReturnType<typeof setTimeout> | undefined
const { user, refresh } = useAuthSession() const { user, refresh } = useAuthSession()
const { fetchData } = useClientApi()
const filteredItems = computed(() => { const filteredItems = computed(() => {
if (selectedFeedId.value === null) { if (selectedFeedId.value === null) {
@ -57,11 +56,11 @@ async function load() {
loading.value = true loading.value = true
try { try {
const [f, i] = await Promise.all([ const [f, i] = await Promise.all([
request<ApiResponse<{ feeds: Feed[] }>>('/api/me/rss/feeds'), fetchData<{ feeds: Feed[] }>('/api/me/rss/feeds'),
request<ApiResponse<{ items: Item[] }>>('/api/me/rss/items'), fetchData<{ items: Item[] }>('/api/me/rss/items'),
]) ])
feeds.value = unwrapApiBody(f).feeds feeds.value = f.feeds
items.value = unwrapApiBody(i).items items.value = i.items
if (selectedFeedId.value !== null && !feeds.value.some((x) => x.id === selectedFeedId.value)) { if (selectedFeedId.value !== null && !feeds.value.some((x) => x.id === selectedFeedId.value)) {
selectedFeedId.value = null selectedFeedId.value = null
} }
@ -75,18 +74,18 @@ onMounted(async () => {
}) })
async function addFeed() { async function addFeed() {
await request('/api/me/rss/feeds', { method: 'POST', body: { feedUrl: feedUrl.value } }) await fetchData('/api/me/rss/feeds', { method: 'POST', body: { feedUrl: feedUrl.value } })
feedUrl.value = '' feedUrl.value = ''
await load() await load()
} }
async function syncAll() { async function syncAll() {
await request('/api/me/rss/sync', { method: 'POST', body: {} }) await fetchData('/api/me/rss/sync', { method: 'POST', body: {} })
await load() await load()
} }
async function removeFeed(id: number) { async function removeFeed(id: number) {
await request(`/api/me/rss/feeds/${id}`, { method: 'DELETE' }) await fetchData(`/api/me/rss/feeds/${id}`, { method: 'DELETE' })
if (selectedFeedId.value === id) { if (selectedFeedId.value === id) {
selectedFeedId.value = null selectedFeedId.value = null
} }
@ -94,7 +93,7 @@ async function removeFeed(id: number) {
} }
async function setItemVis(id: number, visibility: string) { async function setItemVis(id: number, visibility: string) {
await request(`/api/me/rss/items/${id}`, { method: 'PATCH', body: { visibility } }) await fetchData(`/api/me/rss/items/${id}`, { method: 'PATCH', body: { visibility } })
await load() await load()
} }

15
app/pages/me/timeline/index.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuthSession } from '../../../composables/useAuthSession' import { useAuthSession } from '../../../composables/useAuthSession'
import { request, unwrapApiBody, type ApiResponse } from '../../../utils/http/factory'
import { import {
formatOccurredOnDisplay, formatOccurredOnDisplay,
occurredOnToIsoAttr, occurredOnToIsoAttr,
@ -61,6 +60,8 @@ const visibilitySelectItems = [
{ label: '仅链接(凭分享链接访问)', value: 'unlisted' }, { label: '仅链接(凭分享链接访问)', value: 'unlisted' },
] as const ] as const
const { fetchData } = useClientApi()
const events = ref<Ev[]>([]) const events = ref<Ev[]>([])
const loading = ref(true) const loading = ref(true)
const submitLoading = ref(false) const submitLoading = ref(false)
@ -78,8 +79,8 @@ const form = reactive({
async function load() { async function load() {
loading.value = true loading.value = true
try { try {
const res = await request<ApiResponse<{ events: Ev[] }>>('/api/me/timeline') const { events: list } = await fetchData<{ events: Ev[] }>('/api/me/timeline')
events.value = unwrapApiBody(res).events events.value = list
} finally { } finally {
loading.value = false loading.value = false
} }
@ -99,7 +100,7 @@ async function add() {
} }
submitLoading.value = true submitLoading.value = true
try { try {
await request('/api/me/timeline', { await fetchData('/api/me/timeline', {
method: 'POST', method: 'POST',
body: { body: {
occurredOn: occurredOnIso, occurredOn: occurredOnIso,
@ -125,8 +126,7 @@ async function removeEvent(e: Ev) {
} }
deletingId.value = e.id deletingId.value = e.id
try { try {
const res = await request<ApiResponse<{ ok: boolean }>>(`/api/me/timeline/${e.id}`, { method: 'DELETE' }) await fetchData(`/api/me/timeline/${e.id}`, { method: 'DELETE' })
unwrapApiBody(res)
await load() await load()
} finally { } finally {
deletingId.value = null deletingId.value = null
@ -159,11 +159,10 @@ async function updateVisibility(e: Ev, visibility: string) {
} }
updatingVisibilityId.value = e.id updatingVisibilityId.value = e.id
try { try {
const res = await request<ApiResponse<{ event: unknown }>>(`/api/me/timeline/${e.id}`, { await fetchData(`/api/me/timeline/${e.id}`, {
method: 'PUT', method: 'PUT',
body: { visibility }, body: { visibility },
}) })
unwrapApiBody(res)
await load() await load()
} finally { } finally {
updatingVisibilityId.value = null updatingVisibilityId.value = null

Loading…
Cancel
Save