feat(calendar): U10 frontend calendar views with 3 view modes and drawer
This commit is contained in:
parent
8d4145ddf9
commit
8350b02d75
|
|
@ -9,6 +9,10 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^7.0.0",
|
"@ant-design/icons-vue": "^7.0.0",
|
||||||
|
"@fullcalendar/daygrid": "^6.1.0",
|
||||||
|
"@fullcalendar/interaction": "^6.1.0",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.0",
|
||||||
|
"@fullcalendar/vue3": "^6.1.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"@vue-flow/background": "^1.3.0",
|
"@vue-flow/background": "^1.3.0",
|
||||||
|
|
@ -533,6 +537,56 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fullcalendar/core": {
|
||||||
|
"version": "6.1.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@fullcalendar/core/-/core-6.1.21.tgz",
|
||||||
|
"integrity": "sha512-t3u/+sqh3Iq7TWtUnVLcGDUE6OWZh0UD3c04bI/l7lSLAgAKr3kngBmhHiQD1QXpwC8ZN5iNqG7a7gOVixhSKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"preact": "~10.12.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/daygrid": {
|
||||||
|
"version": "6.1.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@fullcalendar/daygrid/-/daygrid-6.1.21.tgz",
|
||||||
|
"integrity": "sha512-QYb1y40RGYLlOxKpYWg8O+7njEnKnFG8Tt7qjnubJGR35s1phQg67E+81y2TyAbbm59p2JFOCXGDk9t6KDujIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/interaction": {
|
||||||
|
"version": "6.1.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@fullcalendar/interaction/-/interaction-6.1.21.tgz",
|
||||||
|
"integrity": "sha512-WPYpqtljDWmU0Xm2cOtFrLlocgxv7cgkOppj34Q6OUUat8a6Cnd6kYo2JR+irP223PE5lBYHFNp1qh7SIpJc0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/timegrid": {
|
||||||
|
"version": "6.1.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@fullcalendar/timegrid/-/timegrid-6.1.21.tgz",
|
||||||
|
"integrity": "sha512-2DnShx/jallGmb8QCkr6pAOu/zuPhJrP7+uTrAtSnbqsX7GF3lTxqSeNGkTQwsgF5g/ia8udhQ+JNYaE+TN1cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fullcalendar/daygrid": "~6.1.21"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/vue3": {
|
||||||
|
"version": "6.1.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@fullcalendar/vue3/-/vue3-6.1.21.tgz",
|
||||||
|
"integrity": "sha512-OGt6WSC+/zz/ej6a0KfIBNl7BYuGchpZU49SsedYyv3WZWbghAE+D8YD6nhH1ia/I4p5Gcsv/nEXgEkT/I8aYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.21",
|
||||||
|
"vue": "^3.0.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
|
@ -2585,6 +2639,17 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.12.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.12.1.tgz",
|
||||||
|
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode.js": {
|
"node_modules/punycode.js": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^7.0.0",
|
"@ant-design/icons-vue": "^7.0.0",
|
||||||
|
"@fullcalendar/daygrid": "^6.1.0",
|
||||||
|
"@fullcalendar/interaction": "^6.1.0",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.0",
|
||||||
|
"@fullcalendar/vue3": "^6.1.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"@vue-flow/background": "^1.3.0",
|
"@vue-flow/background": "^1.3.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<a-drawer
|
||||||
|
:open="open"
|
||||||
|
@close="emit('update:open', false)"
|
||||||
|
placement="right"
|
||||||
|
width="80%"
|
||||||
|
title="日历管理"
|
||||||
|
:bodyStyle="{ padding: '0 24px 24px', display: 'flex', flexDirection: 'column', overflow: 'hidden' }"
|
||||||
|
>
|
||||||
|
<div class="calendar-drawer__toolbar">
|
||||||
|
<a-radio-group :value="store.viewMode" @change="onViewChange">
|
||||||
|
<a-radio-button value="calendar">日历</a-radio-button>
|
||||||
|
<a-radio-button value="card">卡片</a-radio-button>
|
||||||
|
<a-radio-button value="list">列表</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-space>
|
||||||
|
<a-badge :count="store.pendingInvitations.length" :offset="[6, 0]">
|
||||||
|
<a-button @click="emit('manage-invitations')">邀请管理</a-button>
|
||||||
|
</a-badge>
|
||||||
|
<a-button type="primary" @click="emit('create')">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
新建事件
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calendar-drawer__content">
|
||||||
|
<CalendarGrid
|
||||||
|
v-if="store.viewMode === 'calendar'"
|
||||||
|
@create="onCreate"
|
||||||
|
@edit="onEdit"
|
||||||
|
/>
|
||||||
|
<CardView
|
||||||
|
v-else-if="store.viewMode === 'card'"
|
||||||
|
@edit="onEdit"
|
||||||
|
/>
|
||||||
|
<ListView
|
||||||
|
v-else
|
||||||
|
@edit="onEdit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { useCalendarStore } from '@/stores/calendar'
|
||||||
|
import type { ICalendarEvent } from '@/api/calendar'
|
||||||
|
import CalendarGrid from './CalendarGrid.vue'
|
||||||
|
import CardView from './CardView.vue'
|
||||||
|
import ListView from './ListView.vue'
|
||||||
|
|
||||||
|
const store = useCalendarStore()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
open: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:open', value: boolean): void
|
||||||
|
(e: 'create', start?: Date, end?: Date): void
|
||||||
|
(e: 'edit', event: ICalendarEvent): void
|
||||||
|
(e: 'manage-invitations'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function onViewChange(e: { target: { value: string } }): void {
|
||||||
|
store.setViewMode(e.target.value as 'calendar' | 'card' | 'list')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCreate(start?: Date, end?: Date): void {
|
||||||
|
emit('create', start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEdit(event: ICalendarEvent): void {
|
||||||
|
emit('edit', event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-drawer__toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-3) 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-drawer__content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: 1px solid var(--border-color-split);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<div class="calendar-grid">
|
||||||
|
<FullCalendar :options="calendarOptions" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import FullCalendar from '@fullcalendar/vue3'
|
||||||
|
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||||
|
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||||
|
import interactionPlugin from '@fullcalendar/interaction'
|
||||||
|
import type {
|
||||||
|
CalendarOptions,
|
||||||
|
EventInput,
|
||||||
|
DateSelectArg,
|
||||||
|
EventDropArg,
|
||||||
|
EventClickArg,
|
||||||
|
} from '@fullcalendar/core'
|
||||||
|
import { useCalendarStore } from '@/stores/calendar'
|
||||||
|
import type { ICalendarEvent } from '@/api/calendar'
|
||||||
|
|
||||||
|
const store = useCalendarStore()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'create', start?: Date, end?: Date): void
|
||||||
|
(e: 'edit', event: ICalendarEvent): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function toFcEvent(ev: ICalendarEvent): EventInput {
|
||||||
|
const eventType = store.eventTypes.find((t) => t.id === ev.event_type_id)
|
||||||
|
const color = eventType?.color || '#1677ff'
|
||||||
|
return {
|
||||||
|
id: ev.id,
|
||||||
|
title: ev.title,
|
||||||
|
start: ev.start_time,
|
||||||
|
end: ev.end_time,
|
||||||
|
allDay: ev.is_all_day,
|
||||||
|
backgroundColor: color,
|
||||||
|
borderColor: color,
|
||||||
|
classNames: ev.is_invited ? ['fc-event-invited'] : [],
|
||||||
|
extendedProps: { event: ev },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(arg: DateSelectArg): void {
|
||||||
|
emit('create', arg.start, arg.end)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEventDrop(arg: EventDropArg): Promise<void> {
|
||||||
|
const ev = arg.event.extendedProps.event as ICalendarEvent
|
||||||
|
await store.updateEvent(ev.id, {
|
||||||
|
start_time: arg.event.start?.toISOString() ?? ev.start_time,
|
||||||
|
end_time: arg.event.end?.toISOString() ?? ev.end_time,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEventClick(arg: EventClickArg): void {
|
||||||
|
const ev = arg.event.extendedProps.event as ICalendarEvent
|
||||||
|
emit('edit', ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarOptions = computed<CalendarOptions>(() => ({
|
||||||
|
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay',
|
||||||
|
},
|
||||||
|
selectable: true,
|
||||||
|
editable: true,
|
||||||
|
height: '100%',
|
||||||
|
events: store.events.map(toFcEvent),
|
||||||
|
select: handleSelect,
|
||||||
|
eventDrop: handleEventDrop,
|
||||||
|
eventClick: handleEventClick,
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-grid {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid :deep(.fc) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid :deep(.fc-event-invited) {
|
||||||
|
border-style: dashed !important;
|
||||||
|
border-width: 2px !important;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
<template>
|
||||||
|
<div class="calendar-panel" :class="{ 'calendar-panel--highlight': highlight }">
|
||||||
|
<div class="calendar-panel__header">
|
||||||
|
<div class="calendar-panel__title">
|
||||||
|
<CalendarOutlined />
|
||||||
|
<span>日历</span>
|
||||||
|
<span class="calendar-panel__date">{{ todayLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<a-button size="small" type="link" @click="openDrawer">
|
||||||
|
<template #icon><ExpandOutlined /></template>
|
||||||
|
展开
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="store.isLoading && store.events.length === 0" class="calendar-panel__loading">
|
||||||
|
<a-spin size="small" />
|
||||||
|
<span>加载日程...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="store.error" class="calendar-panel__error">
|
||||||
|
<WarningOutlined />
|
||||||
|
<span>{{ store.error }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="todayEvents.length === 0 && upcomingEvents.length === 0" class="calendar-panel__empty">
|
||||||
|
<CalendarOutlined />
|
||||||
|
<span>暂无日程</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="calendar-panel__body">
|
||||||
|
<div v-if="todayEvents.length > 0" class="calendar-panel__section">
|
||||||
|
<div class="calendar-panel__section-title">今日</div>
|
||||||
|
<div
|
||||||
|
v-for="ev in todayEvents"
|
||||||
|
:key="ev.id"
|
||||||
|
class="calendar-panel__item"
|
||||||
|
:class="{ 'calendar-panel__item--invited': ev.is_invited }"
|
||||||
|
@click="onEventClick(ev)"
|
||||||
|
>
|
||||||
|
<span class="calendar-panel__item-time">{{ formatTime(ev.start_time) }}</span>
|
||||||
|
<span class="calendar-panel__item-title">{{ ev.title }}</span>
|
||||||
|
<EventBadge :event="ev" />
|
||||||
|
<a-tag v-if="ev.is_invited" color="purple" size="small">受邀</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="upcomingEvents.length > 0" class="calendar-panel__section">
|
||||||
|
<div class="calendar-panel__section-title">即将到来</div>
|
||||||
|
<div
|
||||||
|
v-for="ev in upcomingEvents"
|
||||||
|
:key="ev.id"
|
||||||
|
class="calendar-panel__item"
|
||||||
|
:class="{ 'calendar-panel__item--invited': ev.is_invited }"
|
||||||
|
@click="onEventClick(ev)"
|
||||||
|
>
|
||||||
|
<span class="calendar-panel__item-time">{{ formatUpcomingTime(ev.start_time) }}</span>
|
||||||
|
<span class="calendar-panel__item-title">{{ ev.title }}</span>
|
||||||
|
<EventBadge :event="ev" />
|
||||||
|
<a-tag v-if="ev.is_invited" color="purple" size="small">受邀</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CalendarDrawer
|
||||||
|
:open="drawerOpen"
|
||||||
|
@update:open="drawerOpen = $event"
|
||||||
|
@create="onCreate"
|
||||||
|
@edit="onEdit"
|
||||||
|
@manage-invitations="onManageInvitations"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
|
import { CalendarOutlined, ExpandOutlined, WarningOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { notification } from 'ant-design-vue'
|
||||||
|
import { useCalendarStore } from '@/stores/calendar'
|
||||||
|
import type { ICalendarEvent } from '@/api/calendar'
|
||||||
|
import EventBadge from './EventBadge.vue'
|
||||||
|
import CalendarDrawer from './CalendarDrawer.vue'
|
||||||
|
|
||||||
|
const store = useCalendarStore()
|
||||||
|
|
||||||
|
const drawerOpen = ref(false)
|
||||||
|
const highlight = ref(false)
|
||||||
|
let highlightTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const todayLabel = new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
weekday: 'short',
|
||||||
|
}).format(new Date())
|
||||||
|
|
||||||
|
const todayEvents = computed(() => {
|
||||||
|
const now = new Date()
|
||||||
|
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString()
|
||||||
|
const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).toISOString()
|
||||||
|
return store.events
|
||||||
|
.filter((e) => e.start_time >= startOfDay && e.start_time <= endOfDay)
|
||||||
|
.sort((a, b) => a.start_time.localeCompare(b.start_time))
|
||||||
|
})
|
||||||
|
|
||||||
|
const upcomingEvents = computed(() => {
|
||||||
|
const now = new Date()
|
||||||
|
const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).toISOString()
|
||||||
|
return store.events
|
||||||
|
.filter((e) => e.start_time > endOfDay)
|
||||||
|
.sort((a, b) => a.start_time.localeCompare(b.start_time))
|
||||||
|
.slice(0, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Highlight animation when new event arrives via WS
|
||||||
|
watch(
|
||||||
|
() => store.events.length,
|
||||||
|
(newLen, oldLen) => {
|
||||||
|
if (newLen > (oldLen ?? 0)) {
|
||||||
|
highlight.value = true
|
||||||
|
if (highlightTimer) clearTimeout(highlightTimer)
|
||||||
|
highlightTimer = setTimeout(() => {
|
||||||
|
highlight.value = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function openDrawer(): void {
|
||||||
|
drawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEventClick(ev: ICalendarEvent): void {
|
||||||
|
onEdit(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCreate(start?: Date, end?: Date): void {
|
||||||
|
const timeHint = start
|
||||||
|
? `预填时间:${formatTime(start.toISOString())}${end ? ' - ' + formatTime(end.toISOString()) : ''}`
|
||||||
|
: ''
|
||||||
|
notification.info({
|
||||||
|
message: '新建事件',
|
||||||
|
description: timeHint
|
||||||
|
? `事件创建器将在 U11 中实现(${timeHint})`
|
||||||
|
: '事件创建器将在 U11 中实现',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEdit(ev: ICalendarEvent): void {
|
||||||
|
if (ev.is_invited) {
|
||||||
|
notification.info({
|
||||||
|
message: '受邀事件',
|
||||||
|
description: `「${ev.title}」的邀请回复功能将在 U11 中实现`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notification.info({
|
||||||
|
message: '事件详情',
|
||||||
|
description: `「${ev.title}」的编辑器将在 U11 中实现`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onManageInvitations(): void {
|
||||||
|
notification.info({
|
||||||
|
message: '邀请管理',
|
||||||
|
description: `你有 ${store.pendingInvitations.length} 个待处理邀请,完整管理界面将在 U11 中实现`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(iso: string): string {
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).format(new Date(iso))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUpcomingTime(iso: string): string {
|
||||||
|
const date = new Date(iso)
|
||||||
|
const now = new Date()
|
||||||
|
const tomorrow = new Date(now)
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||||
|
if (date.toDateString() === tomorrow.toDateString()) {
|
||||||
|
return `明天 ${formatTime(iso)}`
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.loadEvents()
|
||||||
|
store.loadEventTypes()
|
||||||
|
store.loadTags()
|
||||||
|
store.loadInvitations()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-panel {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
transition: background var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel--highlight {
|
||||||
|
background: var(--color-primary-light, rgba(22, 119, 255, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__date {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__loading,
|
||||||
|
.calendar-panel__error,
|
||||||
|
.calendar-panel__empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-8) 0;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__error {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__section {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__section-title {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__item:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__item--invited {
|
||||||
|
border: 1px dashed var(--border-color-hover);
|
||||||
|
background: var(--color-primary-light, rgba(22, 119, 255, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__item-time {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-panel__item-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
<template>
|
||||||
|
<div class="card-view">
|
||||||
|
<div class="card-view__toolbar">
|
||||||
|
<a-radio-group :value="groupBy" @change="onGroupChange">
|
||||||
|
<a-radio-button value="date">按日期</a-radio-button>
|
||||||
|
<a-radio-button value="type">按类型</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-empty v-if="sortedEvents.length === 0" description="暂无日程" class="card-view__empty" />
|
||||||
|
|
||||||
|
<div v-else class="card-view__groups">
|
||||||
|
<div v-for="group in groups" :key="group.key" class="card-view__group">
|
||||||
|
<div class="card-view__group-header">
|
||||||
|
<span class="card-view__group-label">{{ group.label }}</span>
|
||||||
|
<a-tag size="small">{{ group.events.length }}</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="card-view__cards">
|
||||||
|
<div
|
||||||
|
v-for="ev in group.events"
|
||||||
|
:key="ev.id"
|
||||||
|
class="card-view__card"
|
||||||
|
:class="{ 'card-view__card--invited': ev.is_invited }"
|
||||||
|
:style="cardStyle(ev)"
|
||||||
|
@click="emit('edit', ev)"
|
||||||
|
>
|
||||||
|
<div class="card-view__card-header">
|
||||||
|
<span class="card-view__card-time">{{ formatTime(ev.start_time) }}</span>
|
||||||
|
<EventBadge :event="ev" />
|
||||||
|
<a-tag v-if="ev.is_invited" color="purple" size="small">受邀</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="card-view__card-title">{{ ev.title }}</div>
|
||||||
|
<div v-if="ev.location" class="card-view__card-location">
|
||||||
|
<EnvironmentOutlined />
|
||||||
|
<span>{{ ev.location }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { EnvironmentOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { useCalendarStore } from '@/stores/calendar'
|
||||||
|
import type { ICalendarEvent } from '@/api/calendar'
|
||||||
|
import EventBadge from './EventBadge.vue'
|
||||||
|
|
||||||
|
const store = useCalendarStore()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'edit', event: ICalendarEvent): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const groupBy = ref<'date' | 'type'>('date')
|
||||||
|
|
||||||
|
function onGroupChange(e: { target: { value: string } }): void {
|
||||||
|
groupBy.value = e.target.value as 'date' | 'type'
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedEvents = computed(() =>
|
||||||
|
[...store.events].sort((a, b) => a.start_time.localeCompare(b.start_time)),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface CardGroup {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
events: ICalendarEvent[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = computed<CardGroup[]>(() => {
|
||||||
|
const map = new Map<string, ICalendarEvent[]>()
|
||||||
|
if (groupBy.value === 'date') {
|
||||||
|
for (const ev of sortedEvents.value) {
|
||||||
|
const key = ev.start_time.slice(0, 10)
|
||||||
|
if (!map.has(key)) map.set(key, [])
|
||||||
|
map.get(key)!.push(ev)
|
||||||
|
}
|
||||||
|
return Array.from(map.entries()).map(([key, events]) => ({
|
||||||
|
key,
|
||||||
|
label: formatDateLabel(key),
|
||||||
|
events,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
for (const ev of sortedEvents.value) {
|
||||||
|
const key = ev.event_type_id ?? 'none'
|
||||||
|
if (!map.has(key)) map.set(key, [])
|
||||||
|
map.get(key)!.push(ev)
|
||||||
|
}
|
||||||
|
return Array.from(map.entries()).map(([key, events]) => ({
|
||||||
|
key,
|
||||||
|
label:
|
||||||
|
key === 'none'
|
||||||
|
? '未分类'
|
||||||
|
: store.eventTypes.find((t) => t.id === key)?.name ?? '未分类',
|
||||||
|
events,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
function cardStyle(ev: ICalendarEvent): { borderLeftColor: string } {
|
||||||
|
const eventType = store.eventTypes.find((t) => t.id === ev.event_type_id)
|
||||||
|
return { borderLeftColor: eventType?.color || '#1677ff' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(iso: string): string {
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).format(new Date(iso))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateLabel(dateStr: string): string {
|
||||||
|
const date = new Date(dateStr + 'T00:00:00')
|
||||||
|
const today = new Date()
|
||||||
|
if (date.toDateString() === today.toDateString()) return '今天'
|
||||||
|
const tomorrow = new Date(today)
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||||
|
if (date.toDateString() === tomorrow.toDateString()) return '明天'
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
weekday: 'short',
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-view {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__toolbar {
|
||||||
|
padding: 0 var(--space-3) var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__empty {
|
||||||
|
padding: var(--space-8) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__card {
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-left: 3px solid #1677ff;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__card:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__card--invited {
|
||||||
|
border-style: dashed;
|
||||||
|
background: var(--color-primary-light, rgba(22, 119, 255, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__card-time {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__card-title {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-view__card-location {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<span v-if="icon" class="event-badge" :class="`event-badge--${event.source}`" :title="title">
|
||||||
|
<component :is="icon" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { RobotOutlined, ThunderboltOutlined } from '@ant-design/icons-vue'
|
||||||
|
import type { ICalendarEvent } from '@/api/calendar'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
event: ICalendarEvent
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
switch (props.event.source) {
|
||||||
|
case 'agent':
|
||||||
|
return RobotOutlined
|
||||||
|
case 'post_extract':
|
||||||
|
return ThunderboltOutlined
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
switch (props.event.source) {
|
||||||
|
case 'agent':
|
||||||
|
return 'Agent 创建'
|
||||||
|
case 'post_extract':
|
||||||
|
return '对话提取'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.event-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-badge--agent {
|
||||||
|
color: var(--accent-team, #722ed1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-badge--post_extract {
|
||||||
|
color: var(--accent-board, #fa8c16);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
<template>
|
||||||
|
<div class="list-view">
|
||||||
|
<a-empty v-if="sortedEvents.length === 0" description="暂无日程" class="list-view__empty" />
|
||||||
|
<a-table
|
||||||
|
v-else
|
||||||
|
:dataSource="sortedEvents"
|
||||||
|
:columns="columns"
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
:pagination="{ pageSize: 50, showSizeChanger: false }"
|
||||||
|
:customRow="customRow"
|
||||||
|
:scroll="{ y: 'calc(100% - 64px)' }"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'start_time'">
|
||||||
|
<span class="list-view__time">
|
||||||
|
{{ formatDateTime(record.start_time) }}
|
||||||
|
<span class="list-view__time-sep">→</span>
|
||||||
|
{{ formatTime(record.end_time) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'title'">
|
||||||
|
<span class="list-view__title" :class="{ 'list-view__title--invited': record.is_invited }">
|
||||||
|
{{ record.title }}
|
||||||
|
<a-tag v-if="record.is_invited" color="purple" size="small">受邀</a-tag>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'source'">
|
||||||
|
<EventBadge :event="record" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'event_type_id'">
|
||||||
|
<a-tag
|
||||||
|
v-if="record.event_type_id"
|
||||||
|
:color="getTypeColor(record.event_type_id)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getTypeName(record.event_type_id) }}
|
||||||
|
</a-tag>
|
||||||
|
<span v-else class="list-view__muted">—</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'location'">
|
||||||
|
<span v-if="record.location">{{ record.location }}</span>
|
||||||
|
<span v-else class="list-view__muted">—</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useCalendarStore } from '@/stores/calendar'
|
||||||
|
import type { ICalendarEvent } from '@/api/calendar'
|
||||||
|
import EventBadge from './EventBadge.vue'
|
||||||
|
|
||||||
|
const store = useCalendarStore()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'edit', event: ICalendarEvent): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: '时间', dataIndex: 'start_time', key: 'start_time', width: 200 },
|
||||||
|
{ title: '标题', dataIndex: 'title', key: 'title' },
|
||||||
|
{ title: '来源', dataIndex: 'source', key: 'source', width: 60 },
|
||||||
|
{ title: '类型', dataIndex: 'event_type_id', key: 'event_type_id', width: 100 },
|
||||||
|
{ title: '地点', dataIndex: 'location', key: 'location', width: 140 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const sortedEvents = computed(() =>
|
||||||
|
[...store.events].sort((a, b) => a.start_time.localeCompare(b.start_time)),
|
||||||
|
)
|
||||||
|
|
||||||
|
function customRow(record: ICalendarEvent): Record<string, () => void> {
|
||||||
|
return {
|
||||||
|
onClick: () => emit('edit', record),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeName(id: string): string {
|
||||||
|
return store.eventTypes.find((t) => t.id === id)?.name ?? '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeColor(id: string): string {
|
||||||
|
return store.eventTypes.find((t) => t.id === id)?.color ?? 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(iso: string): string {
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).format(new Date(iso))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(iso: string): string {
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).format(new Date(iso))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.list-view {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: var(--space-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view__empty {
|
||||||
|
padding: var(--space-8) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view__time {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view__time-sep {
|
||||||
|
margin: 0 var(--space-1);
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view__title {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view__title--invited {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view__muted {
|
||||||
|
color: var(--text-placeholder);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -74,6 +74,9 @@
|
||||||
<template #skills>
|
<template #skills>
|
||||||
<SkillsView />
|
<SkillsView />
|
||||||
</template>
|
</template>
|
||||||
|
<template #calendar>
|
||||||
|
<CalendarTab />
|
||||||
|
</template>
|
||||||
<template #settings>
|
<template #settings>
|
||||||
<SettingsView />
|
<SettingsView />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -104,6 +107,7 @@ import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { useChatStore } from '@/stores/chat'
|
import { useChatStore } from '@/stores/chat'
|
||||||
import TopNav from './TopNav.vue'
|
import TopNav from './TopNav.vue'
|
||||||
|
|
@ -121,6 +125,7 @@ const KnowledgeBaseView = defineAsyncComponent(() => import('@/views/KnowledgeBa
|
||||||
const EvolutionView = defineAsyncComponent(() => import('@/views/EvolutionView.vue'))
|
const EvolutionView = defineAsyncComponent(() => import('@/views/EvolutionView.vue'))
|
||||||
const SkillsView = defineAsyncComponent(() => import('@/views/SkillsView.vue'))
|
const SkillsView = defineAsyncComponent(() => import('@/views/SkillsView.vue'))
|
||||||
const SettingsView = defineAsyncComponent(() => import('@/views/SettingsView.vue'))
|
const SettingsView = defineAsyncComponent(() => import('@/views/SettingsView.vue'))
|
||||||
|
const CalendarTab = defineAsyncComponent(() => import('./tabs/CalendarTab.vue'))
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
|
|
@ -152,6 +157,7 @@ const topRightTabs: QuadrantTab[] = [
|
||||||
const bottomRightTabs: QuadrantTab[] = [
|
const bottomRightTabs: QuadrantTab[] = [
|
||||||
{ key: 'monitor', label: '监控', icon: DashboardOutlined as Component },
|
{ key: 'monitor', label: '监控', icon: DashboardOutlined as Component },
|
||||||
{ key: 'skills', label: '技能', icon: AppstoreOutlined as Component },
|
{ key: 'skills', label: '技能', icon: AppstoreOutlined as Component },
|
||||||
|
{ key: 'calendar', label: '日历', icon: CalendarOutlined as Component },
|
||||||
{ key: 'settings', label: '设置', icon: SettingOutlined as Component },
|
{ key: 'settings', label: '设置', icon: SettingOutlined as Component },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<CalendarPanel />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import CalendarPanel from '@/components/calendar/CalendarPanel.vue'
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue