| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- <template>
- <div class="dashboard">
- <!-- 统计概览 -->
- <el-row :gutter="16">
- <el-col v-for="card in visibleStatCards" :key="card.key" :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
- <el-card class="stat-card" shadow="hover" @click="$router.push(card.path)">
- <div class="stat-item">
- <div class="stat-icon" :style="{ background: card.color }">
- <el-icon><component :is="card.icon" /></el-icon>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ card.value }}</div>
- <div class="stat-label">{{ card.label }}</div>
- </div>
- </div>
- </el-card>
- </el-col>
- </el-row>
- <el-row :gutter="16" style="margin-top: 16px;">
- <!-- 快捷入口 -->
- <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
- <el-card class="quick-card">
- <template #header>
- <div class="quick-header">
- <span>快捷入口</span>
- </div>
- </template>
- <div class="quick-grid">
- <div
- v-for="item in visibleQuickActions"
- :key="item.key"
- class="quick-item"
- @click="$router.push(item.path)"
- >
- <div class="quick-icon" :style="{ background: item.color }">
- <el-icon><component :is="item.icon" /></el-icon>
- <el-badge v-if="item.badge && stats.todoCount > 0" :value="stats.todoCount" class="todo-badge" />
- </div>
- <div class="quick-text">{{ item.label }}</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <!-- 常用流程 -->
- <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
- <el-card class="quick-card">
- <template #header>
- <div class="quick-header">
- <span>常用流程</span>
- <el-link type="primary" :underline="false" @click="$router.push('/approval/execute')">更多</el-link>
- </div>
- </template>
- <div v-loading="loadingDefinitions" class="common-flow-list">
- <el-empty v-if="!loadingDefinitions && commonFlows.length === 0" description="暂无可用流程" />
- <div
- v-for="def in commonFlows"
- :key="def.id"
- class="common-flow-item"
- @click="handleStart(def)"
- >
- <div class="common-flow-icon">
- <el-icon><Promotion /></el-icon>
- </div>
- <div class="common-flow-info">
- <div class="common-flow-name">{{ def.name }}</div>
- <div v-if="def.category" class="common-flow-category">{{ def.category }}</div>
- </div>
- <el-button type="primary" size="small" @click.stop="handleStart(def)">发起</el-button>
- </div>
- </div>
- </el-card>
- </el-col>
- </el-row>
- <!-- 发起流程弹窗 -->
- <el-dialog v-model="startDialogVisible" title="快速发起流程" width="640px">
- <el-form ref="startFormRef" :model="startForm" :rules="startFormRules" label-width="80px">
- <el-form-item v-show="false">
- <el-input v-model="startForm.processDefinitionId" />
- </el-form-item>
- <el-form-item label="流程名称">
- <el-input v-model="startForm.definitionName" disabled />
- </el-form-item>
- <el-form-item label="标题" prop="title">
- <el-input v-model="startForm.title" placeholder="请输入流程标题" />
- </el-form-item>
- <el-form-item label="表单数据">
- <FlowFormFields v-if="hasFormFields" ref="flowFormFieldsRef" v-model="dynamicFormData" :definition="selectedDefinition" />
- <el-input v-else v-model="startForm.formData" type="textarea" :rows="4" placeholder="请输入表单数据,格式:每行一个字段,如 amount=1000" />
- </el-form-item>
- <el-form-item label="附件">
- <el-upload
- v-model:file-list="fileUpload.attachmentList"
- action="#"
- :http-request="fileUpload.handleUpload"
- :before-upload="beforeFileUpload"
- :before-remove="() => true"
- :limit="5"
- multiple
- :on-preview="fileUpload.handleFilePreview"
- >
- <el-button type="primary" size="small">上传附件</el-button>
- <template #tip>
- <div class="el-upload__tip">支持 Excel、图片等常见格式</div>
- </template>
- <template #file="{ file }">
- <div class="upload-file-item">
- <el-icon><Document /></el-icon>
- <el-link type="primary" @click="fileUpload.openPreview(file.url || file.response)">
- {{ file.name }}
- </el-link>
- </div>
- </template>
- </el-upload>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="startDialogVisible = false">取消</el-button>
- <el-button type="primary" :loading="starting" @click="submitStart">确认发起</el-button>
- </template>
- </el-dialog>
- <!-- 附件预览 -->
- <FilePreview v-model="fileUpload.previewVisible" :url="fileUpload.previewUrl" />
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted, reactive, computed } from 'vue'
- import { useRouter } from 'vue-router'
- import { ElMessage } from 'element-plus'
- import type { FormInstance, FormRules } from 'element-plus'
- import {
- Bell, CircleCheck, Connection, UserFilled, Message,
- Document, TrendCharts, Pointer, EditPen, User, Promotion
- } from '@element-plus/icons-vue'
- import { listTodo, listHandled, listCc } from '@/api/flow/task'
- import { listMyInstance, startInstance } from '@/api/flow/instance'
- import { listDefinition, listEnabled } from '@/api/flow/definition'
- import { listUser } from '@/api/system/user'
- import { getOverview } from '@/api/analysis'
- import { beforeFileUpload, collectAttachmentUrls } from '@/utils/file'
- import { useFileUpload } from '@/composables/useFileUpload'
- import type { FlowDefinition } from '@/types/flow'
- import { useUserStore } from '@/stores/user-store'
- import FlowFormFields from '@/components/FlowFormFields/index.vue'
- import FilePreview from '@/components/FilePreview/index.vue'
- const router = useRouter()
- const userStore = useUserStore()
- const employeeType = computed(() => userStore.userInfo?.employeeType || 'common_user')
- const isFlowAdmin = computed(() => ['flow_manager', 'super_admin'].includes(employeeType.value))
- const isSystemAdmin = computed(() => ['dept_manager', 'super_admin'].includes(employeeType.value))
- const stats = ref({
- todoCount: 0,
- handledCount: 0,
- instanceCount: 0,
- ccCount: 0,
- definitionCount: 0,
- userCount: 0,
- analysisTotal: 0,
- analysisRunning: 0
- })
- const statCardConfigs = computed(() => [
- { key: 'todo', label: '我的待办', value: stats.value.todoCount, color: '#67C23A', icon: Bell, path: '/approval/todo', visible: true },
- { key: 'handled', label: '我的已办', value: stats.value.handledCount, color: '#409EFF', icon: CircleCheck, path: '/approval/handled', visible: true },
- { key: 'mine', label: '我的流程', value: stats.value.instanceCount, color: '#E6A23C', icon: Connection, path: '/approval/mine', visible: true },
- { key: 'cc', label: '我的抄送', value: stats.value.ccCount, color: '#909399', icon: Message, path: '/approval/cc', visible: true },
- { key: 'definition', label: '流程管理', value: stats.value.definitionCount, color: '#409EFF', icon: Document, path: '/flow/definition', visible: isFlowAdmin.value },
- { key: 'analysis', label: '数据看板', value: stats.value.analysisRunning, color: '#13C2C2', icon: TrendCharts, path: '/analysis/flow', visible: isFlowAdmin.value },
- { key: 'user', label: '系统用户', value: stats.value.userCount, color: '#F56C6C', icon: UserFilled, path: '/system/user', visible: isSystemAdmin.value }
- ])
- const visibleStatCards = computed(() => statCardConfigs.value.filter(c => c.visible))
- const quickActionConfigs = computed(() => [
- { key: 'todo', label: '处理待办', color: '#67C23A', icon: Bell, path: '/approval/todo', badge: true },
- { key: 'execute', label: '流程执行', color: '#909399', icon: Pointer, path: '/approval/execute' },
- { key: 'mine', label: '我的流程', color: '#E6A23C', icon: Connection, path: '/approval/mine' },
- { key: 'cc', label: '我的抄送', color: '#909399', icon: Message, path: '/approval/cc' },
- { key: 'designer', label: '设计流程', color: '#409EFF', icon: EditPen, path: '/flow/designer', visible: isFlowAdmin.value },
- { key: 'definition', label: '流程管理', color: '#E6A23C', icon: Document, path: '/flow/definition', visible: isFlowAdmin.value },
- { key: 'user', label: '用户管理', color: '#F56C6C', icon: User, path: '/system/user', visible: isSystemAdmin.value },
- { key: 'analysis', label: '数据看板', color: '#13C2C2', icon: TrendCharts, path: '/analysis/flow', visible: isFlowAdmin.value }
- ])
- const visibleQuickActions = computed(() => quickActionConfigs.value.filter(c => c.visible !== false))
- const loadingDefinitions = ref(false)
- const commonFlows = ref<FlowDefinition[]>([])
- const startDialogVisible = ref(false)
- const starting = ref(false)
- const startFormRef = ref<FormInstance>()
- const startForm = reactive({
- processDefinitionId: 0,
- definitionName: '',
- title: '',
- formData: ''
- })
- const dynamicFormData = ref<Record<string, unknown>>({})
- const selectedDefinition = ref<FlowDefinition | null>(null)
- const flowFormFieldsRef = ref<any>(null)
- const hasFormFields = computed(() => {
- const schema = selectedDefinition.value?.formSchema
- if (!schema) return false
- try {
- const parsed = JSON.parse(schema)
- return Array.isArray(parsed) && parsed.length > 0
- } catch {
- return false
- }
- })
- const fileUpload = reactive(useFileUpload())
- const startFormRules: FormRules = {
- title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
- }
- async function loadStats() {
- try {
- // 所有用户可见的统计
- const [todoRes, handledRes, instRes, ccRes] = await Promise.all([
- listTodo({ pageNum: 1, pageSize: 1 }),
- listHandled({ pageNum: 1, pageSize: 1 }),
- listMyInstance({ pageNum: 1, pageSize: 1 }),
- listCc({ pageNum: 1, pageSize: 1 })
- ])
- stats.value.todoCount = todoRes.total ?? 0
- stats.value.handledCount = handledRes.total ?? 0
- stats.value.instanceCount = instRes.total ?? 0
- stats.value.ccCount = ccRes.total ?? 0
- // 流程管理员 / 超级管理员可见
- if (isFlowAdmin.value) {
- const [defRes, overviewRes] = await Promise.all([
- listDefinition({ pageNum: 1, pageSize: 1 }, { silent: true }),
- getOverview({}, { silent: true })
- ])
- stats.value.definitionCount = defRes.total ?? 0
- stats.value.analysisTotal = overviewRes?.totalInstances ?? 0
- stats.value.analysisRunning = overviewRes?.runningCount ?? 0
- }
- // 部门经理 / 超级管理员可见
- if (isSystemAdmin.value) {
- const userRes = await listUser({ pageNum: 1, pageSize: 1 }, { silent: true })
- stats.value.userCount = userRes.total ?? 0
- }
- } catch {
- // ignore
- }
- }
- async function loadCommonFlows() {
- loadingDefinitions.value = true
- try {
- const res = await listEnabled()
- commonFlows.value = (res || []).slice(0, 6)
- } catch {
- commonFlows.value = []
- } finally {
- loadingDefinitions.value = false
- }
- }
- function handleStart(def: FlowDefinition) {
- startForm.processDefinitionId = def.id
- startForm.definitionName = def.name
- startForm.title = ''
- startForm.formData = ''
- dynamicFormData.value = {}
- selectedDefinition.value = def
- fileUpload.resetFileUpload()
- startDialogVisible.value = true
- }
- async function submitStart() {
- const valid = await startFormRef.value?.validate().catch(() => false)
- if (!valid) return
- starting.value = true
- try {
- const dynamicValid = flowFormFieldsRef.value?.validate() ?? true
- if (dynamicValid === false) return
- let formData: Record<string, unknown> | undefined
- if (selectedDefinition.value?.formSchema) {
- formData = { ...dynamicFormData.value }
- } else if (startForm.formData.trim()) {
- formData = {}
- startForm.formData.split('\n').forEach(line => {
- const idx = line.indexOf('=')
- if (idx > 0) {
- const key = line.substring(0, idx).trim()
- const value = line.substring(idx + 1).trim()
- if (key) {
- const num = Number(value)
- formData![key] = !isNaN(num) && value !== '' ? num : value
- }
- }
- })
- }
- const attachmentUrls = fileUpload.attachmentList.length > 0
- ? collectAttachmentUrls(fileUpload.attachmentList)
- : undefined
- await startInstance(startForm.processDefinitionId, startForm.title || undefined, formData, attachmentUrls)
- ElMessage.success('流程发起成功')
- startDialogVisible.value = false
- router.push('/approval/mine')
- } finally {
- starting.value = false
- }
- }
- onMounted(() => {
- loadStats()
- loadCommonFlows()
- })
- </script>
- <style scoped>
- .dashboard {
- padding: 4px;
- }
- .stat-card {
- cursor: pointer;
- transition: all 0.2s ease;
- margin-bottom: 16px;
- }
- .stat-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
- .stat-item {
- display: flex;
- align-items: center;
- gap: 14px;
- }
- .stat-icon {
- width: 52px;
- height: 52px;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #fff;
- font-size: 26px;
- }
- .stat-value {
- font-size: 26px;
- font-weight: bold;
- color: #303133;
- line-height: 1.2;
- }
- .stat-label {
- font-size: 13px;
- color: #909399;
- margin-top: 4px;
- }
- .quick-card {
- min-height: 280px;
- }
- .quick-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .quick-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
- }
- .quick-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 14px;
- border-radius: 8px;
- background: #f5f7fa;
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .quick-item:hover {
- background: #e6f2ff;
- transform: translateY(-2px);
- }
- .quick-icon {
- position: relative;
- width: 44px;
- height: 44px;
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #fff;
- font-size: 22px;
- }
- .todo-badge :deep(.el-badge__content) {
- position: absolute;
- top: -6px;
- right: -6px;
- }
- .quick-text {
- font-size: 15px;
- font-weight: 500;
- color: #303133;
- }
- .common-flow-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .common-flow-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px;
- border-radius: 8px;
- background: #f5f7fa;
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .common-flow-item:hover {
- background: #e6f2ff;
- }
- .common-flow-icon {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: #409eff;
- color: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 18px;
- }
- .common-flow-info {
- flex: 1;
- min-width: 0;
- }
- .common-flow-name {
- font-size: 14px;
- font-weight: 500;
- color: #303133;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .common-flow-category {
- font-size: 12px;
- color: #909399;
- margin-top: 2px;
- }
- .upload-file-item {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 4px 0;
- }
- </style>
|