up
This commit is contained in:
@@ -66,3 +66,21 @@
|
|||||||
- 新增 `showDeleteModal` + `deletingCalendar` 状态
|
- 新增 `showDeleteModal` + `deletingCalendar` 状态
|
||||||
- `deleteCalendar()` 改为打开确认弹窗,`confirmDelete()` 执行实际删除 API
|
- `deleteCalendar()` 改为打开确认弹窗,`confirmDelete()` 执行实际删除 API
|
||||||
- i18n 新增 `calendar.confirm_delete_message`:zh-CN「确定要删除日历「{name}」吗?此操作不可撤销。」,en 英文版
|
- i18n 新增 `calendar.confirm_delete_message`:zh-CN「确定要删除日历「{name}」吗?此操作不可撤销。」,en 英文版
|
||||||
|
|
||||||
|
## 新增日历管理页面 /calendars/admin
|
||||||
|
|
||||||
|
**功能**:系统管理员查看所有日历列表,包含日程数量、创建者、创建时间。
|
||||||
|
|
||||||
|
**新增文件**:
|
||||||
|
- `src/views/calendar/CalendarAdminList.vue` - 日历管理列表组件
|
||||||
|
|
||||||
|
**路由修改** `src/router/index.js`:
|
||||||
|
- 新增 `/calendars/admin` 路由,指向 `CalendarAdminList.vue`
|
||||||
|
- 设置 `meta: { requireSysAdmin: true }` 要求管理员权限
|
||||||
|
|
||||||
|
**SysAdminView.vue**:
|
||||||
|
- `tabs` 数组新增 `{ id: 'calendar', label: t('calendar.admin_title'), to: '/calendars/admin' }`
|
||||||
|
|
||||||
|
**i18n 新增**:
|
||||||
|
- `zh-CN.json`: `calendar.admin_title = "日历管理"`, `calendar.event_count = "日程数量"`
|
||||||
|
- `en.json`: `calendar.admin_title = "Calendar Admin"`, `calendar.event_count = "Event Count"`
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ type fromDeleteCalendarEvent struct {
|
|||||||
ID uint `json:"id" binding:"required"`
|
ID uint `json:"id" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fromRestoreCalendar struct {
|
||||||
|
ID uint `json:"id" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
calendarUserGroup TabUserGroups
|
calendarUserGroup TabUserGroups
|
||||||
calendarAdmins []uint
|
calendarAdmins []uint
|
||||||
@@ -280,6 +284,72 @@ func ApiCalendar(r *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取所有日历(包括已删除的,系统管理员专用)
|
||||||
|
r.POST("/calendar/list_all", func(ctx *gin.Context) {
|
||||||
|
isAuth, _, _ := AuthenticationAuthority(ctx)
|
||||||
|
if !isAuth {
|
||||||
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 Unscoped 查询所有日历(包括软删除的)
|
||||||
|
var calendars []TabCalendar
|
||||||
|
models.DB.Unscoped().Order("created_at DESC").Find(&calendars)
|
||||||
|
|
||||||
|
type CalendarWithEdit struct {
|
||||||
|
TabCalendar
|
||||||
|
CanEdit bool `json:"canEdit"`
|
||||||
|
}
|
||||||
|
var result []CalendarWithEdit
|
||||||
|
for _, cal := range calendars {
|
||||||
|
result = append(result, CalendarWithEdit{
|
||||||
|
TabCalendar: cal,
|
||||||
|
CanEdit: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ReturnJson(ctx, "apiOK", gin.H{"list": result})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 恢复已删除的日历
|
||||||
|
r.POST("/calendar/restore", func(ctx *gin.Context) {
|
||||||
|
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||||
|
if !isAuth {
|
||||||
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var from fromRestoreCalendar
|
||||||
|
if err := mapstructure.Decode(data, &from); err != nil {
|
||||||
|
ReturnJson(ctx, "jsonErr", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 Unscoped 查询(包括软删除的)
|
||||||
|
var calendar TabCalendar
|
||||||
|
if models.DB.Unscoped().Where("id = ?", from.ID).First(&calendar).Error != nil {
|
||||||
|
ReturnJson(ctx, "calendar_not_find", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复软删除(将 deleted_at 设为 NULL)
|
||||||
|
if models.DB.Unscoped().Model(&calendar).Update("deleted_at", nil).Error != nil {
|
||||||
|
ReturnJson(ctx, "apiErr", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
newContent, _ := json.Marshal(calendar)
|
||||||
|
log := TabCalendarLog{
|
||||||
|
CalendarID: calendar.ID,
|
||||||
|
UserID: user.ID,
|
||||||
|
ActionType: "restore",
|
||||||
|
NewContent: string(newContent),
|
||||||
|
IP: ctx.ClientIP(),
|
||||||
|
}
|
||||||
|
models.DB.Create(&log)
|
||||||
|
ReturnJson(ctx, "apiOK", nil)
|
||||||
|
})
|
||||||
|
|
||||||
// 删除日历
|
// 删除日历
|
||||||
r.POST("/calendar/delete", func(ctx *gin.Context) {
|
r.POST("/calendar/delete", func(ctx *gin.Context) {
|
||||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ export const calendarApi = {
|
|||||||
return api.post('/calendar/calendar/list', {})
|
return api.post('/calendar/calendar/list', {})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取所有日历(包括已删除的,系统管理员专用)
|
||||||
|
getAllCalendars() {
|
||||||
|
return api.post('/calendar/calendar/list_all', {})
|
||||||
|
},
|
||||||
|
|
||||||
// 更新日历
|
// 更新日历
|
||||||
updateCalendar(data) {
|
updateCalendar(data) {
|
||||||
return api.post('/calendar/calendar/update', data)
|
return api.post('/calendar/calendar/update', data)
|
||||||
@@ -21,6 +26,11 @@ export const calendarApi = {
|
|||||||
return api.post('/calendar/calendar/delete', { id })
|
return api.post('/calendar/calendar/delete', { id })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 恢复已删除的日历
|
||||||
|
restoreCalendar(id) {
|
||||||
|
return api.post('/calendar/calendar/restore', { id })
|
||||||
|
},
|
||||||
|
|
||||||
// 获取日历事件
|
// 获取日历事件
|
||||||
getEvents(data) {
|
getEvents(data) {
|
||||||
return api.post('/calendar/calendar/events', data)
|
return api.post('/calendar/calendar/events', data)
|
||||||
|
|||||||
@@ -643,6 +643,13 @@
|
|||||||
"event_save_success": "Event saved successfully",
|
"event_save_success": "Event saved successfully",
|
||||||
"event_delete_success": "Event deleted successfully",
|
"event_delete_success": "Event deleted successfully",
|
||||||
"confirm_delete_event": "Are you sure you want to delete this event?",
|
"confirm_delete_event": "Are you sure you want to delete this event?",
|
||||||
"is_public_event": "Public Event"
|
"is_public_event": "Public Event",
|
||||||
|
"admin_title": "Calendar Admin",
|
||||||
|
"event_count": "Event Count",
|
||||||
|
"restore": "Restore",
|
||||||
|
"restore_success": "Restored successfully",
|
||||||
|
"deleted": "Deleted",
|
||||||
|
"confirm_restore": "Are you sure you want to restore this calendar?",
|
||||||
|
"confirm_restore_message": "Are you sure you want to restore calendar '{name}'?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,6 +644,13 @@
|
|||||||
"event_save_success": "事件保存成功",
|
"event_save_success": "事件保存成功",
|
||||||
"event_delete_success": "事件删除成功",
|
"event_delete_success": "事件删除成功",
|
||||||
"confirm_delete_event": "确定要删除此事件吗?",
|
"confirm_delete_event": "确定要删除此事件吗?",
|
||||||
"is_public_event": "公共日程"
|
"is_public_event": "公共日程",
|
||||||
|
"admin_title": "日历管理",
|
||||||
|
"event_count": "日程数量",
|
||||||
|
"restore": "恢复",
|
||||||
|
"restore_success": "恢复成功",
|
||||||
|
"deleted": "已删除",
|
||||||
|
"confirm_restore": "确定要恢复此日历吗?",
|
||||||
|
"confirm_restore_message": "确定要恢复日历「{name}」吗?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ const router = createRouter({
|
|||||||
name: 'customer-edit',
|
name: 'customer-edit',
|
||||||
component: () => import('@/views/customer/CustomerFormPage.vue'),
|
component: () => import('@/views/customer/CustomerFormPage.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'calendars/admin',
|
||||||
|
name: 'calendars-admin',
|
||||||
|
component: () => import('@/views/calendar/CalendarAdminList.vue'),
|
||||||
|
meta: { requireSysAdmin: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'calendars',
|
path: 'calendars',
|
||||||
name: 'calendars',
|
name: 'calendars',
|
||||||
|
|||||||
@@ -0,0 +1,464 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import { calendarApi } from '@/api/calendar'
|
||||||
|
import { useUsersStore } from '@/stores/users'
|
||||||
|
import { IconCalendar, IconClock, IconEdit, IconRestore } from '@tabler/icons-vue'
|
||||||
|
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||||
|
|
||||||
|
usePageTitle('calendar.admin_title')
|
||||||
|
const { t } = useI18n()
|
||||||
|
const toast = useToastStore()
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
|
||||||
|
const calendars = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const eventCounts = ref({}) // calendarId -> event count
|
||||||
|
|
||||||
|
// 编辑相关
|
||||||
|
const showEditModal = ref(false)
|
||||||
|
const editingCalendar = ref(null)
|
||||||
|
const showDeleteModal = ref(false)
|
||||||
|
const deletingCalendar = ref(null)
|
||||||
|
const showRestoreModal = ref(false)
|
||||||
|
const restoringCalendar = ref(null)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
color: '#3788d9',
|
||||||
|
is_public: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const colorOptions = [
|
||||||
|
'#3788d9',
|
||||||
|
'#06d6a0',
|
||||||
|
'#ff595e',
|
||||||
|
'#ffca3a',
|
||||||
|
'#8b5cf6',
|
||||||
|
'#ec4899'
|
||||||
|
]
|
||||||
|
|
||||||
|
async function fetchCalendars() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 使用 getAllCalendars 获取所有日历(包括已删除的)
|
||||||
|
const { errCode, data } = await calendarApi.getAllCalendars()
|
||||||
|
if (errCode === 0) {
|
||||||
|
calendars.value = data.list || []
|
||||||
|
// 预加载创建者信息
|
||||||
|
calendars.value.forEach(cal => {
|
||||||
|
if (cal.UserID) {
|
||||||
|
usersStore.fetchUser(cal.UserID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 获取每个日历的事件数量
|
||||||
|
fetchEventCounts()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return usersStore.getUsernameFromUserID(userID) || '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCreatorAvatar(userID) {
|
||||||
|
return usersStore.getAvatarUrlFromUserID(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(dateStr) {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return date.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断日历是否已删除(deleted_at 不为 NULL)
|
||||||
|
function isDeleted(calendar) {
|
||||||
|
return calendar.DeletedAt !== null && calendar.DeletedAt !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(calendar) {
|
||||||
|
editingCalendar.value = calendar
|
||||||
|
form.value = {
|
||||||
|
name: calendar.Name,
|
||||||
|
description: calendar.Description || '',
|
||||||
|
color: calendar.Color,
|
||||||
|
is_public: calendar.IsPublic
|
||||||
|
}
|
||||||
|
showEditModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCalendar() {
|
||||||
|
if (!form.value.name.trim()) {
|
||||||
|
toast.error(t('calendar.name_required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { errCode } = await calendarApi.updateCalendar({
|
||||||
|
id: editingCalendar.value.ID,
|
||||||
|
...form.value
|
||||||
|
})
|
||||||
|
if (errCode === 0) {
|
||||||
|
toast.success(t('calendar.update_success'))
|
||||||
|
showEditModal.value = false
|
||||||
|
fetchCalendars()
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteModal(calendar) {
|
||||||
|
deletingCalendar.value = calendar
|
||||||
|
showDeleteModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (!deletingCalendar.value) return
|
||||||
|
try {
|
||||||
|
const { errCode } = await calendarApi.deleteCalendar(deletingCalendar.value.ID)
|
||||||
|
if (errCode === 0) {
|
||||||
|
toast.success(t('calendar.delete_success'))
|
||||||
|
fetchCalendars()
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
} finally {
|
||||||
|
showDeleteModal.value = false
|
||||||
|
deletingCalendar.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRestoreModal(calendar) {
|
||||||
|
restoringCalendar.value = calendar
|
||||||
|
showRestoreModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRestore() {
|
||||||
|
if (!restoringCalendar.value) return
|
||||||
|
try {
|
||||||
|
const { errCode } = await calendarApi.restoreCalendar(restoringCalendar.value.ID)
|
||||||
|
if (errCode === 0) {
|
||||||
|
toast.success(t('calendar.restore_success'))
|
||||||
|
fetchCalendars()
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 拦截器已处理
|
||||||
|
} finally {
|
||||||
|
showRestoreModal.value = false
|
||||||
|
restoringCalendar.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchCalendars)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 p-6 dark:bg-dk-base">
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-dk-text">{{ t('calendar.admin_title') }}</h1>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ t('calendar.calendars') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="fetchCalendars"
|
||||||
|
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
<IconClock :size="16" />
|
||||||
|
{{ t('sysadmin.refresh') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar List Table -->
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||||
|
<svg class="h-6 w-6 animate-spin text-blue-500" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
<span class="ml-2 text-gray-500 dark:text-dk-subtle">{{ t('calendar.loading') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty -->
|
||||||
|
<div v-else-if="calendars.length === 0" class="py-12 text-center">
|
||||||
|
<IconCalendar :size="48" class="mx-auto mb-3 text-gray-300 dark:text-dk-muted" />
|
||||||
|
<p class="text-gray-500 dark:text-dk-subtle">{{ t('calendar.no_calendars') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div v-else class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-dk-muted">
|
||||||
|
<thead class="bg-gray-50 dark:bg-dk-base">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ t('calendar.name') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ t('schedule.event_type') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ t('calendar.event_count') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ t('customer.created_by') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ t('customer.created_at') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ t('common.actions') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-dk-muted">
|
||||||
|
<tr
|
||||||
|
v-for="calendar in calendars"
|
||||||
|
:key="calendar.ID"
|
||||||
|
class="hover:bg-gray-50 dark:hover:bg-dk-base"
|
||||||
|
:class="{ 'opacity-50': isDeleted(calendar) }"
|
||||||
|
>
|
||||||
|
<!-- Name + Color -->
|
||||||
|
<td class="whitespace-nowrap px-6 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="h-4 w-4 rounded-full flex-shrink-0"
|
||||||
|
:style="{ backgroundColor: calendar.Color || '#3788d9' }"
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 font-medium text-gray-900 dark:text-dk-text">
|
||||||
|
{{ calendar.Name }}
|
||||||
|
<span
|
||||||
|
v-if="isDeleted(calendar)"
|
||||||
|
class="inline-flex rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400"
|
||||||
|
>
|
||||||
|
{{ t('calendar.deleted') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="calendar.Description" class="text-sm text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ calendar.Description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Public/Private -->
|
||||||
|
<td class="whitespace-nowrap px-6 py-4">
|
||||||
|
<span
|
||||||
|
class="inline-flex rounded-full px-2 py-1 text-xs font-medium"
|
||||||
|
:class="calendar.IsPublic
|
||||||
|
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
|
||||||
|
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'"
|
||||||
|
>
|
||||||
|
{{ calendar.IsPublic ? t('calendar.public') : t('calendar.private') }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Event Count -->
|
||||||
|
<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">
|
||||||
|
<IconCalendar :size="16" class="text-gray-400" />
|
||||||
|
<span class="font-medium">{{ eventCounts[calendar.ID] || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Creator -->
|
||||||
|
<td class="whitespace-nowrap px-6 py-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
:src="getCreatorAvatar(calendar.UserID)"
|
||||||
|
class="h-6 w-6 rounded-full"
|
||||||
|
alt="avatar"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-900 dark:text-dk-text">
|
||||||
|
{{ getCreatorName(calendar.UserID) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Created At -->
|
||||||
|
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ formatDateTime(calendar.CreatedAt) }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<td class="whitespace-nowrap px-6 py-4">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
v-if="isDeleted(calendar)"
|
||||||
|
@click="openRestoreModal(calendar)"
|
||||||
|
class="rounded p-1.5 text-gray-400 transition-colors hover:bg-green-50 hover:text-green-600 dark:hover:bg-dk-muted"
|
||||||
|
:title="t('calendar.restore')"
|
||||||
|
>
|
||||||
|
<IconRestore :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
@click="openEditModal(calendar)"
|
||||||
|
class="rounded p-1.5 text-gray-400 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-dk-muted"
|
||||||
|
:title="t('common.edit')"
|
||||||
|
>
|
||||||
|
<IconEdit :size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary -->
|
||||||
|
<div v-if="calendars.length > 0" class="mt-4 text-sm text-gray-500 dark:text-dk-subtle">
|
||||||
|
{{ t('calendar.calendars') }}: {{ calendars.length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Calendar Modal -->
|
||||||
|
<div
|
||||||
|
v-if="showEditModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
@click.self="showEditModal = false"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl dark:bg-dk-card">
|
||||||
|
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('calendar.edit_calendar') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.name') }} *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
:placeholder="t('calendar.name_placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.description') }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
rows="3"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition-colors focus:border-blue-500 dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
:placeholder="t('calendar.description_placeholder')"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.color') }}
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
v-for="color in colorOptions"
|
||||||
|
:key="color"
|
||||||
|
@click="form.color = color"
|
||||||
|
class="h-8 w-8 rounded-full transition-transform hover:scale-110"
|
||||||
|
:class="{ 'ring-2 ring-blue-500 ring-offset-2': form.color === color }"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="edit_is_public"
|
||||||
|
v-model="form.is_public"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label for="edit_is_public" class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('calendar.is_public') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
@click="showEditModal = false"
|
||||||
|
class="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:text-gray-300 dark:hover:bg-dk-muted"
|
||||||
|
>
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="updateCalendar"
|
||||||
|
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
{{ t('save') }}
|
||||||
|
</button>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Restore Confirm Dialog -->
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model="showRestoreModal"
|
||||||
|
:title="t('calendar.confirm_restore')"
|
||||||
|
:message="t('calendar.confirm_restore_message', { name: restoringCalendar?.Name || '' })"
|
||||||
|
:confirm-text="t('calendar.restore')"
|
||||||
|
:cancel-text="t('cancel')"
|
||||||
|
@confirm="confirmRestore"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -31,6 +31,7 @@ const tabs = [
|
|||||||
{ id: 'logs', label: t('sysadmin.tab_logs') },
|
{ id: 'logs', label: t('sysadmin.tab_logs') },
|
||||||
{ id: 'operation_logs', label: t('sysadmin.tab_operation_logs') },
|
{ id: 'operation_logs', label: t('sysadmin.tab_operation_logs') },
|
||||||
{ id: 'customer', label: t('customer.title'), to: '/customer' },
|
{ id: 'customer', label: t('customer.title'), to: '/customer' },
|
||||||
|
{ id: 'calendar', label: t('calendar.admin_title'), to: '/calendars/admin' },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function fetchSysAdmins() {
|
async function fetchSysAdmins() {
|
||||||
|
|||||||
Reference in New Issue
Block a user