feat(home): 实现首页消息、流程和项目管理界面

- 添加 MessageDetail 和 MessageList 组件用于消息管理
- 添加 WorkflowDetail 和 WorkflowList 组件用于流程管理
- 添加 ProjectDetail 和 ProjectList 组件用于项目管理
- 实现 FunctionNav 导航组件支持功能切换
- 创建 FeatureList 组件展示不同功能列表
- 开发 OperationSpace 组件提供操作空间详情展示
- 集成所有组件到 Home 主页面布局
- 添加 lucide 图标样式类定义
- 实现 useHome 组合式 API 提供数据管理和状态控制
This commit is contained in:
王宏建
2026-03-10 15:33:36 +08:00
parent b1dba03325
commit b4fa80ba82
13 changed files with 632 additions and 38 deletions
+4
View File
@@ -5,3 +5,7 @@
@utility input {
@apply w-full outline-none ;
}
.lucide {
@apply my-1.5 size-4;
}
+38
View File
@@ -0,0 +1,38 @@
<template>
<div class="space-y-4">
<div>
<h3 class="font-bold text-lg">{{ item.title }}</h3>
<p class="text-sm text-base-content/70 mt-1">{{ item.time }}</p>
</div>
<div class="card bg-base-200">
<div class="card-body p-4">
<p class="text-sm">{{ item.content }}</p>
</div>
</div>
<div class="flex gap-2 flex-wrap">
<button @click="emit('action', 'reply', item.id)" class="btn btn-sm btn-primary">
回复
</button>
<button @click="emit('action', 'markRead', item.id)" class="btn btn-sm btn-outline">
标记已读
</button>
<button @click="emit('action', 'delete', item.id)" class="btn btn-sm btn-ghost text-error">
删除
</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { Message } from '@/composables/useHome'
defineProps<{
item: Message
}>()
const emit = defineEmits<{
action: [action: string, id: number]
}>()
</script>
+36
View File
@@ -0,0 +1,36 @@
<template>
<ul class="list bg-base-100 rounded-box shadow-md">
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide flex items-center justify-between">
<span class="mr-auto">本周任务</span>
<button class="cursor-pointer" title="新增">
<SquarePlus />
</button>
</li>
<li class="list-row" v-for="item in items" :class="{
'bg-primary': selectedId == item.id
}" @click="emit('select', item.id)">
<div class="list-col-grow">
<div>{{ item.title }}</div>
<div class="text-xs uppercase font-semibold opacity-60">{{ item.content }}</div>
</div>
<button class="btn btn-square btn-ghost">
<Trash></Trash>
</button>
</li>
</ul>
</template>
<script setup lang="ts">
import type { Message } from '@/composables/useHome'
import { Trash, SquarePlus } from 'lucide-vue-next';
defineProps<{
items: Message[]
selectedId: number | null
}>()
const emit = defineEmits<{
select: [id: number]
}>()
</script>
+58
View File
@@ -0,0 +1,58 @@
<template>
<div class="space-y-4">
<div>
<h3 class="font-bold text-lg">{{ item.name }}</h3>
<div class="flex gap-2 mt-2">
<span class="badge badge-sm" :class="{
'badge-info': item.status === '进行中',
'badge-ghost': item.status === '规划中',
'badge-success': item.status === '已完成',
'badge-warning': item.status === '已暂停'
}">
{{ item.status }}
</span>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body p-4 space-y-3">
<div class="flex justify-between">
<span class="text-sm text-base-content/70">项目负责人</span>
<span class="text-sm font-medium">{{ item.manager }}</span>
</div>
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-base-content/70">当前进度</span>
<span class="font-medium">{{ item.progress }}%</span>
</div>
<progress class="progress progress-primary w-full" :value="item.progress" max="100"></progress>
</div>
</div>
</div>
<div class="flex gap-2 flex-wrap">
<button @click="emit('action', 'view', item.id)" class="btn btn-sm btn-outline">
查看详情
</button>
<button @click="emit('action', 'edit', item.id)" class="btn btn-sm btn-primary">
编辑项目
</button>
<button @click="emit('action', 'addMember', item.id)" class="btn btn-sm btn-secondary">
添加成员
</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { Project } from '@/composables/useHome'
defineProps<{
item: Project
}>()
const emit = defineEmits<{
action: [action: string, id: number]
}>()
</script>
+47
View File
@@ -0,0 +1,47 @@
<template>
<ul class="list bg-base-100 rounded-box shadow-md">
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide flex items-center justify-between">
<span class="mr-auto">项目列表</span>
<button class="cursor-pointer" title="新增">
<SquarePlus />
</button>
</li>
<li class="list-row" v-for="item in items" :class="{
'bg-primary': selectedId == item.id
}" @click="emit('select', item.id)">
<div class="list-col-grow">
<div>{{ item.name }}</div>
<div class="text-xs uppercase font-semibold opacity-60">{{ item.manager }}</div>
</div>
<div class="flex flex-col gap-1">
<span class="badge badge-sm" :class="{
'badge-info': item.status === '进行中',
'badge-ghost': item.status === '规划中',
'badge-success': item.status === '已完成',
'badge-warning': item.status === '已暂停'
}">
{{ item.status }}
</span>
<progress class="progress progress-primary w-24" :value="item.progress" max="100"></progress>
</div>
<button class="btn btn-square btn-ghost">
<Trash></Trash>
</button>
</li>
</ul>
</template>
<script setup lang="ts">
import type { Project } from '@/composables/useHome'
import { Trash, SquarePlus } from 'lucide-vue-next';
defineProps<{
items: Project[]
selectedId: number | null
}>()
const emit = defineEmits<{
select: [id: number]
}>()
</script>
+54
View File
@@ -0,0 +1,54 @@
<template>
<div class="space-y-4">
<div>
<h3 class="font-bold text-lg">{{ item.name }}</h3>
<div class="flex gap-2 mt-2">
<span class="badge badge-sm" :class="{
'badge-info': item.status === '进行中',
'badge-success': item.status === '已完成',
'badge-ghost': item.status === '草稿',
'badge-warning': item.status === '已暂停'
}">
{{ item.status }}
</span>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body p-4 space-y-2">
<div class="flex justify-between">
<span class="text-sm text-base-content/70">创建人</span>
<span class="text-sm font-medium">{{ item.creator }}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-base-content/70">创建时间</span>
<span class="text-sm font-medium">{{ item.createTime }}</span>
</div>
</div>
</div>
<div class="flex gap-2 flex-wrap">
<button @click="emit('action', 'edit', item.id)" class="btn btn-sm btn-primary">
编辑流程
</button>
<button @click="emit('action', 'view', item.id)" class="btn btn-sm btn-outline">
查看流程图
</button>
<button @click="emit('action', 'start', item.id)" class="btn btn-sm btn-success" :disabled="item.status !== '草稿'">
启动流程
</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { Workflow } from '@/composables/useHome'
defineProps<{
item: Workflow
}>()
const emit = defineEmits<{
action: [action: string, id: number]
}>()
</script>
+43
View File
@@ -0,0 +1,43 @@
<template>
<ul class="list bg-base-100 rounded-box shadow-md">
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide flex items-center justify-between">
<span class="mr-auto">流程列表</span>
<button class="cursor-pointer" title="新增">
<SquarePlus />
</button>
</li>
<li class="list-row" v-for="item in items" :class="{
'bg-primary': selectedId == item.id
}" @click="emit('select', item.id)">
<div class="list-col-grow">
<div>{{ item.name }}</div>
<div class="text-xs uppercase font-semibold opacity-60">{{ item.creator }}</div>
</div>
<span class="badge badge-sm" :class="{
'badge-info': item.status === '进行中',
'badge-success': item.status === '已完成',
'badge-ghost': item.status === '草稿',
'badge-warning': item.status === '已暂停'
}">
{{ item.status }}
</span>
<button class="btn btn-square btn-ghost">
<Trash></Trash>
</button>
</li>
</ul>
</template>
<script setup lang="ts">
import type { Workflow } from '@/composables/useHome'
import { Trash, SquarePlus } from 'lucide-vue-next';
defineProps<{
items: Workflow[]
selectedId: number | null
}>()
const emit = defineEmits<{
select: [id: number]
}>()
</script>
+49
View File
@@ -0,0 +1,49 @@
<template>
<section class="card bg-base-100 shadow-sm h-full overflow-hidden">
<div class="card-body p-0 h-full">
<MessageList
v-if="currentFunction === 'message'"
:items="messages"
:selected-id="selectedMessageId"
@select="emit('selectMessage', $event)"
/>
<WorkflowList
v-else-if="currentFunction === 'workflow'"
:items="workflows"
:selected-id="selectedWorkflowId"
@select="emit('selectWorkflow', $event)"
/>
<ProjectList
v-else
:items="projects"
:selected-id="selectedProjectId"
@select="emit('selectProject', $event)"
/>
</div>
</section>
</template>
<script setup lang="ts">
import type { FunctionType, Message, Workflow, Project } from '@/composables/useHome'
import MessageList from '../common/MessageList.vue'
import WorkflowList from '../common/WorkflowList.vue'
import ProjectList from '../common/ProjectList.vue'
defineProps<{
currentFunction: FunctionType
messages: Message[]
workflows: Workflow[]
projects: Project[]
selectedMessageId: number | null
selectedWorkflowId: number | null
selectedProjectId: number | null
}>()
const emit = defineEmits<{
selectMessage: [id: number]
selectWorkflow: [id: number]
selectProject: [id: number]
}>()
</script>
+32
View File
@@ -0,0 +1,32 @@
<template>
<ul class="menu w-full grow">
<!-- List item -->
<li v-for="item in buttons" class="mb-1">
<button class="is-drawer-close:tooltip is-drawer-close:tooltip-right" :class="{
'bg-primary': currentFunction == item.type
}" :data-tip="item.label" @click="emit('select', item.type)">
<component :is="item.icon"></component>
<span class="is-drawer-close:hidden">{{ item.label }}</span>
</button>
</li>
</ul>
</template>
<script setup lang="ts">
import type { FunctionType } from '@/composables/useHome'
import { Mail, Workflow, FolderKanban } from 'lucide-vue-next'
defineProps<{
currentFunction: FunctionType
}>()
const emit = defineEmits<{
select: [func: FunctionType]
}>()
const buttons = [
{ type: 'message' as FunctionType, label: '消息列表', icon: Mail },
{ type: 'workflow' as FunctionType, label: '流程设计', icon: Workflow },
{ type: 'project' as FunctionType, label: '项目管理', icon: FolderKanban },
] as const
</script>
+59
View File
@@ -0,0 +1,59 @@
<template>
<section class="card bg-base-100 shadow-sm h-full overflow-hidden">
<div class="card-body p-4">
<h2 class="font-bold text-base mb-3">操作空间</h2>
<div class="overflow-y-auto h-[calc(100%-40px)]">
<!-- 空状态 -->
<div
v-if="!selectedMessage && !selectedWorkflow && !selectedProject"
class="flex flex-col items-center justify-center h-full text-base-content/50"
>
<div class="text-4xl mb-2">👈</div>
<p class="text-sm">请从列表中选择一个项目查看详情</p>
</div>
<!-- 消息详情 -->
<MessageDetail
v-if="currentFunction === 'message' && selectedMessage"
:item="selectedMessage"
@action="(action, id) => emit('messageAction', action, id)"
/>
<!-- 流程详情 -->
<WorkflowDetail
v-else-if="currentFunction === 'workflow' && selectedWorkflow"
:item="selectedWorkflow"
@action="(action, id) => emit('workflowAction', action, id)"
/>
<!-- 项目详情 -->
<ProjectDetail
v-else-if="currentFunction === 'project' && selectedProject"
:item="selectedProject"
@action="(action, id) => emit('projectAction', action, id)"
/>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import type { FunctionType, Message, Workflow, Project } from '@/composables/useHome'
import MessageDetail from '../common/MessageDetail.vue'
import WorkflowDetail from '../common/WorkflowDetail.vue'
import ProjectDetail from '../common/ProjectDetail.vue'
defineProps<{
currentFunction: FunctionType
selectedMessage: Message | undefined
selectedWorkflow: Workflow | undefined
selectedProject: Project | undefined
}>()
const emit = defineEmits<{
messageAction: [action: string, id: number]
workflowAction: [action: string, id: number]
projectAction: [action: string, id: number]
}>()
</script>
+72
View File
@@ -0,0 +1,72 @@
<template>
<div class="drawer lg:drawer-open">
<input id="my-drawer-4" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col">
<!-- Navbar -->
<nav class="navbar w-full bg-base-300">
<label for="my-drawer-4" aria-label="打开侧边栏" class="btn btn-square btn-ghost">
<!-- Sidebar toggle icon -->
<PanelLeftOpen class="[.drawer-toggle:checked~.drawer-content_.navbar_&]:hidden" />
<PanelLeftClose class="[.drawer-toggle:checked~.drawer-content_.navbar_&]:block hidden" />
</label>
<div class="px-4">{{ currentFunction }}</div>
</nav>
<!-- Page content here -->
<div class="p-4 flex-1 flex flex-row gap-1">
<div class="col-span-5 min-w-[300px]">
<FeatureList :current-function="currentFunction" :messages="messages" :workflows="workflows"
:projects="projects" :selected-message-id="selectedMessageId" :selected-workflow-id="selectedWorkflowId"
:selected-project-id="selectedProjectId" @select-message="selectMessage" @select-workflow="selectWorkflow"
@select-project="selectProject" />
</div>
<!-- 右侧操作空间 -->
<div class="col-span-5 flex-1">
<OperationSpace :current-function="currentFunction" :selected-message="selectedMessage"
:selected-workflow="selectedWorkflow" :selected-project="selectedProject"
@message-action="handleMessageAction" @workflow-action="handleWorkflowAction"
@project-action="handleProjectAction" />
</div>
</div>
</div>
<div class="drawer-side is-drawer-close:overflow-visible">
<label for="my-drawer-4" aria-label="close sidebar" class="drawer-overlay"></label>
<div class="flex min-h-full flex-col items-start bg-base-200 is-drawer-close:w-14 is-drawer-open:w-64">
<!-- Sidebar content here -->
<FunctionNav :current-function="currentFunction" @select="selectFunction" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useHome } from '@/composables/useHome'
import FunctionNav from './FunctionNav.vue'
import FeatureList from './FeatureList.vue'
import OperationSpace from './OperationSpace.vue'
import { PanelLeftClose, PanelLeftOpen } from 'lucide-vue-next'
const {
currentFunction,
messages,
workflows,
projects,
selectedMessageId,
selectedWorkflowId,
selectedProjectId,
selectedMessage,
selectedWorkflow,
selectedProject,
selectFunction,
selectMessage,
selectWorkflow,
selectProject,
handleMessageAction,
handleWorkflowAction,
handleProjectAction,
} = useHome()
</script>
+137
View File
@@ -0,0 +1,137 @@
import { ref, computed } from 'vue'
export type FunctionType = 'message' | 'workflow' | 'project'
export interface Message {
id: number
title: string
content: string
time: string
unread?: boolean
}
export interface Workflow {
id: number
name: string
status: '进行中' | '已完成' | '草稿' | '已暂停'
creator: string
createTime: string
}
export interface Project {
id: number
name: string
status: '进行中' | '规划中' | '已完成' | '已暂停'
progress: number
manager: string
}
export function useHome() {
const currentFunction = ref<FunctionType>('message')
const selectedMessageId = ref<number | null>(null)
const selectedWorkflowId = ref<number | null>(null)
const selectedProjectId = ref<number | null>(null)
// 模拟数据
const messages= ref<Message[]>([
{ id: 1, title: '系统通知', content: '欢迎使用角色管理系统,祝您工作顺利!', time: '2026-03-10 10:00', unread: true },
{ id: 2, title: '任务提醒', content: '您有一个待处理的任务需要在今天完成。', time: '2026-03-10 11:30', unread: true },
{ id: 3, title: '审批请求', content: '新的流程审批等待您的处理,请及时查看。', time: '2026-03-10 14:20', unread: false },
{ id: 4, title: '会议通知', content: '明天上午 10 点召开项目进度会议,请准时参加。', time: '2026-03-10 15:00', unread: false },
])
const workflows = ref<Workflow[]>([
{ id: 1, name: '请假审批流程', status: '进行中', creator: '张三', createTime: '2026-03-09' },
{ id: 2, name: '采购申请流程', status: '已完成', creator: '李四', createTime: '2026-03-08' },
{ id: 3, name: '项目立项流程', status: '草稿', creator: '王五', createTime: '2026-03-10' },
{ id: 4, name: '费用报销流程', status: '进行中', creator: '赵六', createTime: '2026-03-07' },
])
const projects = ref<Project[]>([
{ id: 1, name: 'ERP 系统升级', status: '进行中', progress: 65, manager: '张三' },
{ id: 2, name: 'CRM 客户管理系统', status: '规划中', progress: 20, manager: '李四' },
{ id: 3, name: '数据分析平台', status: '已完成', progress: 100, manager: '王五' },
{ id: 4, name: '移动办公 APP', status: '进行中', progress: 45, manager: '赵六' },
])
// 计算属性:获取当前选中的项目
const selectedMessage = computed(() =>
messages.value.find(m => m.id === selectedMessageId.value)
)
const selectedWorkflow = computed(() =>
workflows.value.find(w => w.id === selectedWorkflowId.value)
)
const selectedProject = computed(() =>
projects.value.find(p => p.id === selectedProjectId.value)
)
// 切换功能
const selectFunction = (func: FunctionType) => {
currentFunction.value = func
// 清空其他选中项
selectedMessageId.value = null
selectedWorkflowId.value = null
selectedProjectId.value = null
}
// 选择列表项
const selectMessage = (id: number) => {
selectedMessageId.value = id
// 标记为已读
const msg = messages.value.find(m => m.id === id)
if (msg) msg.unread = false
}
const selectWorkflow = (id: number) => {
selectedWorkflowId.value = id
}
const selectProject = (id: number) => {
selectedProjectId.value = id
}
// 操作处理函数
const handleMessageAction = (action: string, messageId: number) => {
console.log(`处理消息 ${messageId}: ${action}`)
// TODO: 实现具体业务逻辑
}
const handleWorkflowAction = (action: string, workflowId: number) => {
console.log(`处理流程 ${workflowId}: ${action}`)
// TODO: 实现具体业务逻辑
}
const handleProjectAction = (action: string, projectId: number) => {
console.log(`处理项目 ${projectId}: ${action}`)
// TODO: 实现具体业务逻辑
}
return {
// 状态
currentFunction,
selectedMessageId,
selectedWorkflowId,
selectedProjectId,
// 数据
messages,
workflows,
projects,
// 计算属性
selectedMessage,
selectedWorkflow,
selectedProject,
// 方法
selectFunction,
selectMessage,
selectWorkflow,
selectProject,
handleMessageAction,
handleWorkflowAction,
handleProjectAction,
}
}
+2 -37
View File
@@ -1,42 +1,7 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import HomePage from '@/components/home/index.vue'
</script>
<template>
<main class="container">
<h1>Home Page</h1>
<p>Welcome to the home page!</p>
<nav>
<RouterLink to="/">Home</RouterLink> |
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/login">登录</RouterLink>
</nav>
</main>
<HomePage />
</template>
<style scoped>
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
nav {
margin-top: 20px;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: none;
margin: 0 10px;
}
a:hover {
color: #535bf2;
text-decoration: underline;
}
</style>