|
|
@@ -60,6 +60,10 @@
|
|
|
<i class="i-material-symbols:restart-alt" />
|
|
|
清空
|
|
|
</button>
|
|
|
+ <button type="button" class="model-import-button" @click="handleImportOpen">
|
|
|
+ <i class="i-material-symbols:upload-file" />
|
|
|
+ 导入模型
|
|
|
+ </button>
|
|
|
<button type="button" class="model-create-button" @click="handleAdd">
|
|
|
<i class="i-material-symbols:add" />
|
|
|
新增模型
|
|
|
@@ -406,6 +410,53 @@
|
|
|
</NModal>
|
|
|
</Teleport>
|
|
|
|
|
|
+ <!-- 导入模型弹窗 -->
|
|
|
+ <Teleport to="body">
|
|
|
+ <NModal
|
|
|
+ v-model:show="showImportModal"
|
|
|
+ preset="card"
|
|
|
+ title="导入流程模型"
|
|
|
+ style="width: min(560px, calc(100vw - 32px))"
|
|
|
+ :mask-closable="false"
|
|
|
+ >
|
|
|
+ <div class="import-model-panel">
|
|
|
+ <n-alert type="info" :show-icon="false">
|
|
|
+ 上传 .bpmn 或 .xml 文件,系统会自动解析 BPMN 内容并创建新的流程模型。
|
|
|
+ </n-alert>
|
|
|
+ <div class="import-model-dropzone" :class="{ 'dragging': importDragging, 'has-file': importFile }" @dragover.prevent="importDragging = true" @dragleave="importDragging = false" @drop.prevent="handleImportDrop">
|
|
|
+ <div v-if="!importFile" class="import-dropzone-placeholder">
|
|
|
+ <i class="i-material-symbols:upload-file" />
|
|
|
+ <span>拖拽文件到此处,或</span>
|
|
|
+ <button type="button" class="import-choose-btn" @click="triggerImportFileInput">
|
|
|
+ 选择文件
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div v-else class="import-dropzone-file">
|
|
|
+ <i class="i-material-symbols:description" />
|
|
|
+ <div class="import-file-info">
|
|
|
+ <span class="import-file-name">{{ importFile.name }}</span>
|
|
|
+ <span class="import-file-size">{{ formatFileSize(importFile.size) }}</span>
|
|
|
+ </div>
|
|
|
+ <button type="button" class="import-file-clear" @click="importFile = null">
|
|
|
+ <i class="i-material-symbols:close" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <input ref="importFileInputRef" type="file" accept=".bpmn,.bpmn20.xml,.xml" style="display: none" @change="handleImportFileChange">
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <n-space justify="end">
|
|
|
+ <n-button @click="showImportModal = false">
|
|
|
+ 取消
|
|
|
+ </n-button>
|
|
|
+ <n-button type="primary" :loading="importLoading" :disabled="!importFile" @click="handleImportSubmit">
|
|
|
+ 导入
|
|
|
+ </n-button>
|
|
|
+ </n-space>
|
|
|
+ </template>
|
|
|
+ </NModal>
|
|
|
+ </Teleport>
|
|
|
+
|
|
|
<VersionHistory
|
|
|
v-if="showVersionHistory"
|
|
|
:model-id="currentModelId"
|
|
|
@@ -417,7 +468,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { CopyOutline, CreateOutline, PauseCircleOutline, PlayCircleOutline, TimeOutline, TrashOutline } from '@vicons/ionicons5'
|
|
|
+import { CopyOutline, CreateOutline, DownloadOutline, PauseCircleOutline, PlayCircleOutline, TimeOutline, TrashOutline } from '@vicons/ionicons5'
|
|
|
import { NIcon, NModal, NTreeSelect } from 'naive-ui'
|
|
|
import { computed, defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
|
@@ -589,6 +640,7 @@ function getActionOptions(row) {
|
|
|
{ label: '编辑信息', key: 'edit', icon: renderIcon(CreateOutline) },
|
|
|
{ label: '版本历史', key: 'versionHistory', icon: renderIcon(TimeOutline) },
|
|
|
{ label: '复制模型', key: 'copy', icon: renderIcon(CopyOutline) },
|
|
|
+ { label: '导出 BPMN', key: 'export', icon: renderIcon(DownloadOutline) },
|
|
|
]
|
|
|
if (row.status === 1) {
|
|
|
opts.splice(1, 0, { label: '发起测试', key: 'startTest', icon: renderIcon(PlayCircleOutline) })
|
|
|
@@ -603,7 +655,7 @@ function getActionOptions(row) {
|
|
|
}
|
|
|
|
|
|
function handleActionSelect(key, row) {
|
|
|
- const map = { edit: handleEdit, startTest: handleStartTest, copy: handleCopy, versionHistory: handleVersionHistory, suspend: handleSuspend, activate: handleActivate, delete: handleDelete }
|
|
|
+ const map = { edit: handleEdit, startTest: handleStartTest, copy: handleCopy, export: handleExport, versionHistory: handleVersionHistory, suspend: handleSuspend, activate: handleActivate, delete: handleDelete }
|
|
|
map[key]?.(row)
|
|
|
}
|
|
|
|
|
|
@@ -747,6 +799,12 @@ const showModal = ref(false)
|
|
|
const modalTitle = ref('新增模型')
|
|
|
const isEdit = ref(false)
|
|
|
const submitLoading = ref(false)
|
|
|
+
|
|
|
+const showImportModal = ref(false)
|
|
|
+const importLoading = ref(false)
|
|
|
+const importFile = ref(null)
|
|
|
+const importDragging = ref(false)
|
|
|
+const importFileInputRef = ref(null)
|
|
|
const formRef = ref(null)
|
|
|
const formData = reactive({
|
|
|
id: '',
|
|
|
@@ -768,6 +826,64 @@ const rules = {
|
|
|
designerType: { required: true, message: '请选择流程模式', trigger: 'change' },
|
|
|
}
|
|
|
|
|
|
+function handleImportOpen() {
|
|
|
+ importFile.value = null
|
|
|
+ importDragging.value = false
|
|
|
+ showImportModal.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function triggerImportFileInput() {
|
|
|
+ importFileInputRef.value?.click()
|
|
|
+}
|
|
|
+
|
|
|
+function handleImportFileChange(e) {
|
|
|
+ const file = e.target.files?.[0]
|
|
|
+ if (file)
|
|
|
+ importFile.value = file
|
|
|
+ e.target.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+function handleImportDrop(e) {
|
|
|
+ importDragging.value = false
|
|
|
+ const file = e.dataTransfer?.files?.[0]
|
|
|
+ if (file && /\.(?:bpmn|xml)$/i.test(file.name))
|
|
|
+ importFile.value = file
|
|
|
+}
|
|
|
+
|
|
|
+function formatFileSize(bytes) {
|
|
|
+ if (!bytes)
|
|
|
+ return ''
|
|
|
+ if (bytes < 1024)
|
|
|
+ return `${bytes} B`
|
|
|
+ if (bytes < 1024 * 1024)
|
|
|
+ return `${(bytes / 1024).toFixed(1)} KB`
|
|
|
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
|
+}
|
|
|
+
|
|
|
+async function handleImportSubmit() {
|
|
|
+ if (!importFile.value)
|
|
|
+ return
|
|
|
+ importLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await flowApi.importModel(importFile.value)
|
|
|
+ if (res.code === 200) {
|
|
|
+ window.$message?.success('导入成功')
|
|
|
+ showImportModal.value = false
|
|
|
+ fetchData()
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ window.$message?.error(res.message || '导入失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('导入失败:', error)
|
|
|
+ window.$message?.error(error?.message || '导入失败')
|
|
|
+ }
|
|
|
+ finally {
|
|
|
+ importLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function handleAdd() {
|
|
|
isEdit.value = false
|
|
|
modalTitle.value = '新增模型'
|
|
|
@@ -970,6 +1086,32 @@ async function handleCopy(row) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+async function handleExport(row) {
|
|
|
+ try {
|
|
|
+ const res = await flowApi.exportModel(row.id)
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ const xml = res.data
|
|
|
+ const blob = new Blob([xml], { type: 'application/xml;charset=utf-8' })
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url
|
|
|
+ a.download = `${row.modelKey || row.modelName || 'model'}.bpmn`
|
|
|
+ document.body.appendChild(a)
|
|
|
+ a.click()
|
|
|
+ document.body.removeChild(a)
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+ window.$message?.success('导出成功')
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ window.$message?.error(res.message || '导出失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('导出失败:', error)
|
|
|
+ window.$message?.error(error?.message || '导出失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
async function handleSuspend(row) {
|
|
|
window.$dialog?.warning({
|
|
|
title: '确认挂起',
|
|
|
@@ -1201,6 +1343,153 @@ onMounted(() => {
|
|
|
background: #1e293b;
|
|
|
}
|
|
|
|
|
|
+.model-import-button {
|
|
|
+ height: 34px;
|
|
|
+ gap: 5px;
|
|
|
+ padding: 0 12px;
|
|
|
+ border: 1px solid #dbe3ee;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: #fff;
|
|
|
+ color: #1d4ed8;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ white-space: nowrap;
|
|
|
+ transition:
|
|
|
+ background-color 150ms ease,
|
|
|
+ border-color 150ms ease,
|
|
|
+ color 150ms ease;
|
|
|
+}
|
|
|
+
|
|
|
+.model-import-button:hover {
|
|
|
+ border-color: #93c5fd;
|
|
|
+ background: #eff6ff;
|
|
|
+ color: #1e40af;
|
|
|
+}
|
|
|
+
|
|
|
+.model-import-button i {
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.import-model-panel {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.import-model-dropzone {
|
|
|
+ border: 2px dashed #e2e8f0;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 28px 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition:
|
|
|
+ border-color 160ms ease,
|
|
|
+ background 160ms ease;
|
|
|
+ cursor: default;
|
|
|
+}
|
|
|
+
|
|
|
+.import-model-dropzone.dragging {
|
|
|
+ border-color: #93c5fd;
|
|
|
+ background: #eff6ff;
|
|
|
+}
|
|
|
+
|
|
|
+.import-model-dropzone.has-file {
|
|
|
+ border-color: #86efac;
|
|
|
+ background: #f0fdf4;
|
|
|
+ padding: 14px 16px;
|
|
|
+ justify-content: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.import-dropzone-placeholder {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.import-dropzone-placeholder i {
|
|
|
+ font-size: 32px;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.import-choose-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.import-choose-btn:hover {
|
|
|
+ color: #1d4ed8;
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+.import-dropzone-file {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ width: 100%;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.import-dropzone-file > i {
|
|
|
+ font-size: 24px;
|
|
|
+ color: #16a34a;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.import-file-info {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.import-file-name {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0f172a;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.import-file-size {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.import-file-clear {
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: transparent;
|
|
|
+ color: #94a3b8;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.import-file-clear:hover {
|
|
|
+ background: #fee2e2;
|
|
|
+ color: #dc2626;
|
|
|
+}
|
|
|
+
|
|
|
.model-toolbar-button i,
|
|
|
.model-create-button i {
|
|
|
font-size: 16px;
|