Files
mp-pilates/packages/app/src/pages/admin/studio.vue
2026-04-05 13:25:54 +08:00

405 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page" :style="{ paddingTop: navBarHeight }">
<CustomNavBar title="工作室设置" show-back />
<!-- Loading state -->
<view v-if="loading" class="skeleton-page">
<view class="skeleton-section" />
<view class="skeleton-section" />
<view class="skeleton-section" />
</view>
<template v-else>
<!-- Banner preview -->
<view class="banner-preview" :style="bannerStyle">
<view class="banner-overlay">
<view class="banner-logo-wrap">
<image v-if="form.logo" class="banner-logo" :src="form.logo" mode="aspectFill" />
<view v-else class="banner-logo-placeholder">
<text class="banner-logo-text">{{ form.name.slice(0, 1) || '🏢' }}</text>
</view>
</view>
<text class="banner-name">{{ form.name || '工作室名称' }}</text>
</view>
</view>
<!-- Basic info card -->
<view class="form-card">
<text class="form-card-title">基本信息</text>
<view class="form-row">
<text class="form-label">工作室名称</text>
<input
class="form-input"
v-model="form.name"
placeholder="请输入名称"
placeholder-style="color:#bbb"
:disabled="saving"
/>
</view>
<view class="form-row">
<text class="form-label">地址</text>
<input
class="form-input"
v-model="form.address"
placeholder="请输入地址"
placeholder-style="color:#bbb"
:disabled="saving"
/>
</view>
<view class="form-row">
<text class="form-label">联系电话</text>
<input
class="form-input"
v-model="form.phone"
type="tel"
placeholder="请输入电话"
placeholder-style="color:#bbb"
:disabled="saving"
/>
</view>
<view class="form-row form-row--last">
<text class="form-label">Logo URL</text>
<input
class="form-input"
v-model="form.logo"
placeholder="图片链接(可选)"
placeholder-style="color:#bbb"
:disabled="saving"
/>
</view>
</view>
<!-- Settings card -->
<view class="form-card">
<text class="form-card-title">预约设置</text>
<view class="form-row">
<view class="label-group">
<text class="form-label">取消限制小时</text>
<text class="form-label-sub">课前多少小时内不允许取消</text>
</view>
<input
class="form-input form-input--short"
type="number"
v-model="form.cancelHoursLimitStr"
placeholder="如2"
placeholder-style="color:#bbb"
:disabled="saving"
/>
</view>
<view class="form-row form-row--last">
<view class="label-group">
<text class="form-label">宣传图 URL</text>
<text class="form-label-sub">首页横幅图片链接</text>
</view>
<input
class="form-input"
v-model="form.bannerUrl"
placeholder="图片链接(可选)"
placeholder-style="color:#bbb"
:disabled="saving"
/>
</view>
</view>
<!-- Location card -->
<view class="form-card">
<text class="form-card-title">位置坐标可选</text>
<view class="form-row">
<text class="form-label">纬度</text>
<input
class="form-input"
type="digit"
v-model="form.latitudeStr"
placeholder="如31.2304"
placeholder-style="color:#bbb"
:disabled="saving"
/>
</view>
<view class="form-row form-row--last">
<text class="form-label">经度</text>
<input
class="form-input"
type="digit"
v-model="form.longitudeStr"
placeholder="如121.4737"
placeholder-style="color:#bbb"
:disabled="saving"
/>
</view>
</view>
<!-- Save button -->
<view class="save-wrap">
<view
class="save-btn"
:class="{ 'save-btn--loading': saving, 'save-btn--disabled': !isDirty }"
@tap="handleSave"
>
<text class="save-btn-text">{{ saving ? '保存中...' : '保存修改' }}</text>
</view>
</view>
</template>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import CustomNavBar from '../../components/CustomNavBar.vue'
import { useAdminStore } from '../../stores/admin'
const adminStore = useAdminStore()
const navBarHeight = ref('64px')
onMounted(() => {
const sys = uni.getSystemInfoSync()
navBarHeight.value = `${(sys.statusBarHeight ?? 20) + Math.round(88 * sys.windowWidth / 750)}px`
})
const form = ref({
name: '',
address: '',
phone: '',
logo: '',
bannerUrl: '',
cancelHoursLimitStr: '2',
latitudeStr: '',
longitudeStr: '',
})
const original = ref({ ...form.value })
const loading = ref(false)
const saving = ref(false)
const isDirty = computed(() =>
JSON.stringify(form.value) !== JSON.stringify(original.value),
)
const bannerStyle = computed(() => {
if (form.value.bannerUrl) {
return `background-image: url(${form.value.bannerUrl}); background-size: cover; background-position: center;`
}
return 'background: linear-gradient(135deg, #1a1a2e, #2d2d5e);'
})
async function fetchStudioInfo() {
loading.value = true
try {
const data = await adminStore.fetchStudioConfig()
const initial = {
name: data.name ?? '',
address: data.address ?? '',
phone: data.phone ?? '',
logo: data.logo ?? '',
bannerUrl: data.bannerUrl ?? '',
cancelHoursLimitStr: String(data.cancelHoursLimit ?? 2),
latitudeStr: data.latitude != null ? String(data.latitude) : '',
longitudeStr: data.longitude != null ? String(data.longitude) : '',
}
form.value = { ...initial }
original.value = { ...initial }
} catch {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
async function handleSave() {
if (!isDirty.value || saving.value) return
const cancelHoursLimit = parseInt(form.value.cancelHoursLimitStr, 10)
if (isNaN(cancelHoursLimit) || cancelHoursLimit < 0) {
uni.showToast({ title: '取消限制小时数无效', icon: 'none' })
return
}
saving.value = true
try {
const payload: Record<string, unknown> = {
name: form.value.name.trim() || undefined,
address: form.value.address.trim() || undefined,
phone: form.value.phone.trim() || undefined,
logo: form.value.logo.trim() || undefined,
bannerUrl: form.value.bannerUrl.trim() || undefined,
cancelHoursLimit,
}
const lat = parseFloat(form.value.latitudeStr)
const lng = parseFloat(form.value.longitudeStr)
if (!isNaN(lat)) payload.latitude = lat
if (!isNaN(lng)) payload.longitude = lng
await adminStore.saveStudioConfig(payload as any)
original.value = { ...form.value }
uni.showToast({ title: '保存成功', icon: 'success' })
} catch (e: any) {
uni.showToast({ title: e?.message ?? '保存失败', icon: 'none' })
} finally {
saving.value = false
}
}
onMounted(fetchStudioInfo)
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f5f3f0;
padding-bottom: 60rpx;
}
/* ── Skeleton ────────────────────────────── */
.skeleton-page { padding: 0 24rpx; padding-top: 280rpx; }
.skeleton-section {
height: 200rpx;
border-radius: 20rpx;
margin-bottom: 24rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 400% 100%;
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
/* ── Banner preview ──────────────────────── */
.banner-preview {
height: 260rpx;
position: relative;
}
.banner-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.35);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.banner-logo-wrap {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
overflow: hidden;
border: 4rpx solid rgba(255,255,255,0.4);
}
.banner-logo { width: 96rpx; height: 96rpx; }
.banner-logo-placeholder {
width: 100%;
height: 100%;
background: #c9a87c;
display: flex;
align-items: center;
justify-content: center;
}
.banner-logo-text { font-size: 40rpx; font-weight: 700; color: #1a1a2e; }
.banner-name { font-size: 32rpx; font-weight: 700; color: #ffffff; }
/* ── Form card ───────────────────────────── */
.form-card {
background: #ffffff;
border-radius: 20rpx;
margin: 24rpx 24rpx 0;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
}
.form-card-title {
font-size: 26rpx;
font-weight: 700;
color: #999;
display: block;
padding: 24rpx 28rpx 0;
letter-spacing: 1rpx;
text-transform: uppercase;
}
.form-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 28rpx;
border-bottom: 1rpx solid #f5f5f5;
&--last { border-bottom: none; }
}
.form-label {
font-size: 28rpx;
color: #555;
width: 180rpx;
flex-shrink: 0;
font-weight: 500;
}
.form-label-sub {
font-size: 20rpx;
color: #bbb;
display: block;
margin-top: 4rpx;
}
.label-group { width: 240rpx; flex-shrink: 0; }
.form-input {
flex: 1;
font-size: 28rpx;
color: #222;
text-align: right;
background: transparent;
}
.form-input--short {
width: 100rpx;
flex: none;
text-align: right;
}
/* ── Save button ─────────────────────────── */
.save-wrap { padding: 40rpx 24rpx; }
.save-btn {
width: 100%;
height: 96rpx;
border-radius: 48rpx;
background: linear-gradient(90deg, #1a1a2e, #2d2d5e);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 20rpx rgba(26,26,46,0.3);
&:active { opacity: 0.85; }
&--loading,
&--disabled {
opacity: 0.5;
box-shadow: none;
}
}
.save-btn-text {
font-size: 32rpx;
font-weight: 700;
color: #c9a87c;
letter-spacing: 2rpx;
}
</style>