我叫洪圣文

This commit is contained in:
2026-05-15 16:36:50 +08:00
parent b6006e0dfe
commit 6687cf0339
36 changed files with 2201 additions and 271 deletions

View File

@@ -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')

View 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>

View 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
}