fix: 对齐营业时间抽屉与日期时间选择交互

This commit is contained in:
2026-02-16 11:35:33 +08:00
parent 14857549ba
commit cfcd905a2b
8 changed files with 1140 additions and 197 deletions

View File

@@ -8,7 +8,7 @@ import type {
import type { SlotType } from '#/api/store-hours'; import type { SlotType } from '#/api/store-hours';
import { Button, Drawer, Textarea } from 'ant-design-vue'; import { Button, Drawer, TimePicker } from 'ant-design-vue';
interface Props { interface Props {
addSlotForm: AddSlotFormState; addSlotForm: AddSlotFormState;
@@ -36,18 +36,35 @@ const emit = defineEmits<{
}>(); }>();
function getInputValue(event: Event) { function getInputValue(event: Event) {
const target = event.target as HTMLInputElement | null; const target = event.target as HTMLInputElement | HTMLTextAreaElement | null;
return target?.value ?? ''; return target?.value ?? '';
} }
function hasFormatMethod(
value: unknown,
): value is { format: (pattern: string) => string } {
return Boolean(
value &&
typeof value === 'object' &&
'format' in value &&
typeof (value as { format?: unknown }).format === 'function',
);
}
function readTimeValue(value: unknown) {
if (typeof value === 'string') return value;
if (hasFormatMethod(value)) return value.format('HH:mm');
return '';
}
</script> </script>
<template> <template>
<Drawer <Drawer
class="add-slot-drawer-wrap"
:open="props.open" :open="props.open"
title="添加时段" title="添加时段"
:width="500" :width="480"
:mask-closable="false" :mask-closable="true"
:body-style="{ paddingBottom: '88px' }"
@update:open="(value) => emit('update:open', value)" @update:open="(value) => emit('update:open', value)"
> >
<div class="form-block"> <div class="form-block">
@@ -111,20 +128,26 @@ function getInputValue(event: Event) {
<div class="time-grid"> <div class="time-grid">
<div class="form-block"> <div class="form-block">
<label class="form-label required">开始时间</label> <label class="form-label required">开始时间</label>
<input <TimePicker
:value="props.addSlotForm.startTime" :value="props.addSlotForm.startTime"
type="time" value-format="HH:mm"
class="native-input" format="HH:mm"
@input="(event) => props.onSetStartTime(getInputValue(event))" :allow-clear="false"
class="native-picker"
input-read-only
@update:value="(value) => props.onSetStartTime(readTimeValue(value))"
/> />
</div> </div>
<div class="form-block"> <div class="form-block">
<label class="form-label required">结束时间</label> <label class="form-label required">结束时间</label>
<input <TimePicker
:value="props.addSlotForm.endTime" :value="props.addSlotForm.endTime"
type="time" value-format="HH:mm"
class="native-input" format="HH:mm"
@input="(event) => props.onSetEndTime(getInputValue(event))" :allow-clear="false"
class="native-picker"
input-read-only
@update:value="(value) => props.onSetEndTime(readTimeValue(value))"
/> />
</div> </div>
</div> </div>
@@ -150,16 +173,18 @@ function getInputValue(event: Event) {
/> />
<span>/小时</span> <span>/小时</span>
</div> </div>
<div class="capacity-hint">该时段内每小时最大接单量</div>
</div> </div>
<div class="form-block"> <div class="form-block">
<label class="form-label">备注</label> <label class="form-label">备注</label>
<Textarea <textarea
:value="props.addSlotForm.remark" :value="props.addSlotForm.remark"
:rows="2" rows="2"
class="native-textarea"
placeholder="可选,如:午市高峰时段" placeholder="可选,如:午市高峰时段"
@update:value="props.onSetRemark" @input="(event) => props.onSetRemark(getInputValue(event))"
/> ></textarea>
</div> </div>
<template #footer> <template #footer>

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { StoreListItemDto } from '#/api/store'; import type { StoreListItemDto } from '#/api/store';
import { Alert, Checkbox, Modal } from 'ant-design-vue'; import { computed } from 'vue';
import { Button, Modal } from 'ant-design-vue';
interface Props { interface Props {
copyCandidates: StoreListItemDto[]; copyCandidates: StoreListItemDto[];
@@ -23,36 +25,51 @@ const emit = defineEmits<{
(event: 'update:open', value: boolean): void; (event: 'update:open', value: boolean): void;
}>(); }>();
function readChecked(event: { target?: { checked?: boolean } }) { const selectedStoreIdSet = computed(() => new Set(props.copyTargetStoreIds));
return Boolean(event.target?.checked);
function isStoreChecked(storeId: string) {
return selectedStoreIdSet.value.has(storeId);
}
function toggleStore(storeId: string) {
emit('toggleStore', {
storeId,
checked: !isStoreChecked(storeId),
});
}
function toggleAll() {
emit('checkAll', !props.isCopyAllChecked);
} }
</script> </script>
<template> <template>
<Modal <Modal
:open="props.open" :open="props.open"
title="复制到其他门店" title="复制营业时间到其他门店"
:confirm-loading="props.isCopySubmitting" :width="650"
ok-text="确认复制" :footer="null"
cancel-text="取消" :mask-closable="true"
@ok="emit('submit')" wrap-class-name="copy-store-modal-wrap"
@update:open="(value) => emit('update:open', value)" @update:open="(value) => emit('update:open', value)"
> >
<div class="copy-modal-content"> <div class="copy-modal-content">
<Alert <div class="copy-modal-warning">
message="将覆盖目标门店的现有设置,请谨慎操作" <span class="copy-modal-warning-icon">!</span>
type="warning" <span>将覆盖目标门店的现有设置请谨慎操作</span>
show-icon </div>
/>
<div class="copy-all-row"> <div class="copy-all-row" @click="toggleAll">
<Checkbox <span
:checked="props.isCopyAllChecked" class="copy-check"
:indeterminate="props.isCopyIndeterminate" :class="{
@change="(event) => emit('checkAll', readChecked(event))" checked: props.isCopyAllChecked,
indeterminate: props.isCopyIndeterminate && !props.isCopyAllChecked,
}"
> >
全选 <span class="copy-check-mark"></span>
</Checkbox> </span>
<span>全选</span>
</div> </div>
<div class="copy-store-list"> <div class="copy-store-list">
@@ -60,34 +77,31 @@ function readChecked(event: { target?: { checked?: boolean } }) {
v-for="store in props.copyCandidates" v-for="store in props.copyCandidates"
:key="store.id" :key="store.id"
class="copy-store-item" class="copy-store-item"
@click=" @click="toggleStore(store.id)"
emit('toggleStore', {
storeId: store.id,
checked: !props.copyTargetStoreIds.includes(store.id),
})
"
> >
<Checkbox <span
:checked="props.copyTargetStoreIds.includes(store.id)" class="copy-check"
@click.stop :class="{ checked: isStoreChecked(store.id) }"
@change=" >
(event) => <span class="copy-check-mark"></span>
emit('storeChange', { </span>
storeId: store.id,
checked: readChecked(event),
})
"
/>
<div class="copy-store-info"> <div class="copy-store-info">
<div class="copy-store-name">{{ store.name }}</div> <div class="copy-store-name">{{ store.name }}</div>
<div class="copy-store-address">{{ store.address || '--' }}</div> <div class="copy-store-address">{{ store.address || '--' }}</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="copy-source-tip"> <div class="copy-modal-footer">
来源门店{{ props.selectedStoreName || '--' }} <Button @click="emit('update:open', false)">取消</Button>
</div> <Button
type="primary"
:loading="props.isCopySubmitting"
:disabled="props.copyTargetStoreIds.length === 0"
@click="emit('submit')"
>
确认复制
</Button>
</div> </div>
</Modal> </Modal>
</template> </template>

View File

@@ -3,7 +3,7 @@ import type { DayEditFormState, SlotTypeOption } from '../types';
import type { SlotType } from '#/api/store-hours'; import type { SlotType } from '#/api/store-hours';
import { Button, Drawer, Select, Switch } from 'ant-design-vue'; import { Button, Drawer, TimePicker } from 'ant-design-vue';
interface Props { interface Props {
dayEditForm: DayEditFormState; dayEditForm: DayEditFormState;
@@ -32,23 +32,56 @@ function getInputValue(event: Event) {
const target = event.target as HTMLInputElement | null; const target = event.target as HTMLInputElement | null;
return target?.value ?? ''; return target?.value ?? '';
} }
function hasFormatMethod(
value: unknown,
): value is { format: (pattern: string) => string } {
return Boolean(
value &&
typeof value === 'object' &&
'format' in value &&
typeof (value as { format?: unknown }).format === 'function',
);
}
function readTimeValue(value: unknown) {
if (typeof value === 'string') return value;
if (hasFormatMethod(value)) return value.format('HH:mm');
return '';
}
function getCheckedValue(event: Event) {
const target = event.target as HTMLInputElement | null;
return Boolean(target?.checked);
}
function getSlotTypePillClass(type: SlotType) {
const value = Number(type);
if (value === 1) return 'tp-biz';
if (value === 2) return 'tp-del';
return 'tp-pick';
}
</script> </script>
<template> <template>
<Drawer <Drawer
class="day-edit-drawer-wrap"
:open="props.open" :open="props.open"
:title="props.title" :title="props.title"
:width="560" :width="520"
:mask-closable="false" :mask-closable="true"
:body-style="{ paddingBottom: '88px' }"
@update:open="(value) => emit('update:open', value)" @update:open="(value) => emit('update:open', value)"
> >
<div class="day-open-row"> <div class="day-open-row">
<Switch <label class="day-open-toggle">
:checked="props.dayEditForm.isOpen" <input
@update:checked="(checked) => props.onSetDayOpen(Boolean(checked))" type="checkbox"
/> :checked="props.dayEditForm.isOpen"
<span>今日营业</span> @change="(event) => props.onSetDayOpen(getCheckedValue(event))"
/>
<span class="day-open-slider"></span>
</label>
<span class="day-open-text">今日营业</span>
<span v-if="!props.dayEditForm.isOpen" class="day-close-hint"> <span v-if="!props.dayEditForm.isOpen" class="day-close-hint">
该日休息不接单 该日休息不接单
</span> </span>
@@ -64,52 +97,67 @@ function getInputValue(event: Event) {
class="slot-edit-card" class="slot-edit-card"
> >
<div class="slot-edit-head"> <div class="slot-edit-head">
<Select <div class="slot-type-pill-group">
:value="slot.type" <button
class="slot-type-select" v-for="item in props.slotTypeOptions"
:options="props.slotTypeOptions" :key="item.value"
@update:value=" type="button"
(value) => props.onSetSlotType(slot.id, Number(value) as SlotType) class="slot-type-pill"
" :class="[
/> getSlotTypePillClass(item.value),
<Button { active: slot.type === item.value },
type="text" ]"
danger @click="props.onSetSlotType(slot.id, item.value)"
size="small" >
{{ item.label }}
</button>
</div>
<button
type="button"
class="slot-remove-btn"
@click="emit('removeSlot', slot.id)" @click="emit('removeSlot', slot.id)"
> >
删除 删除
</Button> </button>
</div> </div>
<div class="time-grid"> <div class="time-grid">
<div class="form-block"> <div class="form-block">
<label class="form-label required">开始</label> <label class="form-sub-label">开始</label>
<input <TimePicker
:value="slot.startTime" :value="slot.startTime"
type="time" value-format="HH:mm"
class="native-input" format="HH:mm"
@input=" :allow-clear="false"
(event) => class="native-picker"
props.onSetSlotStartTime(slot.id, getInputValue(event)) input-read-only
@update:value="
(value) =>
props.onSetSlotStartTime(slot.id, readTimeValue(value))
" "
/> />
</div> </div>
<div class="form-block"> <div class="form-block">
<label class="form-label required">结束</label> <label class="form-sub-label">结束</label>
<input <TimePicker
:value="slot.endTime" :value="slot.endTime"
type="time" value-format="HH:mm"
class="native-input" format="HH:mm"
@input=" :allow-clear="false"
(event) => props.onSetSlotEndTime(slot.id, getInputValue(event)) class="native-picker"
input-read-only
@update:value="
(value) => props.onSetSlotEndTime(slot.id, readTimeValue(value))
" "
/> />
</div> </div>
</div> </div>
<div v-if="slot.type === props.slotTypeDelivery" class="form-block"> <div
<label class="form-label">容量</label> v-if="slot.type === props.slotTypeDelivery"
class="form-block delivery-capacity-block"
>
<label class="capacity-title">配送上限</label>
<div class="capacity-row"> <div class="capacity-row">
<input <input
:value="slot.capacity" :value="slot.capacity"
@@ -127,13 +175,14 @@ function getInputValue(event: Event) {
} }
" "
/> />
<span>/小时</span> <span class="capacity-unit">/小时</span>
</div> </div>
<div class="capacity-tip">每小时最大可接配送订单数</div>
</div> </div>
</div> </div>
<button type="button" class="add-dashed-btn" @click="emit('addSlotRow')"> <button type="button" class="add-dashed-btn" @click="emit('addSlotRow')">
添加时段 + 添加时段
</button> </button>
</div> </div>

View File

@@ -3,7 +3,13 @@ import type { HolidayFormState } from '../types';
import type { HolidayType } from '#/api/store-hours'; import type { HolidayType } from '#/api/store-hours';
import { Button, Drawer, Input, Textarea } from 'ant-design-vue'; import {
Button,
DatePicker,
Drawer,
RangePicker,
TimePicker,
} from 'ant-design-vue';
interface Props { interface Props {
holidayForm: HolidayFormState; holidayForm: HolidayFormState;
@@ -32,18 +38,59 @@ const emit = defineEmits<{
}>(); }>();
function getInputValue(event: Event) { function getInputValue(event: Event) {
const target = event.target as HTMLInputElement | null; const target = event.target as HTMLInputElement | HTMLTextAreaElement | null;
return target?.value ?? ''; return target?.value ?? '';
} }
function hasFormatMethod(
value: unknown,
): value is { format: (pattern: string) => string } {
return Boolean(
value &&
typeof value === 'object' &&
'format' in value &&
typeof (value as { format?: unknown }).format === 'function',
);
}
function readDateValue(value: unknown) {
if (typeof value === 'string') return value;
if (hasFormatMethod(value)) return value.format('YYYY-MM-DD');
return '';
}
function readDateRangeValue(value: unknown): [string, string] {
if (!Array.isArray(value) || value.length < 2) return ['', ''];
return [readDateValue(value[0]), readDateValue(value[1])];
}
function getRangePickerValue(): [string, string] | undefined {
const { rangeStart, rangeEnd } = props.holidayForm;
if (!rangeStart || !rangeEnd) return undefined;
const rangeValue: [string, string] = [rangeStart, rangeEnd];
return rangeValue;
}
function handleRangeChange(value: unknown) {
const [start, end] = readDateRangeValue(value);
props.onSetRangeStart(start);
props.onSetRangeEnd(end);
}
function readTimeValue(value: unknown) {
if (typeof value === 'string') return value;
if (hasFormatMethod(value)) return value.format('HH:mm');
return '';
}
</script> </script>
<template> <template>
<Drawer <Drawer
class="holiday-drawer-wrap"
:open="props.open" :open="props.open"
:title="props.title" :title="props.title"
:width="500" :width="480"
:mask-closable="false" :mask-closable="true"
:body-style="{ paddingBottom: '88px' }"
@update:open="(value) => emit('update:open', value)" @update:open="(value) => emit('update:open', value)"
> >
<div class="form-block"> <div class="form-block">
@@ -94,27 +141,26 @@ function getInputValue(event: Event) {
</div> </div>
<template v-if="props.holidayForm.dateMode === 'single'"> <template v-if="props.holidayForm.dateMode === 'single'">
<input <DatePicker
:value="props.holidayForm.singleDate" :value="props.holidayForm.singleDate"
type="date" value-format="YYYY-MM-DD"
class="native-input" format="YYYY-MM-DD"
@input="(event) => props.onSetSingleDate(getInputValue(event))" :allow-clear="false"
class="native-picker"
input-read-only
@update:value="(value) => props.onSetSingleDate(readDateValue(value))"
/> />
</template> </template>
<template v-else> <template v-else>
<div class="date-range-row"> <div class="date-range-row">
<input <RangePicker
:value="props.holidayForm.rangeStart" :value="getRangePickerValue()"
type="date" value-format="YYYY-MM-DD"
class="native-input" format="YYYY-MM-DD"
@input="(event) => props.onSetRangeStart(getInputValue(event))" :allow-clear="false"
/> class="native-picker native-range-picker"
<span>~</span> input-read-only
<input @update:value="handleRangeChange"
:value="props.holidayForm.rangeEnd"
type="date"
class="native-input"
@input="(event) => props.onSetRangeEnd(getInputValue(event))"
/> />
</div> </div>
</template> </template>
@@ -128,42 +174,54 @@ function getInputValue(event: Event) {
<div class="time-grid"> <div class="time-grid">
<div class="form-block"> <div class="form-block">
<label class="form-sub-label">开始</label> <label class="form-sub-label">开始</label>
<input <TimePicker
:value="props.holidayForm.startTime" :value="props.holidayForm.startTime"
type="time" value-format="HH:mm"
class="native-input" format="HH:mm"
@input="(event) => props.onSetStartTime(getInputValue(event))" :allow-clear="false"
class="native-picker"
input-read-only
@update:value="
(value) => props.onSetStartTime(readTimeValue(value))
"
/> />
</div> </div>
<div class="form-block"> <div class="form-block">
<label class="form-sub-label">结束</label> <label class="form-sub-label">结束</label>
<input <TimePicker
:value="props.holidayForm.endTime" :value="props.holidayForm.endTime"
type="time" value-format="HH:mm"
class="native-input" format="HH:mm"
@input="(event) => props.onSetEndTime(getInputValue(event))" :allow-clear="false"
class="native-picker"
input-read-only
@update:value="(value) => props.onSetEndTime(readTimeValue(value))"
/> />
</div> </div>
</div> </div>
<div class="time-hint">特殊营业日的营业时间段</div>
</div> </div>
<div class="form-block"> <div class="form-block">
<label class="form-label required">原因</label> <label class="form-label required">原因</label>
<Input <input
:value="props.holidayForm.reason" :value="props.holidayForm.reason"
type="text"
class="native-input"
placeholder="如:春节假期、情人节延长营业" placeholder="如:春节假期、情人节延长营业"
@update:value="props.onSetReason" @input="(event) => props.onSetReason(getInputValue(event))"
/> />
</div> </div>
<div class="form-block"> <div class="form-block">
<label class="form-label">备注</label> <label class="form-label">备注</label>
<Textarea <textarea
:value="props.holidayForm.remark" :value="props.holidayForm.remark"
:rows="2" rows="2"
class="native-textarea"
placeholder="可选" placeholder="可选"
@update:value="props.onSetRemark" @input="(event) => props.onSetRemark(getInputValue(event))"
/> ></textarea>
</div> </div>
<template #footer> <template #footer>

View File

@@ -232,29 +232,33 @@ const {
</td> </td>
<td>{{ formatHolidayTime(holiday) }}</td> <td>{{ formatHolidayTime(holiday) }}</td>
<td>{{ holiday.reason || '--' }}</td> <td>{{ holiday.reason || '--' }}</td>
<td> <td class="holiday-op-cell">
<Button <div class="holiday-actions">
type="link"
size="small"
@click="openHolidayDrawer('edit', holiday)"
>
编辑
</Button>
<Popconfirm
title="确认删除该日期配置吗?"
ok-text="确认"
cancel-text="取消"
@confirm="handleDeleteHoliday(holiday.id)"
>
<Button <Button
type="link" type="link"
size="small" size="small"
danger class="holiday-action-btn"
:loading="deletingHolidayId === holiday.id" @click="openHolidayDrawer('edit', holiday)"
> >
删除 编辑
</Button> </Button>
</Popconfirm> <Popconfirm
title="确认删除该日期配置吗?"
ok-text="确认"
cancel-text="取消"
@confirm="handleDeleteHoliday(holiday.id)"
>
<Button
type="link"
size="small"
danger
class="holiday-action-btn holiday-delete-btn"
:loading="deletingHolidayId === holiday.id"
>
删除
</Button>
</Popconfirm>
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -1,58 +1,185 @@
/* 复制到门店弹窗区域样式。 */ /* 复制到其他门店弹窗:样式挂在 Modal wrap class避免 Teleport 后样式丢失。 */
.page-hours { .copy-store-modal-wrap {
.copy-modal-content { .ant-modal {
display: flex; width: 650px !important;
flex-direction: column; max-width: calc(100vw - 32px);
gap: 12px;
} }
.copy-all-row { .ant-modal-content {
padding-bottom: 8px; overflow: hidden;
padding: 0;
border-radius: 14px;
box-shadow: 0 10px 30px rgb(0 0 0 / 12%);
}
.ant-modal-header {
margin-bottom: 0;
padding: 22px 28px 16px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
} }
.copy-store-list { .ant-modal-title {
max-height: 320px; font-size: 17px;
overflow-y: auto; font-weight: 700;
border: 1px solid #f0f0f0; color: #1f2329;
}
.ant-modal-close {
top: 18px;
right: 20px;
width: 32px;
height: 32px;
border-radius: 8px; border-radius: 8px;
color: #8f959e;
transition: all 0.2s ease;
} }
.copy-store-item { .ant-modal-close:hover {
display: flex; color: #4e5969;
gap: 10px; background: #f2f3f5;
align-items: flex-start;
padding: 10px 12px;
cursor: pointer;
} }
.copy-store-item + .copy-store-item { .ant-modal-body {
border-top: 1px solid #f5f5f5; padding: 0;
}
.copy-store-item:hover {
background: #fafcff;
}
.copy-store-info {
flex: 1;
min-width: 0;
}
.copy-store-name {
font-size: 13px;
font-weight: 500;
color: #1a1a2e;
}
.copy-store-address {
margin-top: 2px;
font-size: 12px;
color: #9ca3af;
}
.copy-source-tip {
font-size: 12px;
color: #6b7280;
} }
} }
.copy-modal-content {
padding: 18px 28px 0;
}
.copy-modal-warning {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 18px;
padding: 11px 14px;
font-size: 14px;
font-weight: 500;
color: #c48b26;
background: #fffbe6;
border: 1px solid #f7e4a1;
border-radius: 10px;
}
.copy-modal-warning-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 11px;
font-weight: 700;
color: #fff;
background: #f5b034;
border-radius: 50%;
}
.copy-all-row {
display: flex;
gap: 10px;
align-items: center;
padding: 0 0 12px;
font-size: 16px;
font-weight: 600;
color: #1f2329;
cursor: pointer;
}
.copy-store-list {
max-height: 340px;
padding-top: 12px;
overflow-y: auto;
border-top: 1px solid #f0f0f0;
}
.copy-store-item {
display: flex;
gap: 12px;
align-items: flex-start;
margin-bottom: 10px;
padding: 14px 14px 12px;
background: #fafbfc;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s ease;
}
.copy-store-item:hover {
background: #f3f6fb;
}
.copy-store-info {
flex: 1;
min-width: 0;
}
.copy-store-name {
font-size: 18px;
font-weight: 700;
line-height: 1.4;
color: #1f2329;
}
.copy-store-address {
margin-top: 2px;
font-size: 13px;
line-height: 1.5;
color: #86909c;
}
.copy-check {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
margin-top: 2px;
background: #fff;
border: 1px solid #d9dde3;
border-radius: 6px;
transition: all 0.2s ease;
}
.copy-check-mark {
display: none;
width: 6px;
height: 10px;
border: 2px solid #fff;
border-top: 0;
border-left: 0;
transform: rotate(45deg) translate(-1px, -1px);
}
.copy-check.checked {
background: #1677ff;
border-color: #1677ff;
}
.copy-check.checked .copy-check-mark {
display: block;
}
.copy-check.indeterminate {
background: #1677ff;
border-color: #1677ff;
}
.copy-check.indeterminate .copy-check-mark {
display: block;
width: 10px;
height: 2px;
border: 0;
background: #fff;
transform: none;
}
.copy-modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 14px 28px 18px;
margin-top: 10px;
border-top: 1px solid #f0f0f0;
}

View File

@@ -1,4 +1,650 @@
/* 抽屉与编辑表单样式(新增时段、编辑时段、节假日抽屉)。 */ /* 抽屉与编辑表单样式(新增时段、编辑时段、节假日抽屉)。 */
.add-slot-drawer-wrap,
.day-edit-drawer-wrap,
.holiday-drawer-wrap {
.ant-drawer-content-wrapper {
box-shadow:
0 8px 24px rgb(0 0 0 / 8%),
0 2px 6px rgb(0 0 0 / 4%);
}
.ant-drawer-header {
min-height: 56px;
padding: 0 24px;
border-bottom: 1px solid #f0f0f0;
}
.ant-drawer-title {
font-size: 16px;
font-weight: 600;
color: #1f1f1f;
}
.ant-drawer-close {
margin-inline-start: auto;
margin-inline-end: 0;
color: #999;
border-radius: 6px;
transition: all 0.2s ease;
}
.ant-drawer-close:hover {
color: #1f1f1f;
background: #f5f5f5;
}
.ant-drawer-body {
padding: 20px 24px;
}
.ant-drawer-footer {
padding: 14px 24px;
border-top: 1px solid #f0f0f0;
}
}
.add-slot-drawer-wrap .drawer-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.add-slot-drawer-wrap .drawer-footer .ant-btn {
height: 32px;
padding: 0 16px;
font-size: 13px;
border-radius: 6px;
}
.add-slot-drawer-wrap .form-block {
margin-bottom: 16px;
}
.add-slot-drawer-wrap .form-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
color: #1f1f1f;
}
.add-slot-drawer-wrap .form-label.required::before {
margin-right: 3px;
color: #f5222d;
content: '*';
}
.add-slot-drawer-wrap .type-pill-group {
display: flex;
gap: 8px;
}
.add-slot-drawer-wrap .type-pill {
padding: 6px 16px;
font-size: 13px;
color: #4b5563;
cursor: pointer;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.add-slot-drawer-wrap .type-pill.active.tp-biz {
font-weight: 600;
color: #389e0d;
background: #f6ffed;
border-color: #b7eb8f;
}
.add-slot-drawer-wrap .type-pill.active.tp-del {
font-weight: 600;
color: #1890ff;
background: #e6f7ff;
border-color: #91d5ff;
}
.add-slot-drawer-wrap .type-pill.active.tp-pick {
font-weight: 600;
color: #d46b08;
background: #fff7e6;
border-color: #ffd591;
}
.add-slot-drawer-wrap .day-pill-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.add-slot-drawer-wrap .day-pill {
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 34px;
font-size: 12px;
color: #4b5563;
cursor: pointer;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.add-slot-drawer-wrap .day-pill.selected {
color: #fff;
background: #1677ff;
border-color: #1677ff;
}
.add-slot-drawer-wrap .quick-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.add-slot-drawer-wrap .quick-btn {
padding: 0;
font-size: 11px;
color: #1677ff;
cursor: pointer;
background: transparent;
border: none;
}
.add-slot-drawer-wrap .quick-btn:hover {
text-decoration: underline;
}
.add-slot-drawer-wrap .time-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 12px;
}
.add-slot-drawer-wrap .native-picker,
.day-edit-drawer-wrap .native-picker,
.holiday-drawer-wrap .native-picker {
width: 100%;
}
.add-slot-drawer-wrap .native-picker.ant-picker,
.day-edit-drawer-wrap .native-picker.ant-picker,
.holiday-drawer-wrap .native-picker.ant-picker {
box-sizing: border-box;
width: 100%;
height: 34px;
padding: 0 11px;
border: 1px solid #d9d9d9;
border-radius: 6px;
transition: all 0.2s ease;
}
.add-slot-drawer-wrap .native-picker.ant-picker:hover,
.day-edit-drawer-wrap .native-picker.ant-picker:hover,
.holiday-drawer-wrap .native-picker.ant-picker:hover {
border-color: #1677ff;
}
.add-slot-drawer-wrap .native-picker.ant-picker-focused,
.day-edit-drawer-wrap .native-picker.ant-picker-focused,
.holiday-drawer-wrap .native-picker.ant-picker-focused {
border-color: #1677ff;
box-shadow: 0 0 0 3px rgb(22 119 255 / 10%);
}
.add-slot-drawer-wrap .native-picker .ant-picker-input > input,
.day-edit-drawer-wrap .native-picker .ant-picker-input > input,
.holiday-drawer-wrap .native-picker .ant-picker-input > input {
font-size: 13px;
color: #1f1f1f;
cursor: pointer;
}
.add-slot-drawer-wrap .native-picker .ant-picker-suffix,
.day-edit-drawer-wrap .native-picker .ant-picker-suffix,
.holiday-drawer-wrap .native-picker .ant-picker-suffix {
color: #9ca3af;
}
.add-slot-drawer-wrap .native-input {
box-sizing: border-box;
width: 100%;
height: 34px;
padding: 0 11px;
font-size: 13px;
color: #1f1f1f;
border: 1px solid #d9d9d9;
border-radius: 6px;
outline: none;
transition: all 0.2s ease;
}
.add-slot-drawer-wrap .native-input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 3px rgb(22 119 255 / 10%);
}
.add-slot-drawer-wrap .native-textarea {
box-sizing: border-box;
width: 100%;
padding: 8px 11px;
font-size: 13px;
color: #1f1f1f;
border: 1px solid #d9d9d9;
border-radius: 6px;
outline: none;
resize: vertical;
transition: all 0.2s ease;
}
.add-slot-drawer-wrap .native-textarea:focus {
border-color: #1677ff;
box-shadow: 0 0 0 3px rgb(22 119 255 / 10%);
}
.add-slot-drawer-wrap .capacity-row {
display: flex;
gap: 8px;
align-items: center;
}
.add-slot-drawer-wrap .capacity-input {
max-width: 120px;
}
.add-slot-drawer-wrap .capacity-hint {
margin-top: 4px;
font-size: 11px;
color: #999;
}
.day-edit-drawer-wrap .drawer-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.day-edit-drawer-wrap .drawer-footer .ant-btn {
height: 32px;
padding: 0 16px;
font-size: 13px;
border-radius: 6px;
}
.day-edit-drawer-wrap .day-open-row {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 16px;
}
.day-edit-drawer-wrap .day-open-text {
font-size: 13px;
color: #1f1f1f;
}
.day-edit-drawer-wrap .day-open-toggle {
position: relative;
width: 40px;
height: 22px;
margin: 0;
cursor: pointer;
}
.day-edit-drawer-wrap .day-open-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.day-edit-drawer-wrap .day-open-slider {
position: absolute;
inset: 0;
background: #ccc;
border-radius: 11px;
transition: all 0.2s ease;
}
.day-edit-drawer-wrap .day-open-slider::before {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
content: '';
background: #fff;
border-radius: 50%;
box-shadow: 0 1px 3px rgb(0 0 0 / 15%);
transition: transform 0.2s ease;
}
.day-edit-drawer-wrap .day-open-toggle input:checked + .day-open-slider {
background: #1677ff;
}
.day-edit-drawer-wrap
.day-open-toggle
input:checked
+ .day-open-slider::before {
transform: translateX(18px);
}
.day-edit-drawer-wrap .day-close-hint {
font-size: 12px;
color: #ef4444;
}
.day-edit-drawer-wrap .slot-edit-list.disabled {
pointer-events: none;
opacity: 0.45;
}
.day-edit-drawer-wrap .slot-edit-card {
padding: 12px 14px;
margin-bottom: 10px;
background: #f8f9fb;
border: 1px solid #e5e7eb;
border-radius: 10px;
}
.day-edit-drawer-wrap .slot-edit-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.day-edit-drawer-wrap .slot-type-pill-group {
display: flex;
gap: 8px;
}
.day-edit-drawer-wrap .slot-type-pill {
padding: 4px 10px;
font-size: 12px;
color: #4b5563;
cursor: pointer;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 6px;
}
.day-edit-drawer-wrap .slot-type-pill.active.tp-biz {
font-weight: 600;
color: #389e0d;
background: #f6ffed;
border-color: #b7eb8f;
}
.day-edit-drawer-wrap .slot-type-pill.active.tp-del {
font-weight: 600;
color: #1890ff;
background: #e6f7ff;
border-color: #91d5ff;
}
.day-edit-drawer-wrap .slot-type-pill.active.tp-pick {
font-weight: 600;
color: #d46b08;
background: #fff7e6;
border-color: #ffd591;
}
.day-edit-drawer-wrap .slot-remove-btn {
padding: 0;
font-size: 12px;
color: #ef4444;
cursor: pointer;
background: transparent;
border: none;
}
.day-edit-drawer-wrap .slot-remove-btn:hover {
text-decoration: underline;
}
.day-edit-drawer-wrap .time-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 12px;
}
.day-edit-drawer-wrap .form-block {
margin-bottom: 12px;
}
.day-edit-drawer-wrap .form-sub-label {
display: block;
margin-bottom: 6px;
font-size: 11px;
color: #9ca3af;
}
.day-edit-drawer-wrap .native-input {
box-sizing: border-box;
width: 100%;
height: 34px;
padding: 0 11px;
font-size: 13px;
color: #1f1f1f;
border: 1px solid #d9d9d9;
border-radius: 6px;
outline: none;
transition: all 0.2s ease;
}
.day-edit-drawer-wrap .native-input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 3px rgb(22 119 255 / 10%);
}
.day-edit-drawer-wrap .capacity-row {
display: flex;
gap: 8px;
align-items: center;
}
.day-edit-drawer-wrap .delivery-capacity-block {
margin-top: 2px;
}
.day-edit-drawer-wrap .capacity-title {
display: block;
margin-bottom: 6px;
font-size: 12px;
font-weight: 600;
color: #4b5563;
}
.day-edit-drawer-wrap .capacity-input {
max-width: 120px;
}
.day-edit-drawer-wrap .capacity-unit {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 10px;
font-size: 12px;
font-weight: 500;
color: #475569;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 999px;
}
.day-edit-drawer-wrap .capacity-tip {
margin-top: 6px;
font-size: 11px;
color: #94a3b8;
}
.day-edit-drawer-wrap .add-dashed-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 40px;
margin-top: 4px;
font-size: 13px;
color: #9ca3af;
cursor: pointer;
background: transparent;
border: 1px dashed #d1d5db;
border-radius: 10px;
transition: all 0.2s ease;
}
.day-edit-drawer-wrap .add-dashed-btn:hover {
color: #1677ff;
border-color: #1677ff;
}
.holiday-drawer-wrap .drawer-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.holiday-drawer-wrap .drawer-footer .ant-btn {
height: 32px;
padding: 0 16px;
font-size: 13px;
border-radius: 6px;
}
.holiday-drawer-wrap .form-block {
margin-bottom: 16px;
}
.holiday-drawer-wrap .form-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
color: #1f1f1f;
}
.holiday-drawer-wrap .form-label.required::before {
margin-right: 3px;
color: #f5222d;
content: '*';
}
.holiday-drawer-wrap .type-pill-group {
display: flex;
gap: 8px;
}
.holiday-drawer-wrap .type-pill {
padding: 6px 16px;
font-size: 13px;
color: #4b5563;
cursor: pointer;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.holiday-drawer-wrap .type-pill.active.ht-closed {
font-weight: 600;
color: #ef4444;
background: #fff2f0;
border-color: #ffa39e;
}
.holiday-drawer-wrap .type-pill.active.ht-special {
font-weight: 600;
color: #f59e0b;
background: #fff7e6;
border-color: #ffd591;
}
.holiday-drawer-wrap .date-mode-row {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.holiday-drawer-wrap .date-mode-pill {
padding: 4px 12px;
font-size: 12px;
color: #4b5563;
cursor: pointer;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.holiday-drawer-wrap .date-mode-pill.active {
color: #fff;
background: #1677ff;
border-color: #1677ff;
}
.holiday-drawer-wrap .date-range-row {
display: block;
}
.holiday-drawer-wrap .native-range-picker.ant-picker {
width: 100%;
}
.holiday-drawer-wrap .time-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 12px;
}
.holiday-drawer-wrap .form-sub-label {
display: block;
margin-bottom: 6px;
font-size: 11px;
color: #9ca3af;
}
.holiday-drawer-wrap .native-input {
box-sizing: border-box;
width: 100%;
height: 34px;
padding: 0 11px;
font-size: 13px;
color: #1f1f1f;
border: 1px solid #d9d9d9;
border-radius: 6px;
outline: none;
transition: all 0.2s ease;
}
.holiday-drawer-wrap .native-input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 3px rgb(22 119 255 / 10%);
}
.holiday-drawer-wrap .native-textarea {
box-sizing: border-box;
width: 100%;
padding: 8px 11px;
font-size: 13px;
color: #1f1f1f;
border: 1px solid #d9d9d9;
border-radius: 6px;
outline: none;
resize: vertical;
transition: all 0.2s ease;
}
.holiday-drawer-wrap .native-textarea:focus {
border-color: #1677ff;
box-shadow: 0 0 0 3px rgb(22 119 255 / 10%);
}
.holiday-drawer-wrap .time-hint {
margin-top: 4px;
font-size: 11px;
color: #999;
}
.page-hours { .page-hours {
.type-pill-group { .type-pill-group {
display: flex; display: flex;

View File

@@ -31,7 +31,27 @@
} }
.holiday-table .op-column { .holiday-table .op-column {
width: 120px; width: 140px;
min-width: 140px;
}
.holiday-op-cell {
white-space: nowrap;
}
.holiday-actions {
display: flex;
gap: 8px;
align-items: center;
}
.holiday-action-btn {
padding: 0;
}
.holiday-delete-btn {
width: 56px;
text-align: left;
} }
.holiday-tag { .holiday-tag {