feat:完善快速开始功能

This commit is contained in:
张凯博
2025-08-19 09:34:57 +08:00
parent 81f069cc48
commit a08c903253
10 changed files with 1299 additions and 70 deletions
@@ -10,41 +10,63 @@
<el-scrollbar ref="scrollbarRef" class="scroll-container" @wheel="handleScroll">
<VueDraggable v-model="visitedViews" :animation="150">
<router-link
<router-link
v-for="tag in visitedViews" :key="tag.fullPath"
:class="['tags-item', { active: tagsViewStore.isActive(tag) }]" :to="{ path: tag.path, query: tag.query }"
@click.middle="handleMiddleClick(tag)">
<el-dropdown
v-if="tagsViewStore.isActive(tag)" trigger="contextmenu"
@visible-change="onContextMenuVisibleChange" @click.stop>
<!-- 为所有标签添加右键菜单 -->
<el-dropdown
trigger="contextmenu"
@visible-change="(visible) => onContextMenuVisibleChange(visible, tag)"
@click.stop>
<span class="tag-text">{{ translateRouteTitle(tag.title) }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="refreshSelectedTag(tag)">
<!-- 收藏到快速开始 -->
<el-dropdown-item
@click="addToQuickStart(tag)"
:disabled="isQuickLinkExists(tag)">
<el-icon>
<Star />
</el-icon>
{{ isQuickLinkExists(tag) ? '已收藏' : '收藏到快速开始' }}
</el-dropdown-item>
<el-dropdown-item divided @click="refreshSelectedTag(tag)">
<el-icon>
<Refresh />
</el-icon>
{{ t("navbar.refresh") }}
</el-dropdown-item>
<el-dropdown-item :disabled="tag.affix || visitedViews.length <= 1" @click="closeSelectedTag(tag)">
<el-dropdown-item
:disabled="tag.affix || visitedViews.length <= 1"
@click="closeSelectedTag(tag)">
<el-icon>
<Close />
</el-icon>
{{ t("navbar.close") }}
</el-dropdown-item>
<el-dropdown-item :disabled="isFirstView || !tagsViewStore.isActive(tag)" @click="closeLeftTags">
<el-dropdown-item
:disabled="isFirstView || !tagsViewStore.isActive(tag)"
@click="closeLeftTags">
<el-icon>
<Back />
</el-icon>
{{ t("navbar.closeLeft") }}
</el-dropdown-item>
<el-dropdown-item :disabled="isLastView || !tagsViewStore.isActive(tag)" @click="closeRightTags">
<el-dropdown-item
:disabled="isLastView || !tagsViewStore.isActive(tag)"
@click="closeRightTags">
<el-icon>
<Right />
</el-icon>
{{ t("navbar.closeRight") }}
</el-dropdown-item>
<el-dropdown-item
<el-dropdown-item
:disabled="visitedViews.length <= 1 || !tagsViewStore.isActive(tag)"
@click="closeOtherTags">
<el-icon>
@@ -52,7 +74,10 @@
</el-icon>
{{ t("navbar.closeOther") }}
</el-dropdown-item>
<el-dropdown-item :disabled="visitedViews.length <= 1" @click="closeAllTags(tag)">
<el-dropdown-item
:disabled="visitedViews.length <= 1"
@click="closeAllTags(tag)">
<el-icon>
<Minus />
</el-icon>
@@ -61,7 +86,7 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<span v-else class="tag-text">{{ translateRouteTitle(tag.title) }}</span>
<span v-if="!tag.affix" class="tag-close-btn" @click.prevent.stop="closeSelectedTag(tag)">
<el-icon>
<Close />
@@ -143,6 +168,9 @@ import { resolve } from "path-browserify";
import { translateRouteTitle } from "@/utils/i18n";
import { usePermissionStore, useTagsViewStore } from "@/store";
import { VueDraggable } from "vue-draggable-plus";
import { quickStartManager } from "@/utils/quickStartManager";
import { Star } from "@element-plus/icons-vue";
import { ElMessage } from 'element-plus';
const { t } = useI18n();
@@ -420,16 +448,36 @@ const handleAction = (action: string) => {
/**
* 右键菜单显示状态变化处理函数
*/
const onContextMenuVisibleChange = (visible: boolean) => {
const onContextMenuVisibleChange = (visible: boolean, tag?: TagView) => {
if (visible) {
// 总是获取最新的路由对应标签
selectedTag.value = routePathMap.value.get(route.path) || null;
// 设置当前右键点击的标签
selectedTag.value = tag || routePathMap.value.get(route.path) || null;
} else {
// 关闭菜单时清空选择
selectedTag.value = null;
}
};
/**
* 添加到快速开始
*/
const addToQuickStart = (tag: TagView) => {
try {
const quickLink = quickStartManager.createQuickLinkFromRoute(tag);
quickStartManager.addQuickLink(quickLink);
} catch (error) {
console.error('Failed to add to quick start:', error);
ElMessage.error('添加到快速开始失败');
}
};
/**
* 检查快速链接是否已存在
*/
const isQuickLinkExists = (tag: TagView): boolean => {
return quickStartManager.isLinkExists(tag.fullPath || tag.path);
};
/**
* 向左滚动标签页
*/
+2 -1
View File
@@ -270,6 +270,7 @@ declare global {
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
@@ -324,7 +325,6 @@ declare module 'vue' {
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly ElMessage: UnwrapRef<typeof import('element-plus/es')['ElMessage']>
readonly ElMessageBox: UnwrapRef<typeof import('element-plus/es')['ElMessageBox']>
readonly ElNotification: UnwrapRef<typeof import('element-plus/es')['ElNotification']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
@@ -586,6 +586,7 @@ declare module 'vue' {
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
+2
View File
@@ -8,6 +8,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AddQuickLinkDialog: typeof import('./../views/dashboard/components/AddQuickLinkDialog.vue')['default']
AppLink: typeof import('./../components/AppLink/index.vue')['default']
AppLogo: typeof import('./../layouts/components/AppLogo/index.vue')['default']
AppMain: typeof import('./../layouts/components/AppMain/index.vue')['default']
@@ -112,6 +113,7 @@ declare module 'vue' {
PageSearch: typeof import('./../components/CURD/PageSearch.vue')['default']
Pagination: typeof import('./../components/Pagination/index.vue')['default']
PermissonDrawer: typeof import('./../views/system/role/components/PermissonDrawer.vue')['default']
QuickStart: typeof import('./../views/dashboard/components/QuickStart.vue')['default']
Register: typeof import('./../views/system/auth/components/Register.vue')['default']
ResetPwd: typeof import('./../views/system/auth/components/ResetPwd.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
+185
View File
@@ -0,0 +1,185 @@
import { ElMessage } from 'element-plus';
// 快速链接数据类型
export interface QuickLink {
title: string;
description: string;
icon: string;
color: string;
href: string;
action: 'navigate' | 'external';
id?: string;
}
// 快速开始管理器类
class QuickStartManager {
private storageKey = 'quick-start-links';
private listeners: Array<(links: QuickLink[]) => void> = [];
// 获取所有快速链接
getQuickLinks(): QuickLink[] {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : this.getDefaultLinks();
} catch (error) {
console.error('Failed to load quick links:', error);
return this.getDefaultLinks();
}
}
// 获取默认链接
private getDefaultLinks(): QuickLink[] {
return [
{
id: 'user-management',
title: "用户管理",
description: "管理系统用户信息",
icon: "User",
color: "#409EFF",
href: "/system/user",
action: "navigate"
},
{
id: 'monitor',
title: "系统监控",
description: "监控系统状态",
icon: "Monitor",
color: "#909399",
href: "/monitor",
action: "navigate"
},
{
id: 'baidu',
title: "百度搜索",
description: "访问百度搜索引擎",
icon: "Search",
color: "#3385FF",
href: "https://www.baidu.com",
action: "external"
},
{
id: 'github',
title: "GitHub",
description: "访问代码托管平台",
icon: "Monitor",
color: "#24292e",
href: "https://github.com",
action: "external"
}
];
}
// 保存快速链接
saveQuickLinks(links: QuickLink[]): void {
try {
localStorage.setItem(this.storageKey, JSON.stringify(links));
this.notifyListeners(links);
} catch (error) {
console.error('Failed to save quick links:', error);
ElMessage.error('保存快速链接失败');
}
}
// 添加快速链接
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) {
// 更新现有链接
links[existingIndex] = { ...links[existingIndex], ...link };
ElMessage.success(`已更新快速链接:${link.title}`);
} else {
// 添加新链接
links.push(link);
ElMessage.success(`已添加快速链接:${link.title}`);
}
this.saveQuickLinks(links);
}
// 删除快速链接
removeQuickLink(id: string): void {
const links = this.getQuickLinks();
const filteredLinks = links.filter(link => link.id !== id);
if (filteredLinks.length < links.length) {
this.saveQuickLinks(filteredLinks);
ElMessage.success('已删除快速链接');
}
}
// 从路由信息创建快速链接
createQuickLinkFromRoute(route: any, customTitle?: string, customDescription?: string): QuickLink {
// 根据路径推断图标和颜色
const iconMap: Record<string, { icon: string; color: string }> = {
'/dashboard': { icon: 'House', color: '#409EFF' },
'/system': { icon: 'Setting', color: '#67C23A' },
'/user': { icon: 'User', color: '#409EFF' },
'/role': { icon: 'UserFilled', color: '#E6A23C' },
'/menu': { icon: 'Menu', color: '#F56C6C' },
'/dept': { icon: 'OfficeBuilding', color: '#909399' },
'/monitor': { icon: 'Monitor', color: '#606266' },
'/log': { icon: 'Document', color: '#FF6B6B' },
'/codegen': { icon: 'Code', color: '#4ECDC4' },
'/demo': { icon: 'DataAnalysis', color: '#3385FF' },
};
// 查找匹配的图标配置
let iconConfig = { icon: 'Link', color: '#909399' }; // 默认配置
for (const [path, config] of Object.entries(iconMap)) {
if (route.path.includes(path)) {
iconConfig = config;
break;
}
}
return {
title: customTitle || route.meta?.title || route.name || '未命名页面',
description: customDescription || `快速访问 ${route.meta?.title || route.name || '页面'}`,
icon: iconConfig.icon,
color: iconConfig.color,
href: route.fullPath || route.path,
action: 'navigate',
id: `route-${route.path.replace(/\//g, '-')}-${Date.now()}`
};
}
// 添加监听器
addListener(callback: (links: QuickLink[]) => void): void {
this.listeners.push(callback);
}
// 移除监听器
removeListener(callback: (links: QuickLink[]) => void): void {
const index = this.listeners.indexOf(callback);
if (index > -1) {
this.listeners.splice(index, 1);
}
}
// 通知所有监听器
private notifyListeners(links: QuickLink[]): void {
this.listeners.forEach(callback => {
try {
callback(links);
} catch (error) {
console.error('Error in quick start listener:', error);
}
});
}
// 检查链接是否已存在
isLinkExists(href: string): boolean {
const links = this.getQuickLinks();
return links.some(link => link.href === href);
}
}
// 创建全局实例
export const quickStartManager = new QuickStartManager();
@@ -0,0 +1,363 @@
<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">
<div class="icon-selector">
<div class="selected-icon">
<el-icon :size="24" :color="form.color">
<component :is="form.icon" />
</el-icon>
<span class="ml-2">{{ getIconName(form.icon) }}</span>
</div>
<ElButton size="small" @click="showIconPicker = true">选择图标</ElButton>
</div>
</ElFormItem>
<ElFormItem label="颜色" prop="color">
<ElColorPicker v-model="form.color" />
</ElFormItem>
</ElForm>
<!-- 图标选择器 -->
<ElDialog
v-model="showIconPicker"
title="选择图标"
width="600px"
append-to-body
>
<div class="icon-grid">
<div
v-for="icon in availableIcons"
:key="icon.name"
class="icon-item"
:class="{ active: form.icon === icon.name }"
@click="selectIcon(icon.name)"
>
<el-icon :size="24">
<component :is="icon.name" />
</el-icon>
<span class="icon-name">{{ icon.label }}</span>
</div>
</div>
<template #footer>
<ElButton @click="showIconPicker = false">取消</ElButton>
<ElButton type="primary" @click="showIconPicker = false">确定</ElButton>
</template>
</ElDialog>
<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 {
User,
Setting,
Document,
DataAnalysis,
Monitor,
Tools,
Bell,
Message,
Search,
House,
Files,
Calendar,
ChatDotRound,
Connection,
DataBoard,
Histogram,
PieChart,
TrendCharts,
Operation,
Service,
Guide,
Link
} from "@element-plus/icons-vue";
import { ElMessage, FormInstance, FormRules } from 'element-plus';
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;
color: string;
href: string;
action: 'navigate' | 'external';
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const formRef = ref<FormInstance>();
const showIconPicker = ref(false);
// 对话框显示状态
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',
color: '#409EFF',
href: '',
action: 'navigate'
});
// 可用图标列表
const availableIcons = [
{ name: 'User', label: '用户' },
{ name: 'Setting', label: '设置' },
{ name: 'Document', label: '文档' },
{ name: 'DataAnalysis', label: '分析' },
{ name: 'Monitor', label: '监控' },
{ name: 'Tools', label: '工具' },
{ name: 'Bell', label: '通知' },
{ name: 'Message', label: '消息' },
{ name: 'Search', label: '搜索' },
{ name: 'House', label: '首页' },
{ name: 'Files', label: '文件' },
{ name: 'Calendar', label: '日历' },
{ name: 'ChatDotRound', label: '聊天' },
{ name: 'Connection', label: '连接' },
{ name: 'DataBoard', label: '数据' },
{ name: 'Histogram', label: '柱状图' },
{ name: 'PieChart', label: '饼图' },
{ name: 'TrendCharts', label: '趋势图' },
{ name: 'Operation', label: '操作' },
{ name: 'Service', label: '服务' },
{ name: 'Guide', label: '指南' },
{ name: 'Link', label: '链接' }
];
// 表单验证规则
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 getIconName = (iconName: string) => {
const icon = availableIcons.find(item => item.name === iconName);
return icon ? icon.label : iconName;
};
// 选择图标
const selectIcon = (iconName: string) => {
form.icon = iconName;
};
// 重置表单
const resetForm = () => {
Object.assign(form, {
title: '',
description: '',
icon: 'User',
color: '#409EFF',
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.editData, (editData) => {
if (editData && props.visible) {
// 编辑模式:填充现有数据
Object.assign(form, {
title: editData.title,
description: editData.description,
icon: editData.icon,
color: editData.color,
href: editData.href,
action: editData.action
});
}
}, { immediate: true });
// 监听对话框显示状态
watch(() => props.visible, (visible) => {
if (visible && !props.editData) {
// 新增模式:重置表单
resetForm();
}
});
// 监听链接类型变化,自动调整链接地址格式
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>
.icon-selector {
display: flex;
align-items: center;
gap: 12px;
.selected-icon {
display: flex;
align-items: center;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #f5f7fa;
}
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 12px;
max-height: 300px;
overflow-y: auto;
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 8px;
border: 1px solid #e4e7ed;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #409EFF;
background-color: #f0f9ff;
}
&.active {
border-color: #409EFF;
background-color: #e1f3ff;
}
.icon-name {
font-size: 12px;
color: #606266;
margin-top: 4px;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,320 @@
<template>
<ElCard class="mb-4" shadow="hover">
<template #header>
<div class="flex justify-between items-center">
<span class="font-bold text-16px">快速开始 / 便捷导航</span>
<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-scroll">
<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="20" :color="item.color">
<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 class="link-description">{{ item.description }}</div>
</div>
<div class="link-arrow">
<el-icon size="14" color="#999">
<ArrowRight />
</el-icon>
</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
} 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) => {
selectedLink.value = item;
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 {
height: 280px; // 固定容器高度
overflow: hidden;
.quick-links-scroll {
height: 100%;
overflow-y: auto;
padding: 8px 0;
padding-right: 4px; // 为滚动条留出空间
// 自定义滚动条样式
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
.quick-link-item {
margin-bottom: 8px;
border-radius: 8px;
border: 1px solid transparent;
transition: all 0.3s ease;
&:hover {
background-color: #f5f7fa;
border-color: #e4e7ed;
transform: translateX(4px);
}
&:last-child {
margin-bottom: 0;
}
.link-content-wrapper {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
width: 100%;
}
.link-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 8px;
background-color: #f0f2f5;
margin-right: 12px;
transition: all 0.3s ease;
}
.link-content {
flex: 1;
.link-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 2px;
line-height: 1.4;
display: flex;
align-items: center;
gap: 6px;
.external-link-badge {
font-size: 10px;
font-weight: 400;
color: #fff;
background-color: #409EFF;
padding: 1px 4px;
border-radius: 2px;
line-height: 1.2;
}
}
.link-description {
font-size: 12px;
color: #909399;
line-height: 1.3;
}
}
.link-arrow {
display: flex;
align-items: center;
opacity: 0;
transition: all 0.3s ease;
}
&:hover {
.link-icon {
background-color: #e8f4ff;
}
.link-arrow {
opacity: 1;
transform: translateX(4px);
}
}
}
}
// 删除菜单项样式
:deep(.delete-item) {
color: #f56c6c;
&:hover {
background-color: #fef0f0;
color: #f56c6c;
}
}
</style>
@@ -0,0 +1,160 @@
# 快速开始组件功能说明
## 概述
快速开始组件提供了一个便捷的导航面板,用户可以快速访问常用的页面和外部链接。支持从标签栏右键收藏页面到快速开始列表。
## 功能特性
### 1. 快速链接管理
- ✅ 显示预设的常用链接
- ✅ 支持内部路由跳转和外部链接访问
- ✅ 固定高度容器,内容可滚动
- ✅ 响应式设计,适配不同屏幕尺寸
### 2. 标签栏右键收藏
- ✅ 在任意标签页右击可收藏到快速开始
- ✅ 已收藏的页面显示"已收藏"状态
- ✅ 自动生成合适的图标和颜色
- ✅ 防止重复收藏
### 3. 手动添加链接
- ✅ 通过对话框手动添加自定义链接
- ✅ 支持选择图标和自定义颜色
- ✅ 表单验证确保数据正确性
- ✅ 支持内部链接和外部链接
### 4. 右键管理链接
- ✅ 在快速链接上右击可编辑或删除
- ✅ 编辑功能支持修改所有属性
- ✅ 删除功能带有确认对话框
- ✅ 防止误操作的安全提示
### 4. 数据持久化
- ✅ 使用 localStorage 保存用户配置
- ✅ 页面刷新后数据不丢失
- ✅ 支持导入/导出配置(可扩展)
## 使用方法
### 从标签栏收藏页面
1. 在任意页面的标签上右击
2. 选择"收藏到快速开始"
3. 系统会自动添加到快速开始列表
4. 前往工作台查看新添加的链接
### 手动添加链接
1. 在快速开始组件中点击"添加"按钮
2. 填写链接信息:
- 标题:链接显示名称
- 描述:链接说明文字
- 链接地址:内部路径或外部URL
- 链接类型:内部链接或外部链接
- 图标:从预设图标中选择
- 颜色:自定义图标颜色
3. 点击确定保存
### 管理快速链接
#### 右键菜单操作
1. **编辑链接**:
- 在快速开始组件中右击任意链接
- 选择"编辑链接"
- 修改链接信息后保存
2. **删除链接**:
- 在快速开始组件中右击任意链接
- 选择"删除链接"
- 确认删除操作
### 使用快速链接
- **内部链接**:点击后在当前窗口跳转
- **外部链接**:点击后在新标签页打开,带有"外链"标识
## 技术实现
### 组件结构
```
dashboard/components/
├── QuickStart.vue # 主要的快速开始组件
├── AddQuickLinkDialog.vue # 添加快速链接对话框
├── index.ts # 组件导出文件
└── README.md # 功能说明文档
```
### 核心文件
- `QuickStart.vue`: 主组件,负责显示快速链接列表
- `AddQuickLinkDialog.vue`: 添加/编辑对话框
- `utils/quickStartManager.ts`: 全局数据管理器
- `layouts/components/TagsView/index.vue`: 标签栏组件(已修改)
### 数据管理
使用 `quickStartManager` 全局管理器:
```typescript
import { quickStartManager } from '@/utils/quickStartManager';
// 获取所有链接
const links = quickStartManager.getQuickLinks();
// 添加链接
quickStartManager.addQuickLink(newLink);
// 检查链接是否存在
const exists = quickStartManager.isLinkExists(href);
// 监听数据变化
quickStartManager.addListener(callback);
```
### 图标配置
支持的图标类型:
- User, Setting, Document, DataAnalysis
- Monitor, Tools, Bell, Message, Search
- House, Files, Calendar, ChatDotRound
- Connection, DataBoard, Histogram, PieChart
- TrendCharts, Operation, Service, Guide, Link
### 路径映射
系统会根据路径自动推断合适的图标:
```typescript
const iconMap = {
'/dashboard': { icon: 'House', color: '#409EFF' },
'/system': { icon: 'Setting', color: '#67C23A' },
'/user': { icon: 'User', color: '#409EFF' },
// ... 更多映射
};
```
### 自定义配置
可以通过修改 `quickStartManager.ts` 中的默认配置来自定义预设链接:
```typescript
private getDefaultLinks(): QuickLink[] {
return [
// 自定义默认链接
];
}
```
## 注意事项
1. **存储限制**: localStorage 有大小限制,建议控制链接数量
2. **图标依赖**: 确保使用的图标已在 Element Plus 中导入
3. **路由匹配**: 内部链接需要确保路由存在
4. **安全性**: 外部链接会在新窗口打开,已添加安全参数
## 测试页面
访问 `/demo/quick-start-test` 可以测试收藏功能的完整流程。
@@ -0,0 +1,2 @@
export { default as QuickStart } from './QuickStart.vue';
export { default as AddQuickLinkDialog } from './AddQuickLinkDialog.vue';
+3 -55
View File
@@ -91,21 +91,7 @@
<!-- 右侧:快速开始 / 便捷导航 + XX 指数 + 团队 -->
<ElCol :xl="8" :lg="8" :md="12" :sm="12" :xs="24">
<!-- 快速开始 / 便捷导航 -->
<ElCard class="mb-4 font-bold">
<template #header>
<span class="font-bold">快速开始 / 便捷导航</span>
<ElButton size="small" type="primary" plain style="float: right">
<el-icon>
<Plus />
</el-icon> {{ t('common.add') }}
</ElButton>
</template>
<div class="linkGroup">
<ElLink v-for="(item, index) in links" :key="index" underline="never" :href="item.href">
{{ item.title }}
</ElLink>
</div>
</ElCard>
<QuickStart />
<!-- XX 指数 -->
<ElCard class="mb-4 font-bold" header="XX 指数">
@@ -136,6 +122,7 @@ import { Plus } from "@element-plus/icons-vue";
import { EChartsOption } from 'echarts'
import { useUserStore } from "@/store/index";
import { greetings } from '@/utils/common';
import QuickStart from './components/QuickStart.vue';
const { t } = useI18n();
@@ -360,32 +347,7 @@ const chartOptions = reactive<EChartsOption>({
}]
});
const links = [
{
title: "操作一",
href: "",
},
{
title: "操作二",
href: "",
},
{
title: "操作三",
href: "",
},
{
title: "操作四",
href: "",
},
{
title: "操作五",
href: "",
},
{
title: "操作六",
href: "",
},
];
</script>
@@ -413,19 +375,5 @@ const links = [
}
}
.linkGroup {
padding: 20px 0 8px 24px;
font-size: 0;
&>a {
display: inline-block;
width: 25%;
margin-bottom: 13px;
font-size: 14px;
&:hover {
color: #1890ff;
}
}
}
</style>
@@ -0,0 +1,200 @@
<template>
<div class="app-container">
<ElCard>
<template #header>
<div class="card-header">
<span class="font-bold text-18px">快速开始收藏功能测试</span>
</div>
</template>
<div class="content">
<ElAlert
title="功能说明"
type="info"
:closable="false"
show-icon
>
<p>在任意标签页上右击,选择"收藏到快速开始"即可将当前页面添加到快速开始列表中。</p>
<p>已收藏的页面会显示"已收藏"状态,无法重复收藏。</p>
</ElAlert>
<div class="mt-4">
<h3>测试步骤:</h3>
<ol class="test-steps">
<li><strong>收藏功能:</strong>在当前页面的标签上右击,选择"收藏到快速开始"</li>
<li><strong>查看收藏:</strong>前往工作台页面查看快速开始组件</li>
<li><strong>编辑链接:</strong>在快速开始组件中右击任意链接,选择"编辑链接"</li>
<li><strong>删除链接:</strong>在快速开始组件中右击任意链接,选择"删除链接"</li>
<li><strong>验证跳转:</strong>点击收藏的链接验证跳转功能</li>
</ol>
</div>
<div class="mt-4">
<h3>当前页面信息:</h3>
<ElDescriptions :column="1" border>
<ElDescriptionsItem label="页面标题">
{{ $route.meta?.title || '快速开始测试页面' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="路由路径">
{{ $route.path }}
</ElDescriptionsItem>
<ElDescriptionsItem label="完整路径">
{{ $route.fullPath }}
</ElDescriptionsItem>
<ElDescriptionsItem label="收藏状态">
<ElTag :type="isBookmarked ? 'success' : 'info'">
{{ isBookmarked ? '已收藏' : '未收藏' }}
</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
</div>
<div class="mt-4">
<ElButton
type="primary"
@click="testBookmark"
:disabled="isBookmarked"
>
{{ isBookmarked ? '已收藏' : '手动收藏此页面' }}
</ElButton>
<ElButton
type="success"
@click="goToWorkplace"
class="ml-2"
>
前往工作台查看
</ElButton>
<ElButton
type="danger"
@click="removeBookmark"
:disabled="!isBookmarked"
class="ml-2"
>
删除收藏
</ElButton>
</div>
<div class="mt-4">
<h3>当前快速开始列表:</h3>
<div class="quick-links-preview">
<div
v-for="(link, index) in quickLinks"
:key="index"
class="link-item"
>
<el-icon :color="link.color" class="mr-2">
<component :is="link.icon" />
</el-icon>
<span class="link-title">{{ link.title }}</span>
<ElTag size="small" class="ml-2">{{ link.action }}</ElTag>
</div>
</div>
</div>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { quickStartManager, type QuickLink } from '@/utils/quickStartManager';
defineOptions({
name: "QuickStartTest",
});
const route = useRoute();
const router = useRouter();
// 快速链接列表
const quickLinks = ref<QuickLink[]>(quickStartManager.getQuickLinks());
// 检查当前页面是否已收藏
const isBookmarked = computed(() => {
return quickStartManager.isLinkExists(route.fullPath);
});
// 手动收藏当前页面
const testBookmark = () => {
const quickLink = quickStartManager.createQuickLinkFromRoute(
route,
'快速开始测试页面',
'用于测试快速开始收藏功能的演示页面'
);
quickStartManager.addQuickLink(quickLink);
// 更新快速链接列表
quickLinks.value = quickStartManager.getQuickLinks();
};
// 删除收藏
const removeBookmark = () => {
const links = quickStartManager.getQuickLinks();
const targetLink = links.find(link => link.href === route.fullPath);
if (targetLink?.id) {
quickStartManager.removeQuickLink(targetLink.id);
// 更新快速链接列表
quickLinks.value = quickStartManager.getQuickLinks();
}
};
// 前往工作台
const goToWorkplace = () => {
router.push('/dashboard/workplace');
};
// 监听快速链接变化
const updateQuickLinks = (links: QuickLink[]) => {
quickLinks.value = links;
};
onMounted(() => {
quickStartManager.addListener(updateQuickLinks);
});
onUnmounted(() => {
quickStartManager.removeListener(updateQuickLinks);
});
</script>
<style lang="scss" scoped>
.content {
padding: 20px 0;
}
.test-steps {
padding-left: 20px;
li {
margin-bottom: 8px;
line-height: 1.6;
}
}
.quick-links-preview {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--el-border-color);
border-radius: 4px;
padding: 12px;
.link-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
.link-title {
flex: 1;
font-size: 14px;
}
}
}
</style>