我叫洪圣文
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/* eslint-env node */
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
|
||||
const currentDir = path.resolve('src/views/components/TimePeriodSearch')
|
||||
const componentPath = path.join(currentDir, 'index.vue')
|
||||
const modulePath = path.join(currentDir, 'timePeriod.ts')
|
||||
|
||||
if (!fs.existsSync(componentPath)) {
|
||||
throw new Error('TimePeriodSearch/index.vue must provide the shared views time search component')
|
||||
}
|
||||
|
||||
if (!fs.existsSync(modulePath)) {
|
||||
throw new Error('TimePeriodSearch/timePeriod.ts must provide shared time period helpers')
|
||||
}
|
||||
|
||||
const componentSource = fs.readFileSync(componentPath, 'utf8')
|
||||
const helperSource = fs.readFileSync(modulePath, 'utf8')
|
||||
const transpiled = ts.transpileModule(helperSource, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.ES2020,
|
||||
target: ts.ScriptTarget.ES2020
|
||||
}
|
||||
}).outputText
|
||||
|
||||
const tempDir = path.resolve('node_modules/.cache/time-period-contract')
|
||||
fs.mkdirSync(tempDir, { recursive: true })
|
||||
const tempModulePath = path.join(tempDir, 'timePeriod.mjs')
|
||||
fs.writeFileSync(tempModulePath, transpiled, 'utf8')
|
||||
|
||||
const {
|
||||
buildTimePeriodRange,
|
||||
formatTimePeriodDateTime,
|
||||
getTimePeriodPickerFormat,
|
||||
getTimePeriodPickerType,
|
||||
resolveTimePeriodUnitLabel,
|
||||
shiftTimePeriod
|
||||
} = await import(pathToFileURL(tempModulePath).href)
|
||||
|
||||
assert.deepEqual(buildTimePeriodRange('day', new Date(2026, 4, 13)), [
|
||||
'2026-05-13 00:00:00.000',
|
||||
'2026-05-13 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.deepEqual(buildTimePeriodRange('month', new Date(2026, 4, 13)), [
|
||||
'2026-05-01 00:00:00.000',
|
||||
'2026-05-31 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.deepEqual(buildTimePeriodRange('year', new Date(2026, 4, 13)), [
|
||||
'2026-01-01 00:00:00.000',
|
||||
'2026-12-31 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.deepEqual(buildTimePeriodRange('month', shiftTimePeriod('month', new Date(2026, 4, 13), -1)), [
|
||||
'2026-04-01 00:00:00.000',
|
||||
'2026-04-30 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.equal(formatTimePeriodDateTime(new Date(2026, 4, 13, 8, 9, 10, 11)), '2026-05-13 08:09:10.011')
|
||||
assert.equal(getTimePeriodPickerType('day'), 'date')
|
||||
assert.equal(getTimePeriodPickerFormat('month'), 'YYYY-MM')
|
||||
assert.equal(resolveTimePeriodUnitLabel('year'), '年')
|
||||
|
||||
const componentExpectations = [
|
||||
['component renders unit selector', /time-period-search__unit[\s\S]*timePeriodUnitOptions/],
|
||||
['component renders previous period button', /ArrowLeft[\s\S]*上一个/],
|
||||
['component renders current period button', /Clock[\s\S]*当前/],
|
||||
['component renders next period button', /ArrowRight[\s\S]*下一个/],
|
||||
['component renders date picker by selected unit', /getTimePeriodPickerType\(props\.unit\)/],
|
||||
['component uses fixed eventList-compatible picker width', /time-period-search__picker[\s\S]*width:\s*112px;[\s\S]*flex:\s*0 0 112px;/]
|
||||
]
|
||||
|
||||
const failures = componentExpectations.filter(([, pattern]) => !pattern.test(componentSource))
|
||||
|
||||
if (failures.length) {
|
||||
console.error('TimePeriodSearch contract failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('TimePeriodSearch contract passed')
|
||||
111
frontend/src/views/components/TimePeriodSearch/index.vue
Normal file
111
frontend/src/views/components/TimePeriodSearch/index.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="time-period-search">
|
||||
<el-select class="time-period-search__unit" :model-value="unit" @update:model-value="handleUnitChange">
|
||||
<el-option v-for="item in timePeriodUnitOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
|
||||
<el-button
|
||||
class="time-period-search__button"
|
||||
:icon="ArrowLeft"
|
||||
:title="`上一个${unitLabel}`"
|
||||
@click="shiftPeriod(-1)"
|
||||
/>
|
||||
|
||||
<el-date-picker
|
||||
class="time-period-search__picker"
|
||||
:model-value="baseDate"
|
||||
:type="getTimePeriodPickerType(props.unit)"
|
||||
:format="getTimePeriodPickerFormat(props.unit)"
|
||||
:clearable="false"
|
||||
:editable="false"
|
||||
:placeholder="`选择${unitLabel}`"
|
||||
@update:model-value="handleDateChange"
|
||||
/>
|
||||
|
||||
<el-button
|
||||
class="time-period-search__button"
|
||||
:icon="ArrowRight"
|
||||
:title="`下一个${unitLabel}`"
|
||||
@click="shiftPeriod(1)"
|
||||
/>
|
||||
|
||||
<el-button
|
||||
class="time-period-search__button"
|
||||
:icon="Clock"
|
||||
:title="`当前${unitLabel}`"
|
||||
@click="setCurrentPeriod"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ArrowLeft, ArrowRight, Clock } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getTimePeriodPickerFormat,
|
||||
getTimePeriodPickerType,
|
||||
resolveTimePeriodUnitLabel,
|
||||
shiftTimePeriod,
|
||||
timePeriodUnitOptions,
|
||||
type TimePeriodUnit
|
||||
} from './timePeriod'
|
||||
|
||||
defineOptions({
|
||||
name: 'TimePeriodSearch'
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
unit: TimePeriodUnit
|
||||
modelValue: Date | string | number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:unit': [value: TimePeriodUnit]
|
||||
'update:modelValue': [value: Date]
|
||||
}>()
|
||||
|
||||
const baseDate = computed(() => new Date(props.modelValue))
|
||||
const unitLabel = computed(() => resolveTimePeriodUnitLabel(props.unit))
|
||||
|
||||
const handleUnitChange = (value: TimePeriodUnit) => {
|
||||
emit('update:unit', value)
|
||||
}
|
||||
|
||||
const handleDateChange = (value: Date | string | number | null) => {
|
||||
if (!value) return
|
||||
emit('update:modelValue', new Date(value))
|
||||
}
|
||||
|
||||
const shiftPeriod = (offset: number) => {
|
||||
emit('update:modelValue', shiftTimePeriod(props.unit, baseDate.value, offset))
|
||||
}
|
||||
|
||||
const setCurrentPeriod = () => {
|
||||
emit('update:modelValue', new Date())
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.time-period-search {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.time-period-search__unit {
|
||||
width: 56px;
|
||||
flex: 0 0 56px;
|
||||
}
|
||||
|
||||
.time-period-search__picker {
|
||||
width: 112px;
|
||||
flex: 0 0 112px;
|
||||
}
|
||||
|
||||
.time-period-search__button {
|
||||
width: 28px;
|
||||
flex: 0 0 28px;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
</style>
|
||||
85
frontend/src/views/components/TimePeriodSearch/timePeriod.ts
Normal file
85
frontend/src/views/components/TimePeriodSearch/timePeriod.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export type TimePeriodUnit = 'day' | 'month' | 'year'
|
||||
|
||||
export const timePeriodUnitOptions: { label: string; value: TimePeriodUnit }[] = [
|
||||
{ label: '日', value: 'day' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '年', value: 'year' }
|
||||
]
|
||||
|
||||
const datePickerTypeMap: Record<TimePeriodUnit, 'date' | 'month' | 'year'> = {
|
||||
day: 'date',
|
||||
month: 'month',
|
||||
year: 'year'
|
||||
}
|
||||
|
||||
const datePickerFormatMap: Record<TimePeriodUnit, string> = {
|
||||
day: 'YYYY-MM-DD',
|
||||
month: 'YYYY-MM',
|
||||
year: 'YYYY'
|
||||
}
|
||||
|
||||
const padTimeValue = (value: number, length = 2) => String(value).padStart(length, '0')
|
||||
|
||||
export const formatTimePeriodDateTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = padTimeValue(date.getMonth() + 1)
|
||||
const day = padTimeValue(date.getDate())
|
||||
const hour = padTimeValue(date.getHours())
|
||||
const minute = padTimeValue(date.getMinutes())
|
||||
const second = padTimeValue(date.getSeconds())
|
||||
const millisecond = padTimeValue(date.getMilliseconds(), 3)
|
||||
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}.${millisecond}`
|
||||
}
|
||||
|
||||
export const getTimePeriodPickerType = (unit: TimePeriodUnit) => datePickerTypeMap[unit]
|
||||
|
||||
export const getTimePeriodPickerFormat = (unit: TimePeriodUnit) => datePickerFormatMap[unit]
|
||||
|
||||
export const resolveTimePeriodUnitLabel = (unit: TimePeriodUnit) => {
|
||||
return timePeriodUnitOptions.find(item => item.value === unit)?.label ?? ''
|
||||
}
|
||||
|
||||
export const buildTimePeriodRange = (unit: TimePeriodUnit, date: Date): string[] => {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth()
|
||||
const day = date.getDate()
|
||||
|
||||
if (unit === 'day') {
|
||||
return [
|
||||
formatTimePeriodDateTime(new Date(year, month, day, 0, 0, 0, 0)),
|
||||
formatTimePeriodDateTime(new Date(year, month, day, 23, 59, 59, 999))
|
||||
]
|
||||
}
|
||||
|
||||
if (unit === 'year') {
|
||||
return [
|
||||
formatTimePeriodDateTime(new Date(year, 0, 1, 0, 0, 0, 0)),
|
||||
formatTimePeriodDateTime(new Date(year, 11, 31, 23, 59, 59, 999))
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
formatTimePeriodDateTime(new Date(year, month, 1, 0, 0, 0, 0)),
|
||||
formatTimePeriodDateTime(new Date(year, month + 1, 0, 23, 59, 59, 999))
|
||||
]
|
||||
}
|
||||
|
||||
export const shiftTimePeriod = (unit: TimePeriodUnit, date: Date, offset: number) => {
|
||||
const nextDate = new Date(date)
|
||||
|
||||
if (unit === 'day') {
|
||||
nextDate.setDate(nextDate.getDate() + offset)
|
||||
return nextDate
|
||||
}
|
||||
|
||||
if (unit === 'year') {
|
||||
nextDate.setFullYear(nextDate.getFullYear() + offset)
|
||||
return nextDate
|
||||
}
|
||||
|
||||
// 月份切换以 1 日为锚点,避免 31 日切到短月份时发生日期溢出。
|
||||
nextDate.setDate(1)
|
||||
nextDate.setMonth(nextDate.getMonth() + offset)
|
||||
return nextDate
|
||||
}
|
||||
Reference in New Issue
Block a user