|
@@ -22,7 +22,10 @@
|
|
|
<p v-if="def.category"><strong>分类:</strong>{{ def.category }}</p>
|
|
<p v-if="def.category"><strong>分类:</strong>{{ def.category }}</p>
|
|
|
<p v-if="def.description"><strong>描述:</strong>{{ def.description }}</p>
|
|
<p v-if="def.description"><strong>描述:</strong>{{ def.description }}</p>
|
|
|
</div>
|
|
</div>
|
|
|
- <el-button type="primary" size="small" @click="handleStart(def)">发起流程</el-button>
|
|
|
|
|
|
|
+ <div class="def-actions">
|
|
|
|
|
+ <el-button type="primary" size="small" @click="handleStart(def)">发起流程</el-button>
|
|
|
|
|
+ <el-button type="info" size="small" :icon="View" @click="openPreview(def)">查看详情</el-button>
|
|
|
|
|
+ </div>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</div>
|
|
</div>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
@@ -50,6 +53,60 @@
|
|
|
</template>
|
|
</template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
+ <!-- 流程详情预览弹窗 -->
|
|
|
|
|
+ <el-dialog v-model="previewVisible" title="流程详情" width="640px">
|
|
|
|
|
+ <div v-if="previewDefinition" class="preview-content">
|
|
|
|
|
+ <div class="preview-section">
|
|
|
|
|
+ <h4>基本信息</h4>
|
|
|
|
|
+ <p><strong>流程名称:</strong>{{ previewDefinition.name }}</p>
|
|
|
|
|
+ <p><strong>流程编码:</strong>{{ previewDefinition.code }}</p>
|
|
|
|
|
+ <p v-if="previewDefinition.category"><strong>分类:</strong>{{ previewDefinition.category }}</p>
|
|
|
|
|
+ <p v-if="previewDefinition.description"><strong>描述:</strong>{{ previewDefinition.description }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="previewFormFields.length > 0" class="preview-section">
|
|
|
|
|
+ <h4>表单字段</h4>
|
|
|
|
|
+ <el-table :data="previewFormFields" size="small" border>
|
|
|
|
|
+ <el-table-column prop="label" label="显示名" min-width="120" />
|
|
|
|
|
+ <el-table-column prop="name" label="字段名" min-width="120" />
|
|
|
|
|
+ <el-table-column label="类型" width="90">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ {{ fieldTypeText(row.type) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="必填" width="70" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-checkbox v-model="row.required" disabled />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="previewApprovalNodes.length > 0" class="preview-section">
|
|
|
|
|
+ <h4>审批节点</h4>
|
|
|
|
|
+ <el-timeline>
|
|
|
|
|
+ <el-timeline-item
|
|
|
|
|
+ v-for="(node, index) in previewApprovalNodes"
|
|
|
|
|
+ :key="node.id"
|
|
|
|
|
+ :type="node.type === 'cc-node' ? 'info' : 'primary'"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="preview-node">
|
|
|
|
|
+ <span class="preview-node-index">{{ index + 1 }}</span>
|
|
|
|
|
+ <span class="preview-node-name">{{ node.name }}</span>
|
|
|
|
|
+ <el-tag size="small" :type="node.type === 'cc-node' ? 'info' : 'warning'">
|
|
|
|
|
+ {{ node.type === 'cc-node' ? '抄送' : '审批' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-timeline-item>
|
|
|
|
|
+ </el-timeline>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="previewVisible = false">关闭</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="handleStartFromPreview">发起流程</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
<InstanceDetail v-model="detailVisible" :instance-id="currentInstanceId" />
|
|
<InstanceDetail v-model="detailVisible" :instance-id="currentInstanceId" />
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
@@ -58,11 +115,12 @@
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import type { FormInstance, FormRules } from 'element-plus'
|
|
import type { FormInstance, FormRules } from 'element-plus'
|
|
|
|
|
+import { View } from '@element-plus/icons-vue'
|
|
|
import { listEnabled } from '@/api/flow/definition'
|
|
import { listEnabled } from '@/api/flow/definition'
|
|
|
import { startInstance } from '@/api/flow/instance'
|
|
import { startInstance } from '@/api/flow/instance'
|
|
|
import { uploadFile } from '@/api/file'
|
|
import { uploadFile } from '@/api/file'
|
|
|
import { beforeFileUpload, collectAttachmentUrls } from '@/utils/file'
|
|
import { beforeFileUpload, collectAttachmentUrls } from '@/utils/file'
|
|
|
-import type { FlowDefinition } from '@/types/flow'
|
|
|
|
|
|
|
+import type { FlowDefinition, FormField } from '@/types/flow'
|
|
|
import InstanceDetail from './InstanceDetail.vue'
|
|
import InstanceDetail from './InstanceDetail.vue'
|
|
|
import FlowFormFields from '@/components/FlowFormFields/index.vue'
|
|
import FlowFormFields from '@/components/FlowFormFields/index.vue'
|
|
|
|
|
|
|
@@ -84,6 +142,60 @@ const hasFormFields = computed(() => {
|
|
|
})
|
|
})
|
|
|
const detailVisible = ref(false)
|
|
const detailVisible = ref(false)
|
|
|
const currentInstanceId = ref(0)
|
|
const currentInstanceId = ref(0)
|
|
|
|
|
+const previewVisible = ref(false)
|
|
|
|
|
+const previewDefinition = ref<FlowDefinition | null>(null)
|
|
|
|
|
+
|
|
|
|
|
+const previewFormFields = computed<FormField[]>(() => {
|
|
|
|
|
+ const schema = previewDefinition.value?.formSchema
|
|
|
|
|
+ if (!schema) return []
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsed = JSON.parse(schema)
|
|
|
|
|
+ return Array.isArray(parsed) ? parsed : []
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+interface PreviewNode {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ type: string
|
|
|
|
|
+ name: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const previewApprovalNodes = computed<PreviewNode[]>(() => {
|
|
|
|
|
+ const flowJson = previewDefinition.value?.flowJson
|
|
|
|
|
+ if (!flowJson) return []
|
|
|
|
|
+ try {
|
|
|
|
|
+ const model = JSON.parse(flowJson)
|
|
|
|
|
+ const nodes = Array.isArray(model?.nodes) ? model.nodes : []
|
|
|
|
|
+ return nodes.filter((n: any) => n?.type === 'approval-node' || n?.type === 'cc-node')
|
|
|
|
|
+ .map((n: any) => ({ id: n.id, type: n.type, name: n.name || '未命名节点' }))
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return []
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function fieldTypeText(type?: string): string {
|
|
|
|
|
+ const map: Record<string, string> = {
|
|
|
|
|
+ text: '文本',
|
|
|
|
|
+ number: '数字',
|
|
|
|
|
+ date: '日期',
|
|
|
|
|
+ select: '下拉',
|
|
|
|
|
+ textarea: '多行文本'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[type || ''] || type || '-'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openPreview(def: FlowDefinition) {
|
|
|
|
|
+ previewDefinition.value = def
|
|
|
|
|
+ previewVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleStartFromPreview() {
|
|
|
|
|
+ if (!previewDefinition.value) return
|
|
|
|
|
+ previewVisible.value = false
|
|
|
|
|
+ handleStart(previewDefinition.value)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
async function loadDefinitions() {
|
|
async function loadDefinitions() {
|
|
|
loadingDefinitions.value = true
|
|
loadingDefinitions.value = true
|
|
@@ -134,4 +246,12 @@ onMounted(loadDefinitions)
|
|
|
.def-name { font-size: 16px; font-weight: bold; color: #303133; }
|
|
.def-name { font-size: 16px; font-weight: bold; color: #303133; }
|
|
|
.def-info { font-size: 13px; color: #606266; margin-bottom: 12px; }
|
|
.def-info { font-size: 13px; color: #606266; margin-bottom: 12px; }
|
|
|
.def-info p { margin: 4px 0; }
|
|
.def-info p { margin: 4px 0; }
|
|
|
|
|
+.def-actions { display: flex; gap: 8px; }
|
|
|
|
|
+.preview-content { max-height: 60vh; overflow-y: auto; padding-right: 8px; }
|
|
|
|
|
+.preview-section { margin-bottom: 20px; }
|
|
|
|
|
+.preview-section h4 { margin: 0 0 12px 0; font-size: 15px; color: #303133; border-left: 4px solid #409eff; padding-left: 8px; }
|
|
|
|
|
+.preview-section p { margin: 6px 0; color: #606266; font-size: 14px; }
|
|
|
|
|
+.preview-node { display: flex; align-items: center; gap: 8px; }
|
|
|
|
|
+.preview-node-index { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background-color: #f0f2f5; color: #606266; font-size: 12px; }
|
|
|
|
|
+.preview-node-name { font-size: 14px; color: #303133; }
|
|
|
</style>
|
|
</style>
|