20260916172703
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user