Files
tiantian-system/app/pages/oa.vue
赵忠林 f9e1286ab1 refactor(developer-config): 移除开发者配置页面相关代码和文档
- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue
- 移除开发者文档页面及其导航与样式实现
- 清理开发者侧功能完善工作日志文件
- 删除全局.gitignore配置文件,清理无用忽略规则
- 优化应用配置页面的参数读取和路由结构,解决刷新404问题
- 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入
- 移除对后端配置加密字段的 secret 标记,修正加密异常问题
2026-04-09 07:35:34 +08:00

730 lines
24 KiB
Vue
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dashboard">
<!-- 顶部统计区域 -->
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- 待办任务 -->
<a-card class="stat-card">
<div class="flex items-center justify-between">
<div>
<div class="text-2xl font-bold text-gray-900">12</div>
<div class="mt-1 text-sm text-gray-500">待办任务</div>
</div>
<div class="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
<span class="text-2xl text-blue-600">📋</span>
</div>
</div>
<div class="mt-4 text-xs text-gray-400">今日有 3 项任务到期</div>
</a-card>
<!-- 进行中项目 -->
<a-card class="stat-card">
<div class="flex items-center justify-between">
<div>
<div class="text-2xl font-bold text-gray-900">8</div>
<div class="mt-1 text-sm text-gray-500">进行中项目</div>
</div>
<div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center">
<span class="text-2xl text-green-600">🚀</span>
</div>
</div>
<div class="mt-4 text-xs text-gray-400">其中有 2 个项目需关注</div>
</a-card>
<!-- 在线成员 -->
<a-card class="stat-card">
<div class="flex items-center justify-between">
<div>
<div class="text-2xl font-bold text-gray-900">24/32</div>
<div class="mt-1 text-sm text-gray-500">在线成员</div>
</div>
<div class="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center">
<span class="text-2xl text-purple-600">👥</span>
</div>
</div>
<div class="mt-4 text-xs text-gray-400">75% 在线率</div>
</a-card>
<!-- 文档协作 -->
<a-card class="stat-card">
<div class="flex items-center justify-between">
<div>
<div class="text-2xl font-bold text-gray-900">156</div>
<div class="mt-1 text-sm text-gray-500">文档协作</div>
</div>
<div class="w-12 h-12 rounded-full bg-orange-100 flex items-center justify-center">
<span class="text-2xl text-orange-600">📄</span>
</div>
</div>
<div class="mt-4 text-xs text-gray-400">今日更新 14 个文档</div>
</a-card>
</div>
<!-- 主要内容区域 -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- 左侧项目概览 -->
<div class="lg:col-span-2">
<!-- 快捷操作 -->
<a-card class="mb-6">
<template #title>
<div class="flex items-center justify-between">
<span>📌 快捷操作</span>
<a-button type="link" size="small">更多</a-button>
</div>
</template>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div v-for="action in quickActions" :key="action.label"
class="quick-action-item group"
@click="navigateTo(action.to)">
<div class="quick-action-icon" :style="{ backgroundColor: action.color + '10', color: action.color }">
{{ action.icon }}
</div>
<div class="quick-action-text">
<div class="font-medium">{{ action.label }}</div>
<div class="text-xs text-gray-500">{{ action.desc }}</div>
</div>
</div>
</div>
</a-card>
<!-- 项目进度 -->
<a-card>
<template #title>
<div class="flex items-center justify-between">
<span>🏗 项目进度</span>
<a-button type="link" size="small" @click="navigateTo('/oa/projects')">查看所有</a-button>
</div>
</template>
<a-table :data-source="activeProjects" :pagination="false" size="small">
<a-table-column title="项目名称" data-index="name">
<template #default="{ record }">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: record.color }"></div>
<span>{{ record.name }}</span>
</div>
</template>
</a-table-column>
<a-table-column title="进度" data-index="progress">
<template #default="{ record }">
<div class="flex items-center gap-2">
<a-progress :percent="record.progress" :stroke-color="record.status === '紧急' ? '#ff4d4f' : record.status === '延期' ? '#faad14' : '#52c41a'" size="small" :show-info="false" />
<span class="text-sm">{{ record.progress }}%</span>
</div>
</template>
</a-table-column>
<a-table-column title="状态" data-index="status">
<template #default="{ record }">
<a-tag :color="record.status === '紧急' ? 'red' : record.status === '延期' ? 'orange' : 'green'">
{{ record.status }}
</a-tag>
</template>
</a-table-column>
<a-table-column title="截止日" data-index="deadline">
<template #default="{ record }">
<span class="text-sm">{{ record.deadline }}</span>
</template>
</a-table-column>
</a-table>
</a-card>
<!-- 今日任务 -->
<a-card class="mt-6">
<template #title>
<div class="flex items-center justify-between">
<span> 今日任务</span>
<a-space>
<a-button type="link" size="small" @click="navigateTo('/oa/tasks')">查看所有</a-button>
<a-button type="primary" size="small" @click="showAddTaskModal = true">+ 新建</a-button>
</a-space>
</div>
</template>
<div class="space-y-3">
<div v-for="task in todaysTasks" :key="task.id"
class="task-item"
:class="{ completed: task.completed }"
@click="toggleTask(task.id)">
<div class="task-checkbox">
<a-checkbox :checked="task.completed" />
</div>
<div class="task-details flex-1">
<div class="task-title">{{ task.title }}</div>
<div class="task-meta">
<span class="task-project">{{ task.project }}</span>
<span class="task-due">截止: {{ task.dueDate }}</span>
<span class="task-assignee">负责人: {{ task.assignee }}</span>
</div>
</div>
<div class="task-priority">
<a-tag :color="task.priority === 'high' ? 'red' : task.priority === 'medium' ? 'orange' : 'green'" size="small">
{{ task.priority === 'high' ? '高' : task.priority === 'medium' ? '中' : '低' }}
</a-tag>
</div>
</div>
</div>
</a-card>
</div>
<!-- 右侧团队协作 -->
<div class="space-y-6">
<!-- 团队状态 -->
<a-card>
<template #title>
<div class="flex items-center justify-between">
<span>👥 团队成员</span>
<span class="text-sm text-gray-500">{{ onlineMembers }}/{{ totalMembers }} 在线</span>
</div>
</template>
<div class="space-y-3">
<div v-for="member in teamMembers" :key="member.id" class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="relative">
<a-avatar :src="member.avatar" :size="32" />
<div class="status-indicator" :class="member.status"></div>
</div>
<div>
<div class="font-medium">{{ member.name }}</div>
<div class="text-xs text-gray-500">{{ member.role }}</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">{{ member.tasks }} 任务</span>
<a-button type="link" size="small" @click="startChat(member.id)">聊天</a-button>
</div>
</div>
</div>
</a-card>
<!-- 文档协作 -->
<a-card>
<template #title>
<div class="flex items-center justify-between">
<span>📄 最近文档</span>
<a-button type="link" size="small" @click="navigateTo('/oa/documents')">查看所有</a-button>
</div>
</template>
<div class="space-y-3">
<div v-for="doc in recentDocuments" :key="doc.id"
class="document-item"
@click="openDocument(doc.id)">
<div class="flex items-center gap-3">
<div class="document-icon" :class="'type-' + doc.type">
{{ doc.type === 'doc' ? '📝' : doc.type === 'sheet' ? '📊' : doc.type === 'slide' ? '📽️' : '📋' }}
</div>
<div class="flex-1 min-w-0">
<div class="truncate font-medium">{{ doc.title }}</div>
<div class="text-xs text-gray-500">
{{ doc.owner }} · {{ doc.lastModified }}
</div>
</div>
</div>
<div v-if="doc.collaborators > 0" class="text-xs text-gray-500">
+{{ doc.collaborators }} 人在协作
</div>
</div>
</div>
</a-card>
<!-- 今日会议 -->
<a-card>
<template #title>
<div class="flex items-center justify-between">
<span>🗓 今日会议</span>
<a-button type="link" size="small" @click="navigateTo('/oa/meetings')">查看所有</a-button>
</div>
</template>
<div class="space-y-3">
<div v-for="meeting in todaysMeetings" :key="meeting.id" class="meeting-item">
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">
{{ meeting.type === 'team' ? '👥' : meeting.type === 'client' ? '🤝' : '💬' }}
</div>
</div>
<div class="flex-1">
<div class="font-medium">{{ meeting.title }}</div>
<div class="text-xs text-gray-500 mt-1">
{{ meeting.time }} · {{ meeting.duration }}
</div>
<div class="text-xs text-gray-500">
{{ meeting.participants }} 人参加
</div>
</div>
<a-button type="link" size="small" @click="joinMeeting(meeting.id)">加入</a-button>
</div>
</div>
</div>
</a-card>
<!-- 公告 -->
<a-card>
<template #title>
<div class="flex items-center justify-between">
<span>📢 最新公告</span>
<a-button type="link" size="small" @click="navigateTo('/oa/announcements')">查看所有</a-button>
</div>
</template>
<div class="space-y-3">
<div v-for="ann in recentAnnouncements" :key="ann.id" class="announcement-item">
<div class="flex items-start gap-3">
<div class="announcement-avatar">
<a-avatar :src="ann.avatar" :size="24" />
</div>
<div class="flex-1">
<div class="font-medium">{{ ann.title }}</div>
<div class="text-xs text-gray-500 mt-1">{{ ann.author }} · {{ ann.time }}</div>
</div>
</div>
<div class="announcement-content text-sm text-gray-600 mt-2">
{{ ann.content }}
</div>
</div>
</div>
</a-card>
</div>
</div>
<!-- 添加任务模态框 -->
<a-modal v-model:open="showAddTaskModal" title="新建任务" :footer="null" width="400px">
<a-form :model="newTask" layout="vertical">
<a-form-item label="任务标题">
<a-input v-model:value="newTask.title" placeholder="请输入任务标题" />
</a-form-item>
<a-form-item label="优先级">
<a-select v-model:value="newTask.priority" placeholder="请选择优先级">
<a-select-option value="high"></a-select-option>
<a-select-option value="medium"></a-select-option>
<a-select-option value="low"></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="截止日期">
<a-date-picker v-model:value="newTask.dueDate" placeholder="选择截止日期" />
</a-form-item>
<a-form-item label="分配给">
<a-select v-model:value="newTask.assignee" placeholder="选择负责人">
<a-select-option v-for="member in teamMembers" :key="member.id" :value="member.name">
{{ member.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="所属项目">
<a-select v-model:value="newTask.project" placeholder="选择项目">
<a-select-option v-for="project in activeProjects" :key="project.id" :value="project.name">
{{ project.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" block @click="addNewTask">创建任务</a-button>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { CheckOutlined } from '@ant-design/icons-vue'
useHead({ title: '协同办公 - 概览仪表板 · 葳溯科技' })
definePageMeta({ layout: 'oa' })
interface Project {
id: number
name: string
progress: number
tasks: number
completedTasks: number
deadline: string
status: '正常' | '延期' | '紧急'
owner: string
color: string
}
interface TeamMember {
id: number
name: string
avatar: string
role: string
status: '在线' | '忙碌' | '离开' | '离线'
tasks: number
}
interface QuickTask {
id: number
title: string
priority: 'high' | 'medium' | 'low'
dueDate: string
assignee: string
project: string
completed: boolean
}
interface Document {
id: number
title: string
type: 'doc' | 'sheet' | 'slide' | 'board'
owner: string
lastModified: string
collaborators: number
}
// 快捷操作
const quickActions = [
{ icon: '📝', label: '新建文档', desc: '文档/表格/幻灯片', color: '#1890ff', to: '/oa/documents/create' },
{ icon: '📋', label: '新建任务', desc: '添加待办事项', color: '#52c41a', to: '/oa/tasks/create' },
{ icon: '🗓️', label: '安排会议', desc: '团队/客户会议', color: '#722ed1', to: '/oa/meetings/create' },
{ icon: '🚀', label: '立项申请', desc: '启动新项目', color: '#fa8c16', to: '/oa/projects/create' },
]
// 活动项目数据
const activeProjects = ref<Project[]>([
{ id: 1, name: '智慧园区管理系统', progress: 85, tasks: 42, completedTasks: 36, deadline: '2024-10-30', status: '正常', owner: '张三', color: '#1890ff' },
{ id: 2, name: '客户关系管理升级', progress: 45, tasks: 28, completedTasks: 13, deadline: '2024-11-15', status: '延期', owner: '李四', color: '#52c41a' },
{ id: 3, name: '移动端App开发', progress: 92, tasks: 35, completedTasks: 32, deadline: '2024-10-25', status: '正常', owner: '王五', color: '#722ed1' },
{ id: 4, name: '数据中心建设', progress: 68, tasks: 56, completedTasks: 38, deadline: '2024-11-05', status: '紧急', owner: '赵六', color: '#fa8c16' },
])
// 今日任务
const todaysTasks = ref<QuickTask[]>([
{ id: 1, title: '完成项目需求文档撰写', priority: 'high', dueDate: '2024-10-20 18:00', assignee: '张三', project: '智慧园区管理系统', completed: false },
{ id: 2, title: '客户演示PPT设计', priority: 'medium', dueDate: '2024-10-20 15:00', assignee: '李四', project: '客户关系管理升级', completed: true },
{ id: 3, title: 'API接口联调测试', priority: 'high', dueDate: '2024-10-20 17:00', assignee: '王五', project: '移动端App开发', completed: false },
{ id: 4, title: '服务器运维巡检', priority: 'low', dueDate: '2024-10-20 12:00', assignee: '赵六', project: '数据中心建设', completed: false },
{ id: 5, title: '团队周会纪要整理', priority: 'medium', dueDate: '2024-10-20 16:00', assignee: '张三', project: '日常', completed: false },
])
// 团队成员
const teamMembers = ref<TeamMember[]>([
{ id: 1, name: '张三', avatar: 'https://randomuser.me/api/portraits/men/32.jpg', role: '产品经理', status: '在线', tasks: 8 },
{ id: 2, name: '李四', avatar: 'https://randomuser.me/api/portraits/women/44.jpg', role: 'UI设计师', status: '在线', tasks: 5 },
{ id: 3, name: '王五', avatar: 'https://randomuser.me/api/portraits/men/67.jpg', role: '前端工程师', status: '忙碌', tasks: 12 },
{ id: 4, name: '赵六', avatar: 'https://randomuser.me/api/portraits/women/23.jpg', role: '后端工程师', status: '在线', tasks: 9 },
{ id: 5, name: '钱七', avatar: 'https://randomuser.me/api/portraits/men/89.jpg', role: '测试工程师', status: '离开', tasks: 7 },
{ id: 6, name: '孙八', avatar: 'https://randomuser.me/api/portraits/women/56.jpg', role: '运维工程师', status: '离线', tasks: 4 },
])
// 最近文档
const recentDocuments = ref<Document[]>([
{ id: 1, title: '产品需求文档_V3.2', type: 'doc', owner: '张三', lastModified: '今天 10:30', collaborators: 3 },
{ id: 2, title: 'Q3季度销售报表', type: 'sheet', owner: '李四', lastModified: '今天 09:15', collaborators: 5 },
{ id: 3, title: '项目启动会简报', type: 'slide', owner: '王五', lastModified: '昨天 16:45', collaborators: 2 },
{ id: 4, title: '技术架构讨论纪要', type: 'doc', owner: '赵六', lastModified: '昨天 14:20', collaborators: 4 },
])
// 今日会议
const todaysMeetings = ref([
{ id: 1, title: '产品设计评审会', type: 'team', time: '14:00-15:00', duration: '1小时', participants: 8 },
{ id: 2, title: '客户演示会议', type: 'client', time: '16:30-17:30', duration: '1小时', participants: 6 },
{ id: 3, title: '技术方案讨论会', type: 'team', time: '11:00-12:00', duration: '1小时', participants: 5 },
])
// 最新公告
const recentAnnouncements = ref([
{ id: 1, title: '关于启用新的绩效考核制度', content: '自下月起,公司正式启用新的季度绩效考核制度,请各位同事认真学习相关文档。', author: '人力资源部', avatar: 'https://randomuser.me/api/portraits/men/12.jpg', time: '今天 09:00' },
{ id: 2, title: '国庆节放假通知', content: '根据国家法定节假日安排公司将于10月1日至7日放假10月8日正常上班。', author: '行政部', avatar: 'https://randomuser.me/api/portraits/women/34.jpg', time: '昨天 17:30' },
])
// 在线成员统计
const onlineMembers = computed(() => teamMembers.value.filter(m => m.status === '在线' || m.status === '忙碌').length)
const totalMembers = computed(() => teamMembers.value.length)
// 添加任务模态框状态
const showAddTaskModal = ref(false)
const newTask = ref({
title: '',
priority: 'medium',
dueDate: null,
assignee: '',
project: ''
})
// 方法
function toggleTask(taskId: number) {
const task = todaysTasks.value.find(t => t.id === taskId)
if (task) {
task.completed = !task.completed
}
}
function addNewTask() {
if (!newTask.value.title.trim()) {
message.error('请填写任务标题')
return
}
const newId = todaysTasks.value.length + 1
todaysTasks.value.unshift({
id: newId,
title: newTask.value.title,
priority: newTask.value.priority,
dueDate: newTask.value.dueDate ? newTask.value.dueDate.format('YYYY-MM-DD HH:mm') : '今天 18:00',
assignee: newTask.value.assignee || '张三',
project: newTask.value.project || '日常',
completed: false
})
message.success('任务创建成功')
showAddTaskModal.value = false
resetNewTask()
}
function resetNewTask() {
newTask.value = {
title: '',
priority: 'medium',
dueDate: null,
assignee: '',
project: ''
}
}
function startChat(userId: number) {
navigateTo(`/oa/chat?user=${userId}`)
}
function openDocument(docId: number) {
navigateTo(`/oa/documents/${docId}`)
}
function joinMeeting(meetingId: number) {
message.info(`正在加入会议 ${meetingId}`)
// 实际应用中这里是跳转到视频会议页面
}
</script>
<style scoped>
.dashboard {
min-height: calc(100vh - 140px);
}
/* 统计卡片 */
.stat-card {
background: linear-gradient(135deg, #fff 0%, #fafafa 100%);
border: 1px solid #f0f0f0;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: #d9d9d9;
}
/* 快捷操作 */
.quick-action-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid #f0f0f0;
}
.quick-action-item:hover {
background-color: #fafafa;
border-color: #d9d9d9;
transform: translateY(-1px);
}
.quick-action-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
transition: all 0.2s ease;
}
.quick-action-text {
flex: 1;
min-width: 0;
}
/* 任务项 */
.task-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid #f0f0f0;
}
.task-item:hover {
background-color: #fafafa;
border-color: #d9d9d9;
}
.task-item.completed {
opacity: 0.7;
}
.task-item.completed .task-title {
text-decoration: line-through;
color: #999;
}
.task-checkbox {
flex-shrink: 0;
}
.task-details {
min-width: 0;
}
.task-title {
font-weight: 500;
margin-bottom: 4px;
}
.task-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 12px;
color: #666;
}
.task-project::before {
content: "#";
margin-right: 2px;
opacity: 0.6;
}
.task-due {
color: #fa8c16;
}
/* 成员状态指示器 */
.status-indicator {
position: absolute;
bottom: 0;
right: 0;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid #fff;
}
.status-indicator.在线 {
background-color: #52c41a;
}
.status-indicator.忙碌 {
background-color: #fa8c16;
}
.status-indicator.离开 {
background-color: #fadb14;
}
.status-indicator.离线 {
background-color: #d9d9d9;
}
/* 文档项 */
.document-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid #f0f0f0;
}
.document-item:hover {
background-color: #fafafa;
border-color: #d9d9d9;
}
.document-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.document-icon.type-doc {
background-color: #e6f7ff;
color: #1890ff;
}
.document-icon.type-sheet {
background-color: #f6ffed;
color: #52c41a;
}
.document-icon.type-slide {
background-color: #fff7e6;
color: #fa8c16;
}
.document-icon.type-board {
background-color: #f9f0ff;
color: #722ed1;
}
/* 会议项 */
.meeting-item {
padding: 12px;
border-radius: 8px;
border: 1px solid #f0f0f0;
transition: all 0.2s ease;
}
.meeting-item:hover {
background-color: #fafafa;
border-color: #d9d9d9;
}
/* 公告项 */
.announcement-item {
padding: 12px;
border-radius: 8px;
border: 1px solid #f0f0f0;
transition: all 0.2s ease;
}
.announcement-item:hover {
background-color: #fafafa;
border-color: #d9d9d9;
}
.announcement-avatar {
flex-shrink: 0;
}
.announcement-content {
line-height: 1.6;
}
/* 响应式调整 */
@media (max-width: 768px) {
.dashboard {
padding: 12px;
}
.task-meta {
flex-direction: column;
gap: 4px;
}
:deep(.ant-card) {
margin-bottom: 12px;
}
}
</style>