添加相关功能

This commit is contained in:
黑小马 2025-07-23 17:15:25 +08:00
parent 5a533e15ce
commit 6547123a83
10 changed files with 896 additions and 8 deletions

View File

@ -9,8 +9,11 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.11",
"element-plus": "^2.10.4",
"vue": "^3.5.17", "vue": "^3.5.17",
"@tailwindcss/vite": "^4.1.11" "vue-router": "4",
"vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",

View File

@ -2,9 +2,7 @@
</script> </script>
<template> <template>
<div> <router-view />
</div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,5 +1,7 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import './style.css' import './style.css'
import App from './App.vue' import App from './App.vue'
import router from './router/index.js'
createApp(App).mount('#app') import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
createApp(App).use(router).use(ElementPlus).mount('#app')

24
web/src/router/index.js Normal file
View File

@ -0,0 +1,24 @@
import { createMemoryHistory, createRouter } from 'vue-router'
import index from '../views/index.vue'
import home from '../views/home.vue'
import appcenter from '../views/appcenter.vue'
const routes = [
{
path: '/', component: index,
children:[
{path: '', redirect: '/home'},
{ path: '/home', component: home },
{ path: '/app-center', component: appcenter }
]
},
]
const router = createRouter({
history: createMemoryHistory(),
routes,
})
export default router;

View File

@ -1 +1,12 @@
@import 'tailwindcss'; @import 'tailwindcss';
/*
* 配置教程 https://juejin.cn/post/7480450288421109787
*/
@theme inline {
--color-primary: #085ce6;
--color-secondary: #FFB81C;
--color-dark: #12192C;
--color-light: #F5F7FA;
--color-muted: #6B7280;
--font-display: 'Inter', 'system-ui', 'sans-serif';
}

646
web/src/views/appcenter.vue Normal file
View File

@ -0,0 +1,646 @@
<template>
<div class="min-h-screen bg-gray-50 p-6">
<!-- 页面标题 -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-gray-800 mb-2">应用中心</h1>
<p class="text-gray-600">发现和使用企业内部的各类应用系统</p>
</div>
<!-- 搜索和筛选 -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-8">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div class="relative w-full md:w-1/3">
<el-input
v-model="searchQuery"
placeholder="搜索应用..."
:prefix-icon="Search"
clearable
></el-input>
</div>
<div class="flex flex-wrap gap-2">
<el-button
v-for="category in categories"
:key="category.id"
:type="activeCategory === category.id ? 'primary' : 'text'"
@click="activeCategory = category.id"
>
{{ category.name }}
</el-button>
<el-button type="text" @click="activeCategory = 'all'">全部</el-button>
</div>
</div>
</div>
<!-- 我的应用 -->
<div class="mb-10">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800">我的应用</h2>
<div class="flex items-center gap-2">
<el-tooltip content="拖拽可调整顺序" placement="top">
<el-button type="text" icon="el-icon-rank" size="small"></el-button>
</el-tooltip>
<el-tooltip content="管理我的应用" placement="top">
<el-button type="text" icon="el-icon-setting" @click="showManageMyApps = true"></el-button>
</el-tooltip>
</div>
</div>
<!-- 使用draggable实现拖拽排序 -->
<draggable
v-model="myApps"
group="people"
@start="drag=true"
@end="onDragEnd"
class="grid grid-cols-1 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4"
>
<template #item="{element}">
<el-card
class="app-card cursor-pointer hover:shadow-lg transition-shadow duration-300 relative"
@click="openApp(element)"
>
<div class="absolute top-2 left-2 drag-handle cursor-move opacity-50 hover:opacity-100 transition-opacity mt-1">
<el-icon size="16"><Rank /></el-icon>
</div>
<!-- 卡片内容保持不变 -->
<div class="absolute top-2 right-2">
<el-button
type="text"
:icon="Delete"
@click.stop="confirmRemoveFromMyApps(element)"
></el-button>
</div>
<div class="flex flex-col items-center p-4">
<div class="w-14 h-14 rounded-lg bg-primary/10 flex items-center justify-center mb-3">
<el-icon :size="24" :class="element.icon"></el-icon>
</div>
<h3 class="font-medium text-gray-800 mb-1 text-center truncate w-full">{{ element.name }}</h3>
<p class="text-xs text-gray-500 text-center truncate w-full">{{ element.description }}</p>
</div>
</el-card>
</template>
</draggable>
<!-- 没有我的应用时的提示 -->
<div
v-if="myApps.length === 0 && !searchQuery.trim()"
class="col-span-full flex flex-col items-center justify-center py-10 text-gray-500"
>
<el-icon class="text-4xl mb-3"><Collection /></el-icon>
<p>您还没有添加任何应用到"我的应用"</p>
<el-button type="text" size="small" @click="showManageMyApps = true">添加应用</el-button>
</div>
<!-- 搜索无结果提示 -->
<div
v-if="filteredMyApps.length === 0 && searchQuery.trim()"
class="col-span-full flex flex-col items-center justify-center py-10 text-gray-500"
>
<el-icon class="text-4xl mb-3"><Search /></el-icon>
<p>没有找到匹配的应用</p>
</div>
</div>
<!-- 全部应用 -->
<div>
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800">全部应用</h2>
<span class="text-sm text-gray-500">{{ filteredAllApps.length }} 个应用</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<el-card
v-for="app in filteredAllApps"
:key="app.id"
class="app-card cursor-pointer hover:shadow-lg transition-shadow duration-300"
@click="openApp(app)"
>
<div class="flex flex-col items-center p-4">
<div class="w-14 h-14 rounded-lg bg-primary/10 flex items-center justify-center mb-3">
<el-icon :size="24" :class="app.icon"></el-icon>
</div>
<h3 class="font-medium text-gray-800 mb-1 text-center truncate w-full">{{ app.name }}</h3>
<p class="text-xs text-gray-500 text-center truncate w-full">{{ app.description }}</p>
<!-- 添加到我的应用按钮 -->
<el-button
v-if="!isAppInMyApps(app.id)"
type="text"
size="mini"
icon="el-icon-plus"
class="mt-2"
@click.stop="addToMyApps(app)"
>
添加
</el-button>
</div>
</el-card>
<!-- 没有应用时的提示 -->
<div
v-if="filteredAllApps.length === 0"
class="col-span-full flex flex-col items-center justify-center py-10 text-gray-500"
>
<el-icon class="text-4xl mb-3"><Search /></el-icon>
<p>没有找到匹配的应用</p>
</div>
</div>
</div>
<!-- 管理我的应用对话框 -->
<el-dialog
title="管理我的应用"
:visible.sync="showManageMyApps"
width="40%"
:before-close="handleCloseManageMyApps"
>
<template #content>
<div class="p-4">
<p class="text-sm text-gray-600 mb-4">从下方选择您常用的应用添加到"我的应用"</p>
<el-input
v-model="manageSearchQuery"
placeholder="搜索应用..."
:prefix-icon="Search"
clearable
class="mb-4"
>
</el-input>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<el-checkbox-group v-model="selectedApps">
<el-checkbox
v-for="app in filteredManageApps"
:key="app.id"
:label="app.id"
class="flex items-center"
>
<el-icon :class="app.icon" class="mr-2"></el-icon>
<span class="text-sm">{{ app.name }}</span>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
<template #footer>
<el-button @click="showManageMyApps = false">取消</el-button>
<el-button type="primary" @click="saveMyApps">保存</el-button>
</template>
</el-dialog>
<!-- 删除确认对话框 -->
<el-dialog
title="确认删除"
:model-value="showDeleteConfirm"
width="30%"
>
<p>确定要从"我的应用"中移除 <span class="font-medium">{{ deletingAppName }}</span> </p>
<template #footer>
<el-button @click="showDeleteConfirm = false">取消</el-button>
<el-button type="danger" @click="confirmDelete">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import draggable from 'vuedraggable'
import {
Collection,
Search,
Delete,
Remove,
Rank,
//
Monitor,
Document,
Setting,
User,
Message,
Calendar,
PieChart,
Download,
Upload,
Folder,
Clock,
Bell,
Lock,
Share,
Star
} from '@element-plus/icons-vue'
import { ElMessage, ElDialog } from 'element-plus'
//
const categories = ref([
{ id: 'workflow', name: '工作流程' },
{ id: 'data', name: '数据分析' },
{ id: 'communication', name: '沟通协作' },
{ id: 'document', name: '文档管理' },
{ id: 'system', name: '系统工具' }
])
//
interface App {
id: number
name: string
description: string
icon: any
category: string
url: string
}
const drag=ref(false)
//
const allApps = ref<App[]>([
{
id: 1,
name: '项目管理系统',
description: '管理项目进度、任务分配和资源',
icon: Monitor,
category: 'workflow',
url: '/project-management'
},
{
id: 2,
name: '文档中心',
description: '企业级文档管理和协作平台',
icon: Document,
category: 'document',
url: '/document-center'
},
{
id: 3,
name: '企业邮箱',
description: '安全可靠的企业邮件系统',
icon: Message,
category: 'communication',
url: '/mail'
},
{
id: 4,
name: '人力资源系统',
description: '员工信息管理和人事流程',
icon: User,
category: 'workflow',
url: '/hr-system'
},
{
id: 5,
name: '数据分析平台',
description: '企业数据可视化和分析工具',
icon: PieChart,
category: 'data',
url: '/data-analysis'
},
{
id: 6,
name: '日程安排',
description: '团队和个人日程管理',
icon: Calendar,
category: 'workflow',
url: '/calendar'
},
{
id: 7,
name: '知识库',
description: '企业知识沉淀和共享平台',
icon: Folder,
category: 'document',
url: '/knowledge-base'
},
{
id: 8,
name: '视频会议',
description: '高清视频会议和远程协作',
icon: PieChart,
category: 'communication',
url: '/video-conference'
},
{
id: 9,
name: '系统设置',
description: '企业系统配置和管理',
icon: Setting,
category: 'system',
url: '/system-settings'
},
{
id: 10,
name: '数据备份',
description: '企业数据定期备份和恢复',
icon: Download,
category: 'system',
url: '/data-backup'
},
{
id: 11,
name: '报表生成器',
description: '自定义报表和数据导出',
icon: PieChart,
category: 'data',
url: '/report-generator'
},
{
id: 12,
name: '移动办公',
description: '随时随地处理工作事务',
icon: PieChart,
category: 'workflow',
url: '/mobile-office'
},
{
id: 13,
name: '文件传输',
description: '安全高效的大文件传输工具',
icon: Upload,
category: 'system',
url: '/file-transfer'
},
{
id: 14,
name: '工单系统',
description: 'IT服务请求和问题跟踪',
icon: PieChart,
category: 'workflow',
url: '/work-order'
},
{
id: 15,
name: '实时监控',
description: '系统性能和状态实时监控',
icon: Clock,
category: 'system',
url: '/monitoring'
},
{
id: 16,
name: '企业公告',
description: '公司通知和重要消息发布',
icon: Bell,
category: 'communication',
url: '/announcements'
},
{
id: 17,
name: '权限管理',
description: '用户权限和访问控制',
icon: Lock,
category: 'system',
url: '/permission-management'
},
{
id: 18,
name: '数据同步',
description: '多系统间数据实时同步',
icon: PieChart,
category: 'data',
url: '/data-sync'
},
{
id: 19,
name: '协作白板',
description: '团队在线协作和头脑风暴',
icon: Share,
category: 'communication',
url: '/collaboration-board'
},
{
id: 20,
name: '应用商店',
description: '发现和安装企业应用',
icon: Star,
category: 'system',
url: '/app-store'
}
])
// ID
const myAppIds = ref<number[]>([])
//
const searchQuery = ref('')
const activeCategory = ref('all')
const showManageMyApps = ref(false)
const manageSearchQuery = ref('')
const selectedApps = ref<number[]>([])
//
const showDeleteConfirm = ref(false)
const deletingAppId = ref<number | null>(null)
const deletingAppName = ref('')
//
const myApps = computed<App[]>({
get() {
// myAppIds
return myAppIds.value.map(id => allApps.value.find(app => app.id === id)).filter(Boolean) as App[]
},
set(newValue) {
// myAppIds
myAppIds.value = newValue.map(app => app.id)
}
})
//
const filteredMyApps = computed(() => {
if (!searchQuery.value.trim()) return myApps.value
const query = searchQuery.value.toLowerCase().trim()
return myApps.value.filter(app =>
app.name.toLowerCase().includes(query) ||
app.description.toLowerCase().includes(query)
)
})
//
const filteredAllApps = computed(() => {
let apps = allApps.value
//
if (activeCategory.value !== 'all') {
apps = apps.filter(app => app.category === activeCategory.value)
}
//
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase().trim()
apps = apps.filter(app =>
app.name.toLowerCase().includes(query) ||
app.description.toLowerCase().includes(query)
)
}
return apps
})
//
const filteredManageApps = computed(() => {
if (!manageSearchQuery.value.trim()) return allApps.value
const query = manageSearchQuery.value.toLowerCase().trim()
return allApps.value.filter(app =>
app.name.toLowerCase().includes(query) ||
app.description.toLowerCase().includes(query)
)
})
//
const isAppInMyApps = (appId: number) => {
return myAppIds.value.includes(appId)
}
//
const addToMyApps = (app: App) => {
if (!isAppInMyApps(app.id)) {
myAppIds.value.push(app.id)
saveMyAppsToLocalStorage()
//
ElMessage({
message: `已将"${app.name}"添加到我的应用`,
type: 'success'
})
}
}
//
const confirmRemoveFromMyApps = (app: App) => {
console.log(app)
deletingAppId.value = app.id
deletingAppName.value = app.name
showDeleteConfirm.value = true
}
//
const confirmDelete = () => {
if (deletingAppId.value) {
myAppIds.value = myAppIds.value.filter(id => id !== deletingAppId.value)
saveMyAppsToLocalStorage()
//
ElMessage({
message: `已从我的应用中移除"${deletingAppName.value}"`,
type: 'info'
})
//
showDeleteConfirm.value = false
deletingAppId.value = null
deletingAppName.value = ''
}
}
//
const openApp = (app: App) => {
//
console.log(`Opening app: ${app.name} (${app.url})`)
//
ElMessage({
message: `正在打开"${app.name}"`,
type: 'info'
})
}
//
const saveMyApps = () => {
myAppIds.value = [...selectedApps.value]
saveMyAppsToLocalStorage()
showManageMyApps.value = false
//
ElMessage({
message: '我的应用设置已保存',
type: 'success'
})
}
//
const handleCloseManageMyApps = (done: () => void) => {
//
selectedApps.value = [...myAppIds.value]
done()
}
//
const onDragEnd = () => {
drag.value=false
//
saveMyAppsToLocalStorage()
ElMessage({
message: '应用顺序已更新',
type: 'success',
duration: 1000
})
}
//
const saveMyAppsToLocalStorage = () => {
localStorage.setItem('myApps', JSON.stringify(myAppIds.value))
}
//
const loadMyAppsFromLocalStorage = () => {
const savedMyApps = localStorage.getItem('myApps')
if (savedMyApps) {
try {
myAppIds.value = JSON.parse(savedMyApps)
} catch (e) {
console.error('Failed to parse saved my apps', e)
// 使
myAppIds.value = [1, 2, 3, 4]
}
} else {
//
myAppIds.value = [1, 2, 3, 4]
}
//
selectedApps.value = [...myAppIds.value]
}
//
onMounted(() => {
loadMyAppsFromLocalStorage()
})
</script>
<style scoped>
.app-card {
border-radius: 10px;
transition: all 0.3s ease;
}
.app-card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* 拖拽相关样式 */
.drag-handle {
transition: all 0.2s;
}
.drag-handle:hover {
color: #409eff;
}
/* 隐藏拖拽过程中的默认高亮样式 */
:deep(.ghost) {
opacity: 0.5;
}
:deep(.dragging) {
opacity: 0.8;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="flex flex-col h-screen overflow-hidden bg-gray-50">
<!-- 顶部导航栏 -->
<header class="bg-primary text-white shadow-md h-16 flex items-center justify-between px-6">
<!-- 左侧Logo和菜单按钮 -->
<div class="flex items-center">
<button @click="isCollapse = !isCollapse" class="mr-4 text-white focus:outline-none">
<el-icon><Fold /></el-icon>
</button>
<div class="flex items-center">
<el-icon><Platform /></el-icon>
<span class="ml-2 text-lg font-semibold hidden md:block">企业内网门户</span>
</div>
</div>
<!-- 右侧用户信息 -->
<div class="flex items-center">
<div class="relative mr-6">
<button class="relative text-white focus:outline-none">
<el-icon><Bell /></el-icon>
<span class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">3</span>
</button>
</div>
<div class="flex items-center">
<img src="https://picsum.photos/200/200?random=1" alt="User Avatar" class="w-8 h-8 rounded-full object-cover">
<span class="ml-2 hidden sm:block">管理员</span>
<el-icon><CaretBottom /></el-icon>
</div>
</div>
</header>
<!-- 主内容区 -->
<div class="flex flex-1 overflow-hidden">
<!-- 侧边栏菜单 -->
<aside class="bg-white shadow-md z-10 transition-all duration-300" :style="{ width: isCollapse ? '64px' : '200px' }">
<el-menu
default-active="2"
class="el-menu-vertical-demo h-full"
:collapse="isCollapse"
@open="handleOpen"
@close="handleClose"
router
>
<el-menu-item index="/home">
<el-icon><IconMenu /></el-icon>
<template #title>首页</template>
</el-menu-item>
<el-menu-item index="/app-center">
<el-icon><IconMenu /></el-icon>
<template #title>应用中心</template>
</el-menu-item>
<el-sub-menu index="2">
<template #title>
<el-icon><Location /></el-icon>
<span>导航一</span>
</template>
<el-menu-item-group>
<template #title><span>分组一</span></template>
<el-menu-item index="1-1">菜单项一</el-menu-item>
<el-menu-item index="1-2">菜单项二</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组二">
<el-menu-item index="1-3">菜单项三</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title><span>菜单项四</span></template>
<el-menu-item index="1-4-1">子菜单项一</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="3" disabled>
<el-icon><Document /></el-icon>
<template #title>导航三</template>
</el-menu-item>
<el-menu-item index="4">
<el-icon><Setting /></el-icon>
<template #title>导航四</template>
</el-menu-item>
</el-menu>
</aside>
<!-- 内容区域 -->
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
<router-view></router-view>
</main>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import {
Document,
Menu as IconMenu,
Location,
Setting,
// 使
Fold,
Platform,
Bell,
CaretBottom
} from '@element-plus/icons-vue'
const isCollapse = ref(false)
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>
<style scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
</style>

70
web/src/views/home.vue Normal file
View File

@ -0,0 +1,70 @@
<script setup lang="ts">
</script>
<template>
<div class="max-w-7xl mx-auto">
<h1 class="text-2xl font-bold text-gray-800 mb-6">欢迎使用企业内网门户</h1>
<!-- 页面内容示例 -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-4">系统公告</h2>
<el-alert
title="系统升级通知"
type="info"
description="本系统将于本周六凌晨2点进行例行维护预计维护时间为2小时。维护期间系统将暂停服务请提前做好工作安排。"
show-icon
:closable="false"
></el-alert>
</div>
<!-- 统计卡片示例 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<el-card class="bg-white rounded-lg shadow-md p-4">
<div slot="header" class="flex justify-between items-center">
<span>项目统计</span>
<el-button icon="el-icon-refresh" circle size="small" type="text"></el-button>
</div>
<div class="flex items-center justify-center">
<el-progress :percentage="75" type="circle" :width="120"></el-progress>
</div>
<div class="text-center mt-2">
<p class="text-gray-500 text-sm">进行中项目</p>
<p class="text-blue-600 text-2xl font-bold">12/16</p>
</div>
</el-card>
<el-card class="bg-white rounded-lg shadow-md p-4">
<div slot="header" class="flex justify-between items-center">
<span>待办事项</span>
<el-button icon="el-icon-refresh" circle size="small" type="text"></el-button>
</div>
<div class="flex items-center justify-center">
<el-progress :percentage="40" type="circle" :width="120" color="#f56c6c"></el-progress>
</div>
<div class="text-center mt-2">
<p class="text-gray-500 text-sm">今日待办</p>
<p class="text-red-500 text-2xl font-bold">6/15</p>
</div>
</el-card>
<el-card class="bg-white rounded-lg shadow-md p-4">
<div slot="header" class="flex justify-between items-center">
<span>通知消息</span>
<el-button icon="el-icon-refresh" circle size="small" type="text"></el-button>
</div>
<div class="flex items-center justify-center">
<el-progress :percentage="20" type="circle" :width="120" color="#e6a23c"></el-progress>
</div>
<div class="text-center mt-2">
<p class="text-gray-500 text-sm">未读消息</p>
<p class="text-orange-500 text-2xl font-bold">3/15</p>
</div>
</el-card>
</div>
</div>
</template>
<style scoped>
</style>

13
web/src/views/index.vue Normal file
View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import Layout from "./components/layout.vue";
</script>
<template>
<layout></layout>
</template>
<style scoped>
</style>

View File

@ -6,6 +6,7 @@ import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
tailwindcss(), tailwindcss(
),
], ],
}) })