mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
Merge pull request #184 from 1014TaoTao/v2.0.0
refactor(dashboard): 重构工作台页面并移除快速开始组件
This commit is contained in:
@@ -8,6 +8,7 @@ export default {
|
|||||||
edit: "Edit",
|
edit: "Edit",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
add: "Add",
|
add: "Add",
|
||||||
|
clear: "Clear",
|
||||||
export: "Export",
|
export: "Export",
|
||||||
import: "Import",
|
import: "Import",
|
||||||
query: "Query",
|
query: "Query",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default {
|
|||||||
edit: "编辑",
|
edit: "编辑",
|
||||||
delete: "删除",
|
delete: "删除",
|
||||||
add: "添加",
|
add: "添加",
|
||||||
|
clear: "清除",
|
||||||
export: "导出",
|
export: "导出",
|
||||||
import: "导入",
|
import: "导入",
|
||||||
query: "查询",
|
query: "查询",
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
|
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
|
||||||
if (view.keepAlive) {
|
if (view.keepAlive) {
|
||||||
cachedViews.value.push(viewName);
|
cachedViews.value.push(viewName);
|
||||||
console.log("cachedViews",cachedViews.value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import { ElMessage } from 'element-plus';
|
|||||||
// 快速链接数据类型
|
// 快速链接数据类型
|
||||||
export interface QuickLink {
|
export interface QuickLink {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
|
||||||
icon: string;
|
icon: string;
|
||||||
href: string;
|
href: string;
|
||||||
action: 'navigate' | 'external';
|
|
||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,40 +26,7 @@ class QuickStartManager {
|
|||||||
|
|
||||||
// 获取默认链接
|
// 获取默认链接
|
||||||
private getDefaultLinks(): QuickLink[] {
|
private getDefaultLinks(): QuickLink[] {
|
||||||
return [
|
return [];
|
||||||
{
|
|
||||||
id: 'user-management',
|
|
||||||
title: "用户管理",
|
|
||||||
description: "管理系统用户信息",
|
|
||||||
icon: "User",
|
|
||||||
href: "/system/user",
|
|
||||||
action: "navigate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'monitor',
|
|
||||||
title: "系统监控",
|
|
||||||
description: "监控系统状态",
|
|
||||||
icon: "Monitor",
|
|
||||||
href: "/monitor",
|
|
||||||
action: "navigate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'baidu',
|
|
||||||
title: "百度搜索",
|
|
||||||
description: "访问百度搜索引擎",
|
|
||||||
icon: "Search",
|
|
||||||
href: "https://www.baidu.com",
|
|
||||||
action: "external"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'github',
|
|
||||||
title: "GitHub",
|
|
||||||
description: "访问代码托管平台",
|
|
||||||
icon: "Monitor",
|
|
||||||
href: "https://github.com",
|
|
||||||
action: "external"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存快速链接
|
// 保存快速链接
|
||||||
@@ -78,9 +43,6 @@ class QuickStartManager {
|
|||||||
addQuickLink(link: QuickLink): void {
|
addQuickLink(link: QuickLink): void {
|
||||||
const links = this.getQuickLinks();
|
const links = this.getQuickLinks();
|
||||||
|
|
||||||
// 生成唯一ID
|
|
||||||
link.id = link.id || `link-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
|
|
||||||
// 检查是否已存在相同路径的链接
|
// 检查是否已存在相同路径的链接
|
||||||
const existingIndex = links.findIndex(l => l.href === link.href);
|
const existingIndex = links.findIndex(l => l.href === link.href);
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
@@ -105,28 +67,19 @@ class QuickStartManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空所有快速链接
|
||||||
|
clearQuickLinks(): void {
|
||||||
|
this.saveQuickLinks([]);
|
||||||
|
}
|
||||||
// 从路由或菜单信息创建快速链接
|
// 从路由或菜单信息创建快速链接
|
||||||
createQuickLinkFromRoute(route: any, customTitle?: string, customDescription?: string): QuickLink {
|
createQuickLinkFromRoute(route: any, customTitle?: string): QuickLink {
|
||||||
// 优先使用路由对象上的icon字段,如果没有则使用默认图标
|
|
||||||
let routeIcon = route.icon || 'Link';
|
|
||||||
|
|
||||||
// 处理Element Plus图标名称,转换为组件名称
|
|
||||||
if (routeIcon.startsWith('el-icon-')) {
|
|
||||||
// 将 el-icon-Odometer 转换为 Odometer
|
|
||||||
routeIcon = routeIcon.replace('el-icon-', '');
|
|
||||||
// 首字母大写
|
|
||||||
routeIcon = routeIcon.charAt(0).toUpperCase() + routeIcon.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定最终使用的标题 - 优先使用route.title
|
// 确定最终使用的标题 - 优先使用route.title
|
||||||
const finalTitle = customTitle || route.title || route.name || '未命名页面';
|
const finalTitle = customTitle || route.title || route.name || '未命名页面';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
description: customDescription || `快速访问 ${route.title || route.name || '页面'}`,
|
icon: route.icon,
|
||||||
icon: routeIcon,
|
|
||||||
href: route.fullPath || route.path,
|
href: route.fullPath || route.path,
|
||||||
action: 'navigate',
|
|
||||||
id: `route-${route.path.replace(/\//g, '-')}-${Date.now()}`
|
id: `route-${route.path.replace(/\//g, '-')}-${Date.now()}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ElDialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
:title="isEditMode ? '编辑快速链接' : '添加快速链接'"
|
|
||||||
width="500px"
|
|
||||||
:before-close="handleClose"
|
|
||||||
>
|
|
||||||
<ElForm
|
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="80px"
|
|
||||||
label-position="left"
|
|
||||||
>
|
|
||||||
<ElFormItem label="标题" prop="title">
|
|
||||||
<ElInput
|
|
||||||
v-model="form.title"
|
|
||||||
placeholder="请输入链接标题"
|
|
||||||
maxlength="20"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="描述" prop="description">
|
|
||||||
<ElInput
|
|
||||||
v-model="form.description"
|
|
||||||
placeholder="请输入链接描述"
|
|
||||||
maxlength="50"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="链接地址" prop="href">
|
|
||||||
<ElInput
|
|
||||||
v-model="form.href"
|
|
||||||
placeholder="请输入链接地址,如:/user 或 https://www.example.com"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="链接类型" prop="action">
|
|
||||||
<ElRadioGroup v-model="form.action">
|
|
||||||
<ElRadio label="navigate">内部链接</ElRadio>
|
|
||||||
<ElRadio label="external">外部链接</ElRadio>
|
|
||||||
</ElRadioGroup>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="图标" prop="icon">
|
|
||||||
<IconSelect v-model="form.icon" width="400px" />
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
|
|
||||||
</ElForm>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<ElButton @click="handleClose">取消</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ElMessage, FormInstance, FormRules } from 'element-plus';
|
|
||||||
import IconSelect from '@/components/IconSelect/index.vue';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
visible: boolean;
|
|
||||||
editData?: QuickLink | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'update:visible', value: boolean): void;
|
|
||||||
(e: 'confirm', value: QuickLink): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuickLink {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: string;
|
|
||||||
href: string;
|
|
||||||
action: 'navigate' | 'external';
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
const emit = defineEmits<Emits>();
|
|
||||||
|
|
||||||
const formRef = ref<FormInstance>();
|
|
||||||
|
|
||||||
// 对话框显示状态
|
|
||||||
const dialogVisible = computed({
|
|
||||||
get: () => props.visible,
|
|
||||||
set: (value) => emit('update:visible', value)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 是否为编辑模式
|
|
||||||
const isEditMode = computed(() => !!props.editData);
|
|
||||||
|
|
||||||
// 表单数据
|
|
||||||
const form = reactive<QuickLink>({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
icon: 'user',
|
|
||||||
href: '',
|
|
||||||
action: 'navigate'
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 表单验证规则
|
|
||||||
const rules: FormRules = {
|
|
||||||
title: [
|
|
||||||
{ required: true, message: '请输入链接标题', trigger: 'blur' },
|
|
||||||
{ min: 1, max: 20, message: '标题长度在 1 到 20 个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
description: [
|
|
||||||
{ required: true, message: '请输入链接描述', trigger: 'blur' },
|
|
||||||
{ min: 1, max: 50, message: '描述长度在 1 到 50 个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
href: [
|
|
||||||
{ required: true, message: '请输入链接地址', trigger: 'blur' },
|
|
||||||
{
|
|
||||||
validator: (rule, value, callback) => {
|
|
||||||
if (form.action === 'external') {
|
|
||||||
// 外部链接需要是完整的URL
|
|
||||||
const urlPattern = /^https?:\/\/.+/;
|
|
||||||
if (!urlPattern.test(value)) {
|
|
||||||
callback(new Error('外部链接必须以 http:// 或 https:// 开头'));
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 内部链接需要以 / 开头
|
|
||||||
if (!value.startsWith('/')) {
|
|
||||||
callback(new Error('内部链接必须以 / 开头'));
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trigger: 'blur'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
action: [
|
|
||||||
{ required: true, message: '请选择链接类型', trigger: 'change' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
const resetForm = () => {
|
|
||||||
Object.assign(form, {
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
icon: 'user',
|
|
||||||
href: '',
|
|
||||||
action: 'navigate'
|
|
||||||
});
|
|
||||||
formRef.value?.clearValidate();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理关闭
|
|
||||||
const handleClose = () => {
|
|
||||||
resetForm();
|
|
||||||
emit('update:visible', false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理确认
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await formRef.value.validate();
|
|
||||||
emit('confirm', { ...form });
|
|
||||||
handleClose();
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('请检查表单输入');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听对话框显示状态,处理表单数据
|
|
||||||
watch(() => props.visible, (visible) => {
|
|
||||||
if (visible) {
|
|
||||||
if (props.editData) {
|
|
||||||
// 编辑模式:填充现有数据
|
|
||||||
console.log('编辑模式 - 填充数据:', props.editData);
|
|
||||||
Object.assign(form, {
|
|
||||||
title: props.editData.title,
|
|
||||||
description: props.editData.description,
|
|
||||||
icon: props.editData.icon,
|
|
||||||
href: props.editData.href,
|
|
||||||
action: props.editData.action
|
|
||||||
});
|
|
||||||
console.log('表单数据已填充:', form);
|
|
||||||
} else {
|
|
||||||
// 新增模式:重置表单
|
|
||||||
console.log('新增模式 - 重置表单');
|
|
||||||
resetForm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听编辑数据变化(当对话框已经打开时)
|
|
||||||
watch(() => props.editData, (editData) => {
|
|
||||||
if (props.visible && editData) {
|
|
||||||
// 编辑模式:填充现有数据
|
|
||||||
Object.assign(form, {
|
|
||||||
title: editData.title,
|
|
||||||
description: editData.description,
|
|
||||||
icon: editData.icon,
|
|
||||||
href: editData.href,
|
|
||||||
action: editData.action
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, { immediate: false });
|
|
||||||
|
|
||||||
// 监听链接类型变化,自动调整链接地址格式
|
|
||||||
watch(() => form.action, (newAction) => {
|
|
||||||
if (newAction === 'external' && form.href && !form.href.startsWith('http')) {
|
|
||||||
form.href = 'https://';
|
|
||||||
} else if (newAction === 'navigate' && form.href && !form.href.startsWith('/')) {
|
|
||||||
form.href = '/';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ElCard class="mb-4" shadow="hover">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<el-tooltip content="快速访问常用功能,支持内部路由跳转和外部链接打开。可以自定义添加、编辑和删除快捷方式。" placement="top">
|
|
||||||
<el-icon class="cursor-help" size="16">
|
|
||||||
<QuestionFilled />
|
|
||||||
</el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
<span class="font-bold text-16px">快速开始 / 便捷导航</span>
|
|
||||||
</div>
|
|
||||||
<ElButton size="small" type="primary" plain @click="handleAddQuickLink">
|
|
||||||
<el-icon>
|
|
||||||
<Plus />
|
|
||||||
</el-icon>
|
|
||||||
{{ t('common.add') }}
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="quick-links-container">
|
|
||||||
<div class="quick-links-grid">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in quickLinks"
|
|
||||||
:key="index"
|
|
||||||
class="quick-link-item"
|
|
||||||
@click="handleQuickLinkClick(item)"
|
|
||||||
>
|
|
||||||
<!-- 为快速链接添加右键菜单 -->
|
|
||||||
<el-dropdown
|
|
||||||
trigger="contextmenu"
|
|
||||||
@click.stop
|
|
||||||
@visible-change="(visible) => onContextMenuChange(visible, item)"
|
|
||||||
>
|
|
||||||
<div class="link-content-wrapper">
|
|
||||||
<div class="link-icon">
|
|
||||||
<el-icon :size="24">
|
|
||||||
<component :is="item.icon" />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
<div class="link-content">
|
|
||||||
<div class="link-title">
|
|
||||||
{{ item.title }}
|
|
||||||
<span v-if="item.action === 'external'" class="external-link-badge">外链</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item @click="handleEditLink(item)">
|
|
||||||
<el-icon>
|
|
||||||
<Edit />
|
|
||||||
</el-icon>
|
|
||||||
编辑链接
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item
|
|
||||||
@click="handleDeleteLink(item)"
|
|
||||||
:disabled="!item.id"
|
|
||||||
class="delete-item"
|
|
||||||
>
|
|
||||||
<el-icon>
|
|
||||||
<Delete />
|
|
||||||
</el-icon>
|
|
||||||
删除链接
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 添加/编辑快速链接对话框 -->
|
|
||||||
<AddQuickLinkDialog
|
|
||||||
v-model:visible="addDialogVisible"
|
|
||||||
:edit-data="selectedLink"
|
|
||||||
@confirm="handleAddConfirm"
|
|
||||||
/>
|
|
||||||
</ElCard>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
Plus,
|
|
||||||
ArrowRight,
|
|
||||||
Edit,
|
|
||||||
Delete,
|
|
||||||
QuestionFilled
|
|
||||||
} from "@element-plus/icons-vue";
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import AddQuickLinkDialog from './AddQuickLinkDialog.vue';
|
|
||||||
import { quickStartManager, type QuickLink } from '@/utils/quickStartManager';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// 添加对话框显示状态
|
|
||||||
const addDialogVisible = ref(false);
|
|
||||||
|
|
||||||
// 快速链接数据 - 使用全局管理器
|
|
||||||
const quickLinks = ref<QuickLink[]>(quickStartManager.getQuickLinks());
|
|
||||||
|
|
||||||
// 处理快速链接点击
|
|
||||||
const handleQuickLinkClick = (item: QuickLink) => {
|
|
||||||
if (item.action === 'navigate' && item.href) {
|
|
||||||
// 内部路由跳转
|
|
||||||
router.push(item.href).catch(() => {
|
|
||||||
ElMessage.warning(`路由 ${item.href} 不存在,请检查配置`);
|
|
||||||
});
|
|
||||||
ElMessage.success(`正在跳转到:${item.title}`);
|
|
||||||
} else if (item.action === 'external' && item.href) {
|
|
||||||
// 外部链接跳转
|
|
||||||
window.open(item.href, '_blank', 'noopener,noreferrer');
|
|
||||||
ElMessage.success(`正在打开外部链接:${item.title}`);
|
|
||||||
} else {
|
|
||||||
ElMessage.info(`${item.title} 功能待开发`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 当前选中的链接(用于右键菜单)
|
|
||||||
const selectedLink = ref<QuickLink | null>(null);
|
|
||||||
|
|
||||||
// 处理添加快速链接
|
|
||||||
const handleAddQuickLink = () => {
|
|
||||||
selectedLink.value = null; // 清空选中项,表示新增
|
|
||||||
addDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理右键菜单显示状态变化
|
|
||||||
const onContextMenuChange = (visible: boolean, item?: QuickLink) => {
|
|
||||||
if (visible && item) {
|
|
||||||
selectedLink.value = item;
|
|
||||||
} else {
|
|
||||||
selectedLink.value = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理编辑链接
|
|
||||||
const handleEditLink = (item: QuickLink) => {
|
|
||||||
// 深拷贝链接数据,避免直接修改原数据
|
|
||||||
console.log('开始编辑链接:', item);
|
|
||||||
selectedLink.value = { ...item };
|
|
||||||
console.log('设置选中链接:', selectedLink.value);
|
|
||||||
addDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理删除链接
|
|
||||||
const handleDeleteLink = (item: QuickLink) => {
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
`确定要删除快速链接"${item.title}"吗?`,
|
|
||||||
'删除确认',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
}
|
|
||||||
).then(() => {
|
|
||||||
if (item.id) {
|
|
||||||
quickStartManager.removeQuickLink(item.id);
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
// 用户取消删除
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理添加/编辑确认
|
|
||||||
const handleAddConfirm = (newLink: QuickLink) => {
|
|
||||||
if (selectedLink.value?.id) {
|
|
||||||
// 编辑模式:更新现有链接
|
|
||||||
newLink.id = selectedLink.value.id;
|
|
||||||
quickStartManager.addQuickLink(newLink); // addQuickLink 方法会处理更新逻辑
|
|
||||||
} else {
|
|
||||||
// 新增模式:添加新链接
|
|
||||||
quickStartManager.addQuickLink(newLink);
|
|
||||||
}
|
|
||||||
selectedLink.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听快速链接变化
|
|
||||||
const updateQuickLinks = (links: QuickLink[]) => {
|
|
||||||
quickLinks.value = links;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件挂载时添加监听器
|
|
||||||
onMounted(() => {
|
|
||||||
quickStartManager.addListener(updateQuickLinks);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 组件卸载时移除监听器
|
|
||||||
onUnmounted(() => {
|
|
||||||
quickStartManager.removeListener(updateQuickLinks);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
// 快速链接容器样式
|
|
||||||
.quick-links-container {
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
.quick-links-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 32px;
|
|
||||||
max-width: 400px; /* 限制最大宽度 */
|
|
||||||
margin: 0 auto; /* 居中对齐 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-link-item {
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--el-border-color-light);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--el-box-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-content-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-content {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.link-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.4;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.external-link-badge {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 400;
|
|
||||||
background-color: var(--el-color-primary);
|
|
||||||
color: var(--el-color-primary-contrast);
|
|
||||||
padding: 1px 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
line-height: 1.2;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 暗色主题适配 - 使用项目标准的暗色主题选择器
|
|
||||||
html.dark .external-link-badge {
|
|
||||||
background-color: var(--el-color-primary-dark-2);
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.link-icon {
|
|
||||||
background-color: var(--el-color-primary-light-9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除菜单项样式
|
|
||||||
:deep(.delete-item) {
|
|
||||||
color: var(--el-color-danger);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-color-danger-light-9);
|
|
||||||
color: var(--el-color-danger-dark-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default as QuickStart } from './QuickStart.vue';
|
|
||||||
export { default as AddQuickLinkDialog } from './AddQuickLinkDialog.vue';
|
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
<span class="font-bold">进行中的项目</span>
|
<span class="font-bold">进行中的项目</span>
|
||||||
<ElLink href="" type="primary" underline="never" style="float: right">全部项目</ElLink>
|
<ElLink href="" type="primary" underline="never" style="float: right">全部项目</ElLink>
|
||||||
</template>
|
</template>
|
||||||
<ElRow>
|
<el-empty v-if="projectNotice.length === 0" :image-size="80" description="暂无数据" />
|
||||||
|
<ElRow v-else>
|
||||||
<ElCol v-for="item in projectNotice" :key="`card-${item.id}`" :xl="8" :lg="8" :md="12" :sm="24" :xs="24">
|
<ElCol v-for="item in projectNotice" :key="`card-${item.id}`" :xl="8" :lg="8" :md="12" :sm="24" :xs="24">
|
||||||
<ElCard :key="item.id" shadow="hover">
|
<ElCard :key="item.id" shadow="hover">
|
||||||
<ElDescriptions :column="1">
|
<ElDescriptions :column="1">
|
||||||
@@ -61,58 +62,108 @@
|
|||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
<!-- 通知公告 -->
|
<!-- 通知公告 -->
|
||||||
<ElCard shadow="hover" class="mt-4">
|
<ElCard shadow="hover" class="mt-4">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="font-bold">通知公告</span>
|
<span class="font-bold">通知公告</span>
|
||||||
<ElLink href="https://service.fastapiadmin.com/" target="_blank" type="primary" underline="never">更多</ElLink>
|
<ElLink href="https://service.fastapiadmin.com/" target="_blank" type="primary" underline="never">更多</ElLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<ElTimeline>
|
<el-empty v-if="noticeList.length === 0" :image-size="80" description="暂无数据" />
|
||||||
<ElTimelineItem v-for="(item, index) in noticeList" :key="item.id" :type="index === 0 ? 'primary' : 'info'">
|
<ElTimeline>
|
||||||
<div class="bg-[var(--el-fill-color-light)] rounded-lg p-4 border border-[var(--el-border-color)] hover:shadow-md transition-shadow">
|
<ElTimelineItem v-for="(item, index) in noticeList" :key="item.id" :type="index === 0 ? 'primary' : 'info'">
|
||||||
<div class="flex justify-between items-start mb-2">
|
<div class="bg-[var(--el-fill-color-light)] rounded-lg p-4 border border-[var(--el-border-color)] hover:shadow-md transition-shadow">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex justify-between items-start mb-2">
|
||||||
<span class="font-medium text-[var(--el-text-color-primary)]">{{ item.notice_title }}</span>
|
<div class="flex items-center gap-2">
|
||||||
<el-tag size="small" :type="getNoticeTypeColor(item.notice_type)">
|
<span class="font-medium text-[var(--el-text-color-primary)]">{{ item.notice_title }}</span>
|
||||||
{{ getNoticeTypeText(item.notice_type) }}
|
<el-tag size="small" :type="getNoticeTypeColor(item.notice_type)">
|
||||||
</el-tag>
|
{{ getNoticeTypeText(item.notice_type) }}
|
||||||
</div>
|
</el-tag>
|
||||||
<span class="text-xs text-[var(--el-text-color-regular)]">{{ formatTime(item.created_at) }}</span>
|
</div>
|
||||||
</div>
|
<span class="text-xs text-[var(--el-text-color-regular)]">{{ formatTime(item.created_at) }}</span>
|
||||||
<div class="text-sm text-[var(--el-text-color-regular)] mb-3 line-clamp-2">{{ item.notice_content }}</div>
|
</div>
|
||||||
<div class="flex justify-between items-center text-xs">
|
<div class="text-sm text-[var(--el-text-color-regular)] mb-3 line-clamp-2">{{ item.notice_content }}</div>
|
||||||
<span class="text-[var(--el-text-color-regular)]">{{ item.creator?.name }} 发布</span>
|
<div class="flex justify-between items-center text-xs">
|
||||||
<el-tooltip placement="top" :content="item.description || item.notice_content">
|
<span class="text-[var(--el-text-color-regular)]">{{ item.creator?.name }} 发布</span>
|
||||||
<ElLink href="https://service.fastapiadmin.com/" target="_blank" type="primary">详情↗</ElLink>
|
<el-tooltip placement="top" :content="item.description || item.notice_content">
|
||||||
</el-tooltip>
|
<ElLink href="https://service.fastapiadmin.com/" target="_blank" type="primary">详情↗</ElLink>
|
||||||
</div>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</ElTimelineItem>
|
</div>
|
||||||
</ElTimeline>
|
</ElTimelineItem>
|
||||||
</ElCard>
|
</ElTimeline>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
<!-- 团队 -->
|
<!-- 团队 -->
|
||||||
<ElCard shadow="hover" class="mt-4">
|
<ElCard shadow="hover" class="mt-4">
|
||||||
<template #header>
|
<template #header>
|
||||||
<span class="font-bold">团队</span>
|
<span class="font-bold">团队</span>
|
||||||
</template>
|
</template>
|
||||||
<ElRow :gutter="16">
|
<el-empty v-if="projectNotice.length === 0" :image-size="80" description="暂无数据" />
|
||||||
<ElCol v-for="item in projectNotice" :key="`members-item-${item.id}`" :span="8" class="mb-3">
|
<ElRow v-else :gutter="16">
|
||||||
<ElLink underline="never" :href="item.href" class="flex items-center hover:bg-[var(--el-fill-color-light)] p-2 rounded transition-colors">
|
<ElCol v-for="item in projectNotice" :key="`members-item-${item.id}`" :span="8" class="mb-3">
|
||||||
<ElAvatar :src="item.avatar" size="small" class="mr-2" />
|
<ElLink underline="never" :href="item.href" class="flex items-center hover:bg-[var(--el-fill-color-light)] p-2 rounded transition-colors">
|
||||||
<span class="text-sm truncate text-[var(--el-text-color-regular)]">{{ item.member }}</span>
|
<ElAvatar :src="item.avatar" size="small" class="mr-2" />
|
||||||
</ElLink>
|
<span class="text-sm truncate text-[var(--el-text-color-regular)]">{{ item.member }}</span>
|
||||||
</ElCol>
|
</ElLink>
|
||||||
</ElRow>
|
</ElCol>
|
||||||
</ElCard>
|
</ElRow>
|
||||||
|
</ElCard>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
|
|
||||||
<!-- 右侧:快速开始 / 便捷导航 + XX 指数 -->
|
<!-- 右侧:快速开始 / 便捷导航 + XX 指数 -->
|
||||||
<ElCol :xl="8" :lg="8" :md="12" :sm="12" :xs="24">
|
<ElCol :xl="8" :lg="8" :md="12" :sm="12" :xs="24">
|
||||||
<!-- 快速开始 / 便捷导航 -->
|
<!-- 快速开始 / 便捷导航 -->
|
||||||
<QuickStart />
|
<ElCard shadow="hover" class="mb-4" >
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-tooltip content="快速访问常用功能,支持内部路由跳转和外部链接打开。" placement="top">
|
||||||
|
<el-icon class="cursor-help" size="16">
|
||||||
|
<QuestionFilled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
<span class="font-bold">快速开始 / 便捷导航</span>
|
||||||
|
</div>
|
||||||
|
<ElButton size="small" type="danger" plain @click="clearBookmarks()">
|
||||||
|
<el-icon>
|
||||||
|
<Close />
|
||||||
|
</el-icon>
|
||||||
|
{{ t('common.clear') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ElRow v-if="quickLinks.length > 0" :gutter="12">
|
||||||
|
<ElCol
|
||||||
|
v-for="(item, index) in quickLinks"
|
||||||
|
:key="index"
|
||||||
|
:span="8"
|
||||||
|
class="group mb-4"
|
||||||
|
>
|
||||||
|
<ElButton
|
||||||
|
type="default"
|
||||||
|
class="w-full h-20 flex items-center justify-start px-4 relative"
|
||||||
|
@click="handleQuickLinkClick(item)"
|
||||||
|
>
|
||||||
|
<el-icon v-if="item.icon && item.icon.startsWith('el-icon')">
|
||||||
|
<component :is="item.icon.replace('el-icon-', '')" />
|
||||||
|
</el-icon>
|
||||||
|
<div v-else-if="item.icon" :class="`i-svg:${item.icon} mr-2`" />
|
||||||
|
<div v-else :class="`i-svg:menu mr-2`" />
|
||||||
|
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<el-icon
|
||||||
|
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 "
|
||||||
|
@click.stop="handleDeleteLink(item)"
|
||||||
|
>
|
||||||
|
<CircleCloseFilled />
|
||||||
|
</el-icon>
|
||||||
|
</ElButton>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<el-empty v-else :image-size="80" description="暂无数据" />
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
<!-- XX 指数 -->
|
<!-- XX 指数 -->
|
||||||
<ElCard class="mb-4 font-bold" header="XX 指数">
|
<ElCard class="mb-4 font-bold" header="XX 指数">
|
||||||
@@ -128,16 +179,25 @@
|
|||||||
import { EChartsOption } from 'echarts'
|
import { EChartsOption } from 'echarts'
|
||||||
import { useUserStore } from "@/store/index";
|
import { useUserStore } from "@/store/index";
|
||||||
import { greetings } from '@/utils/common';
|
import { greetings } from '@/utils/common';
|
||||||
import QuickStart from './components/QuickStart.vue';
|
|
||||||
import NoticeAPI, { NoticeTable } from '@/api/system/notice';
|
import NoticeAPI, { NoticeTable } from '@/api/system/notice';
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, reactive } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { QuestionFilled, Close, CircleCloseFilled } from "@element-plus/icons-vue";
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { quickStartManager, type QuickLink } from '@/utils/quickStartManager';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const timefix = greetings();
|
const timefix = greetings();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// 通知公告数据
|
// 通知公告数据
|
||||||
const noticeList = ref<NoticeTable[]>([]);
|
const noticeList = ref<NoticeTable[]>([]);
|
||||||
|
|
||||||
|
// 快速链接数据
|
||||||
|
const quickLinks = ref<QuickLink[]>(quickStartManager.getQuickLinks());
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (time: string | undefined) => {
|
const formatTime = (time: string | undefined) => {
|
||||||
if (!time) return '';
|
if (!time) return '';
|
||||||
@@ -195,9 +255,68 @@ const getNoticeList = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 组件挂载时获取数据
|
// 处理快速链接点击
|
||||||
|
const handleQuickLinkClick = (item: QuickLink) => {
|
||||||
|
if (item.href) {
|
||||||
|
// 内部路由跳转
|
||||||
|
router.push(item.href).catch(() => {
|
||||||
|
ElMessage.warning(`路由 ${item.href} 不存在,请检查配置`);
|
||||||
|
});
|
||||||
|
ElMessage.success(`进入:${item.title}`);
|
||||||
|
} else {
|
||||||
|
ElMessage.info(`${item.title} 功能待开发`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理删除链接
|
||||||
|
const handleDeleteLink = (item: QuickLink) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定要取消收藏"${item.title}"吗?`,
|
||||||
|
'取消收藏确认',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
if (item.id) {
|
||||||
|
quickStartManager.removeQuickLink(item.id);
|
||||||
|
ElMessage.success(`已取消收藏:${item.title}`);
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// 用户取消删除
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearBookmarks = () => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要清空收藏吗?',
|
||||||
|
'清空收藏确认',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
quickStartManager.clearQuickLinks();
|
||||||
|
ElMessage.success('已清空收藏');
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听快速链接变化
|
||||||
|
const updateQuickLinks = (links: QuickLink[]) => {
|
||||||
|
quickLinks.value = links;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时获取数据和添加监听器
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getNoticeList();
|
getNoticeList();
|
||||||
|
quickStartManager.addListener(updateQuickLinks);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时移除监听器
|
||||||
|
onUnmounted(() => {
|
||||||
|
quickStartManager.removeListener(updateQuickLinks);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -279,7 +398,6 @@ const projectNotice = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const chartOptions = reactive<EChartsOption>({
|
const chartOptions = reactive<EChartsOption>({
|
||||||
tooltip: { trigger: 'item' },
|
tooltip: { trigger: 'item' },
|
||||||
legend: { data: ['个人', '团队', '部门'] },
|
legend: { data: ['个人', '团队', '部门'] },
|
||||||
@@ -307,18 +425,9 @@ const chartOptions = reactive<EChartsOption>({
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.line-clamp-2 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 最小化自定义样式,主要使用UnoCSS和Element Plus内置样式 */
|
/* 最小化自定义样式,主要使用UnoCSS和Element Plus内置样式 */
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user