diff --git a/.workbuddy/memory/2026-05-06.md b/.workbuddy/memory/2026-05-06.md
index 4395f28..50d9e60 100644
--- a/.workbuddy/memory/2026-05-06.md
+++ b/.workbuddy/memory/2026-05-06.md
@@ -42,3 +42,27 @@
**前端**:添加 `getColorByScheduleType()` 函数,`getEvents` 中使用 scheduleType 映射颜色。
**后端**:只存储 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 英文版
diff --git a/frontend/ops_vue_js/src/components/ConfirmDialog.vue b/frontend/ops_vue_js/src/components/ConfirmDialog.vue
index 704bae7..31c2289 100644
--- a/frontend/ops_vue_js/src/components/ConfirmDialog.vue
+++ b/frontend/ops_vue_js/src/components/ConfirmDialog.vue
@@ -15,7 +15,7 @@
* 或者作为组件使用 v-model:
*
*/
-import { ref, watch } from "vue";
+import { ref, watch, onMounted, getCurrentInstance } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps({
@@ -47,7 +47,11 @@ const props = defineProps({
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() {
emit("update:modelValue", false);
diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json
index 017c91e..45e1c1c 100644
--- a/frontend/ops_vue_js/src/i18n/en.json
+++ b/frontend/ops_vue_js/src/i18n/en.json
@@ -1,4 +1,6 @@
{
+ "delete": "Delete",
+ "cancel": "Cancel",
"common": {
"actions": "Actions",
"search": "Search",
@@ -625,6 +627,7 @@
"update_success": "Calendar updated successfully",
"delete_success": "Calendar deleted successfully",
"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...",
"add_event": "Add Event",
"edit_event": "Edit Event",
diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json
index d7e8bea..7c28b72 100644
--- a/frontend/ops_vue_js/src/i18n/zh-CN.json
+++ b/frontend/ops_vue_js/src/i18n/zh-CN.json
@@ -1,4 +1,6 @@
{
+ "delete": "删除",
+ "cancel": "取消",
"common": {
"actions": "操作",
"search": "搜索",
@@ -625,6 +627,7 @@
"update_success": "更新成功",
"delete_success": "删除成功",
"confirm_delete": "确定要删除此日历吗?",
+ "confirm_delete_message": "确定要删除日历「{name}」吗?此操作不可撤销。",
"loading": "加载中...",
"add_event": "添加事件",
"edit_event": "编辑事件",
diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue
index ae14464..8d26126 100644
--- a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue
+++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue
@@ -13,6 +13,7 @@ import { useUserStore } from "@/stores/user"
import { calendarApi } from "@/api/calendar"
import { useDateUtils } from "@/composables/useDateUtils"
import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCalendar.vue"
+import ConfirmDialog from "@/components/ConfirmDialog.vue"
const route = useRoute()
const router = useRouter()
@@ -62,6 +63,8 @@ const pageData = ref({
lastEventsSnapshot: null,
})
+const showDeleteModal = ref(false)
+
// 选中/取消选中事件
function unseleEvent(eventID) {
const target = calendarOptions.value.events.find(item => item.id === eventID)
@@ -187,19 +190,23 @@ async function saveEvent() {
}
async function deleteEvent() {
- if (!confirm(t('calendar.confirm_delete_event'))) return
+ showDeleteModal.value = true
+}
+async function confirmDeleteEvent() {
try {
const result = await calendarApi.deleteEvent(eventData.value.id)
if (result.errCode === 0) {
- toast.success(t('calendar.event_delete_success'))
+ toast.success(t("calendar.event_delete_success"))
closeEventModal()
getEvents()
} else {
- toast.error(t('message.server_error'))
+ toast.error(t("message.server_error"))
}
} catch {
// 拦截器已处理
+ } finally {
+ showDeleteModal.value = false
}
}
@@ -319,12 +326,20 @@ const calendarOptions = ref({
},
headerToolbar: {
- left: "prevYear,prev,today,next,nextYear",
- center: "title",
- right: "",
+ left: "backToList,prevYear,prev,today,next,nextYear",
+ center: "myTitle",
+ right: "title",
},
customButtons: {
+ backToList: {
+ text: t("calendar.calendars"),
+ click() { router.push("/calendars") },
+ },
+ myTitle: {
+ text: calendarInfo.value?.Name || "",
+ disabled: true,
+ },
prevYear: {
text: t("schedule.previous_year"),
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, () => {
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.nextYear.text = t("schedule.next_year")
calendarOptions.value.customButtons.prev.text = t("schedule.previous_month")
@@ -674,9 +695,35 @@ onMounted(() => {
+
+
+