<template>
|
<view class="page">
|
<scroll-view class="page-scroll" scroll-y="true" :show-scrollbar="false">
|
<view class="hero">
|
<image class="hero-image" src="/static/merchant-header-hampton.jpg" mode="aspectFill"></image>
|
<view class="hero-mask"></view>
|
<view class="hero-nav"><view class="hero-back" @tap="goBack"><image class="hero-back-icon" src="/static/icons/lucide/back-left-white.svg" mode="aspectFit"></image></view></view>
|
<view class="hero-title-wrap"><text class="hero-title">营业时间</text><text class="hero-subtitle">编辑门店营业时间段</text></view>
|
</view>
|
|
<view class="content">
|
<page-feedback v-if="loadState != 'success'" :state="loadState" loading-text="营业时间加载中" error-text="营业时间加载失败,点击重试" :trace-id="traceId" @retry="loadBusinessHours"></page-feedback>
|
<view v-else>
|
<view class="hours-card">
|
<view class="card-heading">
|
<view class="heading-left"><view class="clock-wrap"><image class="clock-icon" src="/static/icons/lucide/clock-3.svg" mode="aspectFit"></image></view><view><text class="heading-title">营业时间段</text><text class="heading-desc">支持添加多个营业时间段</text></view></view>
|
<view class="count-tag"><text>已设置 {{businessHours.length}} 段</text></view>
|
</view>
|
|
<view class="hours-list">
|
<view v-for="(item, index) in businessHours" :key="index" class="hours-row">
|
<view class="minus-button" @tap="removeHour(index)"><text>−</text></view>
|
<text class="hours-text" @tap="showEditSheet(index)">{{item.start}}-{{item.end}}</text>
|
<view class="period-tag"><text>时段 {{index + 1}}</text></view>
|
</view>
|
<view class="add-hour" @tap="showAddSheet"><text class="add-plus">+</text><text>添加营业时间段</text></view>
|
</view>
|
</view>
|
<view class="save-button" :class="saving ? 'save-button-disabled' : ''" @tap="saveHours"><text>{{saving ? '保存中' : '保存'}}</text></view>
|
<view class="bottom-space"></view>
|
</view>
|
</view>
|
</scroll-view>
|
|
<view v-if="addSheetVisible" class="sheet-layer">
|
<view class="sheet-mask" @tap="closeAddSheet"></view>
|
<view class="time-sheet">
|
<view class="sheet-handle"></view>
|
<view class="sheet-header"><text class="sheet-title">{{editingHourIndex >= 0 ? '编辑营业时间段' : '添加营业时间段'}}</text><view class="close-sheet" @tap="closeAddSheet"><text>×</text></view></view>
|
<view class="time-fields">
|
<view class="time-field" :class="activeTimeField == 'start' ? 'active' : ''" @tap="selectTimeField('start')"><text class="time-label">开始时间</text><text class="time-value">{{startTime}}</text></view>
|
<view class="time-field" :class="activeTimeField == 'end' ? 'active' : ''" @tap="selectTimeField('end')"><text class="time-label">结束时间</text><text class="time-value">{{endTime}}</text></view>
|
</view>
|
<picker-view class="time-picker" :value="timePickerIndexes" :indicator-style="pickerIndicatorStyle" :mask-style="pickerMaskStyle" @change="onTimePickerChange">
|
<picker-view-column><view v-for="(hour, index) in hours" :key="hour" class="picker-item" :class="timePickerIndexes[0] == index ? 'selected' : ''">{{hour}} 时</view></picker-view-column>
|
<picker-view-column><view v-for="(minute, index) in minutes" :key="minute" class="picker-item" :class="timePickerIndexes[1] == index ? 'selected' : ''">{{minute}} 分</view></picker-view-column>
|
</picker-view>
|
<view class="sheet-actions"><view class="sheet-cancel" @tap="closeAddSheet"><text>取消</text></view><view class="sheet-confirm" @tap="confirmAddHour"><text>{{editingHourIndex >= 0 ? '确认修改' : '确认添加'}}</text></view></view>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { apiEnvironment, useMerchantManagementDemoDataInDevelopment } from '../../config/api.uts'
|
import { hasActiveSession } from '../../services/session.uts'
|
import { MerchantBusinessHoursPeriod, loadDemoMerchantBusinessHours, loadMerchantBusinessHours, updateDemoMerchantBusinessHours, updateMerchantBusinessHours } from '../../services/merchant-management.uts'
|
|
const useDemoData = apiEnvironment == 'development' && useMerchantManagementDemoDataInDevelopment
|
const businessHours = ref<Array<MerchantBusinessHoursPeriod>>([])
|
const loadState = ref('loading')
|
const traceId = ref('')
|
const saving = ref(false)
|
let cancelHoursRequest: (() => void) | null = null
|
const addSheetVisible = ref(false)
|
const editingHourIndex = ref(-1)
|
const activeTimeField = ref('start')
|
const startTime = ref('08:00')
|
const endTime = ref('12:00')
|
const hours = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']
|
const minutes = ['00', '15', '30', '45']
|
const timePickerIndexes = ref([8, 0])
|
const pickerIndicatorStyle = 'height: 42px; background-color: rgba(255, 243, 235, 0.58); border-top: 1px solid #FFD9C9; border-bottom: 1px solid #FFD9C9;'
|
const pickerMaskStyle = 'background: transparent;'
|
|
const goBack = () => {
|
uni.navigateBack()
|
}
|
|
const loadBusinessHours = () => {
|
if (cancelHoursRequest != null) {
|
cancelHoursRequest()
|
cancelHoursRequest = null
|
}
|
loadState.value = 'loading'
|
traceId.value = ''
|
if (useDemoData) {
|
businessHours.value = loadDemoMerchantBusinessHours()
|
loadState.value = 'success'
|
return
|
}
|
if (!hasActiveSession()) {
|
loadState.value = 'error'
|
return
|
}
|
cancelHoursRequest = loadMerchantBusinessHours((periods: Array<MerchantBusinessHoursPeriod>, currentTraceId: string) => {
|
cancelHoursRequest = null
|
businessHours.value = periods
|
traceId.value = currentTraceId
|
loadState.value = 'success'
|
}, (error: any) => {
|
cancelHoursRequest = null
|
traceId.value = error.traceId != null ? error.traceId : ''
|
loadState.value = 'error'
|
})
|
}
|
|
const setPickerTime = (time: string) => {
|
const parts = time.split(':')
|
const hourIndex = hours.indexOf(parts[0])
|
const minuteIndex = minutes.indexOf(parts[1])
|
timePickerIndexes.value = [hourIndex >= 0 ? hourIndex : 8, minuteIndex >= 0 ? minuteIndex : 0]
|
}
|
|
const showAddSheet = () => {
|
if (businessHours.value.length >= 6) {
|
uni.showToast({ title: '最多可添加 6 个营业时段', icon: 'none' })
|
return
|
}
|
activeTimeField.value = 'start'
|
editingHourIndex.value = -1
|
startTime.value = '08:00'
|
endTime.value = '12:00'
|
setPickerTime(startTime.value)
|
addSheetVisible.value = true
|
}
|
|
const showEditSheet = (index: number) => {
|
const period = businessHours.value[index]
|
if (period == null) return
|
editingHourIndex.value = index
|
activeTimeField.value = 'start'
|
startTime.value = period.start
|
endTime.value = period.end
|
setPickerTime(startTime.value)
|
addSheetVisible.value = true
|
}
|
|
const closeAddSheet = () => {
|
addSheetVisible.value = false
|
editingHourIndex.value = -1
|
}
|
|
const selectTimeField = (field: string) => {
|
activeTimeField.value = field
|
setPickerTime(field == 'start' ? startTime.value : endTime.value)
|
}
|
|
const onTimePickerChange = (event: any) => {
|
const indexes = event.detail.value as Array<number>
|
timePickerIndexes.value = indexes
|
const value = hours[indexes[0]] + ':' + minutes[indexes[1]]
|
if (activeTimeField.value == 'start') {
|
startTime.value = value
|
} else {
|
endTime.value = value
|
}
|
}
|
|
const getTimeMinutes = (time: string) => {
|
const parts = time.split(':')
|
return parseInt(parts[0]) * 60 + parseInt(parts[1])
|
}
|
|
const confirmAddHour = () => {
|
const startMinutes = getTimeMinutes(startTime.value)
|
const endMinutes = getTimeMinutes(endTime.value)
|
if (startMinutes >= endMinutes) {
|
uni.showToast({ title: '结束时间需晚于开始时间', icon: 'none' })
|
return
|
}
|
const overlapHour = businessHours.value.find((item, index) => {
|
return index != editingHourIndex.value && startMinutes < getTimeMinutes(item.end) && endMinutes > getTimeMinutes(item.start)
|
})
|
if (overlapHour != null) {
|
uni.showToast({ title: '时间段不能与已有时段重叠', icon: 'none' })
|
return
|
}
|
if (editingHourIndex.value >= 0) {
|
businessHours.value.splice(editingHourIndex.value, 1, { start: startTime.value, end: endTime.value })
|
} else {
|
businessHours.value.push({ start: startTime.value, end: endTime.value })
|
}
|
addSheetVisible.value = false
|
}
|
|
const removeHour = (index: number) => {
|
if (businessHours.value.length <= 1) {
|
uni.showToast({ title: '请至少保留一个营业时段', icon: 'none' })
|
return
|
}
|
businessHours.value.splice(index, 1)
|
}
|
|
const saveHours = () => {
|
if (saving.value || loadState.value != 'success') return
|
saving.value = true
|
const finish = (periods: Array<MerchantBusinessHoursPeriod>) => {
|
businessHours.value = periods
|
saving.value = false
|
uni.showToast({ title: '营业时间已保存', icon: 'success' })
|
}
|
if (useDemoData) {
|
finish(updateDemoMerchantBusinessHours(businessHours.value))
|
return
|
}
|
updateMerchantBusinessHours(businessHours.value, finish, (error: any) => {
|
saving.value = false
|
uni.showToast({ title: error.message != null ? error.message : '营业时间保存失败', icon: 'none' })
|
})
|
}
|
|
onLoad(() => {
|
loadBusinessHours()
|
})
|
</script>
|
|
<style>
|
.page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; background-color: #F8F7F4; }
|
.page-scroll { flex: 1; height: 0; background-color: #F8F7F4; }
|
.hero { height: 382rpx; position: relative; overflow: hidden; }
|
.hero-image { position: absolute; top: 0; left: 0; width: 100%; height: 382rpx; }
|
.hero-mask { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: linear-gradient(180deg, rgba(27, 25, 23, 0.22) 0%, rgba(27, 25, 23, 0.58) 100%); }
|
.hero-nav { display: flex; height: 154rpx; padding: 82rpx 38rpx 0; position: relative; box-sizing: border-box; }
|
.hero-back { display: flex; width: 58rpx; height: 58rpx; align-items: center; justify-content: center; }
|
.hero-back-icon { width: 34rpx; height: 34rpx; }
|
.hero-title-wrap { display: flex; flex-direction: column; align-items: center; position: relative; margin-top: 50rpx; }
|
.hero-title { color: #FFFFFF; font-size: 42rpx; line-height: 59rpx; font-weight: 700; }
|
.hero-subtitle { color: rgba(255, 255, 255, 0.90); font-size: 25rpx; line-height: 35rpx; margin-top: 4rpx; }
|
|
.content { min-height: 1000rpx; margin-top: -52rpx; padding: 34rpx 36rpx 0; border-radius: 44rpx 44rpx 0 0; background-color: #F8F7F4; position: relative; box-sizing: border-box; }
|
.hours-card { padding: 38rpx 30rpx 30rpx; border-radius: 34rpx; background-color: #FFFFFF; box-shadow: 0 18rpx 38rpx rgba(51, 49, 45, 0.06); }
|
.card-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
|
.heading-left { display: flex; flex-direction: row; align-items: center; }
|
.clock-wrap { display: flex; width: 62rpx; height: 62rpx; margin-right: 18rpx; justify-content: center; align-items: center; border-radius: 31rpx; background-color: #FFF1E9; }
|
.clock-icon { width: 32rpx; height: 32rpx; }
|
.heading-title { display: block; color: #202B3E; font-size: 30rpx; line-height: 42rpx; font-weight: 700; }
|
.heading-desc { display: block; color: #9AA5B4; font-size: 22rpx; line-height: 31rpx; margin-top: 3rpx; }
|
.count-tag { padding: 10rpx 16rpx; border-radius: 24rpx; color: #FF624C; font-size: 22rpx; line-height: 31rpx; font-weight: 600; background-color: #FFF2EC; }
|
.hours-list { padding: 7rpx 22rpx 24rpx; margin-top: 30rpx; border-radius: 30rpx; background-color: #F8F7F4; }
|
.hours-row { display: flex; min-height: 88rpx; flex-direction: row; align-items: center; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #EBE7E0; }
|
.minus-button { display: flex; width: 34rpx; height: 34rpx; margin-right: 20rpx; justify-content: center; align-items: center; border-radius: 17rpx; color: #FFFFFF; font-size: 27rpx; line-height: 32rpx; font-weight: 700; background-color: #1D2939; }
|
.hours-text { flex: 1; color: #202B3E; font-size: 30rpx; line-height: 42rpx; font-weight: 700; }
|
.period-tag { padding: 9rpx 17rpx; border-radius: 23rpx; color: #657184; font-size: 22rpx; line-height: 31rpx; background-color: #FFFFFF; }
|
.add-hour { display: flex; height: 72rpx; padding: 0 24rpx; margin: 26rpx auto 0; justify-content: center; align-items: center; border-width: 1rpx; border-style: solid; border-color: #FFD1BD; border-radius: 36rpx; color: #FF5A42; font-size: 25rpx; line-height: 35rpx; font-weight: 600; background-color: #FFFCF9; box-sizing: border-box; }
|
.add-plus { font-size: 31rpx; line-height: 35rpx; margin-right: 8rpx; font-weight: 400; }
|
.save-button { display: flex; height: 104rpx; margin-top: 30rpx; justify-content: center; align-items: center; border-radius: 52rpx; color: #FFFFFF; font-size: 32rpx; line-height: 45rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF6A22 0%, #FF8F20 100%); box-shadow: 0 16rpx 28rpx rgba(255, 110, 35, 0.18); }
|
.save-button-disabled { opacity: 0.62; }
|
.bottom-space { height: 110rpx; }
|
|
.sheet-layer { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 30; }
|
.sheet-mask { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(25, 31, 42, 0.42); animation: mask-fade-in 180ms ease-out both; }
|
.time-sheet { padding: 18rpx 34rpx 42rpx; border-radius: 42rpx 42rpx 0 0; background-color: #FFFFFF; box-sizing: border-box; position: absolute; right: 0; bottom: 0; left: 0; animation: sheet-slide-up 260ms cubic-bezier(0.22, 0.8, 0.28, 1) both; }
|
.sheet-handle { width: 64rpx; height: 7rpx; margin: 0 auto; border-radius: 5rpx; background-color: #D8DEE7; }
|
.sheet-header { display: flex; height: 86rpx; flex-direction: row; align-items: center; justify-content: center; position: relative; }
|
.sheet-title { color: #202B3E; font-size: 32rpx; line-height: 45rpx; font-weight: 700; }
|
.close-sheet { display: flex; width: 58rpx; height: 58rpx; justify-content: center; align-items: center; color: #95A0AF; font-size: 45rpx; line-height: 58rpx; position: absolute; right: 0; top: 14rpx; }
|
.time-fields { display: flex; height: 102rpx; flex-direction: row; border-radius: 22rpx; background-color: #F7F8FA; }
|
.time-field { display: flex; flex: 1; height: 102rpx; padding: 16rpx 22rpx; flex-direction: column; justify-content: center; border-radius: 22rpx; box-sizing: border-box; }
|
.time-field.active { background-color: #FFF1EA; }
|
.time-label { color: #8B97A8; font-size: 22rpx; line-height: 30rpx; }
|
.time-value { color: #273345; font-size: 30rpx; line-height: 40rpx; margin-top: 4rpx; font-weight: 700; }
|
.time-field.active .time-label, .time-field.active .time-value { color: #FF684B; }
|
.time-picker { width: 100%; height: 352rpx; margin-top: 18rpx; }
|
.picker-item { height: 84rpx; color: #A6AFBB; font-size: 28rpx; line-height: 84rpx; text-align: center; }
|
.picker-item.selected { color: #263245; font-weight: 700; }
|
.sheet-actions { display: flex; height: 86rpx; margin-top: 20rpx; flex-direction: row; }
|
.sheet-cancel, .sheet-confirm { display: flex; justify-content: center; align-items: center; border-radius: 43rpx; font-size: 29rpx; line-height: 41rpx; font-weight: 700; }
|
.sheet-cancel { width: 190rpx; margin-right: 20rpx; color: #657184; background-color: #F1F3F6; }
|
.sheet-confirm { flex: 1; color: #FFFFFF; background-image: linear-gradient(90deg, #FF7C22 0%, #FF513C 100%); }
|
@keyframes mask-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
@keyframes sheet-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
</style>
|