feat: add EventDetail with edit form, occurrence grid, split
This commit is contained in:
@@ -3,6 +3,7 @@ import { useCalendar } from './composables/useCalendar.js'
|
|||||||
import DropZone from './components/DropZone.vue'
|
import DropZone from './components/DropZone.vue'
|
||||||
import HeaderBar from './components/HeaderBar.vue'
|
import HeaderBar from './components/HeaderBar.vue'
|
||||||
import EventList from './components/EventList.vue'
|
import EventList from './components/EventList.vue'
|
||||||
|
import EventDetail from './components/EventDetail.vue'
|
||||||
|
|
||||||
const { isLoaded } = useCalendar()
|
const { isLoaded } = useCalendar()
|
||||||
</script>
|
</script>
|
||||||
@@ -18,7 +19,7 @@ const { isLoaded } = useCalendar()
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<EventList />
|
<EventList />
|
||||||
<div class="detail-area">
|
<div class="detail-area">
|
||||||
<div class="empty">从左侧选择一个日程进行编辑</div>
|
<EventDetail />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
253
src/components/EventDetail.vue
Normal file
253
src/components/EventDetail.vue
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useCalendar } from '../composables/useCalendar.js'
|
||||||
|
import { DOW_EN } from '../lib/weekday.js'
|
||||||
|
import OccurrenceGrid from './OccurrenceGrid.vue'
|
||||||
|
|
||||||
|
const { selectedEvent, updateEvent, deleteEvent, splitEvent, toggleOccurrence } = useCalendar()
|
||||||
|
|
||||||
|
// Local form state, synced when selection changes
|
||||||
|
const form = ref({})
|
||||||
|
const showSplit = ref(false)
|
||||||
|
const splitDate = ref('')
|
||||||
|
|
||||||
|
function syncForm(ev) {
|
||||||
|
if (!ev) { form.value = {}; return }
|
||||||
|
form.value = {
|
||||||
|
summary: ev.summary || '',
|
||||||
|
location: ev.location || '',
|
||||||
|
description: ev.description || '',
|
||||||
|
dtstart_time: ev.dtstartTime || '',
|
||||||
|
dtend_time: ev.dtendTime || '',
|
||||||
|
is_recurring: ev.rrule !== null,
|
||||||
|
freq: ev.rrule?.freq || 'WEEKLY',
|
||||||
|
interval: ev.rrule?.interval || 1,
|
||||||
|
byday: ev.rrule?.byday || 'MO',
|
||||||
|
until_date: ev.rrule?.untilDate || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedEvent, syncForm, { immediate: true })
|
||||||
|
|
||||||
|
function applyChanges() {
|
||||||
|
const ev = selectedEvent.value
|
||||||
|
if (!ev) return
|
||||||
|
const patch = {
|
||||||
|
summary: form.value.summary,
|
||||||
|
location: form.value.location,
|
||||||
|
description: form.value.description,
|
||||||
|
dtstartTime: form.value.dtstart_time,
|
||||||
|
dtendTime: form.value.dtend_time,
|
||||||
|
}
|
||||||
|
if (form.value.is_recurring) {
|
||||||
|
patch.rrule = {
|
||||||
|
freq: form.value.freq,
|
||||||
|
interval: Number(form.value.interval) || 1,
|
||||||
|
byday: form.value.freq === 'WEEKLY' ? form.value.byday : null,
|
||||||
|
untilDate: form.value.until_date || ev.rrule?.untilDate || '',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
patch.rrule = null
|
||||||
|
patch.exdates = []
|
||||||
|
}
|
||||||
|
updateEvent(ev.uid, patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete() {
|
||||||
|
const ev = selectedEvent.value
|
||||||
|
if (!ev) return
|
||||||
|
if (confirm(`确定删除"${ev.summary}"?`)) {
|
||||||
|
deleteEvent(ev.uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSplit() {
|
||||||
|
const ev = selectedEvent.value
|
||||||
|
if (!ev || !ev.rrule) return
|
||||||
|
if (!splitDate.value) {
|
||||||
|
alert('请选择拆分日期')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (splitDate.value <= ev.dtstartDate || splitDate.value > ev.rrule.untilDate) {
|
||||||
|
alert('拆分日期必须在开始日期和结束日期之间')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
splitEvent(ev.uid, splitDate.value)
|
||||||
|
showSplit.value = false
|
||||||
|
splitDate.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function onToggleOcc(date) {
|
||||||
|
const ev = selectedEvent.value
|
||||||
|
if (!ev) return
|
||||||
|
toggleOccurrence(ev.uid, date)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="!selectedEvent" class="empty">从左侧选择一个日程进行编辑</div>
|
||||||
|
<div v-else class="detail">
|
||||||
|
<!-- Basic info -->
|
||||||
|
<div class="section-title">基本信息</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>标题 (SUMMARY)</label>
|
||||||
|
<input type="text" v-model="form.summary" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>开始时间</label>
|
||||||
|
<input type="time" v-model="form.dtstart_time" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>结束时间</label>
|
||||||
|
<input type="time" v-model="form.dtend_time" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>地点 (LOCATION)</label>
|
||||||
|
<input type="text" v-model="form.location" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>描述 (DESCRIPTION)</label>
|
||||||
|
<textarea v-model="form.description"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RRULE -->
|
||||||
|
<div class="section-title">重复规则 (RRULE)</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="form.is_recurring" /> 启用重复
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="form.is_recurring">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>频率</label>
|
||||||
|
<select v-model="form.freq">
|
||||||
|
<option value="WEEKLY">每周 (WEEKLY)</option>
|
||||||
|
<option value="DAILY">每天 (DAILY)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>间隔 (INTERVAL)</label>
|
||||||
|
<input type="number" v-model.number="form.interval" min="1" style="width: 80px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>重复到 (UNTIL)</label>
|
||||||
|
<input type="date" v-model="form.until_date" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group" v-if="form.freq === 'WEEKLY'">
|
||||||
|
<label>星期 (BYDAY)</label>
|
||||||
|
<select v-model="form.byday">
|
||||||
|
<option v-for="(d, i) in DOW_EN" :key="d" :value="d">
|
||||||
|
{{ ['周日','周一','周二','周三','周四','周五','周六'][i] }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Occurrence grid -->
|
||||||
|
<div v-if="form.is_recurring" class="section-title">occurrence / 排除日期</div>
|
||||||
|
<OccurrenceGrid
|
||||||
|
v-if="form.is_recurring && selectedEvent.rrule"
|
||||||
|
:event="selectedEvent"
|
||||||
|
@toggle="onToggleOcc"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<button class="btn btn-primary" @click="applyChanges">应用更改</button>
|
||||||
|
<button
|
||||||
|
v-if="selectedEvent.rrule"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
@click="showSplit = !showSplit"
|
||||||
|
>
|
||||||
|
✂ 拆分此日程
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" @click="onDelete">删除此日程</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Split panel -->
|
||||||
|
<div v-if="showSplit && selectedEvent.rrule" class="split-panel">
|
||||||
|
<p>从指定日期起,把此重复事件拆成前后两段。</p>
|
||||||
|
<input type="date" v-model="splitDate" />
|
||||||
|
<button class="btn btn-primary" @click="onSplit">确认拆分</button>
|
||||||
|
<button class="btn btn-secondary" @click="showSplit = false">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail { padding: 24px 32px; }
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 20px 0 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
.form-group { margin-bottom: 16px; }
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #34495e;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d5dbdb;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.form-group textarea { resize: vertical; min-height: 60px; }
|
||||||
|
.form-row { display: flex; gap: 16px; }
|
||||||
|
.form-row .form-group { flex: 1; }
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 7px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.btn-primary { background: #3498db; color: #fff; }
|
||||||
|
.btn-primary:hover { background: #2980b9; }
|
||||||
|
.btn-secondary { background: #ecf0f1; color: #2c3e50; }
|
||||||
|
.btn-secondary:hover { background: #d5dbdb; }
|
||||||
|
.btn-danger { background: #e74c3c; color: #fff; }
|
||||||
|
.btn-danger:hover { background: #c0392b; }
|
||||||
|
.split-panel {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.split-panel p { width: 100%; font-size: 13px; color: #7f8c8d; margin-bottom: 8px; }
|
||||||
|
.split-panel input { padding: 6px 10px; border: 1px solid #d5dbdb; border-radius: 6px; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user