20260916172703

This commit is contained in:
oneao committed 2026-09-16 17:27:04 +08:00
1 parent dce29f250e
commit de05498552
25 files changed
+1141 -1315

No files matched your search

@@ -87,15 +87,20 @@ export function sqlLiteral(value, field, endOfRange = false) {
}
let normalized = String(value)
const isDate = field.b_type === 'date' || field.b_type === 'datetime'
if (isDate && value && typeof value.format === 'function') {
normalized = value.format('YYYY-MM-DD')
} else if (isDate) {
normalized = String(value).slice(0, 10)
}
if (field.b_type === 'datetime') {
normalized += endOfRange ? ' 23:59:59' : ' 00:00:00'
if (field.b_type === 'date' || field.b_type === 'datetime') {
const isDateTimeField = field.b_type === 'datetime'
// dayjs 对象按真实时刻格式化;字符串沿用自身精度(10 位=到日,更长=带时分秒)
const text =
value && typeof value.format === 'function'
? value.format(isDateTimeField ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')
: String(value)
if (isDateTimeField && text.length > 10) {
// 值自带时分秒(datetime 控件产出)时按原时刻比较,不夹到当天起 / 当天末
normalized = text
} else {
normalized = text.slice(0, 10)
if (isDateTimeField) normalized += endOfRange ? ' 23:59:59' : ' 00:00:00'
}
}
return `N'${escapeSqlText(normalized)}'`
@@ -19,6 +19,11 @@ const emit = defineEmits(['update:modelValue', 'enter'])
const isRange = computed(() => isRangeQuery(props.item))
const isNumeric = computed(() => isNumericField(props.item))
const isDate = computed(() => isDateQuery(props.item))
// 日期区间两端控件类型随字段业务类型走:datetime 出时间列,date 只到日
const isDateTime = computed(() => props.item.field.b_type === 'datetime')
// datetime 端点用秒级精度:分钟精度下「当天末」表达不出来,结束端默认时间还会被
// normalizeDateForFormat 归零成 23:59:00,导致 23:59:01 之后的数据查不到
const dateFormatProps = computed(() => (isDateTime.value ? { format: 'YYYY-MM-DD HH:mm:ss' } : {}))
const isCheckbox = computed(() => props.item.field.b_type === 'checkbox')
const isSelect = computed(
() => props.item.config.b_component === 'select' || props.item.field.b_type === 'select',
@@ -80,18 +85,26 @@ function isDateBeforeLimit(valueToCompare) {
class="fms-query-control fms-query-control__range"
:class="{ 'fms-query-control__range--compact': compact }"
>
<FmsField
v-if="isDate"
class="fms-query-control__date-range"
type="range"
:model-value="modelValue"
:disabled="disabled"
:control-props="{
valueFormat: 'YYYY-MM-DD',
disabledDate: (date) => isDateAfterLimit(date) || isDateBeforeLimit(date),
}"
@update:model-value="emit('update:modelValue', $event)"
/>
<!-- 日期区间:两端是两个独立控件,各自只受另一端约束;不接 enter —— Enter 已被日期面板用于开合 -->
<template v-if="isDate">
<FmsField
v-model="startValue"
:type="isDateTime ? 'datetime' : 'date'"
:disabled="disabled"
:control-props="{
...dateFormatProps,
timeDefault: 'start',
disabledDate: isDateAfterLimit,
}"
/>
<span class="fms-query-control__separator">-</span>
<FmsField
v-model="endValue"
:type="isDateTime ? 'datetime' : 'date'"
:disabled="disabled"
:control-props="{ ...dateFormatProps, timeDefault: 'end', disabledDate: isDateBeforeLimit }"
/>
</template>
<template v-else>
<FmsField
v-model="startValue"
@@ -156,4 +156,18 @@
width: 1em;
height: 1em;
flex-shrink: 0;
animation: button-spin 0.7s linear infinite;
}
@keyframes button-spin {
to {
transform: rotate(360deg);
}
}
/* 尊重系统“减少动态效果”偏好:静止展示加载图标,避免诱发晕动不适 */
@media (prefers-reduced-motion: reduce) {
:deep(.button-spinner) {
animation: none;
}
}
@@ -27,6 +27,12 @@ const props = defineProps({
start: { type: [Date, null], default: null },
end: { type: [Date, null], default: null },
hoverValue: { type: [Date, null], default: null },
/** 无值时的默认时间:start=当天起,end=当天末,空=当前时刻;精度由 format 决定 */
timeDefault: {
type: String,
default: '',
validator: (value) => ['', 'start', 'end'].includes(value),
},
pickerConfig: {
type: Object,
default: () => analyzeDateFormat(DEFAULT_DATE_FORMAT),
@@ -136,7 +142,14 @@ const monthOptions = computed(() => {
})
/* ---------- 时间状态 ---------- */
const time = ref(props.modelValue ?? new Date())
/** 单选模式下无值时的时间初值,与 startTime / endTime 的日边界约定一致 */
function defaultTime() {
if (props.timeDefault === 'start') return dayjs().startOf('day').toDate()
if (props.timeDefault === 'end') return dayjs().endOf('day').toDate()
return new Date()
}
const time = ref(props.modelValue ?? defaultTime())
const startTime = ref(props.start ?? dayjs().startOf('day').toDate())
const endTime = ref(props.end ?? dayjs().endOf('day').toDate())
@@ -1,6 +1,6 @@
<script setup>
import { computed, nextTick, ref, watch } from 'vue'
import { Calendar, ChevronDown, X } from '@lucide/vue'
import { X } from '@lucide/vue'
import Popover from '../popover/popover.vue'
import CalendarPanel from './calendar-panel.vue'
import {
@@ -22,6 +22,12 @@ const props = defineProps({
clearable: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
disabledDate: { type: Function, default: () => false },
/** 无值时的默认时间:start=当天起,end=当天末,空=当前时刻;精度由 format 决定 */
timeDefault: {
type: String,
default: '',
validator: (value) => ['', 'start', 'end'].includes(value),
},
})
const emit = defineEmits(['update:modelValue', 'change'])
@@ -143,9 +149,6 @@ watch(pickerConfig, () => {
>
<template #trigger>
<div class="fms-date-trigger" @click="toggle">
<span class="fms-date-prefix" aria-hidden="true">
<Calendar :size="15" />
</span>
<input
class="fms-date-input"
type="text"
@@ -167,7 +170,6 @@ watch(pickerConfig, () => {
>
<X :size="14" aria-hidden="true" />
</button>
<ChevronDown v-else :size="15" class="fms-date-arrow" aria-hidden="true" />
</span>
</div>
</template>
@@ -177,6 +179,7 @@ watch(pickerConfig, () => {
ref="panelRef"
type="date"
:model-value="draftValue"
:time-default="timeDefault"
:picker-config="pickerConfig"
:disabled-date="disabledDate"
@pick="handlePick"
@@ -3,21 +3,20 @@
* 沿用 fms 主题 CSS 变量(见 src/theme/tokens.css)
* ============================================================ */
/* 与 Input 一致:宽度完全交给容器,不设 min/max 宽度下限,
窄容器(如查询区并排的两个日期控件)下可自由收缩 */
.fms-date-picker,
.fms-range-picker {
display: block;
width: 100%;
/* 窄容器下保持日历可读(7 列日期格子约 31px),
浮层跟随触发器宽度,视觉统一不受影响 */
min-width: 240px;
}
/* ---------- 触发器(类 Input 外观) ---------- */
.fms-date-trigger,
.fms-range-trigger {
position: relative;
display: flex;
align-items: center;
gap: 6px;
/* min-height 而非 height:与 Input/Select 一致,内容不足时撑到控件高度,
被放进更矮的容器(如表格编辑单元格)时可被 min-height:0 压缩 */
min-height: var(--fms-control-height);
@@ -49,12 +48,6 @@
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fms-primary) 18%, transparent);
}
.fms-date-prefix {
display: inline-flex;
color: var(--fms-text-secondary);
flex-shrink: 0;
}
.fms-date-input,
.fms-range-input {
flex: 1;
@@ -66,6 +59,7 @@
font-size: var(--fms-font-size);
font-family: inherit;
cursor: pointer;
padding: 0;
}
.fms-range-input {
@@ -77,13 +71,27 @@
flex-shrink: 0;
}
/* 清除按钮绝对定位在右侧,脱离文档流:
触发器宽度恒定,文字不会因按钮显隐而左右跳动 */
.fms-date-suffix {
position: absolute;
top: 0;
right: var(--fms-control-padding-x);
bottom: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--fms-text-secondary);
flex-shrink: 0;
pointer-events: none;
}
.fms-date-suffix > * {
pointer-events: auto;
}
/* 尾部无常驻图标,清除按钮常态占位但透明,hover / 聚焦时淡入接管。
* 用 opacity 而非 display:none 切换:既不消耗额外布局空间(绝对定位),
* 又不会因显隐改变输入框宽度造成文字位移(对齐 antd 的处理方式)。 */
.fms-date-clear {
display: inline-flex;
align-items: center;
@@ -94,6 +102,15 @@
background: transparent;
color: var(--fms-text-secondary);
cursor: pointer;
opacity: 0;
pointer-events: none;
transition: opacity var(--fms-duration-base) var(--fms-ease-standard);
}
.fms-date-trigger:is(:hover, :focus-within) .fms-date-clear,
.fms-range-trigger:is(:hover, :focus-within) .fms-date-clear {
opacity: 1;
pointer-events: auto;
}
.fms-date-clear:hover {
@@ -111,11 +128,6 @@
outline-offset: 1px;
}
.fms-date-picker.is-open .fms-date-arrow,
.fms-range-picker.is-open .fms-date-arrow {
transform: rotate(180deg);
}
/* 禁用态 */
.fms-date-picker.is-disabled .fms-date-trigger,
.fms-range-picker.is-disabled .fms-range-trigger {
@@ -134,6 +146,9 @@
.fms-calendar-panel {
--fms-cal-h: 282px; /* 274px 日历内容 + 8px 底部留白,时间区使用同一总高度 */
width: 100%;
/* 日历可读下限(7 列日期格子约 31px)挂在面板自身:触发器可以随容器收缩,
浮层取「触发器等宽与内容固有宽度的较大值」,因此面板宽度不被窄触发器拖垮 */
min-width: 240px;
user-select: none;
-webkit-user-select: none;
}
@@ -379,6 +394,8 @@
height: var(--fms-cal-h);
display: flex;
flex-direction: column;
/* 日历 7 列 × 31px 的固有宽度:带时间选择时由时间列把浮层撑宽,日历不会被压扁 */
min-width: 217px;
padding-right: 8px;
}
@@ -1,6 +1,6 @@
<script setup>
import { computed, nextTick, ref, watch } from 'vue'
import { Calendar, ChevronDown, X } from '@lucide/vue'
import { X } from '@lucide/vue'
import Popover from '../popover/popover.vue'
import CalendarPanel from './calendar-panel.vue'
import {
@@ -199,9 +199,6 @@ watch(pickerConfig, () => {
>
<template #trigger>
<div class="fms-range-trigger" @click="toggle">
<span class="fms-date-prefix" aria-hidden="true">
<Calendar :size="15" />
</span>
<input
class="fms-range-input"
type="text"
@@ -235,7 +232,6 @@ watch(pickerConfig, () => {
>
<X :size="14" aria-hidden="true" />
</button>
<ChevronDown v-else :size="15" class="fms-date-arrow" aria-hidden="true" />
</span>
</div>
</template>