diff --git a/app/Http/Controllers/Auth/Admin/Permission.php b/app/Http/Controllers/Auth/Admin/Permission.php index 3a294c9..16e471c 100644 --- a/app/Http/Controllers/Auth/Admin/Permission.php +++ b/app/Http/Controllers/Auth/Admin/Permission.php @@ -85,7 +85,7 @@ class Permission extends Controller { $validated = $request->validate([ 'title' => 'required|string|max:50', - 'name' => 'required|string|max:100|unique:auth_permissions,name', + 'name' => 'required|string|max:100|unique:auth_permission,name', 'type' => 'required|in:menu,api,button', 'route' => 'nullable|string|max:200', 'component' => 'nullable|string|max:200', @@ -127,7 +127,7 @@ class Permission extends Controller { $validated = $request->validate([ 'title' => 'nullable|string|max:50', - 'name' => 'nullable|string|max:100|unique:auth_permissions,name,' . $id, + 'name' => 'nullable|string|max:100|unique:auth_permission,name,' . $id, 'type' => 'nullable|in:menu,api,button', 'route' => 'nullable|string|max:200', 'component' => 'nullable|string|max:200', diff --git a/app/Http/Controllers/Auth/Admin/Role.php b/app/Http/Controllers/Auth/Admin/Role.php index 310e625..583c545 100644 --- a/app/Http/Controllers/Auth/Admin/Role.php +++ b/app/Http/Controllers/Auth/Admin/Role.php @@ -75,7 +75,7 @@ class Role extends Controller 'sort' => 'nullable|integer|min:0', 'status' => 'nullable|integer|in:0,1', 'permission_ids' => 'nullable|array', - 'permission_ids.*' => 'integer|exists:auth_permissions,id', + 'permission_ids.*' => 'integer|exists:auth_permission,id', ]); $result = $this->roleService->create($validated); @@ -99,7 +99,7 @@ class Role extends Controller 'sort' => 'nullable|integer|min:0', 'status' => 'nullable|integer|in:0,1', 'permission_ids' => 'nullable|array', - 'permission_ids.*' => 'integer|exists:auth_permissions,id', + 'permission_ids.*' => 'integer|exists:auth_permission,id', ]); $result = $this->roleService->update($id, $validated); @@ -171,7 +171,7 @@ class Role extends Controller { $validated = $request->validate([ 'permission_ids' => 'required|array', - 'permission_ids.*' => 'integer|exists:auth_permissions,id', + 'permission_ids.*' => 'integer|exists:auth_permission,id', ]); $this->roleService->assignPermissions($id, $validated['permission_ids']); diff --git a/app/Services/System/DictionaryService.php b/app/Services/System/DictionaryService.php index 6525501..cf46fb4 100644 --- a/app/Services/System/DictionaryService.php +++ b/app/Services/System/DictionaryService.php @@ -46,24 +46,24 @@ class DictionaryService public function getAll(): array { - $cacheKey = 'system:dictionaries:all'; - $dictionaries = Cache::get($cacheKey); + $cacheKey = 'system:dictionary:all'; + $dictionary = Cache::get($cacheKey); - if ($dictionaries === null) { - $dictionaries = Dictionary::where('status', true) + if ($dictionary === null) { + $dictionary = Dictionary::where('status', true) ->with(['activeItems']) ->orderBy('sort') ->get() ->toArray(); - Cache::put($cacheKey, $dictionaries, 3600); + Cache::put($cacheKey, $dictionary, 3600); } - return $dictionaries; + return $dictionary; } public function getById(int $id): ?array { - $cacheKey = 'system:dictionaries:' . $id; + $cacheKey = 'system:dictionary:' . $id; $dictionary = Cache::get($cacheKey); if ($dictionary === null) { @@ -83,7 +83,7 @@ class DictionaryService public function getByCode(string $code): ?array { - $cacheKey = 'system:dictionaries:code:' . $code; + $cacheKey = 'system:dictionary:code:' . $code; $dictionary = Cache::get($cacheKey); if ($dictionary === null) { @@ -131,7 +131,7 @@ class DictionaryService { Validator::make($data, [ 'name' => 'required|string|max:100', - 'code' => 'required|string|max:50|unique:system_dictionaries,code', + 'code' => 'required|string|max:50|unique:system_dictionary,code', ])->validate(); $dictionary = Dictionary::create($data); @@ -146,7 +146,7 @@ class DictionaryService Validator::make($data, [ 'name' => 'sometimes|required|string|max:100', - 'code' => 'sometimes|required|string|max:50|unique:system_dictionaries,code,' . $id, + 'code' => 'sometimes|required|string|max:50|unique:system_dictionary,code,' . $id, ])->validate(); $dictionary->update($data); @@ -186,14 +186,14 @@ class DictionaryService private function clearCache($dictionaryId = null): void { // 清理所有字典列表缓存 - Cache::forget('system:dictionaries:all'); + Cache::forget('system:dictionary:all'); if ($dictionaryId) { // 清理特定字典的缓存 $dictionary = Dictionary::find($dictionaryId); if ($dictionary) { - Cache::forget('system:dictionaries:' . $dictionaryId); - Cache::forget('system:dictionaries:code:' . $dictionary->code); + Cache::forget('system:dictionary:' . $dictionaryId); + Cache::forget('system:dictionary:code:' . $dictionary->code); Cache::forget('system:dictionary:' . $dictionary->code); } } else { @@ -201,7 +201,7 @@ class DictionaryService $codes = Dictionary::pluck('code')->toArray(); foreach ($codes as $code) { Cache::forget('system:dictionary:' . $code); - Cache::forget('system:dictionaries:code:' . $code); + Cache::forget('system:dictionary:code:' . $code); } } } @@ -235,7 +235,7 @@ class DictionaryService public function createItem(array $data): DictionaryItem { Validator::make($data, [ - 'dictionary_id' => 'required|exists:system_dictionaries,id', + 'dictionary_id' => 'required|exists:system_dictionary,id', 'label' => 'required|string|max:100', 'value' => 'required|string|max:100', ])->validate(); @@ -251,7 +251,7 @@ class DictionaryService $item = DictionaryItem::findOrFail($id); Validator::make($data, [ - 'dictionary_id' => 'sometimes|required|exists:system_dictionaries,id', + 'dictionary_id' => 'sometimes|required|exists:system_dictionary,id', 'label' => 'sometimes|required|string|max:100', 'value' => 'sometimes|required|string|max:100', ])->validate(); @@ -316,12 +316,12 @@ class DictionaryService if ($allItems === null) { // 获取所有启用的字典 - $dictionaries = Dictionary::where('status', true) + $dictionary = Dictionary::where('status', true) ->orderBy('sort') ->get(); $result = []; - foreach ($dictionaries as $dictionary) { + foreach ($dictionary as $dictionary) { $items = $dictionary->activeItems->toArray(); // 格式化字典项值 $items = $this->formatItemsByType($items, $dictionary->value_type); diff --git a/resources/admin/src/layouts/components/message.vue b/resources/admin/src/layouts/components/message.vue new file mode 100644 index 0000000..43ab8cb --- /dev/null +++ b/resources/admin/src/layouts/components/message.vue @@ -0,0 +1,499 @@ + + emit('update:visible', val)" :title="$t('common.messages')" placement="right" width="400" :footer="null" class="message-drawer" :bodyStyle="{padding: '0'}" @openChange="handleDrawerOpen"> + + + + + {{ $t('common.markAllAsRead') }} + + + {{ $t('common.clearAll') }} + + + + + + + + + + + + + + + + + + + {{ msg.title }} + + {{ msg.content }} + + + {{ notificationStore.formatNotificationTime(msg.created_at) }} + + + + + + + + + + + + + + + + + + + diff --git a/resources/admin/src/layouts/components/userbar.vue b/resources/admin/src/layouts/components/userbar.vue index 0551bee..bc5da12 100644 --- a/resources/admin/src/layouts/components/userbar.vue +++ b/resources/admin/src/layouts/components/userbar.vue @@ -8,63 +8,13 @@ - + - + - - - - - {{ $t('common.messages') }} - - - {{ $t('common.markAllAsRead') }} - - - {{ $t('common.clearAll') }} - - - - - - - - - - - - - - - - - - - {{ msg.title }} - - {{ msg.content }} - - - {{ notificationStore.formatNotificationTime(msg.created_at) }} - - - - - - - - - - - - - - - - + @@ -133,6 +83,9 @@ + + + @@ -148,6 +101,7 @@ import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, Fullscr import { useI18n } from 'vue-i18n' import search from './search.vue' import task from './task.vue' +import messageDrawer from './message.vue' // 定义组件名称(多词命名) defineOptions({ @@ -164,19 +118,6 @@ const isFullscreen = ref(false) const searchVisible = ref(false) const taskVisible = ref(false) const messageVisible = ref(false) -const currentMessageType = ref('all') -const messagesPage = ref(1) -const messagesPageSize = ref(10) -const notificationsList = ref([]) - -// 未读消息数量 -const messageCount = computed(() => notificationStore.unreadCount) - -// 消息总数(用于分页) -const messagesTotal = computed(() => notificationStore.total) - -// 从 store 获取消息数据 -const messages = computed(() => notificationsList.value) // 任务数据 const tasks = ref([ @@ -205,6 +146,9 @@ const tasks = ref([ const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length) +// 未读消息数量 +const messageCount = computed(() => notificationStore.unreadCount) + // 切换全屏 const toggleFullscreen = () => { if (!document.fullscreenElement) { @@ -221,50 +165,10 @@ const handleFullscreenChange = () => { isFullscreen.value = !!document.fullscreenElement } -// 加载未读通知 -const loadNotifications = async () => { - try { - await notificationStore.fetchUnreadCount() - await loadUnreadNotifications() - } catch (error) { - console.error('加载通知失败:', error) - } -} - -// 加载未读通知列表 -const loadUnreadNotifications = async () => { - try { - const res = await notificationStore.fetchUnreadNotifications({ - page: messagesPage.value, - page_size: messagesPageSize.value, - type: currentMessageType.value === 'all' ? null : currentMessageType.value, - }) - notificationsList.value = res.list || [] - } catch (error) { - console.error('加载未读通知列表失败:', error) - } -} - -// 加载所有通知 -const loadAllNotifications = async () => { - try { - await notificationStore.fetchNotifications({ - page: messagesPage.value, - page_size: messagesPageSize.value, - type: currentMessageType.value === 'all' ? null : currentMessageType.value, - }) - notificationsList.value = notificationStore.notifications - } catch (error) { - console.error('加载通知列表失败:', error) - } -} onMounted(() => { document.addEventListener('fullscreenchange', handleFullscreenChange) - // 加载通知数据 - loadNotifications() - // 注意:WebSocket 已在 App.vue 中统一初始化,这里不需要重复调用 // 只需要处理消息即可,消息处理已经在 useWebSocket 中注册 }) @@ -278,115 +182,6 @@ const showSearch = () => { searchVisible.value = true } -// 清除消息 -const clearMessages = async () => { - Modal.confirm({ - title: t('common.confirmClear'), - content: t('common.confirmClearMessages'), - okText: t('common.confirm'), - cancelText: t('common.cancel'), - onOk: async () => { - try { - await notificationStore.clearReadNotifications() - message.success(t('common.cleared')) - notificationsList.value = [] - } catch (error) { - console.error('清空消息失败:', error) - } - }, - }) -} - -// 标记消息为已读 -const handleMessageRead = async (msg) => { - if (!msg.is_read) { - try { - await notificationStore.markAsRead(msg.id) - // 更新本地状态 - const notification = notificationsList.value.find((n) => n.id === msg.id) - if (notification) { - notification.is_read = true - notification.read_at = new Date().toISOString() - } - } catch (error) { - console.error('标记已读失败:', error) - } - } -} - -// 标记所有消息为已读 -const markAllAsRead = async () => { - Modal.confirm({ - title: '确认全部已读', - content: '确定要将所有消息标记为已读吗?', - okText: '确定', - cancelText: '取消', - onOk: async () => { - try { - await notificationStore.markAllAsRead() - message.success(t('common.markedAsRead')) - // 更新本地状态 - notificationsList.value.forEach((n) => { - n.is_read = true - n.read_at = n.read_at || new Date().toISOString() - }) - } catch (error) { - console.error('标记全部已读失败:', error) - } - }, - }) -} - -// 删除消息 -const handleDeleteMessage = async (msgId) => { - Modal.confirm({ - title: '确认删除', - content: '确定要删除这条消息吗?', - okText: '确定', - cancelText: '取消', - onOk: async () => { - try { - await notificationStore.deleteNotification(msgId) - message.success('删除成功') - // 更新本地状态 - const index = notificationsList.value.findIndex((n) => n.id === msgId) - if (index !== -1) { - notificationsList.value.splice(index, 1) - } - } catch (error) { - console.error('删除消息失败:', error) - } - }, - }) -} - -// 切换消息类型 -const changeMessageType = async (type) => { - currentMessageType.value = type - messagesPage.value = 1 - if (type === 'all') { - await loadAllNotifications() - } else { - await loadUnreadNotifications() - } -} - -// 分页变化 -const handleMessagePageChange = async (page) => { - messagesPage.value = page - if (currentMessageType.value === 'all') { - await loadAllNotifications() - } else { - await loadUnreadNotifications() - } -} - -// 下拉框打开时加载数据 -const handleMessageDropdownOpen = async (open) => { - if (open) { - await loadNotifications() - } -} // 显示任务抽屉 const showTasks = () => { @@ -539,277 +334,4 @@ const handleLogout = () => { } } -.dropdown-card { - width: 400px; - max-height: 540px; - display: flex; - flex-direction: column; - background: #fff; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); - border-radius: 8px; - overflow: hidden; - - :deep(.ant-card-head) { - padding: 12px 16px; - min-height: auto; - border-bottom: 1px solid #e8e8e8; - background: linear-gradient(to bottom, #fafbfc, #fff); - - .ant-card-head-title { - padding: 0; - width: 100%; - } - } - - :deep(.ant-card-body) { - padding: 0; - flex: 1; - overflow: hidden; - display: flex; - flex-direction: column; - } - :deep(.ant-dropdown-menu-body) { - padding: 0; - } -} - -.message-header { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - font-size: 15px; - font-weight: 600; - color: #262626; -} - -.message-tabs { - padding: 10px 16px 8px; - border-bottom: 1px solid #e8e8e8; - background: #fafbfc; - - :deep(.ant-tabs) { - .ant-tabs-nav { - margin-bottom: 0; - &::before { - display: none; - } - } - - .ant-tabs-tab { - padding: 6px 12px; - margin: 0 4px; - font-size: 13px; - font-weight: 500; - border-radius: 6px; - transition: all 0.3s ease; - color: #595959; - - &:hover { - background-color: rgba(24, 144, 255, 0.06); - color: #1890ff; - } - - &.ant-tabs-tab-active { - background-color: #e6f7ff; - color: #1890ff; - font-weight: 600; - } - } - - .ant-tabs-ink-bar { - display: none; - } - } -} - -.message-list { - flex: 1; - overflow-y: auto; - padding: 8px; - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-thumb { - background: #d9d9d9; - border-radius: 3px; - transition: background 0.3s; - - &:hover { - background: #bfbfbf; - } - } - - &::-webkit-scrollbar-track { - background: #f5f5f5; - border-radius: 3px; - } - - .message-item { - display: flex; - align-items: flex-start; - padding: 14px 16px; - margin-bottom: 6px; - border-radius: 8px; - border: 1px solid transparent; - position: relative; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - background: #fff; - - &:hover { - background: #f5f7fa; - border-color: #e8e8e8; - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - - .delete-btn { - opacity: 1; - transform: scale(1); - } - } - - &:last-child { - margin-bottom: 0; - } - - &.unread { - background: linear-gradient(135deg, rgba(24, 144, 255, 0.05) 0%, rgba(24, 144, 255, 0.02) 100%); - border: 1px solid rgba(24, 144, 255, 0.15); - border-left: 4px solid #1890ff; - padding-left: 14px; - - &:hover { - background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(24, 144, 255, 0.04) 100%); - border-color: rgba(24, 144, 255, 0.25); - } - - .message-title { - color: #1890ff; - font-weight: 600; - } - } - } - - .message-content { - flex: 1; - margin-right: 32px; - display: flex; - flex-direction: column; - gap: 6px; - - .message-title { - font-size: 14px; - color: #262626; - font-weight: 500; - line-height: 1.5; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - transition: color 0.3s; - } - - .message-content-text { - font-size: 13px; - color: #595959; - line-height: 1.6; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - word-break: break-word; - } - - .message-time { - font-size: 12px; - color: #8c8c8c; - font-weight: 400; - } - } - - .delete-btn { - position: absolute; - top: 10px; - right: 10px; - width: 28px; - height: 28px; - padding: 0; - border-radius: 6px; - opacity: 0; - transform: scale(0.85); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - display: flex; - align-items: center; - justify-content: center; - background: rgba(0, 0, 0, 0.04); - - &:hover { - background: rgba(255, 77, 79, 0.1); - transform: scale(1); - opacity: 1 !important; - } - } - - &:deep(.ant-empty) { - padding: 40px 20px; - - .ant-empty-description { - color: #8c8c8c; - font-size: 13px; - } - } -} - -.message-pagination { - padding: 10px 16px; - border-top: 1px solid #e8e8e8; - background: #fafbfc; - display: flex; - justify-content: center; - - :deep(.ant-pagination) { - .ant-pagination-item { - border-radius: 6px; - border-color: #d9d9d9; - transition: all 0.3s; - font-weight: 500; - - &:hover { - border-color: #1890ff; - color: #1890ff; - } - - &.ant-pagination-item-active { - background: #1890ff; - border-color: #1890ff; - - a { - color: #fff; - } - } - } - - .ant-pagination-prev, - .ant-pagination-next { - border-radius: 6px; - transition: all 0.3s; - - &:hover { - border-color: #1890ff; - - .ant-pagination-item-link { - color: #1890ff; - border-color: #1890ff; - } - } - - .ant-pagination-item-link { - border-radius: 6px; - } - } - } -} diff --git a/resources/admin/src/pages/auth/permission/components/SaveForm.vue b/resources/admin/src/pages/auth/permission/components/SaveForm.vue index 543a528..c825885 100644 --- a/resources/admin/src/pages/auth/permission/components/SaveForm.vue +++ b/resources/admin/src/pages/auth/permission/components/SaveForm.vue @@ -4,7 +4,7 @@ - + diff --git a/resources/admin/src/pages/system/dictionary/components/DictionaryDialog.vue b/resources/admin/src/pages/system/dictionary/components/DictionaryDialog.vue index ffd5781..97bda4d 100644 --- a/resources/admin/src/pages/system/dictionary/components/DictionaryDialog.vue +++ b/resources/admin/src/pages/system/dictionary/components/DictionaryDialog.vue @@ -3,7 +3,7 @@ - + @@ -36,7 +36,7 @@ - + @@ -155,8 +155,8 @@ const setData = (data) => { code: data.code || '', value_type: data.value_type || 'string', description: data.description || '', - status: data.status !== undefined ? data.status : null, - sort: data.sort !== undefined ? data.sort : 0, + status: data.status !== undefined ? Number(data.status) : null, + sort: data.sort !== undefined ? Number(data.sort) : 0, } } } @@ -174,8 +174,8 @@ const handleSubmit = async () => { code: form.value.code, value_type: form.value.value_type, description: form.value.description, - status: form.value.status, - sort: form.value.sort, + status: form.value.status !== null ? Number(form.value.status) : null, + sort: Number(form.value.sort || 0), } let res = {}