Merge pull request #184 from 1014TaoTao/v2.0.0

refactor(dashboard): 重构工作台页面并移除快速开始组件
This commit is contained in:
fastapiadmin
2025-09-30 01:05:31 +08:00
committed by GitHub
8 changed files with 179 additions and 648 deletions
+1
View File
@@ -8,6 +8,7 @@ export default {
edit: "Edit",
delete: "Delete",
add: "Add",
clear: "Clear",
export: "Export",
import: "Import",
query: "Query",
+1
View File
@@ -8,6 +8,7 @@ export default {
edit: "编辑",
delete: "删除",
add: "添加",
clear: "清除",
export: "导出",
import: "导入",
query: "查询",
@@ -39,7 +39,6 @@ export const useTagsViewStore = defineStore("tagsView", () => {
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
if (view.keepAlive) {
cachedViews.value.push(viewName);
console.log("cachedViews",cachedViews.value)
}
}
+7 -54
View File
@@ -3,10 +3,8 @@ import { ElMessage } from 'element-plus';
// 快速链接数据类型
export interface QuickLink {
title: string;
description: string;
icon: string;
href: string;
action: 'navigate' | 'external';
id?: string;
}
@@ -28,40 +26,7 @@ class QuickStartManager {
// 获取默认链接
private getDefaultLinks(): QuickLink[] {
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"
}
];
return [];
}
// 保存快速链接
@@ -78,9 +43,6 @@ class QuickStartManager {
addQuickLink(link: QuickLink): void {
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);
if (existingIndex !== -1) {
@@ -105,28 +67,19 @@ class QuickStartManager {
}
}
// 从路由或菜单信息创建快速链接
createQuickLinkFromRoute(route: any, customTitle?: string, customDescription?: 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);
// 清空所有快速链接
clearQuickLinks(): void {
this.saveQuickLinks([]);
}
// 从路由或菜单信息创建快速链接
createQuickLinkFromRoute(route: any, customTitle?: string): QuickLink {
// 确定最终使用的标题 - 优先使用route.title
const finalTitle = customTitle || route.title || route.name || '未命名页面';
return {
title: finalTitle,
description: customDescription || `快速访问 ${route.title || route.name || '页面'}`,
icon: routeIcon,
icon: route.icon,
href: route.fullPath || route.path,
action: 'navigate',
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';
+126 -17
View File
@@ -33,7 +33,8 @@
<span class="font-bold">进行中的项目</span>
<ElLink href="" type="primary" underline="never" style="float: right">全部项目</ElLink>
</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">
<ElCard :key="item.id" shadow="hover">
<ElDescriptions :column="1">
@@ -69,6 +70,7 @@
<ElLink href="https://service.fastapiadmin.com/" target="_blank" type="primary" underline="never">更多</ElLink>
</div>
</template>
<el-empty v-if="noticeList.length === 0" :image-size="80" description="暂无数据" />
<ElTimeline>
<ElTimelineItem v-for="(item, index) in noticeList" :key="item.id" :type="index === 0 ? 'primary' : 'info'">
<div class="bg-[var(--el-fill-color-light)] rounded-lg p-4 border border-[var(--el-border-color)] hover:shadow-md transition-shadow">
@@ -98,7 +100,8 @@
<template #header>
<span class="font-bold">团队</span>
</template>
<ElRow :gutter="16">
<el-empty v-if="projectNotice.length === 0" :image-size="80" description="暂无数据" />
<ElRow v-else :gutter="16">
<ElCol v-for="item in projectNotice" :key="`members-item-${item.id}`" :span="8" class="mb-3">
<ElLink underline="never" :href="item.href" class="flex items-center hover:bg-[var(--el-fill-color-light)] p-2 rounded transition-colors">
<ElAvatar :src="item.avatar" size="small" class="mr-2" />
@@ -112,7 +115,55 @@
<!-- 右侧快速开始 / 便捷导航 + XX 指数 -->
<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 指数 -->
<ElCard class="mb-4 font-bold" header="XX 指数">
@@ -128,16 +179,25 @@
import { EChartsOption } from 'echarts'
import { useUserStore } from "@/store/index";
import { greetings } from '@/utils/common';
import QuickStart from './components/QuickStart.vue';
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 timefix = greetings();
const { t } = useI18n();
const router = useRouter();
// 通知公告数据
const noticeList = ref<NoticeTable[]>([]);
// 快速链接数据
const quickLinks = ref<QuickLink[]>(quickStartManager.getQuickLinks());
// 格式化时间
const formatTime = (time: string | undefined) => {
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(() => {
getNoticeList();
quickStartManager.addListener(updateQuickLinks);
});
// 组件卸载时移除监听器
onUnmounted(() => {
quickStartManager.removeListener(updateQuickLinks);
});
defineOptions({
@@ -279,7 +398,6 @@ const projectNotice = [
];
const chartOptions = reactive<EChartsOption>({
tooltip: { trigger: 'item' },
legend: { data: ['个人', '团队', '部门'] },
@@ -307,18 +425,9 @@ const chartOptions = reactive<EChartsOption>({
}]
});
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 最小化自定义样式,主要使用UnoCSS和Element Plus内置样式 */
</style>