更新调整修复

This commit is contained in:
2026-02-19 14:05:30 +08:00
parent f0af965412
commit e26ba12150
7 changed files with 539 additions and 518 deletions

View File

@@ -85,7 +85,7 @@ class Permission extends Controller
{ {
$validated = $request->validate([ $validated = $request->validate([
'title' => 'required|string|max:50', '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', 'type' => 'required|in:menu,api,button',
'route' => 'nullable|string|max:200', 'route' => 'nullable|string|max:200',
'component' => 'nullable|string|max:200', 'component' => 'nullable|string|max:200',
@@ -127,7 +127,7 @@ class Permission extends Controller
{ {
$validated = $request->validate([ $validated = $request->validate([
'title' => 'nullable|string|max:50', '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', 'type' => 'nullable|in:menu,api,button',
'route' => 'nullable|string|max:200', 'route' => 'nullable|string|max:200',
'component' => 'nullable|string|max:200', 'component' => 'nullable|string|max:200',

View File

@@ -75,7 +75,7 @@ class Role extends Controller
'sort' => 'nullable|integer|min:0', 'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1', 'status' => 'nullable|integer|in:0,1',
'permission_ids' => 'nullable|array', 'permission_ids' => 'nullable|array',
'permission_ids.*' => 'integer|exists:auth_permissions,id', 'permission_ids.*' => 'integer|exists:auth_permission,id',
]); ]);
$result = $this->roleService->create($validated); $result = $this->roleService->create($validated);
@@ -99,7 +99,7 @@ class Role extends Controller
'sort' => 'nullable|integer|min:0', 'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1', 'status' => 'nullable|integer|in:0,1',
'permission_ids' => 'nullable|array', 'permission_ids' => 'nullable|array',
'permission_ids.*' => 'integer|exists:auth_permissions,id', 'permission_ids.*' => 'integer|exists:auth_permission,id',
]); ]);
$result = $this->roleService->update($id, $validated); $result = $this->roleService->update($id, $validated);
@@ -171,7 +171,7 @@ class Role extends Controller
{ {
$validated = $request->validate([ $validated = $request->validate([
'permission_ids' => 'required|array', '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']); $this->roleService->assignPermissions($id, $validated['permission_ids']);

View File

@@ -46,24 +46,24 @@ class DictionaryService
public function getAll(): array public function getAll(): array
{ {
$cacheKey = 'system:dictionaries:all'; $cacheKey = 'system:dictionary:all';
$dictionaries = Cache::get($cacheKey); $dictionary = Cache::get($cacheKey);
if ($dictionaries === null) { if ($dictionary === null) {
$dictionaries = Dictionary::where('status', true) $dictionary = Dictionary::where('status', true)
->with(['activeItems']) ->with(['activeItems'])
->orderBy('sort') ->orderBy('sort')
->get() ->get()
->toArray(); ->toArray();
Cache::put($cacheKey, $dictionaries, 3600); Cache::put($cacheKey, $dictionary, 3600);
} }
return $dictionaries; return $dictionary;
} }
public function getById(int $id): ?array public function getById(int $id): ?array
{ {
$cacheKey = 'system:dictionaries:' . $id; $cacheKey = 'system:dictionary:' . $id;
$dictionary = Cache::get($cacheKey); $dictionary = Cache::get($cacheKey);
if ($dictionary === null) { if ($dictionary === null) {
@@ -83,7 +83,7 @@ class DictionaryService
public function getByCode(string $code): ?array public function getByCode(string $code): ?array
{ {
$cacheKey = 'system:dictionaries:code:' . $code; $cacheKey = 'system:dictionary:code:' . $code;
$dictionary = Cache::get($cacheKey); $dictionary = Cache::get($cacheKey);
if ($dictionary === null) { if ($dictionary === null) {
@@ -131,7 +131,7 @@ class DictionaryService
{ {
Validator::make($data, [ Validator::make($data, [
'name' => 'required|string|max:100', 'name' => 'required|string|max:100',
'code' => 'required|string|max:50|unique:system_dictionaries,code', 'code' => 'required|string|max:50|unique:system_dictionary,code',
])->validate(); ])->validate();
$dictionary = Dictionary::create($data); $dictionary = Dictionary::create($data);
@@ -146,7 +146,7 @@ class DictionaryService
Validator::make($data, [ Validator::make($data, [
'name' => 'sometimes|required|string|max:100', '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(); ])->validate();
$dictionary->update($data); $dictionary->update($data);
@@ -186,14 +186,14 @@ class DictionaryService
private function clearCache($dictionaryId = null): void private function clearCache($dictionaryId = null): void
{ {
// 清理所有字典列表缓存 // 清理所有字典列表缓存
Cache::forget('system:dictionaries:all'); Cache::forget('system:dictionary:all');
if ($dictionaryId) { if ($dictionaryId) {
// 清理特定字典的缓存 // 清理特定字典的缓存
$dictionary = Dictionary::find($dictionaryId); $dictionary = Dictionary::find($dictionaryId);
if ($dictionary) { if ($dictionary) {
Cache::forget('system:dictionaries:' . $dictionaryId); Cache::forget('system:dictionary:' . $dictionaryId);
Cache::forget('system:dictionaries:code:' . $dictionary->code); Cache::forget('system:dictionary:code:' . $dictionary->code);
Cache::forget('system:dictionary:' . $dictionary->code); Cache::forget('system:dictionary:' . $dictionary->code);
} }
} else { } else {
@@ -201,7 +201,7 @@ class DictionaryService
$codes = Dictionary::pluck('code')->toArray(); $codes = Dictionary::pluck('code')->toArray();
foreach ($codes as $code) { foreach ($codes as $code) {
Cache::forget('system:dictionary:' . $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 public function createItem(array $data): DictionaryItem
{ {
Validator::make($data, [ Validator::make($data, [
'dictionary_id' => 'required|exists:system_dictionaries,id', 'dictionary_id' => 'required|exists:system_dictionary,id',
'label' => 'required|string|max:100', 'label' => 'required|string|max:100',
'value' => 'required|string|max:100', 'value' => 'required|string|max:100',
])->validate(); ])->validate();
@@ -251,7 +251,7 @@ class DictionaryService
$item = DictionaryItem::findOrFail($id); $item = DictionaryItem::findOrFail($id);
Validator::make($data, [ 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', 'label' => 'sometimes|required|string|max:100',
'value' => 'sometimes|required|string|max:100', 'value' => 'sometimes|required|string|max:100',
])->validate(); ])->validate();
@@ -316,12 +316,12 @@ class DictionaryService
if ($allItems === null) { if ($allItems === null) {
// 获取所有启用的字典 // 获取所有启用的字典
$dictionaries = Dictionary::where('status', true) $dictionary = Dictionary::where('status', true)
->orderBy('sort') ->orderBy('sort')
->get(); ->get();
$result = []; $result = [];
foreach ($dictionaries as $dictionary) { foreach ($dictionary as $dictionary) {
$items = $dictionary->activeItems->toArray(); $items = $dictionary->activeItems->toArray();
// 格式化字典项值 // 格式化字典项值
$items = $this->formatItemsByType($items, $dictionary->value_type); $items = $this->formatItemsByType($items, $dictionary->value_type);

View File

@@ -0,0 +1,499 @@
<template>
<a-drawer :open="visible" @update:open="(val) => emit('update:visible', val)" :title="$t('common.messages')" placement="right" width="400" :footer="null" class="message-drawer" :bodyStyle="{padding: '0'}" @openChange="handleDrawerOpen">
<!-- 消息头部操作 -->
<div class="drawer-header-actions">
<a-space>
<a-button v-if="messageCount > 0" type="link" size="small" @click="markAllAsRead">
{{ $t('common.markAllAsRead') }}
</a-button>
<a-button type="link" size="small" @click="clearMessages">
{{ $t('common.clearAll') }}
</a-button>
</a-space>
</div>
<!-- 消息类型筛选 -->
<div class="message-tabs">
<a-tabs v-model:activeKey="currentMessageType" size="small" @change="changeMessageType">
<a-tab-pane key="all" :tab="$t('common.all')" />
<a-tab-pane key="notification" :tab="$t('common.notification')" />
<a-tab-pane key="task" :tab="$t('common.task')" />
<a-tab-pane key="warning" :tab="$t('common.warning')" />
</a-tabs>
</div>
<!-- 消息列表 -->
<div class="message-list">
<div v-for="msg in messages" :key="msg.id" class="message-item" :class="{ unread: !msg.is_read }" @click="handleMessageRead(msg)">
<div class="message-content">
<div class="message-title">{{ msg.title }}</div>
<div class="message-content-text">
{{ msg.content }}
</div>
<div class="message-time">
{{ notificationStore.formatNotificationTime(msg.created_at) }}
</div>
</div>
<a-button type="text" size="small" danger class="delete-btn" @click.stop="handleDeleteMessage(msg.id)">
<DeleteOutlined />
</a-button>
</div>
<a-empty v-if="messages.length === 0" :description="$t('common.noMessages')" />
</div>
<!-- 分页 -->
<div v-if="messagesTotal > messagesPageSize" class="message-pagination">
<a-pagination v-model:current="messagesPage" v-model:pageSize="messagesPageSize" :total="messagesTotal" size="small" :show-size-changer="false" @change="handleMessagePageChange" />
</div>
</a-drawer>
</template>
<script setup>
import { ref, computed } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { useNotificationStore } from '@/stores/modules/notification'
import { DeleteOutlined } from '@ant-design/icons-vue'
import { useI18n } from 'vue-i18n'
// 定义组件名称
defineOptions({
name: 'MessageDrawer',
})
const { t } = useI18n()
const notificationStore = useNotificationStore()
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:visible'])
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 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 && res.list) ? res.list : []
} catch (error) {
console.error('加载未读通知列表失败:', error)
notificationsList.value = []
}
}
// 加载所有通知
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)
notificationsList.value = []
}
}
// 清除消息
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 handleDrawerOpen = async (open) => {
if (open) {
await loadNotifications()
}
}
</script>
<style scoped lang="scss">
.message-drawer {
:deep(.ant-drawer-body) {
padding: 0 !important;
display: flex;
flex-direction: column;
height: 100%;
}
:deep(.ant-drawer-header) {
border-bottom: 1px solid #e8e8e8;
padding: 16px 24px;
}
:deep(.ant-drawer-title) {
font-size: 16px;
font-weight: 600;
color: #262626;
}
}
.drawer-header-actions {
padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
background: #fafbfc;
display: flex;
justify-content: flex-end;
}
.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;
}
}
}
}
</style>

View File

@@ -8,63 +8,13 @@
</a-tooltip> </a-tooltip>
<!-- 消息通知 --> <!-- 消息通知 -->
<a-dropdown v-model:open="messageVisible" :trigger="['click']" placement="bottomRight" @openChange="handleMessageDropdownOpen"> <a-tooltip :title="$t('common.messages')">
<a-badge :count="messageCount" :offset="[-5, 5]"> <a-badge :count="messageCount" :offset="[-5, 5]">
<a-button type="text" class="action-btn"> <a-button type="text" class="action-btn" @click="messageVisible = true">
<BellOutlined /> <BellOutlined />
</a-button> </a-button>
</a-badge> </a-badge>
<template #overlay> </a-tooltip>
<a-card class="dropdown-card" :bordered="false">
<template #title>
<div class="message-header">
<span>{{ $t('common.messages') }}</span>
<a-space size="small">
<a-button v-if="messageCount > 0" type="link" size="small" @click="markAllAsRead">
{{ $t('common.markAllAsRead') }}
</a-button>
<a-button type="link" size="small" @click="clearMessages">
{{ $t('common.clearAll') }}
</a-button>
</a-space>
</div>
</template>
<!-- 消息类型筛选 -->
<div class="message-tabs">
<a-tabs v-model:activeKey="currentMessageType" size="small" @change="changeMessageType">
<a-tab-pane key="all" :tab="$t('common.all')" />
<a-tab-pane key="notification" :tab="$t('common.notification')" />
<a-tab-pane key="task" :tab="$t('common.task')" />
<a-tab-pane key="warning" :tab="$t('common.warning')" />
</a-tabs>
</div>
<div class="message-list">
<div v-for="msg in messages" :key="msg.id" class="message-item" :class="{ unread: !msg.is_read }" @click="handleMessageRead(msg)">
<div class="message-content">
<div class="message-title">{{ msg.title }}</div>
<div class="message-content-text">
{{ msg.content }}
</div>
<div class="message-time">
{{ notificationStore.formatNotificationTime(msg.created_at) }}
</div>
</div>
<a-button type="text" size="small" danger class="delete-btn" @click.stop="handleDeleteMessage(msg.id)">
<DeleteOutlined />
</a-button>
</div>
<a-empty v-if="messages.length === 0" :description="$t('common.noMessages')" />
</div>
<!-- 分页 -->
<div v-if="messagesTotal > messagesPageSize" class="message-pagination">
<a-pagination v-model:current="messagesPage" v-model:pageSize="messagesPageSize" :total="messagesTotal" size="small" :show-size-changer="false" @change="handleMessagePageChange" />
</div>
</a-card>
</template>
</a-dropdown>
<!-- 任务列表 --> <!-- 任务列表 -->
<a-tooltip :title="$t('common.taskCenter')"> <a-tooltip :title="$t('common.taskCenter')">
@@ -133,6 +83,9 @@
<!-- 菜单搜索弹窗 --> <!-- 菜单搜索弹窗 -->
<search v-model:visible="searchVisible" /> <search v-model:visible="searchVisible" />
<!-- 消息抽屉 -->
<message-drawer v-model:visible="messageVisible" />
<!-- 任务抽屉 --> <!-- 任务抽屉 -->
<task v-model:visible="taskVisible" v-model:tasks="tasks" /> <task v-model:visible="taskVisible" v-model:tasks="tasks" />
</template> </template>
@@ -148,6 +101,7 @@ import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, Fullscr
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import search from './search.vue' import search from './search.vue'
import task from './task.vue' import task from './task.vue'
import messageDrawer from './message.vue'
// 定义组件名称(多词命名) // 定义组件名称(多词命名)
defineOptions({ defineOptions({
@@ -164,19 +118,6 @@ const isFullscreen = ref(false)
const searchVisible = ref(false) const searchVisible = ref(false)
const taskVisible = ref(false) const taskVisible = ref(false)
const messageVisible = 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([ const tasks = ref([
@@ -205,6 +146,9 @@ const tasks = ref([
const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length) const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length)
// 未读消息数量
const messageCount = computed(() => notificationStore.unreadCount)
// 切换全屏 // 切换全屏
const toggleFullscreen = () => { const toggleFullscreen = () => {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
@@ -221,50 +165,10 @@ const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement 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(() => { onMounted(() => {
document.addEventListener('fullscreenchange', handleFullscreenChange) document.addEventListener('fullscreenchange', handleFullscreenChange)
// 加载通知数据
loadNotifications()
// 注意WebSocket 已在 App.vue 中统一初始化,这里不需要重复调用 // 注意WebSocket 已在 App.vue 中统一初始化,这里不需要重复调用
// 只需要处理消息即可,消息处理已经在 useWebSocket 中注册 // 只需要处理消息即可,消息处理已经在 useWebSocket 中注册
}) })
@@ -278,115 +182,6 @@ const showSearch = () => {
searchVisible.value = true 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 = () => { 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;
}
}
}
}
</style> </style>

View File

@@ -4,7 +4,7 @@
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item label="权限名称" name="title" required> <a-form-item label="权限名称" name="title" required>
<a-input v-model:value="form.title" placeholder="如:用户管理" allow-clear maxlength="50" show-count /> <a-input v-model:value="form.title" placeholder="如:用户管理" allow-clear :maxlength="50" show-count />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">

View File

@@ -3,7 +3,7 @@
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }"> <a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<!-- 字典名称 --> <!-- 字典名称 -->
<a-form-item label="字典名称" name="name" required> <a-form-item label="字典名称" name="name" required>
<a-input v-model:value="form.name" placeholder="如:用户状态" allow-clear maxlength="50" show-count /> <a-input v-model:value="form.name" placeholder="如:用户状态" allow-clear :maxlength="50" show-count />
</a-form-item> </a-form-item>
<!-- 字典编码 --> <!-- 字典编码 -->
@@ -36,7 +36,7 @@
<!-- 描述 --> <!-- 描述 -->
<a-form-item label="描述" name="description"> <a-form-item label="描述" name="description">
<a-textarea v-model:value="form.description" placeholder="请输入字典描述" :rows="3" maxlength="200" show-count /> <a-textarea v-model:value="form.description" placeholder="请输入字典描述" :rows="3" :maxlength="200" show-count />
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -155,8 +155,8 @@ const setData = (data) => {
code: data.code || '', code: data.code || '',
value_type: data.value_type || 'string', value_type: data.value_type || 'string',
description: data.description || '', description: data.description || '',
status: data.status !== undefined ? data.status : null, status: data.status !== undefined ? Number(data.status) : null,
sort: data.sort !== undefined ? data.sort : 0, sort: data.sort !== undefined ? Number(data.sort) : 0,
} }
} }
} }
@@ -174,8 +174,8 @@ const handleSubmit = async () => {
code: form.value.code, code: form.value.code,
value_type: form.value.value_type, value_type: form.value.value_type,
description: form.value.description, description: form.value.description,
status: form.value.status, status: form.value.status !== null ? Number(form.value.status) : null,
sort: form.value.sort, sort: Number(form.value.sort || 0),
} }
let res = {} let res = {}