petstore-frontend/src/pages/mine/Store.vue
2026-04-17 10:57:31 +08:00

463 lines
13 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>
<div class="page-shell store-page">
<view class="store-nav nav-gradient" :style="navSafeStyle">
<view class="nav-back" @click="goBackToMine"><AppIcon name="back" :size="18" color="#ffffff" /></view>
<text class="nav-title">店铺设置</text>
<view class="nav-placeholder"></view>
</view>
<div class="store-body">
<!-- 说明条填充顶部视觉交代用途 -->
<div class="store-hero">
<view class="store-hero-icon" aria-hidden="true">
<AppIcon name="store" :size="22" color="#15803d" />
</view>
<div class="store-hero-text">
<div class="store-hero-title">门店资料</div>
<div class="store-hero-desc">以下信息会用于预约展示与客户联系建议保持准确易读</div>
</div>
</div>
<div class="store-card">
<div class="store-card-head">
<span class="store-card-kicker">基础信息</span>
</div>
<div class="store-field">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="store" :size="15" color="#475569" /></span>
<text>店铺名称</text>
</div>
<input v-model="form.name" class="van-field store-input" placeholder="请输入店铺名称" />
</div>
<div class="store-field">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="phone" :size="15" color="#475569" /></span>
<text>联系电话</text>
</div>
<input v-model="form.phone" type="tel" class="van-field store-input" placeholder="请输入联系电话" maxlength="20" />
</div>
<div class="store-field">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="pin" :size="15" color="#475569" /></span>
<text>地址</text>
</div>
<div class="address-row">
<input v-model="form.address" class="van-field store-input address-input" placeholder="输入地址或点右侧选点" />
<button type="button" class="pick-location-btn" :disabled="pickingLocation" @click="pickAddress">
地图选点
</button>
</div>
<div v-if="hasMapPin" class="geo-hint">已选地图位置,可再微调上方文字后保存</div>
</div>
<div class="store-field store-field-last">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="report" :size="15" color="#475569" /></span>
<text>简介</text>
</div>
<textarea
v-model="form.intro"
class="van-field store-input intro-textarea"
placeholder="一句话介绍门店特色、服务亮点等"
/>
</div>
</div>
<div class="store-card" style="margin-top: 14px">
<div class="store-card-head">
<span class="store-card-kicker">预约放号</span>
</div>
<div class="store-field">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="pin" :size="15" color="#475569" /></span>
<text>每日首个号源</text>
</div>
<picker mode="selector" :range="HALF_HOUR_TIME_SLOTS" :value="bookingStartIdx" @change="onBookingStartChange">
<view class="app-form-input app-form-picker store-picker-like">
<text>{{ form.bookingDayStart || '请选择' }}</text>
<text class="app-form-arrow"></text>
</view>
</picker>
</div>
<div class="store-field store-field-last">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="pin" :size="15" color="#475569" /></span>
<text>每日最后一个号源</text>
</div>
<picker mode="selector" :range="HALF_HOUR_TIME_SLOTS" :value="bookingLastIdx" @change="onBookingLastChange">
<view class="app-form-input app-form-picker store-picker-like">
<text>{{ form.bookingLastSlotStart || '请选择' }}</text>
<text class="app-form-arrow"></text>
</view>
</picker>
<div class="booking-hint">半小时一档;末号须不早于首号。保存后对新建预约生效。</div>
</div>
</div>
<div class="store-actions">
<button
class="van-button van-button--primary van-button--block store-save-btn"
:loading="saving"
@click="saveStore"
>
保存设置
</button>
<div class="store-actions-hint">保存后立即生效,并写入本地缓存。</div>
</div>
<div class="store-footer-tip">宠伴生活馆 · 商家版</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import AppIcon from '../../components/AppIcon.vue'
import { navigateTo } from '../../utils/globalState.js'
import { updateStore, getStoreDetail } from '../../api/index.js'
import { HALF_HOUR_TIME_SLOTS, halfHourPickerIndex } from '../../utils/halfHourTime.js'
import { getStoreSession, setStoreSession } from '../../utils/session.js'
defineEmits(['change-page'])
const storeInfo = getStoreSession()
const goBackToMine = () => {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
return
}
navigateTo('mine')
}
const navSafeStyle = (() => {
const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20
let navHeight = statusBarHeight + 44
const menuRect = uni.getMenuButtonBoundingClientRect?.()
if (menuRect && menuRect.top && menuRect.height) {
const verticalGap = Math.max(menuRect.top - statusBarHeight, 4)
navHeight = statusBarHeight + verticalGap * 2 + menuRect.height
}
return `padding-top:${statusBarHeight}px;height:${navHeight}px;`
})()
const saving = ref(false)
const pickingLocation = ref(false)
const form = ref({
name: '',
phone: '',
address: '',
intro: '',
latitude: null,
longitude: null,
bookingDayStart: '09:00',
bookingLastSlotStart: '21:30'
})
const toHHmm = (v) => {
if (v == null || v === '') return ''
if (typeof v === 'string') return v.length >= 5 ? v.slice(0, 5) : v
if (Array.isArray(v) && v.length >= 2) {
return `${String(v[0]).padStart(2, '0')}:${String(v[1]).padStart(2, '0')}`
}
return ''
}
const bookingStartIdx = computed(() => halfHourPickerIndex(form.value.bookingDayStart))
const bookingLastIdx = computed(() => halfHourPickerIndex(form.value.bookingLastSlotStart))
const onBookingStartChange = (e) => {
const i = Number(e.detail.value)
form.value.bookingDayStart = HALF_HOUR_TIME_SLOTS[i] || '09:00'
}
const onBookingLastChange = (e) => {
const i = Number(e.detail.value)
form.value.bookingLastSlotStart = HALF_HOUR_TIME_SLOTS[i] || '21:30'
}
const hasMapPin = computed(() => form.value.latitude != null && form.value.longitude != null)
const toCoord = (v) => {
if (v == null || v === '') return null
const n = typeof v === 'number' ? v : parseFloat(v)
return Number.isFinite(n) ? n : null
}
const pickAddress = () => {
pickingLocation.value = true
uni.chooseLocation({
success: (res) => {
const name = res.name || ''
const addr = res.address || ''
form.value.address = [name, addr].filter(Boolean).join(' · ') || addr || name
form.value.latitude = toCoord(res.latitude)
form.value.longitude = toCoord(res.longitude)
},
fail: (err) => {
const msg = (err && err.errMsg) || ''
if (/cancel|取消/i.test(msg)) return
uni.showToast({ title: '无法打开选点,请手动填写', icon: 'none' })
},
complete: () => {
pickingLocation.value = false
}
})
}
const saveStore = async () => {
const i0 = HALF_HOUR_TIME_SLOTS.indexOf(form.value.bookingDayStart)
const i1 = HALF_HOUR_TIME_SLOTS.indexOf(form.value.bookingLastSlotStart)
if (i0 < 0 || i1 < 0 || i1 < i0) {
uni.showToast({ title: '末号须不早于首号', icon: 'none' })
return
}
saving.value = true
const res = await updateStore({ id: storeInfo.id, ...form.value })
saving.value = false
if (res.code === 200) {
uni.showToast({ title: '保存成功', icon: 'success' })
const merged = res.data ? { ...storeInfo, ...res.data } : { ...storeInfo, ...form.value }
setStoreSession(merged)
} else {
uni.showToast({ title: res.message || '保存失败', icon: 'none' })
}
}
onMounted(async () => {
const base = {
name: storeInfo.name || '',
phone: storeInfo.phone || '',
address: storeInfo.address || '',
intro: storeInfo.intro || '',
latitude: toCoord(storeInfo.latitude),
longitude: toCoord(storeInfo.longitude),
bookingDayStart: toHHmm(storeInfo.bookingDayStart) || '09:00',
bookingLastSlotStart: toHHmm(storeInfo.bookingLastSlotStart) || '21:30'
}
if (storeInfo.id) {
const res = await getStoreDetail(storeInfo.id)
if (res.code === 200 && res.data) {
const d = res.data
base.bookingDayStart = toHHmm(d.bookingDayStart) || base.bookingDayStart
base.bookingLastSlotStart = toHHmm(d.bookingLastSlotStart) || base.bookingLastSlotStart
if (d.name != null) base.name = d.name
if (d.phone != null) base.phone = d.phone
if (d.address != null) base.address = d.address
if (d.intro != null) base.intro = d.intro
if (d.latitude != null) base.latitude = toCoord(d.latitude)
if (d.longitude != null) base.longitude = toCoord(d.longitude)
}
}
form.value = base
})
</script>
<style scoped>
.store-page {
min-height: 100vh;
background: var(--pet-bg, #f5f7fb);
padding-bottom: 120rpx;
}
.nav-placeholder {
width: 32px;
}
.store-nav {
padding: 14px 16px 12px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 10;
}
.nav-back {
font-size: 20px;
color: #fff;
}
.nav-title {
font-size: 18px;
font-weight: 700;
color: #fff;
}
.store-body {
padding: 12px 14px 28px;
}
.store-hero {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 16px 14px;
border-radius: 16px;
background: linear-gradient(135deg, #ffffff 0%, #ecfdf3 55%, #f0fdf4 100%);
border: 1px solid #d8f0e2;
box-shadow: var(--pet-card-shadow, 0 8px 24px rgba(15, 23, 42, 0.06));
margin-bottom: 14px;
}
.store-hero-icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid #bbf7d0;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.store-hero-title {
font-size: 17px;
font-weight: 700;
color: #14532d;
letter-spacing: 0.02em;
}
.store-hero-desc {
margin-top: 6px;
font-size: 13px;
line-height: 1.55;
color: #4b5563;
}
.store-card {
background: #fff;
border: 1px solid #e6ecf4;
border-radius: 16px;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.06);
padding: 4px 4px 8px;
overflow: hidden;
}
.store-card-head {
padding: 14px 14px 6px;
}
.store-card-kicker {
font-size: 12px;
font-weight: 700;
color: #94a3b8;
letter-spacing: 0.04em;
}
.store-field {
padding: 12px 12px 4px;
}
.store-field + .store-field {
border-top: 1px solid #f1f5f9;
}
.store-field-label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #374151;
}
.store-ico {
width: 28px;
height: 28px;
border-radius: 8px;
background: #f1f5f9;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 输入区:略抬高层级,避免贴边过紧 */
.store-input {
margin-top: 0 !important;
}
.address-row {
display: flex;
align-items: stretch;
gap: 10px;
}
.address-input {
flex: 1;
min-width: 0;
}
.pick-location-btn {
flex-shrink: 0;
padding: 0 14px;
min-height: 44px;
border-radius: 12px;
border: 1px solid #2db96d;
background: #f0fdf4;
color: #15803d;
font-size: 14px;
font-weight: 600;
line-height: 44px;
-webkit-tap-highlight-color: transparent;
}
.pick-location-btn:active:not([disabled]) {
opacity: 0.92;
transform: translateY(1px);
}
.pick-location-btn[disabled] {
opacity: 0.55;
}
.geo-hint {
margin-top: 8px;
font-size: 12px;
color: #2db96d;
line-height: 1.4;
}
.intro-textarea {
min-height: 112px !important;
line-height: 1.55 !important;
padding-top: 12px !important;
padding-bottom: 12px !important;
resize: none;
}
.store-field-last {
padding-bottom: 14px;
}
.store-actions {
margin-top: 18px;
}
.store-save-btn {
margin-top: 0 !important;
box-shadow: 0 10px 26px rgba(34, 197, 94, 0.35) !important;
}
.store-actions-hint {
margin-top: 10px;
text-align: center;
font-size: 12px;
color: #94a3b8;
line-height: 1.45;
}
.store-footer-tip {
margin-top: 28px;
text-align: center;
font-size: 12px;
color: #cbd5e1;
}
.store-picker-like {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 44px;
padding: 0 12px;
border-radius: 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
font-size: 15px;
color: #111827;
}
.booking-hint {
margin-top: 8px;
font-size: 12px;
color: #94a3b8;
line-height: 1.45;
}
</style>