463 lines
13 KiB
Vue
463 lines
13 KiB
Vue
<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>
|