feat: 支持画廊图片更新

This commit is contained in:
richarjiang
2026-04-15 13:58:51 +08:00
parent 7ce7cef77c
commit 6ab16f508a
20 changed files with 1671 additions and 247 deletions

View File

@@ -14,27 +14,36 @@
<!-- Circular logo -->
<view class="logo-circle">
<image
v-if="logoImage"
class="logo-img"
:src="logoImage"
mode="aspectFill"
/>
<view v-else class="logo-placeholder">
<text>{{ studioName.slice(0, 1) || 'F' }}</text>
</view>
</view>
<!-- Studio name -->
<text class="studio-name">Focus Core</text>
<text class="studio-name">{{ studioName }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { StudioConfig } from '@mp-pilates/shared'
defineProps<{
const props = defineProps<{
studioInfo: StudioConfig | null
}>()
const bannerImage = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/bannerBg.jpg'
const logoImage = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/logo.jpg'
const fallbackBannerImage = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/bannerBg.jpg'
const fallbackLogoImage = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/logo.jpg'
const bannerImage = computed(() => props.studioInfo?.bannerUrl || fallbackBannerImage)
const logoImage = computed(() => props.studioInfo?.logo || fallbackLogoImage)
const studioName = computed(() => props.studioInfo?.name || 'Focus Core')
</script>
<style lang="scss" scoped>
@@ -94,10 +103,16 @@ const logoImage = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/im
}
.logo-placeholder {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
font-size: 64rpx;
font-weight: 800;
color: #333;
letter-spacing: 4rpx;
display: flex;
align-items: center;
justify-content: center;
}
.studio-name {

View File

@@ -37,22 +37,18 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { StudioConfig } from '@mp-pilates/shared'
import {
DEFAULT_STUDIO_GALLERY_PHOTOS,
type StudioConfig,
} from '@mp-pilates/shared'
const props = defineProps<{
studioInfo: StudioConfig | null
}>()
const defaultGalleryPhotos = [
'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/place_1.jpg',
'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/place_2.jpg',
'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/place_3.jpg',
'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/place_4.jpg',
]
const galleryPhotos = computed(() => {
const photos = props.studioInfo?.photos?.filter(Boolean) ?? []
return photos.length ? photos : defaultGalleryPhotos
return photos.length ? photos : [...DEFAULT_STUDIO_GALLERY_PHOTOS]
})
function previewPhoto(index: number) {

View File

@@ -316,7 +316,7 @@ async function loadData() {
adminStore.fetchFlashSales(),
adminStore.fetchCardTypes(),
])
items.value = [...salesResult.data]
items.value = [...salesResult.items]
total.value = salesResult.total
cardTypes.value = [...cardTypesResult]
} catch {
@@ -329,7 +329,7 @@ async function loadData() {
async function reloadSales() {
try {
const result = await adminStore.fetchFlashSales()
items.value = [...result.data]
items.value = [...result.items]
total.value = result.total
} catch {
// silent

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,8 @@ import type {
FlashSaleAdminItem,
CreateFlashSaleDto,
UpdateFlashSaleDto,
CreateStudioUploadCredentialDto,
StudioUploadCredential,
} from '@mp-pilates/shared'
interface LegacyPaginatedData<T> {
@@ -141,6 +143,15 @@ export const useAdminStore = defineStore('admin', () => {
return data
}
async function createStudioUploadCredential(
dto: CreateStudioUploadCredentialDto,
): Promise<StudioUploadCredential> {
return post<StudioUploadCredential>(
'/admin/studio/upload-credentials',
dto as unknown as Record<string, unknown>,
)
}
// ── Orders ───────────────────────────────────────────────────────
async function fetchAdminOrders(params: {
page?: number
@@ -278,6 +289,7 @@ export const useAdminStore = defineStore('admin', () => {
// Studio
fetchStudioConfig,
saveStudioConfig,
createStudioUploadCredential,
// Orders
fetchAdminOrders,
// Bookings

View File

@@ -0,0 +1,72 @@
import type {
CreateStudioUploadCredentialDto,
StudioAssetType,
StudioUploadCredential,
} from '@mp-pilates/shared'
import type { useAdminStore } from '../stores/admin'
type AdminStore = ReturnType<typeof useAdminStore>
function inferContentType(fileName: string): string | undefined {
const extension = fileName.split('.').pop()?.toLowerCase()
if (extension === 'jpg' || extension === 'jpeg') {
return 'image/jpeg'
}
if (extension === 'png') {
return 'image/png'
}
if (extension === 'webp') {
return 'image/webp'
}
if (extension === 'heic') {
return 'image/heic'
}
if (extension === 'heif') {
return 'image/heif'
}
return undefined
}
function uploadToCos(filePath: string, credential: StudioUploadCredential): Promise<void> {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: credential.uploadUrl,
filePath,
name: 'file',
formData: credential.formData as unknown as Record<string, string>,
success: (result) => {
if (result.statusCode >= 200 && result.statusCode < 300) {
resolve()
return
}
const body = typeof result.data === 'string' ? result.data : JSON.stringify(result.data)
const code = body.match(/<Code>([^<]+)<\/Code>/)?.[1]
const message = body.match(/<Message>([^<]+)<\/Message>/)?.[1]
const detail = code || message ? `${code ?? 'COS'}: ${message ?? body}` : body
reject(new Error(`COS 上传失败 (${result.statusCode}) ${detail}`))
},
fail: (error) => {
reject(new Error(error.errMsg || 'COS 上传失败'))
},
})
})
}
export async function uploadStudioAsset(params: {
adminStore: AdminStore
filePath: string
fileName: string
assetType: StudioAssetType
}): Promise<string> {
const payload: CreateStudioUploadCredentialDto = {
fileName: params.fileName,
contentType: inferContentType(params.fileName),
assetType: params.assetType,
}
const credential = await params.adminStore.createStudioUploadCredential(payload)
await uploadToCos(params.filePath, credential)
return credential.fileUrl
}