更新调整修复
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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);
|
||||
|
||||
499
resources/admin/src/layouts/components/message.vue
Normal file
499
resources/admin/src/layouts/components/message.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user