up
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "管理"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +151,15 @@ 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>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<RouterLink
|
||||||
|
v-if="isCalendarAdmin"
|
||||||
|
to="/calendars/admin"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<IconSettings :size="16" />
|
||||||
|
{{ t('calendar.admin_panel') }}
|
||||||
|
</RouterLink>
|
||||||
<button
|
<button
|
||||||
@click="openCreateModal"
|
@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"
|
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"
|
||||||
@@ -156,6 +168,7 @@ onMounted(fetchCalendars)
|
|||||||
{{ t('calendar.create_calendar') }}
|
{{ t('calendar.create_calendar') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Calendar List -->
|
<!-- Calendar List -->
|
||||||
<div class="px-6 py-3">
|
<div class="px-6 py-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user