更新调整修复

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([
'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',

View File

@@ -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']);

View File

@@ -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);

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-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-button type="text" class="action-btn">
<a-button type="text" class="action-btn" @click="messageVisible = true">
<BellOutlined />
</a-button>
</a-badge>
<template #overlay>
<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>
<!-- 任务列表 -->
<a-tooltip :title="$t('common.taskCenter')">
@@ -133,6 +83,9 @@
<!-- 菜单搜索弹窗 -->
<search v-model:visible="searchVisible" />
<!-- 消息抽屉 -->
<message-drawer v-model:visible="messageVisible" />
<!-- 任务抽屉 -->
<task v-model:visible="taskVisible" v-model:tasks="tasks" />
</template>
@@ -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;
}
}
}
}
</style>

View File

@@ -4,7 +4,7 @@
<a-row :gutter="16">
<a-col :span="12">
<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-col>
<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-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>
<!-- 字典编码 -->
@@ -36,7 +36,7 @@
<!-- 描述 -->
<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>
@@ -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 = {}