up
This commit is contained in:
@@ -42,3 +42,27 @@
|
|||||||
**前端**:添加 `getColorByScheduleType()` 函数,`getEvents` 中使用 scheduleType 映射颜色。
|
**前端**:添加 `getColorByScheduleType()` 函数,`getEvents` 中使用 scheduleType 映射颜色。
|
||||||
|
|
||||||
**后端**:只存储 ScheduleType,不处理颜色逻辑。颜色完全由前端 `colorOptions` 控制。
|
**后端**:只存储 ScheduleType,不处理颜色逻辑。颜色完全由前端 `colorOptions` 控制。
|
||||||
|
|
||||||
|
## CalendarDetail 滚动标题快照对比
|
||||||
|
|
||||||
|
**功能**:每次 getEvents 存快照,数据变化或宽度变化时重新计算标题滚动。
|
||||||
|
|
||||||
|
**实现**:
|
||||||
|
- `pageData.lastEventsSnapshot`:存储上一次的 JSON 快照
|
||||||
|
- `getEvents()` 末尾对比快照,变化则 `setTimeout(recalcScrollTitles, 150)`
|
||||||
|
- `ResizeObserver` 监听日历容器宽度变化,防抖 150ms 后重算
|
||||||
|
- `applyScrollToTitle()`:清除旧状态 → 测量 overflow → 设置 `--scroll-distance` 和 `data-truncated` 属性
|
||||||
|
|
||||||
|
## CalendarList 编辑/删除按钮改用 canEdit
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
- `CalendarList.vue`:编辑/删除按钮 `v-if` 条件从 `calendar.UserID === userStore.userInfo?.ID` 改为 `calendar.canEdit`
|
||||||
|
- 移除废弃的 `useUserStore` 导入
|
||||||
|
|
||||||
|
## CalendarList 删除改用 ConfirmDialog 组件
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
- `CalendarList.vue` 导入并使用 `ConfirmDialog` 组件
|
||||||
|
- 新增 `showDeleteModal` + `deletingCalendar` 状态
|
||||||
|
- `deleteCalendar()` 改为打开确认弹窗,`confirmDelete()` 执行实际删除 API
|
||||||
|
- i18n 新增 `calendar.confirm_delete_message`:zh-CN「确定要删除日历「{name}」吗?此操作不可撤销。」,en 英文版
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* 或者作为组件使用 v-model:
|
* 或者作为组件使用 v-model:
|
||||||
* <ConfirmDialog v-model="show" @confirm="..." @cancel="..." />
|
* <ConfirmDialog v-model="show" @confirm="..." @cancel="..." />
|
||||||
*/
|
*/
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch, onMounted, getCurrentInstance } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -47,7 +47,11 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(["update:modelValue", "confirm", "cancel"]);
|
const emit = defineEmits(["update:modelValue", "confirm", "cancel"]);
|
||||||
|
|
||||||
const { t } = useI18n();
|
// 优先用组件内 i18n,Teleport 场景下可能为空,从全局补足
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const { t: componentT } = useI18n();
|
||||||
|
const globalT = instance?.appContext?.config?.globalProperties?.$i18n?.t;
|
||||||
|
const t = (key, ...args) => componentT(key, ...args) || globalT?.(key, ...args) || key;
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
emit("update:modelValue", false);
|
emit("update:modelValue", false);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"delete": "Delete",
|
||||||
|
"cancel": "Cancel",
|
||||||
"common": {
|
"common": {
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@@ -625,6 +627,7 @@
|
|||||||
"update_success": "Calendar updated successfully",
|
"update_success": "Calendar updated successfully",
|
||||||
"delete_success": "Calendar deleted successfully",
|
"delete_success": "Calendar deleted successfully",
|
||||||
"confirm_delete": "Are you sure you want to delete this calendar?",
|
"confirm_delete": "Are you sure you want to delete this calendar?",
|
||||||
|
"confirm_delete_message": "Are you sure you want to delete calendar '{name}'? This action cannot be undone.",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"add_event": "Add Event",
|
"add_event": "Add Event",
|
||||||
"edit_event": "Edit Event",
|
"edit_event": "Edit Event",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"delete": "删除",
|
||||||
|
"cancel": "取消",
|
||||||
"common": {
|
"common": {
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
@@ -625,6 +627,7 @@
|
|||||||
"update_success": "更新成功",
|
"update_success": "更新成功",
|
||||||
"delete_success": "删除成功",
|
"delete_success": "删除成功",
|
||||||
"confirm_delete": "确定要删除此日历吗?",
|
"confirm_delete": "确定要删除此日历吗?",
|
||||||
|
"confirm_delete_message": "确定要删除日历「{name}」吗?此操作不可撤销。",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"add_event": "添加事件",
|
"add_event": "添加事件",
|
||||||
"edit_event": "编辑事件",
|
"edit_event": "编辑事件",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useUserStore } from "@/stores/user"
|
|||||||
import { calendarApi } from "@/api/calendar"
|
import { calendarApi } from "@/api/calendar"
|
||||||
import { useDateUtils } from "@/composables/useDateUtils"
|
import { useDateUtils } from "@/composables/useDateUtils"
|
||||||
import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCalendar.vue"
|
import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCalendar.vue"
|
||||||
|
import ConfirmDialog from "@/components/ConfirmDialog.vue"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -62,6 +63,8 @@ const pageData = ref({
|
|||||||
lastEventsSnapshot: null,
|
lastEventsSnapshot: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showDeleteModal = ref(false)
|
||||||
|
|
||||||
// 选中/取消选中事件
|
// 选中/取消选中事件
|
||||||
function unseleEvent(eventID) {
|
function unseleEvent(eventID) {
|
||||||
const target = calendarOptions.value.events.find(item => item.id === eventID)
|
const target = calendarOptions.value.events.find(item => item.id === eventID)
|
||||||
@@ -187,19 +190,23 @@ async function saveEvent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteEvent() {
|
async function deleteEvent() {
|
||||||
if (!confirm(t('calendar.confirm_delete_event'))) return
|
showDeleteModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeleteEvent() {
|
||||||
try {
|
try {
|
||||||
const result = await calendarApi.deleteEvent(eventData.value.id)
|
const result = await calendarApi.deleteEvent(eventData.value.id)
|
||||||
if (result.errCode === 0) {
|
if (result.errCode === 0) {
|
||||||
toast.success(t('calendar.event_delete_success'))
|
toast.success(t("calendar.event_delete_success"))
|
||||||
closeEventModal()
|
closeEventModal()
|
||||||
getEvents()
|
getEvents()
|
||||||
} else {
|
} else {
|
||||||
toast.error(t('message.server_error'))
|
toast.error(t("message.server_error"))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 拦截器已处理
|
// 拦截器已处理
|
||||||
|
} finally {
|
||||||
|
showDeleteModal.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,12 +326,20 @@ const calendarOptions = ref({
|
|||||||
},
|
},
|
||||||
|
|
||||||
headerToolbar: {
|
headerToolbar: {
|
||||||
left: "prevYear,prev,today,next,nextYear",
|
left: "backToList,prevYear,prev,today,next,nextYear",
|
||||||
center: "title",
|
center: "myTitle",
|
||||||
right: "",
|
right: "title",
|
||||||
},
|
},
|
||||||
|
|
||||||
customButtons: {
|
customButtons: {
|
||||||
|
backToList: {
|
||||||
|
text: t("calendar.calendars"),
|
||||||
|
click() { router.push("/calendars") },
|
||||||
|
},
|
||||||
|
myTitle: {
|
||||||
|
text: calendarInfo.value?.Name || "",
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
prevYear: {
|
prevYear: {
|
||||||
text: t("schedule.previous_year"),
|
text: t("schedule.previous_year"),
|
||||||
click() { calendarRef.value.getApi().prevYear(); getEvents() },
|
click() { calendarRef.value.getApi().prevYear(); getEvents() },
|
||||||
@@ -439,9 +454,15 @@ const calendarOptions = ref({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听日历信息变化
|
||||||
|
watch(calendarInfo, () => {
|
||||||
|
calendarOptions.value.customButtons.myTitle.text = calendarInfo.value?.Name || ""
|
||||||
|
})
|
||||||
|
|
||||||
// 监听语言变化
|
// 监听语言变化
|
||||||
watch(locale, () => {
|
watch(locale, () => {
|
||||||
calendarOptions.value.locale = locale.value
|
calendarOptions.value.locale = locale.value
|
||||||
|
calendarOptions.value.customButtons.backToList.text = t("calendar.calendars")
|
||||||
calendarOptions.value.customButtons.prevYear.text = t("schedule.previous_year")
|
calendarOptions.value.customButtons.prevYear.text = t("schedule.previous_year")
|
||||||
calendarOptions.value.customButtons.nextYear.text = t("schedule.next_year")
|
calendarOptions.value.customButtons.nextYear.text = t("schedule.next_year")
|
||||||
calendarOptions.value.customButtons.prev.text = t("schedule.previous_month")
|
calendarOptions.value.customButtons.prev.text = t("schedule.previous_month")
|
||||||
@@ -674,9 +695,35 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirm Dialog -->
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model="showDeleteModal"
|
||||||
|
:title="t('calendar.delete_event')"
|
||||||
|
:message="t('calendar.confirm_delete_event')"
|
||||||
|
:confirm-text="t('delete')"
|
||||||
|
:cancel-text="t('cancel')"
|
||||||
|
danger
|
||||||
|
@confirm="confirmDeleteEvent"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 日历名称按钮样式 */
|
||||||
|
:deep(.fc-myTitle-button) {
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
cursor: default !important;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
:deep(.fc-myTitle-button:disabled) {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* 父容器作为裁剪视口 */
|
/* 父容器作为裁剪视口 */
|
||||||
:deep(.fc-daygrid-event .fc-event-title-container) {
|
:deep(.fc-daygrid-event .fc-event-title-container) {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
|
|||||||
@@ -5,20 +5,21 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { usePageTitle } from '@/composables/usePageTitle'
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
import { calendarApi } from '@/api/calendar'
|
import { calendarApi } from '@/api/calendar'
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
import { IconPlus, IconCalendar, IconTrash, IconEdit } from '@tabler/icons-vue'
|
import { IconPlus, IconCalendar, IconTrash, IconEdit } from '@tabler/icons-vue'
|
||||||
|
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||||
|
|
||||||
usePageTitle('appname.calendar')
|
usePageTitle('appname.calendar')
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToastStore()
|
const toast = useToastStore()
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
const calendars = ref([])
|
const calendars = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const showCreateModal = ref(false)
|
const showCreateModal = ref(false)
|
||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
|
const showDeleteModal = ref(false)
|
||||||
const editingCalendar = ref(null)
|
const editingCalendar = ref(null)
|
||||||
|
const deletingCalendar = ref(null)
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -112,12 +113,14 @@ async function updateCalendar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCalendar(calendar) {
|
async function deleteCalendar(calendar) {
|
||||||
if (!confirm(t('calendar.confirm_delete'))) {
|
deletingCalendar.value = calendar
|
||||||
return
|
showDeleteModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (!deletingCalendar.value) return
|
||||||
try {
|
try {
|
||||||
const { errCode } = await calendarApi.deleteCalendar(calendar.ID)
|
const { errCode } = await calendarApi.deleteCalendar(deletingCalendar.value.ID)
|
||||||
if (errCode === 0) {
|
if (errCode === 0) {
|
||||||
toast.success(t('calendar.delete_success'))
|
toast.success(t('calendar.delete_success'))
|
||||||
fetchCalendars()
|
fetchCalendars()
|
||||||
@@ -126,6 +129,9 @@ async function deleteCalendar(calendar) {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 拦截器已处理
|
// 拦截器已处理
|
||||||
|
} finally {
|
||||||
|
showDeleteModal.value = false
|
||||||
|
deletingCalendar.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,14 +194,14 @@ onMounted(fetchCalendars)
|
|||||||
|
|
||||||
<div class="flex items-center gap-1" @click.stop>
|
<div class="flex items-center gap-1" @click.stop>
|
||||||
<button
|
<button
|
||||||
v-if="calendar.UserID === userStore.userInfo?.ID"
|
v-if="calendar.canEdit"
|
||||||
@click="openEditModal(calendar)"
|
@click="openEditModal(calendar)"
|
||||||
class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dk-muted"
|
class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dk-muted"
|
||||||
>
|
>
|
||||||
<IconEdit :size="16" />
|
<IconEdit :size="16" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="calendar.UserID === userStore.userInfo?.ID"
|
v-if="calendar.canEdit"
|
||||||
@click="deleteCalendar(calendar)"
|
@click="deleteCalendar(calendar)"
|
||||||
class="rounded p-1 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-dk-muted"
|
class="rounded p-1 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-dk-muted"
|
||||||
>
|
>
|
||||||
@@ -373,4 +379,15 @@ onMounted(fetchCalendars)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirm Dialog -->
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model="showDeleteModal"
|
||||||
|
:title="t('calendar.confirm_delete')"
|
||||||
|
:message="t('calendar.confirm_delete_message', { name: deletingCalendar?.Name || '' })"
|
||||||
|
:confirm-text="t('delete')"
|
||||||
|
:cancel-text="t('cancel')"
|
||||||
|
danger
|
||||||
|
@confirm="confirmDelete"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user