This commit is contained in:
2026-05-06 22:20:51 +08:00
parent cd2c90ffb4
commit 4cbb266bec
6 changed files with 70 additions and 49 deletions
+33 -3
View File
@@ -286,27 +286,51 @@ func ApiCalendar(r *gin.RouterGroup) {
} }
}) })
// 获取所有日历(包括已删除的,系统管理员专用) // 获取所有日历(包括已删除的,管理员专用)
r.POST("/calendar/list_all", func(ctx *gin.Context) { r.POST("/calendar/list_all", func(ctx *gin.Context) {
isAuth, _, _ := AuthenticationAuthority(ctx) isAuth, user, _ := AuthenticationAuthority(ctx)
if !isAuth { if !isAuth {
ReturnJson(ctx, "userCookieError", nil) ReturnJson(ctx, "userCookieError", nil)
return return
} }
// 限制只有日历管理员可访问
if !slices.Contains(calendarAdmins, user.ID) {
ReturnJson(ctx, "permission_denied", nil)
return
}
// 使用 Unscoped 查询所有日历(包括软删除的) // 使用 Unscoped 查询所有日历(包括软删除的)
var calendars []TabCalendar var calendars []TabCalendar
models.DB.Unscoped().Order("created_at DESC").Find(&calendars) models.DB.Unscoped().Order("created_at DESC").Find(&calendars)
// 一次性查询所有日历的事件数量(仅统计未删除的事件)
type calendarEventCount struct {
CalendarID uint `gorm:"column:calendar_id"`
Cnt int `gorm:"column:cnt"`
}
var rows []calendarEventCount
models.DB.Model(&TabCalendarEvent{}).
Select("calendar_id, COUNT(*) as cnt").
Where("deleted_at IS NULL").
Group("calendar_id").
Scan(&rows)
eventCountMap := make(map[uint]int)
for _, row := range rows {
eventCountMap[row.CalendarID] = row.Cnt
}
type CalendarWithEdit struct { type CalendarWithEdit struct {
TabCalendar TabCalendar
CanEdit bool `json:"canEdit"` CanEdit bool `json:"canEdit"`
EventCount int `json:"event_count"`
} }
var result []CalendarWithEdit var result []CalendarWithEdit
for _, cal := range calendars { for _, cal := range calendars {
result = append(result, CalendarWithEdit{ result = append(result, CalendarWithEdit{
TabCalendar: cal, TabCalendar: cal,
CanEdit: true, CanEdit: true,
EventCount: eventCountMap[cal.ID],
}) })
} }
ReturnJson(ctx, "apiOK", gin.H{"list": result}) ReturnJson(ctx, "apiOK", gin.H{"list": result})
@@ -320,6 +344,12 @@ func ApiCalendar(r *gin.RouterGroup) {
return return
} }
// 限制只有日历管理员可操作
if !slices.Contains(calendarAdmins, user.ID) {
ReturnJson(ctx, "permission_denied", nil)
return
}
var from fromRestoreCalendar var from fromRestoreCalendar
if err := mapstructure.Decode(data, &from); err != nil { if err := mapstructure.Decode(data, &from); err != nil {
ReturnJson(ctx, "jsonErr", nil) ReturnJson(ctx, "jsonErr", nil)
+4 -2
View File
@@ -444,7 +444,8 @@
"security": "Security", "security": "Security",
"security_description": "Manage your account security settings", "security_description": "Manage your account security settings",
"my_groups": "My Groups", "my_groups": "My Groups",
"no_groups": "Not joined any groups yet" "no_groups": "Not joined any groups yet",
"admin_panel": "Admin"
}, },
"button": { "button": {
"submit": "Submit", "submit": "Submit",
@@ -656,6 +657,7 @@
"restore_success": "Restored successfully", "restore_success": "Restored successfully",
"deleted": "Deleted", "deleted": "Deleted",
"confirm_restore": "Are you sure you want to restore this calendar?", "confirm_restore": "Are you sure you want to restore this calendar?",
"confirm_restore_message": "Are you sure you want to restore calendar '{name}'?" "confirm_restore_message": "Are you sure you want to restore calendar '{name}'?",
"admin_panel": "Admin"
} }
} }
+4 -2
View File
@@ -444,7 +444,8 @@
"security": "安全设置", "security": "安全设置",
"security_description": "管理您的账户安全设置", "security_description": "管理您的账户安全设置",
"my_groups": "我的群组", "my_groups": "我的群组",
"no_groups": "暂未加入任何群组" "no_groups": "暂未加入任何群组",
"admin_panel": "管理"
}, },
"button": { "button": {
"submit": "提交", "submit": "提交",
@@ -657,6 +658,7 @@
"restore_success": "恢复成功", "restore_success": "恢复成功",
"deleted": "已删除", "deleted": "已删除",
"confirm_restore": "确定要恢复此日历吗?", "confirm_restore": "确定要恢复此日历吗?",
"confirm_restore_message": "确定要恢复日历「{name}」吗?" "confirm_restore_message": "确定要恢复日历「{name}」吗?",
"admin_panel": "管理"
} }
} }
+6
View File
@@ -64,6 +64,11 @@ export const useUserStore = defineStore('user', () => {
// 是否系统管理员(后端直接返回) // 是否系统管理员(后端直接返回)
const isSysAdmin = ref(false) const isSysAdmin = ref(false)
// 是否为日历管理员(在 calendar_admin 群组中)
const isCalendarAdmin = computed(() =>
groups.value.some(g => g.name === 'calendar_admin')
)
// 用户加入的群组名称列表(计算属性) // 用户加入的群组名称列表(计算属性)
const groupNames = computed(() => groups.value.map(g => g.name)) const groupNames = computed(() => groups.value.map(g => g.name))
@@ -130,6 +135,7 @@ export const useUserStore = defineStore('user', () => {
userCookie, userCookie,
isLoggedIn, isLoggedIn,
isSysAdmin, isSysAdmin,
isCalendarAdmin,
groups, groups,
groupNames, groupNames,
cookieValue, cookieValue,
@@ -15,7 +15,6 @@ const usersStore = useUsersStore()
const calendars = ref([]) const calendars = ref([])
const loading = ref(false) const loading = ref(false)
const eventCounts = ref({}) // calendarId -> event count
// 编辑相关 // 编辑相关
const showEditModal = ref(false) const showEditModal = ref(false)
@@ -54,8 +53,6 @@ async function fetchCalendars() {
usersStore.fetchUser(cal.UserID) usersStore.fetchUser(cal.UserID)
} }
}) })
// 获取每个日历的事件数量
fetchEventCounts()
} }
} catch { } catch {
// 拦截器已处理 // 拦截器已处理
@@ -64,35 +61,6 @@ async function fetchCalendars() {
} }
} }
async function fetchEventCounts() {
// 获取一年前到现在的事件,用于统计
const now = new Date()
const oneYearAgo = new Date()
oneYearAgo.setFullYear(now.getFullYear() - 1)
const startStr = oneYearAgo.toISOString().split('T')[0]
const endStr = now.toISOString().split('T')[0]
try {
const { errCode, data } = await calendarApi.getEvents({
start_date: startStr,
end_date: endStr
})
if (errCode === 0 && data.list) {
// 按日历ID统计事件数量
const counts = {}
data.list.forEach(event => {
if (event.CalendarID) {
counts[event.CalendarID] = (counts[event.CalendarID] || 0) + 1
}
})
eventCounts.value = counts
}
} catch {
// 忽略错误
}
}
function getCreatorName(userID) { function getCreatorName(userID) {
return usersStore.getUsernameFromUserID(userID) || '...' return usersStore.getUsernameFromUserID(userID) || '...'
} }
@@ -304,7 +272,7 @@ onMounted(fetchCalendars)
<td class="whitespace-nowrap px-6 py-4 text-center"> <td class="whitespace-nowrap px-6 py-4 text-center">
<div class="inline-flex items-center gap-1 text-gray-900 dark:text-dk-text"> <div class="inline-flex items-center gap-1 text-gray-900 dark:text-dk-text">
<IconCalendar :size="16" class="text-gray-400" /> <IconCalendar :size="16" class="text-gray-400" />
<span class="font-medium">{{ eventCounts[calendar.ID] || 0 }}</span> <span class="font-medium">{{ calendar.event_count ?? 0 }}</span>
</div> </div>
</td> </td>
@@ -1,17 +1,20 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' 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 { IconPlus, IconCalendar, IconTrash, IconEdit } from '@tabler/icons-vue' import { IconPlus, IconCalendar, IconTrash, IconEdit, IconSettings } from '@tabler/icons-vue'
import { useUserStore } from '@/stores/user'
import ConfirmDialog from '@/components/ConfirmDialog.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 isCalendarAdmin = computed(() => userStore.isCalendarAdmin)
const calendars = ref([]) const calendars = ref([])
const loading = ref(false) const loading = ref(false)
@@ -148,13 +151,23 @@ onMounted(fetchCalendars)
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between border-b border-gray-100 px-6 py-4 dark:border-dk-muted"> <div class="flex items-center justify-between border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('calendar.calendars') }}</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('calendar.calendars') }}</h3>
<button <div class="flex items-center gap-2">
@click="openCreateModal" <RouterLink
class="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700" v-if="isCalendarAdmin"
> to="/calendars/admin"
<IconPlus :size="16" /> class="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:text-gray-300 dark:hover:bg-dk-muted"
{{ t('calendar.create_calendar') }} >
</button> <IconSettings :size="16" />
{{ t('calendar.admin_panel') }}
</RouterLink>
<button
@click="openCreateModal"
class="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700"
>
<IconPlus :size="16" />
{{ t('calendar.create_calendar') }}
</button>
</div>
</div> </div>
<!-- Calendar List --> <!-- Calendar List -->