refactor(api)!: align client with canonical HTTP contract

Route all feature requests through business API modules, sync generated DTOs, and add communication tests and architecture documentation.

BREAKING CHANGE: migrate the client to plural resource paths, canonical snake_case fields, ISO timestamps, and explicit paginated responses.
This commit is contained in:
2026-09-30 12:04:58 +08:00
parent 00c6eccb63
commit 8ba0212eec
34 changed files with 1137 additions and 1623 deletions
+15 -13
View File
@@ -9,12 +9,14 @@ import CloudEditModal, {
import CloudLikeButton from '@/features/clouds/components/CloudLikeButton.vue'
import ImageDetailModal from '@/features/clouds/components/ImageDetailModal.vue'
import MiniLocationMap from '@/shared/map/components/MiniLocationMap.vue'
import { ApiError, apiRequest, toApiCloud } from '@/lib/api'
import { ApiError } from '@/lib/api'
import { listClouds } from '@/features/clouds/api'
import type { PublicCloudListQuery } from '@/types/api-contract'
import { useAuthStore } from '@/features/auth/stores/auth'
import { useCloudsStore } from '@/features/clouds/stores/clouds'
import { useProfileStore } from '@/features/profile/stores/profile'
import type { ApiCloud, BackendCloudInfo } from '@/types/api'
import type { CloudDetail } from '@/types/view-models'
interface GalleryCloud {
id: string
@@ -85,7 +87,7 @@ function formatCoordinate(value: number | null) {
const normalizedSearch = computed(() => searchQuery.value.trim())
const isUserSearch = computed(() => normalizedSearch.value.startsWith('@'))
function toGalleryCloud(row: ApiCloud) {
function toGalleryCloud(row: CloudDetail) {
return {
id: row.id,
user_id: row.user_id,
@@ -100,7 +102,7 @@ function toGalleryCloud(row: ApiCloud) {
status: row.status,
is_hidden: row.is_hidden,
cloudTypeName: row.cloud_type_name || '未知',
cloudTypeRarity: row.cloud_type_rarity_value ?? 0,
cloudTypeRarity: row.cloud_type_rarity ?? 0,
username: row.username || '匿名',
receivedLikeCount: row.received_like_count,
} satisfies GalleryCloud
@@ -111,23 +113,23 @@ async function loadPage(page: number) {
loadError.value = ''
try {
const params = new URLSearchParams({ page: String(page), page_size: String(PAGE_SIZE) })
const query: PublicCloudListQuery = { page, page_size: PAGE_SIZE }
if (normalizedSearch.value) {
params.set('filter', isUserSearch.value ? 'owner' : 'type')
params.set('value', normalizedSearch.value)
query.filter = isUserSearch.value ? 'owner' : 'type'
query.value = normalizedSearch.value
} else if (selectedTypeId.value !== 'all') {
const typeName = cloudsStore.cloudTypes.find(
type => type.id === selectedTypeId.value,
)?.name
if (typeName) {
params.set('filter', 'type')
params.set('value', typeName)
query.filter = 'type'
query.value = typeName
}
}
const rows = await apiRequest<BackendCloudInfo[]>(`/cloud?${params}`, { auth: false })
galleryItems.value = rows.map(toApiCloud).map(toGalleryCloud)
hasNextPage.value = rows.length === PAGE_SIZE
const result = await listClouds(query)
galleryItems.value = result.items.map(toGalleryCloud)
hasNextPage.value = result.has_more
currentPage.value = page
} catch (error) {
if (error instanceof ApiError && error.status === 404 && isUserSearch.value) {
@@ -232,7 +234,7 @@ async function submitEditModal(value: CloudEditFormValue) {
try {
const updated = await profileStore.updateCloud(authStore.user.id, current.id, {
type_id: value.cloudCategoryId,
cloud_type_id: value.cloudCategoryId,
latitude: blurCoordinate(value.latitude as number),
longitude: blurCoordinate(value.longitude as number),
description: value.description.trim(),