refactor event

This commit is contained in:
Timi
2026-08-05 22:50:22 +08:00
parent 22ff13b1fe
commit 1243068fc3
46 changed files with 961 additions and 789 deletions
+459
View File
@@ -0,0 +1,459 @@
<template>
<section class="register-record-calendar">
<div class="calendar-head">
<t-button class="month-btn" shape="square" variant="text" @click="shiftMonth(-1)">
<template #icon>
<t-icon name="chevron-left" />
</template>
</t-button>
<h2 v-text="monthTitle" />
<div class="head-actions">
<t-button class="month-btn" shape="square" variant="text" @click="shiftMonth(1)">
<template #icon>
<t-icon name="chevron-right" />
</template>
</t-button>
<t-button
v-if="showEventFilter"
class="filter-btn"
theme="primary"
size="extra-small"
variant="outline"
@click="eventFilterVisible = true"
content="登记事件"
>
<template #icon>
<t-icon name="filter" />
</template>
</t-button>
</div>
</div>
<div class="week-line">
<span v-for="item in weekNameList" :key="item" v-text="item" />
</div>
<div class="calendar-grid">
<button
v-for="item in calendarDayList"
:key="item.key"
class="day-cell"
:class="{muted: !item.inMonth, today: item.isToday, marked: 0 < item.records.length}"
type="button"
:disabled="!item.records.length"
@click="openDayRecords(item)"
>
<span class="date" v-text="item.day" />
<span v-if="item.records.length" class="count" v-text="`${item.records.length} 条`" />
</button>
</div>
<div v-if="loading" class="empty-block">正在加载登记日历</div>
<div v-else-if="!currentMonthFilteredRecords.length" class="empty-block" v-text="emptyText" />
<t-popup v-model="dayRecordVisible" placement="bottom">
<div class="day-record-panel">
<div class="popup-head">
<h3 v-text="selectedDayTitle" />
<t-button theme="default" variant="text" size="small" @click="dayRecordVisible = false">
关闭
</t-button>
</div>
<div class="popup-body">
<t-cell-group v-if="selectedDayRecords.length" theme="card">
<t-cell
v-for="item in selectedDayRecords"
:key="item.id"
:title="getRecordEventName(item)"
:description="getRecordCustomerName(item)"
:note="`${formatRecordTime(item.registeredAt)}`"
/>
</t-cell-group>
<div v-else class="empty-block">该日暂无登记事件记录</div>
</div>
</div>
</t-popup>
<t-popup v-model="eventFilterVisible" placement="bottom">
<div class="filter-popup">
<div class="popup-head">
<div class="title">登记事件筛选</div>
<t-button theme="default" variant="text" size="small" @click="eventFilterVisible = false">
关闭
</t-button>
</div>
<div class="popup-body">
<t-checkbox-group v-model="selectedEventIdListProxy" class="items">
<t-checkbox
class="item"
v-for="item in eventList"
:key="item.id"
:value="item.id"
:label="item.name"
block
borderless
/>
</t-checkbox-group>
</div>
</div>
</t-popup>
</section>
</template>
<script setup lang="ts">
import type {EventItem} from "@/types/EventItem";
import type {EventRecord} from "@/types/EventRecord";
import {Time} from "timi-web";
interface CalendarDay {
key: string;
day: string;
dateText: string;
inMonth: boolean;
isToday: boolean;
records: EventRecord[];
}
const props = withDefaults(defineProps<{
eventList: EventItem[];
customerName?: string;
emptyText?: string;
loading?: boolean;
modelValue: Date;
records: EventRecord[];
selectedEventIdList: string[];
showEventFilter?: boolean;
}>(), {
customerName: "",
emptyText: "当前月份暂无匹配登记",
loading: false,
showEventFilter: true
});
const emit = defineEmits<{
"update:modelValue": [value: Date];
"update:selectedEventIdList": [value: string[]];
}>();
const weekNameList = ["日", "一", "二", "三", "四", "五", "六"];
const dayRecordVisible = ref(false);
const eventFilterVisible = ref(false);
const selectedDayTitle = ref("");
const selectedDayRecords = ref<EventRecord[]>([]);
const selectedEventIdListProxy = computed({
get: () => props.selectedEventIdList,
set: value => emit("update:selectedEventIdList", value)
});
const eventNameMap = computed(() => {
const map = new Map<string, string>();
for (const item of props.eventList) {
if (item.id) {
map.set(item.id, item.name || "未命名事件");
}
}
return map;
});
const selectedEventIdSet = computed(() => new Set(props.selectedEventIdList));
const filteredRecords = computed(() => {
if (!props.showEventFilter || !props.selectedEventIdList.length) {
return props.records;
}
return props.records.filter(item => item.eventId && selectedEventIdSet.value.has(item.eventId));
});
const currentMonthFilteredRecords = computed(() => filteredRecords.value.filter(item => isInCurrentMonth(item.registeredAt)));
const monthTitle = computed(() => `${props.modelValue.getFullYear()} 年 ${props.modelValue.getMonth() + 1} 月`);
const calendarDayList = computed<CalendarDay[]>(() => {
const beginDate = getCalendarBeginDate(props.modelValue);
const todayText = Time.formatDate(new Date());
const list: CalendarDay[] = [];
for (let index = 0; index < 42; index += 1) {
const date = addDays(beginDate, index);
const dateText = Time.formatDate(date);
list.push({
key: dateText,
day: String(date.getDate()),
dateText,
inMonth: date.getMonth() === props.modelValue.getMonth(),
isToday: dateText === todayText,
records: filteredRecords.value.filter(item => getRecordDateText(item) === dateText)
});
}
return list;
});
function shiftMonth(offset: number): void {
emit("update:modelValue", addMonths(props.modelValue, offset));
}
function openDayRecords(day: CalendarDay): void {
if (!day.records.length) {
return;
}
selectedDayTitle.value = `${day.dateText} 登记事件记录`;
selectedDayRecords.value = [...day.records].sort((left, right) => (right.registeredAt || 0) - (left.registeredAt || 0));
dayRecordVisible.value = true;
}
function getRecordEventName(record?: EventRecord): string {
if (!record) {
return "";
}
return record.event?.name || (record.eventId ? eventNameMap.value.get(record.eventId) : "") || "未知事件";
}
function getRecordCustomerName(record: EventRecord): string {
return record.customer?.name || props.customerName || record.customerId || "未知客户";
}
function formatRecordTime(value?: number): string {
if (!value) {
return "-";
}
const date = Time.toDateObject(value);
if (!date) {
return "-";
}
return Time.formatDateTime(date);
}
function getRecordDateText(record: EventRecord): string {
const date = Time.toDateObject(record.registeredAt);
return date ? Time.formatDate(date) : "";
}
function isInCurrentMonth(value?: number): boolean {
const date = Time.toDateObject(value);
return !!date && date.getFullYear() === props.modelValue.getFullYear() && date.getMonth() === props.modelValue.getMonth();
}
function getMonthStart(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
}
function getCalendarBeginDate(date: Date): Date {
const monthStart = getMonthStart(date);
return addDays(monthStart, -monthStart.getDay());
}
function addDays(date: Date, days: number): Date {
const value = new Date(date);
value.setDate(value.getDate() + days);
return value;
}
function addMonths(date: Date, months: number): Date {
return new Date(date.getFullYear(), date.getMonth() + months, 1, 0, 0, 0, 0);
}
</script>
<style scoped lang="less">
.register-record-calendar {
gap: .875rem;
padding: 1rem;
display: flex;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: .75rem;
flex-direction: column;
.calendar-head {
display: flex;
align-items: center;
justify-content: space-between;
h2 {
margin: 0;
color: #0f172a;
font-size: 1rem;
font-weight: 700;
}
.month-btn {
width: 2.5rem;
height: 2.5rem;
}
.head-actions {
gap: .5rem;
display: flex;
align-items: center;
}
}
.week-line,
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.week-line {
color: #52607a;
font-size: .75rem;
text-align: center;
}
.calendar-grid {
gap: .375rem;
.day-cell {
padding: .25rem;
height: 2.5rem;
display: flex;
min-width: 0;
color: #0f172a;
border: 1px solid var(--td-component-border);
background: #fff;
text-align: left;
align-items: flex-start;
border-radius: .25rem;
flex-direction: column;
.date {
font-size: .875rem;
font-weight: 700;
}
.count {
width: 100%;
color: #2563eb;
overflow: hidden;
font-size: .625rem;
font-weight: 700;
white-space: nowrap;
text-overflow: ellipsis;
}
&.muted {
color: #94a3b8;
background: #f8fafc;
}
&.today {
border-color: #2563eb;
}
&.marked {
background: #eff6ff;
}
&:disabled {
cursor: default;
}
}
}
.empty-block {
padding: 1rem;
color: #52607a;
text-align: center;
background: #f7faff;
border-radius: .75rem;
}
}
.day-record-panel,
.filter-popup {
gap: .875rem;
padding: 1rem 0 calc(1rem + env(safe-area-inset-bottom, 0)) 0;
display: flex;
background: #f5f5f8;
border-radius: 1rem 1rem 0 0;
flex-direction: column;
max-height: min(80vh, 40rem);
h3,
.title {
margin: 0;
color: #0f172a;
font-size: 1rem;
text-align: center;
font-weight: 700;
}
.popup-head {
gap: .5rem;
display: flex;
padding: 0 1rem;
align-items: center;
justify-content: space-between;
}
.popup-body {
flex: 1;
overflow-y: auto;
}
}
.filter-popup {
padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom, 0)) 1rem;
.popup-head,
.popup-body {
padding: 0;
}
.items {
gap: .5rem;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
.item {
border-radius: .5rem;
}
}
}
:global(html.theme-dark) {
.register-record-calendar {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
.calendar-head {
h2 {
color: #e2e8f0;
}
}
.week-line {
color: #94a3b8;
}
.calendar-grid {
.day-cell {
color: #e2e8f0;
background: rgba(30, 41, 59, .9);
.count {
color: #60a5fa;
}
&.muted {
color: #94a3b8;
background: rgba(15, 23, 42, .7);
}
&.marked {
background: rgba(37, 99, 235, .2);
}
}
}
.empty-block {
color: #94a3b8;
background: rgba(30, 41, 59, .9);
}
}
.day-record-panel,
.filter-popup {
background: #10161f;
h3,
.title {
color: #e2e8f0;
}
}
}
</style>