|
@@ -8,16 +8,11 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<el-tabs v-model="activeTab">
|
|
<el-tabs v-model="activeTab">
|
|
|
- <el-tab-pane label="发起流程" name="start">
|
|
|
|
|
|
|
+ <el-tab-pane label="发起流程" name="start" lazy>
|
|
|
<div v-if="loadingDefinitions" v-loading="true" style="height: 200px;" />
|
|
<div v-if="loadingDefinitions" v-loading="true" style="height: 200px;" />
|
|
|
<el-empty v-else-if="definitionList.length === 0" description="暂无可用的流程" />
|
|
<el-empty v-else-if="definitionList.length === 0" description="暂无可用的流程" />
|
|
|
<div v-else class="definition-list">
|
|
<div v-else class="definition-list">
|
|
|
- <el-card
|
|
|
|
|
- v-for="def in definitionList"
|
|
|
|
|
- :key="def.id"
|
|
|
|
|
- class="definition-card"
|
|
|
|
|
- shadow="hover"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <el-card v-for="def in definitionList" :key="def.id" class="definition-card" shadow="hover">
|
|
|
<div class="def-header">
|
|
<div class="def-header">
|
|
|
<el-tag type="success" size="small">已发布</el-tag>
|
|
<el-tag type="success" size="small">已发布</el-tag>
|
|
|
<span class="def-name">{{ def.name }}</span>
|
|
<span class="def-name">{{ def.name }}</span>
|
|
@@ -31,83 +26,21 @@
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</div>
|
|
</div>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
-
|
|
|
|
|
- <el-tab-pane label="我发起的流程" name="mine">
|
|
|
|
|
- <el-table v-loading="loadingInstances" :data="instanceList" border>
|
|
|
|
|
- <el-table-column type="index" label="序号" width="60" />
|
|
|
|
|
- <el-table-column prop="definitionName" label="流程名称" />
|
|
|
|
|
- <el-table-column prop="title" label="标题" />
|
|
|
|
|
- <el-table-column prop="status" label="状态" width="100">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-tag :type="instanceStatusTagType(row.status)">
|
|
|
|
|
- {{ instanceStatusText(row.status) }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column prop="currentNodeName" label="当前节点">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- {{ row.currentNodeName || row.currentNode || '-' }}
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column prop="startTime" label="发起时间" />
|
|
|
|
|
- <el-table-column label="操作" width="120">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-button link type="primary" @click="handleView(row)">查看详情</el-button>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- </el-table>
|
|
|
|
|
- <el-pagination
|
|
|
|
|
- v-model:current-page="queryParams.pageNum"
|
|
|
|
|
- v-model:page-size="queryParams.pageSize"
|
|
|
|
|
- :total="total"
|
|
|
|
|
- :page-sizes="[10, 20, 50]"
|
|
|
|
|
- layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
- class="pagination"
|
|
|
|
|
- @current-change="loadInstances"
|
|
|
|
|
- @size-change="loadInstances"
|
|
|
|
|
- />
|
|
|
|
|
- </el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
</el-tabs>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
|
|
|
|
|
- <!-- 发起流程弹窗 -->
|
|
|
|
|
<el-dialog v-model="startDialogVisible" title="发起流程" width="640px">
|
|
<el-dialog v-model="startDialogVisible" title="发起流程" width="640px">
|
|
|
<el-form ref="startFormRef" :model="startForm" :rules="startFormRules" label-width="80px">
|
|
<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 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="表单数据">
|
|
<el-form-item label="表单数据">
|
|
|
- <FlowFormFields v-if="hasFormFields" ref="flowFormFieldsRef" v-model="dynamicFormData" :definition="selectedDefinition" />
|
|
|
|
|
|
|
+ <FlowFormFields v-if="hasFormFields" v-model="dynamicFormData" :definition="selectedDefinition" />
|
|
|
<el-input v-else v-model="startForm.formData" type="textarea" :rows="4" placeholder="请输入表单数据,格式:每行一个字段,如 amount=1000" />
|
|
<el-input v-else v-model="startForm.formData" type="textarea" :rows="4" placeholder="请输入表单数据,格式:每行一个字段,如 amount=1000" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="附件">
|
|
<el-form-item label="附件">
|
|
|
- <el-upload
|
|
|
|
|
- v-model:file-list="attachmentList"
|
|
|
|
|
- action="#"
|
|
|
|
|
- :http-request="handleUpload"
|
|
|
|
|
- :before-upload="beforeFileUpload"
|
|
|
|
|
- :before-remove="() => true"
|
|
|
|
|
- :limit="5"
|
|
|
|
|
- multiple
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <el-upload v-model:file-list="attachmentList" action="#" :http-request="handleUpload" :before-upload="beforeFileUpload" :before-remove="() => true" :limit="5" multiple>
|
|
|
<el-button type="primary" size="small">上传附件</el-button>
|
|
<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="openPreview(getUploadUrl(file))">
|
|
|
|
|
- {{ file.name }}
|
|
|
|
|
- </el-link>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
</el-upload>
|
|
</el-upload>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
@@ -117,15 +50,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
|
|
|
- <!-- 流程详情弹窗 -->
|
|
|
|
|
- <InstanceDetail
|
|
|
|
|
- v-model="detailVisible"
|
|
|
|
|
- :instance-id="currentInstanceId"
|
|
|
|
|
- @approved="loadInstances"
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- <!-- 附件预览 -->
|
|
|
|
|
- <FilePreview v-model="previewVisible" :url="previewUrl" />
|
|
|
|
|
|
|
+ <InstanceDetail v-model="detailVisible" :instance-id="currentInstanceId" />
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -133,201 +58,80 @@
|
|
|
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 { Document } from '@element-plus/icons-vue'
|
|
|
|
|
import { listEnabled } from '@/api/flow/definition'
|
|
import { listEnabled } from '@/api/flow/definition'
|
|
|
-import { listMyInstance, startInstance } from '@/api/flow/instance'
|
|
|
|
|
|
|
+import { startInstance } from '@/api/flow/instance'
|
|
|
import { uploadFile } from '@/api/file'
|
|
import { uploadFile } from '@/api/file'
|
|
|
-import { beforeFileUpload, collectAttachmentUrls, getUploadUrl } from '@/utils/file'
|
|
|
|
|
-import { instanceStatusText, instanceStatusTagType } from '@/utils/flow'
|
|
|
|
|
-import type { FlowDefinition, FlowInstance } from '@/types/flow'
|
|
|
|
|
|
|
+import { beforeFileUpload, collectAttachmentUrls } from '@/utils/file'
|
|
|
|
|
+import type { FlowDefinition } 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'
|
|
|
-import FilePreview from '@/components/FilePreview/index.vue'
|
|
|
|
|
|
|
|
|
|
const activeTab = ref('start')
|
|
const activeTab = ref('start')
|
|
|
const loadingDefinitions = ref(false)
|
|
const loadingDefinitions = ref(false)
|
|
|
-const loadingInstances = ref(false)
|
|
|
|
|
const submitting = ref(false)
|
|
const submitting = ref(false)
|
|
|
const definitionList = ref<FlowDefinition[]>([])
|
|
const definitionList = ref<FlowDefinition[]>([])
|
|
|
-const instanceList = ref<FlowInstance[]>([])
|
|
|
|
|
-const total = ref(0)
|
|
|
|
|
-const queryParams = reactive({ pageNum: 1, pageSize: 10 })
|
|
|
|
|
-
|
|
|
|
|
const startDialogVisible = ref(false)
|
|
const startDialogVisible = ref(false)
|
|
|
const startFormRef = ref<FormInstance>()
|
|
const startFormRef = ref<FormInstance>()
|
|
|
-const startForm = ref({
|
|
|
|
|
- processDefinitionId: 0 as number,
|
|
|
|
|
- definitionName: '',
|
|
|
|
|
- title: '',
|
|
|
|
|
- formData: ''
|
|
|
|
|
-})
|
|
|
|
|
|
|
+const startForm = ref({ processDefinitionId: 0 as number, definitionName: '', title: '', formData: '' })
|
|
|
const dynamicFormData = ref<Record<string, unknown>>({})
|
|
const dynamicFormData = ref<Record<string, unknown>>({})
|
|
|
const selectedDefinition = ref<FlowDefinition | null>(null)
|
|
const selectedDefinition = ref<FlowDefinition | null>(null)
|
|
|
-const flowFormFieldsRef = ref<any>(null)
|
|
|
|
|
-
|
|
|
|
|
|
|
+const attachmentList = ref<any[]>([])
|
|
|
|
|
+const startFormRules: FormRules = { title: [{ required: true, message: '请输入标题', trigger: 'blur' }] }
|
|
|
const hasFormFields = computed(() => {
|
|
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 s = selectedDefinition.value?.formSchema
|
|
|
|
|
+ if (!s) return false
|
|
|
|
|
+ try { const a = JSON.parse(s); return Array.isArray(a) && a.length > 0 } catch { return false }
|
|
|
})
|
|
})
|
|
|
-const attachmentList = ref<any[]>([])
|
|
|
|
|
-const startFormRules: FormRules = {
|
|
|
|
|
- title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
const detailVisible = ref(false)
|
|
const detailVisible = ref(false)
|
|
|
const currentInstanceId = ref(0)
|
|
const currentInstanceId = ref(0)
|
|
|
-const previewVisible = ref(false)
|
|
|
|
|
-const previewUrl = ref('')
|
|
|
|
|
-
|
|
|
|
|
-function openPreview(url: string) {
|
|
|
|
|
- if (!url) return
|
|
|
|
|
- previewUrl.value = url
|
|
|
|
|
- previewVisible.value = true
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleFilePreview(file: any) {
|
|
|
|
|
- const url = getUploadUrl(file)
|
|
|
|
|
- if (url) openPreview(url)
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
async function loadDefinitions() {
|
|
async function loadDefinitions() {
|
|
|
loadingDefinitions.value = true
|
|
loadingDefinitions.value = true
|
|
|
- try {
|
|
|
|
|
- const res = await listEnabled()
|
|
|
|
|
- definitionList.value = res
|
|
|
|
|
- } finally {
|
|
|
|
|
- loadingDefinitions.value = false
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function loadInstances() {
|
|
|
|
|
- loadingInstances.value = true
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await listMyInstance(queryParams)
|
|
|
|
|
- instanceList.value = res.list
|
|
|
|
|
- total.value = res.total
|
|
|
|
|
- } finally {
|
|
|
|
|
- loadingInstances.value = false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ try { definitionList.value = await listEnabled() } finally { loadingDefinitions.value = false }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function handleStart(def: FlowDefinition) {
|
|
function handleStart(def: FlowDefinition) {
|
|
|
- startForm.value.processDefinitionId = def.id
|
|
|
|
|
- startForm.value.definitionName = def.name
|
|
|
|
|
- startForm.value.title = ''
|
|
|
|
|
- startForm.value.formData = ''
|
|
|
|
|
- dynamicFormData.value = {}
|
|
|
|
|
- selectedDefinition.value = def
|
|
|
|
|
- attachmentList.value = []
|
|
|
|
|
|
|
+ startForm.value.processDefinitionId = def.id; startForm.value.definitionName = def.name
|
|
|
|
|
+ startForm.value.title = ''; startForm.value.formData = ''; dynamicFormData.value = {}
|
|
|
|
|
+ selectedDefinition.value = def; attachmentList.value = []
|
|
|
startDialogVisible.value = true
|
|
startDialogVisible.value = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function handleUpload(options: any) {
|
|
async function handleUpload(options: any) {
|
|
|
- try {
|
|
|
|
|
- const res = await uploadFile(options.file)
|
|
|
|
|
- options.onSuccess(res)
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- options.onError(e)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ try { const r = await uploadFile(options.file); options.onSuccess(r) } catch (e) { options.onError(e) }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function submitStart() {
|
|
async function submitStart() {
|
|
|
- const valid = await startFormRef.value?.validate().catch(() => false)
|
|
|
|
|
- if (!valid) return
|
|
|
|
|
- // 校验动态表单字段;无动态字段时视为校验通过
|
|
|
|
|
- const dynamicValid = flowFormFieldsRef.value?.validate() ?? true
|
|
|
|
|
- if (dynamicValid === false) return
|
|
|
|
|
|
|
+ const v = await startFormRef.value?.validate().catch(() => false)
|
|
|
|
|
+ if (!v) return
|
|
|
submitting.value = true
|
|
submitting.value = true
|
|
|
try {
|
|
try {
|
|
|
- let formData: Record<string, unknown> | undefined
|
|
|
|
|
- if (selectedDefinition.value?.formSchema) {
|
|
|
|
|
- formData = { ...dynamicFormData.value }
|
|
|
|
|
- } else if (startForm.value.formData.trim()) {
|
|
|
|
|
- // 兼容旧版无表单字段配置的流程
|
|
|
|
|
- formData = {}
|
|
|
|
|
- startForm.value.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
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ let fd: Record<string, unknown> | undefined
|
|
|
|
|
+ if (selectedDefinition.value?.formSchema) { fd = { ...dynamicFormData.value } }
|
|
|
|
|
+ else if (startForm.value.formData.trim()) {
|
|
|
|
|
+ fd = {}
|
|
|
|
|
+ startForm.value.formData.split('\n').forEach(l => {
|
|
|
|
|
+ const i = l.indexOf('='); if (i > 0) {
|
|
|
|
|
+ const k = l.substring(0, i).trim(), val = l.substring(i + 1).trim()
|
|
|
|
|
+ if (k) { const n = Number(val); fd![k] = !isNaN(n) && val !== '' ? n : val }
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
- const attachmentUrls = attachmentList.value.length > 0
|
|
|
|
|
- ? collectAttachmentUrls(attachmentList.value)
|
|
|
|
|
- : undefined
|
|
|
|
|
- await startInstance(startForm.value.processDefinitionId, startForm.value.title || undefined, formData, attachmentUrls)
|
|
|
|
|
- ElMessage.success('流程发起成功')
|
|
|
|
|
- startDialogVisible.value = false
|
|
|
|
|
- activeTab.value = 'mine'
|
|
|
|
|
- loadInstances()
|
|
|
|
|
- } finally {
|
|
|
|
|
- submitting.value = false
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleView(row: FlowInstance) {
|
|
|
|
|
- currentInstanceId.value = row.id
|
|
|
|
|
- detailVisible.value = true
|
|
|
|
|
|
|
+ const au = attachmentList.value.length > 0 ? collectAttachmentUrls(attachmentList.value) : undefined
|
|
|
|
|
+ await startInstance(startForm.value.processDefinitionId, startForm.value.title || undefined, fd, au)
|
|
|
|
|
+ ElMessage.success('流程发起成功'); startDialogVisible.value = false
|
|
|
|
|
+ } finally { submitting.value = false }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-onMounted(() => {
|
|
|
|
|
- loadDefinitions()
|
|
|
|
|
- loadInstances()
|
|
|
|
|
-})
|
|
|
|
|
|
|
+onMounted(loadDefinitions)
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
-.card-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-.definition-list {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
|
|
- gap: 16px;
|
|
|
|
|
-}
|
|
|
|
|
-.definition-card {
|
|
|
|
|
- cursor: default;
|
|
|
|
|
-}
|
|
|
|
|
-.def-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- margin-bottom: 12px;
|
|
|
|
|
-}
|
|
|
|
|
-.def-name {
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #303133;
|
|
|
|
|
-}
|
|
|
|
|
-.def-info {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: #606266;
|
|
|
|
|
- margin-bottom: 12px;
|
|
|
|
|
-}
|
|
|
|
|
-.def-info p {
|
|
|
|
|
- margin: 4px 0;
|
|
|
|
|
-}
|
|
|
|
|
-.pagination {
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
- justify-content: flex-end;
|
|
|
|
|
-}
|
|
|
|
|
-.upload-file-item {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- padding: 4px 0;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.card-header { display: flex; justify-content: space-between; align-items: center; }
|
|
|
|
|
+.definition-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
|
|
|
|
|
+.definition-card { cursor: default; }
|
|
|
|
|
+.def-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
|
|
|
+.def-name { font-size: 16px; font-weight: bold; color: #303133; }
|
|
|
|
|
+.def-info { font-size: 13px; color: #606266; margin-bottom: 12px; }
|
|
|
|
|
+.def-info p { margin: 4px 0; }
|
|
|
</style>
|
|
</style>
|