mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-02 16:37:53 +00:00
feat:完善快速开始功能
This commit is contained in:
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 向左滚动标签页
|
||||
*/
|
||||
|
||||
Vendored
+2
-1
@@ -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']>
|
||||
|
||||
Vendored
+2
@@ -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']
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user