| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905 |
- <template>
- <!-- 预约兽医:见 doc/app/预约服务/预约服务接口说明.md -->
- <view :class="pageRootClass" class="tab-page bv-page" :style="pageStyle">
- <scroll-view scroll-y class="bv-scroll" enable-back-to-top :show-scrollbar="false">
- <!-- 上:兽医信息 -->
- <view class="bv-card bv-card--top">
- <view class="bv-hero">
- <up-avatar
- v-if="photoSrc"
- shape="circle"
- :src="photoSrc"
- size="72"
- font-size="26"
- />
- <up-avatar
- v-else
- shape="circle"
- :text="avatarText"
- size="72"
- font-size="26"
- bg-color="#9ca3af"
- color="#ffffff"
- />
- <view class="bv-hero__right">
- <text class="bv-hero__title text-body">{{ vetTitle }}</text>
- <text class="bv-hero__sub text-body">{{ vetSub }}</text>
- <text class="bv-hero__contact text-body">{{ contactLine }}</text>
- </view>
- </view>
- <text class="bv-line text-body"><text class="bv-line__k">{{ $t('bookingVetPage.labelIntro') }}</text>{{ introBody }}</text>
- <text class="bv-line text-body"><text class="bv-line__k">{{ $t('bookingVetPage.labelAddress') }}</text>{{ detailAddress }}</text>
- <text class="bv-line text-body"><text class="bv-line__k">{{ $t('bookingVetPage.labelHours') }}</text>{{ serviceHours }}</text>
- <text class="bv-line text-body"><text class="bv-line__k">{{ $t('bookingVetPage.labelArea') }}</text>{{ serviceArea }}</text>
- <text class="bv-line text-body"><text class="bv-line__k">{{ $t('bookingVetPage.labelFee') }}</text>{{ feeLine }}</text>
- </view>
- <!-- 下:选择时间 + 预约 -->
- <view class="bv-card bv-card--bottom">
- <text class="bv-section-title text-body">{{ $t('bookingVetPage.pickTimeTitle') }}</text>
- <scroll-view scroll-x class="bv-date-scroll" :show-scrollbar="false" enable-flex>
- <view class="bv-date-row">
- <view
- v-for="chip in dateChips"
- :key="chip.key"
- class="bv-chip"
- :class="{
- 'bv-chip--on': !chip.weekdayDisabled && selectedDateKey === chip.key,
- 'bv-chip--disabled': chip.weekdayDisabled
- }"
- role="button"
- @tap="onDateChipTap(chip)"
- >
- <view class="bv-chip__stack">
- <text class="bv-chip__name text-body">{{ chip.weekdayLabel }}</text>
- <text v-if="!chip.weekdayDisabled" class="bv-chip__date text-body">{{ chip.dateShort }}</text>
- <text v-else class="bv-chip__full text-body">{{ $t('bookingVetPage.dayNotAvailable') }}</text>
- </view>
- </view>
- </view>
- </scroll-view>
- <up-divider :hairline="true" line-color="#e5e7eb" margin="12rpx 0 8rpx" />
- <up-button
- type="success"
- shape="circle"
- :disabled="bookBtnDisabled"
- :text="bookBtnText"
- @click="openBookPopup"
- />
- </view>
- <view class="bv-footer-spacer" />
- </scroll-view>
- <!-- 预约信息弹窗 -->
- <up-popup
- :show="bookPopupShow"
- mode="center"
- :round="16"
- :close-on-click-overlay="true"
- :safe-area-inset-bottom="true"
- :custom-style="bookPopupStyle"
- @update:show="bookPopupShow = $event"
- >
- <view class="bv-popup">
- <scroll-view scroll-y class="bv-popup__scroll" :show-scrollbar="false">
- <text class="bv-popup__h1 text-body">{{ $t('bookingVetPage.popupBookTitle') }}</text>
- <text class="bv-popup__row text-body">{{ $t('bookingVetPage.popupVetName') }}{{ vetTitle }}</text>
- <text class="bv-popup__row text-body">{{ $t('bookingVetPage.popupBookDate') }}{{ popupDateLine }}</text>
- <text class="bv-popup__h2 text-body">{{ $t('bookingVetPage.popupServiceTitle') }}</text>
- <up-form
- ref="bookFormRef"
- label-position="top"
- :model="formModel"
- :rules="formRules"
- label-width="100%"
- :border-bottom="false"
- error-type="toast"
- >
- <up-form-item :label="$t('bookingVetPage.formBooker')" prop="bookerName" required>
- <up-input v-model="formModel.bookerName" border="surround" clearable />
- </up-form-item>
- <up-form-item :label="$t('bookingVetPage.formPhone')" prop="phone" required>
- <up-input v-model="formModel.phone" type="number" maxlength="11" border="surround" clearable />
- </up-form-item>
- <up-form-item :label="$t('bookingVetPage.formSlot')" prop="timeSlot" required>
- <view class="bv-slot-row" role="button" @tap="openTimePicker">
- <up-input
- v-model="formModel.timeSlot"
- readonly
- border="surround"
- :placeholder="$t('bookingVetPage.formSlotPh')"
- />
- <up-icon name="arrow-right" color="#78716c" :size="18" />
- </view>
- </up-form-item>
- <up-form-item :label="$t('bookingVetPage.formAddress')" prop="address" required>
- <up-textarea
- v-model="formModel.address"
- height="120"
- count
- maxlength="200"
- border="surround"
- :placeholder="$t('bookingVetPage.formAddressPh')"
- />
- </up-form-item>
- <up-form-item :label="$t('bookingVetPage.formNeed')" prop="needDesc" required>
- <up-textarea
- v-model="formModel.needDesc"
- height="120"
- count
- maxlength="300"
- border="surround"
- :placeholder="$t('bookingVetPage.formNeedPh')"
- />
- </up-form-item>
- </up-form>
- </scroll-view>
- <view class="bv-popup__actions">
- <up-button
- class="bv-popup__btn bv-popup__btn--cancel"
- :text="$t('bookingVetPage.btnCancel')"
- shape="circle"
- plain
- hairline
- @click="closeBookPopup"
- />
- <up-button
- class="bv-popup__btn"
- type="success"
- :loading="submitting"
- :text="submitting ? $t('bookingVetPage.submitting') : $t('bookingVetPage.btnSubmit')"
- shape="circle"
- @click="onSubmitBook"
- />
- </view>
- </view>
- </up-popup>
- <up-picker
- :show="timePickerShow"
- :columns="[timeSlotOptions]"
- v-model="pickerModel"
- :show-toolbar="true"
- :title="$t('bookingVetPage.formSlot')"
- popup-mode="bottom"
- :close-on-click-overlay="true"
- :round="12"
- @update:show="timePickerShow = $event"
- @confirm="onTimePickerConfirm"
- />
- </view>
- </template>
- <script>
- import UAvatar from 'uview-plus/components/u-avatar/u-avatar.vue'
- import UButton from 'uview-plus/components/u-button/u-button.vue'
- import UDivider from 'uview-plus/components/u-divider/u-divider.vue'
- import UForm from 'uview-plus/components/u-form/u-form.vue'
- import UFormItem from 'uview-plus/components/u-form-item/u-form-item.vue'
- import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
- import UInput from 'uview-plus/components/u-input/u-input.vue'
- import UPicker from 'uview-plus/components/u-picker/u-picker.vue'
- import UPopup from 'uview-plus/components/u-popup/u-popup.vue'
- import UTextarea from 'uview-plus/components/u-textarea/u-textarea.vue'
- import tabPage from '@/mixins/tabPage'
- import pageViewport from '@/mixins/pageViewport'
- import { resolveResourceUrl } from '@/utils/resourceUrl'
- import { ensureApiToken } from '@/utils/apiAuth'
- import { useUserStore } from '@/store/user'
- import {
- BOOKING_PROVIDER_TYPE,
- checkAppointmentBooked,
- listBookingDates,
- loadBookingResourceCache,
- submitBookingAppointment
- } from '@/api/bookingService'
- const VET_RESOURCE_TYPE = '004001'
- function pad2(n) {
- return `${n}`.padStart(2, '0')
- }
- /** 每 2 小时一档;用于默认时段与服务时段生成 */
- const DEFAULT_TIME_SLOTS = (() => {
- const r = []
- for (let h = 0; h < 24; h += 2) {
- const start = `${pad2(h)}:00`
- const endH = h + 2
- const end = endH >= 24 ? '24:00' : `${pad2(endH)}:00`
- r.push(`${start}~${end}`)
- }
- return r
- })()
- function parseHourMinute(hm) {
- if (!hm || typeof hm !== 'string') return null
- const parts = hm.trim().split(':')
- if (parts.length < 2) return null
- const h = parseInt(parts[0], 10)
- const m = parseInt(parts[1], 10)
- if (!Number.isFinite(h) || !Number.isFinite(m)) return null
- return { h, m }
- }
- function buildTimeSlotsFromService(start, end, stepHours = 2) {
- const s = parseHourMinute(start)
- const e = parseHourMinute(end)
- if (!s || !e || e.h <= s.h) {
- return DEFAULT_TIME_SLOTS.slice()
- }
- const slots = []
- for (let h = s.h; h < e.h; h += stepHours) {
- const endH = Math.min(h + stepHours, e.h)
- if (endH <= h) break
- slots.push(`${pad2(h)}:00~${pad2(endH)}:00`)
- }
- return slots.length ? slots : DEFAULT_TIME_SLOTS.slice()
- }
- function parseServiceWeekdays(raw) {
- if (raw == null || String(raw).trim() === '') return null
- const set = new Set()
- for (const part of String(raw).split(',')) {
- const n = parseInt(part.trim(), 10)
- if (n >= 1 && n <= 7) set.add(n)
- }
- return set.size ? set : null
- }
- function slotIndexFromHour(slots) {
- const list = slots && slots.length ? slots : DEFAULT_TIME_SLOTS
- const h = new Date().getHours()
- const target = `${pad2(Math.floor(h / 2) * 2)}:00`
- const idx = list.findIndex((s) => s.startsWith(target))
- return idx >= 0 ? idx : 0
- }
- export default {
- components: {
- 'up-avatar': UAvatar,
- 'up-button': UButton,
- 'up-divider': UDivider,
- 'up-form': UForm,
- 'up-form-item': UFormItem,
- 'up-icon': UIcon,
- 'up-input': UInput,
- 'up-picker': UPicker,
- 'up-popup': UPopup,
- 'up-textarea': UTextarea
- },
- mixins: [tabPage, pageViewport],
- data() {
- const slots = DEFAULT_TIME_SLOTS.slice()
- const defaultSlot = slots[slotIndexFromHour(slots)] || slots[0]
- return {
- navTitleKey: 'bookingVetPage.navTitle',
- vetId: '',
- resourceType: VET_RESOURCE_TYPE,
- resource: null,
- apiDates: [],
- bookedDateMap: {},
- selectedDateKey: '',
- bookPopupShow: false,
- timePickerShow: false,
- submitting: false,
- pickerModel: [defaultSlot],
- formModel: {
- bookerName: '',
- phone: '',
- timeSlot: defaultSlot,
- address: '',
- needDesc: ''
- },
- bookPopupStyle: {
- width: '90%',
- maxWidth: '680px',
- maxHeight: '78vh',
- overflow: 'hidden',
- display: 'flex',
- flexDirection: 'column'
- }
- }
- },
- computed: {
- allowedWeekdays() {
- return parseServiceWeekdays(this.resource && this.resource.serviceWeekdays)
- },
- timeSlotOptions() {
- const r = this.resource || {}
- return buildTimeSlotsFromService(r.serviceStartTime, r.serviceEndTime)
- },
- dateChips() {
- return (this.apiDates || []).map((d) => {
- const key = d.appointDate || ''
- const weekday = d.weekday
- const allowed = this.allowedWeekdays
- const weekdayDisabled =
- allowed != null && weekday != null && !allowed.has(Number(weekday))
- const booked = !!this.bookedDateMap[key]
- return {
- key,
- dateShort: d.dateMmDd || '',
- weekday,
- weekdayLabel: this.weekdayLabelFor(d),
- booked,
- weekdayDisabled
- }
- })
- },
- selectedDateBooked() {
- return !!this.bookedDateMap[this.selectedDateKey]
- },
- bookBtnDisabled() {
- const chip = this.dateChips.find((c) => c.key === this.selectedDateKey)
- if (!chip || chip.weekdayDisabled) return true
- return this.selectedDateBooked
- },
- bookBtnText() {
- if (this.selectedDateBooked) return this.$t('bookingVetPage.btnBooked')
- return this.$t('bookingVetPage.btnBook')
- },
- photoSrc() {
- const url = this.resource && this.resource.photoFileUrl
- return url ? resolveResourceUrl(url) : ''
- },
- avatarText() {
- const name = (this.resource && this.resource.resourceName) || ''
- return name.slice(0, 1) || '?'
- },
- vetTitle() {
- return (this.resource && this.resource.resourceName) || this.$t('bookingVetPage.noResource')
- },
- vetSub() {
- return (this.resource && this.resource.affiliatedUnit) || this.$t('bookingServicePage.noUnit')
- },
- introBody() {
- const intro = (this.resource && this.resource.introduction) || ''
- return intro.trim() || this.$t('bookingVetPage.noIntro')
- },
- contactLine() {
- const phone = this.resource && this.resource.contactPhone
- if (!phone) return this.$t('bookingVetPage.contactLabel') + '—'
- return this.$t('bookingVetPage.contactLabel') + phone
- },
- detailAddress() {
- const addr = (this.resource && this.resource.detailAddress) || ''
- return addr.trim() || this.$t('bookingVetPage.noAddress')
- },
- serviceHours() {
- const start = this.resource && this.resource.serviceStartTime
- const end = this.resource && this.resource.serviceEndTime
- if (start && end) {
- return this.$t('bookingVetPage.hoursTpl', { start, end })
- }
- return this.$t('bookingVetPage.noHours')
- },
- serviceArea() {
- const area = (this.resource && this.resource.serviceArea) || ''
- return area.trim() || this.$t('bookingVetPage.noArea')
- },
- feeLine() {
- const fee = this.resource && this.resource.feeStandard
- if (fee == null || fee === '') return this.$t('bookingVetPage.noFee')
- return this.$t('bookingVetPage.feeTpl', { fee })
- },
- popupDateLine() {
- const chip = this.dateChips.find((c) => c.key === this.selectedDateKey)
- if (!chip || chip.weekdayDisabled) return ''
- return this.formatMdWeekday(chip)
- },
- formRules() {
- return {
- bookerName: [
- {
- required: true,
- message: this.$t('bookingVetPage.errBooker'),
- trigger: ['blur', 'change']
- }
- ],
- phone: [
- {
- required: true,
- message: this.$t('bookingVetPage.errPhone'),
- trigger: ['blur', 'change']
- },
- {
- pattern: /^1[3-9]\d{9}$/,
- message: this.$t('bookingVetPage.errPhoneFmt'),
- trigger: ['blur', 'change']
- }
- ],
- timeSlot: [
- {
- required: true,
- message: this.$t('bookingVetPage.errSlot'),
- trigger: ['change']
- }
- ],
- address: [
- {
- required: true,
- message: this.$t('bookingVetPage.errAddress'),
- trigger: ['blur', 'change']
- }
- ],
- needDesc: [
- {
- required: true,
- message: this.$t('bookingVetPage.errNeed'),
- trigger: ['blur', 'change']
- }
- ]
- }
- }
- },
- onLoad(query) {
- if (!ensureApiToken()) return
- const q = query || {}
- this.vetId = q.id ? decodeURIComponent(String(q.id)) : ''
- if (q.resourceType) {
- try {
- this.resourceType = decodeURIComponent(String(q.resourceType))
- } catch (e) {
- this.resourceType = String(q.resourceType)
- }
- }
- this.loadResource()
- this.loadBookingDates()
- },
- methods: {
- weekdayLabelFor(d) {
- const w = d && d.weekday
- if (w != null && w >= 1 && w <= 7) {
- const dowKey = w === 7 ? 0 : w
- return this.$t(`bookingServicePage.wd${dowKey}`)
- }
- return (d && d.weekdayName) || ''
- },
- loadResource() {
- if (!this.vetId) {
- uni.showToast({ title: this.$t('bookingVetPage.noResource'), icon: 'none' })
- setTimeout(() => uni.navigateBack(), 1500)
- return
- }
- this.resource = loadBookingResourceCache(this.resourceType, this.vetId)
- if (!this.resource) {
- uni.showToast({ title: this.$t('bookingVetPage.noResource'), icon: 'none' })
- setTimeout(() => uni.navigateBack(), 1500)
- return
- }
- if (this.apiDates.length) {
- this.refreshBookedFlags()
- }
- },
- loadBookingDates() {
- listBookingDates()
- .then((res) => {
- this.apiDates = Array.isArray(res.data) ? res.data : []
- this.$nextTick(() => {
- this.ensureValidDateSelection()
- if (this.vetId) {
- this.refreshBookedFlags()
- }
- })
- })
- .catch(() => {
- this.apiDates = []
- })
- },
- refreshBookedFlags() {
- if (!this.vetId || !this.apiDates.length) return Promise.resolve()
- const providerId = Number(this.vetId) || this.vetId
- const dates = this.apiDates.map((d) => d.appointDate).filter(Boolean)
- return Promise.all(
- dates.map((appointDate) =>
- checkAppointmentBooked({
- providerType: BOOKING_PROVIDER_TYPE.VET,
- providerId,
- appointDate
- })
- .then((res) => ({
- appointDate,
- booked: !!(res.data && res.data.booked)
- }))
- .catch(() => ({ appointDate, booked: false }))
- )
- ).then((results) => {
- const next = { ...this.bookedDateMap }
- results.forEach(({ appointDate, booked }) => {
- next[appointDate] = booked
- })
- this.bookedDateMap = next
- this.$nextTick(() => this.ensureValidDateSelection())
- })
- },
- fetchDateBooked(appointDate) {
- if (!this.vetId || !appointDate) return Promise.resolve(false)
- return checkAppointmentBooked({
- providerType: BOOKING_PROVIDER_TYPE.VET,
- providerId: Number(this.vetId) || this.vetId,
- appointDate
- })
- .then((res) => {
- const booked = !!(res.data && res.data.booked)
- this.bookedDateMap = { ...this.bookedDateMap, [appointDate]: booked }
- return booked
- })
- .catch(() => false)
- },
- formatMdWeekday(chip) {
- if (!chip || !chip.key) return ''
- return `${chip.dateShort}(${chip.weekdayLabel})`
- },
- ensureValidDateSelection() {
- const cur = this.dateChips.find((c) => c.key === this.selectedDateKey && !c.weekdayDisabled)
- if (cur) return
- const ok = this.dateChips.find((c) => !c.weekdayDisabled)
- this.selectedDateKey = ok ? ok.key : ''
- },
- onDateChipTap(chip) {
- if (chip.weekdayDisabled) {
- uni.showToast({
- title: this.$t('bookingVetPage.dayNotAvailable'),
- icon: 'none'
- })
- return
- }
- this.selectedDateKey = chip.key
- this.fetchDateBooked(chip.key)
- },
- openBookPopup() {
- if (!this.resource || !this.vetId) {
- uni.showToast({ title: this.$t('bookingVetPage.noResource'), icon: 'none' })
- return
- }
- const sel = this.dateChips.find((c) => c.key === this.selectedDateKey)
- if (!sel || sel.weekdayDisabled) {
- uni.showToast({
- title: this.$t('bookingVetPage.toastPickDate'),
- icon: 'none'
- })
- return
- }
- if (this.selectedDateBooked) return
- this.resetBookForm()
- this.bookPopupShow = true
- },
- closeBookPopup() {
- this.bookPopupShow = false
- },
- resetBookForm() {
- const slots = this.timeSlotOptions
- const idx = slotIndexFromHour(slots)
- const slot = slots[idx] || slots[0] || ''
- const store = useUserStore()
- const name = store.displayName()
- this.formModel = {
- bookerName: name || '',
- phone: '',
- timeSlot: slot,
- address: '',
- needDesc: ''
- }
- this.pickerModel = [slot]
- this.$nextTick(() => {
- this.$refs.bookFormRef?.clearValidate?.()
- })
- },
- openTimePicker() {
- const slots = this.timeSlotOptions
- const cur = this.formModel.timeSlot
- const ok = slots.includes(cur)
- this.pickerModel = [ok ? cur : slots[slotIndexFromHour(slots)] || slots[0]]
- this.timePickerShow = true
- },
- onTimePickerConfirm(e) {
- const v = e.value && e.value[0]
- if (v) {
- this.formModel.timeSlot = v
- this.pickerModel = [v]
- }
- },
- onSubmitBook() {
- if (this.submitting) return
- this.$refs.bookFormRef
- ?.validate?.()
- .then(() => {
- this.submitting = true
- return submitBookingAppointment({
- resourceType: this.resourceType,
- resourceId: Number(this.vetId) || this.vetId,
- appointDate: this.selectedDateKey,
- appointeeName: (this.formModel.bookerName || '').trim(),
- contactPhone: (this.formModel.phone || '').trim(),
- timeSlot: this.formModel.timeSlot,
- serviceAddress: (this.formModel.address || '').trim(),
- serviceRequirement: (this.formModel.needDesc || '').trim()
- })
- })
- .then(() => {
- this.bookedDateMap = { ...this.bookedDateMap, [this.selectedDateKey]: true }
- uni.showToast({
- title: this.$t('bookingVetPage.toastSubmitOk'),
- icon: 'success'
- })
- this.bookPopupShow = false
- })
- .catch(() => {})
- .finally(() => {
- this.submitting = false
- })
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import '@/styles/morandi.scss';
- @import '@/styles/tab-page.scss';
- .bv-page {
- // display: flex;
- // flex-direction: column;
- min-width: 0;
- width: 100%;
- height: 100%;
- min-height: 100%;
- overflow: hidden;
- box-sizing: border-box;
- background: $morandi-bg-page;
- }
- .bv-scroll {
- height: 100%;
- flex: 1;
- // min-height: 0;
- min-width: 0;
- // height: 0;
- box-sizing: border-box;
- padding: 20rpx 24rpx 24rpx;
- }
- .bv-card {
- background: #ffffff;
- border-radius: 16rpx;
- border: 1rpx solid $morandi-border-soft;
- padding: 24rpx;
- box-sizing: border-box;
- }
- .bv-card--top {
- margin-bottom: 10rpx;
- }
- .bv-hero {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 20rpx;
- min-width: 0;
- margin-bottom: 20rpx;
- }
- .bv-hero__right {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 8rpx;
- }
- .bv-hero__title {
- font-size: 32rpx;
- font-weight: 600;
- color: #111827;
- line-height: 1.35;
- word-break: break-word;
- }
- .bv-hero__sub {
- font-size: 24rpx;
- color: $morandi-text-secondary;
- line-height: 1.45;
- word-break: break-word;
- }
- .bv-hero__contact {
- font-size: 22rpx;
- color: $morandi-text-muted;
- line-height: 1.45;
- word-break: break-word;
- }
- .bv-line {
- display: block;
- font-size: 24rpx;
- line-height: 1.55;
- color: #111827;
- margin-top: 12rpx;
- word-break: break-word;
- }
- .bv-line__k {
- color: $morandi-text-muted;
- margin-right: 4rpx;
- }
- .bv-section-title {
- display: block;
- font-size: 28rpx;
- font-weight: 600;
- color: #111827;
- margin-bottom: 16rpx;
- }
- .bv-date-scroll {
- width: 100%;
- white-space: nowrap;
- }
- .bv-date-row {
- display: inline-flex;
- flex-direction: row;
- align-items: stretch;
- gap: 12rpx;
- padding: 4rpx 0 8rpx;
- min-width: min-content;
- }
- .bv-chip {
- flex-shrink: 0;
- min-width: 96rpx;
- padding: 12rpx 16rpx;
- border-radius: 12rpx;
- border: 1rpx solid #e5e7eb;
- background: #ffffff;
- box-sizing: border-box;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- }
- .bv-chip--on {
- background: #22c55e;
- border-color: #16a34a;
- }
- .bv-chip--disabled {
- opacity: 0.75;
- background: #f9fafb;
- border-color: #e5e7eb;
- }
- .bv-chip__full {
- font-size: 22rpx;
- color: #9ca3af;
- font-weight: 500;
- }
- .bv-chip--disabled .bv-chip__name {
- color: #9ca3af;
- }
- .bv-chip__all {
- font-size: 26rpx;
- color: #111827;
- font-weight: 500;
- }
- .bv-chip--on .bv-chip__all {
- color: #ffffff;
- font-weight: 600;
- }
- .bv-chip__stack {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 4rpx;
- }
- .bv-chip__name {
- font-size: 24rpx;
- color: #111827;
- font-weight: 500;
- }
- .bv-chip__date {
- font-size: 22rpx;
- color: #374151;
- }
- .bv-chip--on .bv-chip__name,
- .bv-chip--on .bv-chip__date {
- color: #ffffff;
- font-weight: 600;
- }
- .bv-footer-spacer {
- height: 32rpx;
- }
- .bv-popup {
- display: flex;
- flex-direction: column;
- min-width: 0;
- max-height: 78vh;
- padding: 24rpx 24rpx 16rpx;
- box-sizing: border-box;
- }
- .bv-popup__scroll {
- flex: 1;
- min-height: 0;
- max-height: 56vh;
- }
- .bv-popup__h1 {
- display: block;
- font-size: 30rpx;
- font-weight: 600;
- color: #111827;
- margin-bottom: 20rpx;
- }
- .bv-popup__h2 {
- display: block;
- font-size: 28rpx;
- font-weight: 600;
- color: #111827;
- margin: 24rpx 0 16rpx;
- }
- .bv-popup__row {
- display: block;
- font-size: 26rpx;
- color: #374151;
- line-height: 1.5;
- margin-bottom: 8rpx;
- word-break: break-word;
- }
- .bv-slot-row {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 8rpx;
- min-width: 0;
- }
- .bv-slot-row :deep(.u-input) {
- flex: 1;
- min-width: 0;
- }
- .bv-popup__actions {
- display: flex;
- flex-direction: row;
- gap: 20rpx;
- padding-top: 16rpx;
- border-top: 1rpx solid #e5e7eb;
- margin-top: 8rpx;
- }
- .bv-popup__btn {
- flex: 1;
- min-width: 0;
- }
- .bv-popup__btn--cancel {
- background: #ffffff !important;
- }
- .bv-page.lang-bo {
- .bv-hero__title {
- font-size: 28rpx;
- line-height: 1.75;
- }
- }
- </style>
|