|
@@ -6,8 +6,8 @@
|
|
|
<el-card class="upload-section">
|
|
|
<div class="input-method-select" v-if="inputMethod != 'done'">
|
|
|
<el-radio-group v-model="inputMethod">
|
|
|
- <el-radio-button label="upload">文件上传</el-radio-button>
|
|
|
- <el-radio-button label="online">在线输入</el-radio-button>
|
|
|
+ <el-radio-button value="upload">文件上传</el-radio-button>
|
|
|
+ <el-radio-button value="online">在线输入</el-radio-button>
|
|
|
</el-radio-group>
|
|
|
</div>
|
|
|
|
|
@@ -50,8 +50,86 @@
|
|
|
<el-progress v-if="uploadProgress > 0" :percentage="uploadProgress" :status="uploadStatus"
|
|
|
class="progress-bar" />
|
|
|
</div>
|
|
|
+ <!-- 在线输入节点与边 -->
|
|
|
+
|
|
|
+
|
|
|
<div v-else class="upload-area">
|
|
|
- <router-view></router-view>
|
|
|
+ <div class="online-input">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <!-- 节点输入列 -->
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="input-section">
|
|
|
+ <h4>输入节点</h4>
|
|
|
+ <div v-for="(node, index) in nodes" :key="index" class="input-row">
|
|
|
+ <el-row :gutter="10" align="middle">
|
|
|
+ <el-col :span="4">
|
|
|
+ <div class="node-id">ID:{{ node.id }}</div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-select v-model="node.type" placeholder="选择类型" @change="handleNodeTypeChange(index)">
|
|
|
+ <el-option label="S" value="S" />
|
|
|
+ <el-option label="D" value="D" />
|
|
|
+ <el-option label="I" value="I" />
|
|
|
+ </el-select>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="10">
|
|
|
+ <el-input v-model="node.name" placeholder="输入节点名称" />
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-button v-if="index < nodes.length - 1" @click="deleteNode(index)" type="danger" plain
|
|
|
+ size="small">
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <!-- 边输入列 -->
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="input-section">
|
|
|
+ <h4>输入边</h4>
|
|
|
+ <div v-for="(edge, index) in edges" :key="index" class="input-row">
|
|
|
+ <el-row :gutter="10" align="middle">
|
|
|
+ <el-col :span="4">
|
|
|
+ <div class="node-id">ID:{{ index + 1 }}</div>
|
|
|
+ </el-col>
|
|
|
+ <!-- 起始节点 -->
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-select v-model="edge.from" placeholder="起始节点" :disabled="nodeOptions.length === 0"
|
|
|
+ @change="handleEdgeChange(index)">
|
|
|
+ <el-option v-for="node in nodeOptions" :key="node.id"
|
|
|
+ :label="`ID:${node.id}-${node.name || '未命名'}`" :value="node.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <!-- 终止节点 -->
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-select v-model="edge.to" placeholder="终止节点" :disabled="nodeOptions.length === 0"
|
|
|
+ @change="handleEdgeChange(index)">
|
|
|
+ <el-option v-for="node in nodeOptions" :key="node.id"
|
|
|
+ :label="`ID:${node.id}-${node.name || '未命名'}`" :value="node.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <!-- 删除按钮 -->
|
|
|
+ <el-col :span="4">
|
|
|
+ <el-button v-if="index < edges.length - 1" @click="deleteEdge(index)" type="danger" plain
|
|
|
+ size="small">
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+ <el-button type="primary" @click="handleValidation" class="validate-button">
|
|
|
+ 开始输入验证
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
@@ -140,7 +218,7 @@
|
|
|
import { ref, computed, onMounted, inject } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
-import { Upload, ArrowDown } from '@element-plus/icons-vue'
|
|
|
+import { Upload, ArrowDown, Plus } from '@element-plus/icons-vue'
|
|
|
import { getData, postData, deleteData, postFile } from '@/api/axios.js'
|
|
|
|
|
|
|
|
@@ -149,18 +227,186 @@ const useAnalyzeInfo = inject('analyzeInfo')
|
|
|
|
|
|
// 响应式数据
|
|
|
const inputMethod = ref('upload')
|
|
|
+// 上传的节点和边文件
|
|
|
const nodeFile = ref(null)
|
|
|
const edgeFile = ref(null)
|
|
|
+// 在线输入的节点和边
|
|
|
+const nodes = ref([{ id: 1, type: '', name: '' }])
|
|
|
+const edges = ref([{ from: '', to: '' }])
|
|
|
+
|
|
|
const uploadProgress = ref(0)
|
|
|
const uploadStatus = ref('')
|
|
|
const fileHistory = ref([])
|
|
|
const router = useRouter()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
// 计算属性
|
|
|
+// 是否允许上传文件
|
|
|
const canUpload = computed(() => {
|
|
|
return nodeFile.value && edgeFile.value
|
|
|
})
|
|
|
+// 在线输入边时节点的可选项
|
|
|
+const nodeOptions = computed(() => {
|
|
|
+ return nodes.value
|
|
|
+ .filter(node => node.type !== '')
|
|
|
+ .map(node => ({
|
|
|
+ id: node.id,
|
|
|
+ name: node.name || '未命名',
|
|
|
+ type: node.type
|
|
|
+ }))
|
|
|
+})
|
|
|
|
|
|
// 方法
|
|
|
+
|
|
|
+// 在线输入自动添加节点
|
|
|
+const handleNodeTypeChange = (index) => {
|
|
|
+ // 当最后一个节点选择类型后自动添加新行
|
|
|
+ if (index === nodes.value.length - 1) {
|
|
|
+ nodes.value.push({ id: nodes.value.length + 1, type: '', name: '' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 删除节点
|
|
|
+const deleteNode = (index) => {
|
|
|
+ // 保存原始ID列表
|
|
|
+ const originalIds = nodes.value.map(n => n.id)
|
|
|
+ const deletedId = originalIds[index]
|
|
|
+ // 删除关联边
|
|
|
+ edges.value = edges.value.filter(edge =>
|
|
|
+ edge.from !== deletedId && edge.to !== deletedId
|
|
|
+ )
|
|
|
+ // 删除节点并创建新数组
|
|
|
+ const newNodes = [...nodes.value]
|
|
|
+ newNodes.splice(index, 1)
|
|
|
+
|
|
|
+ // 创建ID映射表(旧ID -> 新ID)
|
|
|
+ const idMap = new Map()
|
|
|
+ newNodes.forEach((node, i) => {
|
|
|
+ const newId = i + 1
|
|
|
+ idMap.set(node.id, newId) // 记录原始ID到新ID的映射
|
|
|
+ node.id = newId // 更新节点ID
|
|
|
+ })
|
|
|
+
|
|
|
+ // 更新节点数组
|
|
|
+ nodes.value = newNodes
|
|
|
+
|
|
|
+ // 更新边数据中的ID引用
|
|
|
+ edges.value = edges.value.map(edge => ({
|
|
|
+ from: idMap.get(edge.from),
|
|
|
+ to: idMap.get(edge.to)
|
|
|
+ }))
|
|
|
+ // 确保至少保留一个空行
|
|
|
+ if (nodes.value.length === 0) {
|
|
|
+ nodes.value.push({ id: 1, type: '', name: '' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取有效节点ID列表
|
|
|
+const validNodeIds = computed(() => {
|
|
|
+ return nodes.value
|
|
|
+ .filter(node => node.type !== '')
|
|
|
+ .map(node => node.id)
|
|
|
+})
|
|
|
+
|
|
|
+// 在线输入自动添加边
|
|
|
+const handleEdgeChange = (index) => {
|
|
|
+ const currentEdge = edges.value[index]
|
|
|
+ // 当最后一个边填写完整后自动添加新行
|
|
|
+ if (index === edges.value.length - 1 && currentEdge.from && currentEdge.to) {
|
|
|
+ edges.value.push({ from: '', to: '' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 删除边
|
|
|
+const deleteEdge = (index) => {
|
|
|
+ edges.value.splice(index, 1)
|
|
|
+}
|
|
|
+
|
|
|
+// 验证在线输入
|
|
|
+const handleValidation = async () => {
|
|
|
+ const errors = []
|
|
|
+ const edgeSet = new Set()
|
|
|
+
|
|
|
+
|
|
|
+ // 检查边数据
|
|
|
+ // 因为始终有空行,所有没有输入的时候长度为1
|
|
|
+ if(edges.value.length == 1){
|
|
|
+ errors.push(`没有输入边`)
|
|
|
+ }
|
|
|
+ if(nodes.value.length == 1){
|
|
|
+ errors.push(`没有输入节点`)
|
|
|
+ }
|
|
|
+ edges.value.slice(0, -1).forEach((edge, index) => {
|
|
|
+ // 检查节点是否存在
|
|
|
+ if (!validNodeIds.value.includes(edge.from)) {
|
|
|
+ errors.push(`边 ${index + 1}: 起始节点 ${edge.from} 不存在`)
|
|
|
+ }
|
|
|
+ if (!validNodeIds.value.includes(edge.to)) {
|
|
|
+ errors.push(`边 ${index + 1}: 终止节点 ${edge.to} 不存在`)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自环边检查
|
|
|
+ if (edge.from === edge.to) {
|
|
|
+ errors.push(`边 ${index + 1}: 不允许连接自身(自环边)`)
|
|
|
+ }
|
|
|
+ // 归一化两个方向的边
|
|
|
+ const [min, max] = [edge.from, edge.to].sort()
|
|
|
+ // 默认检查无向边
|
|
|
+ const checkDirection = false
|
|
|
+ const edgeKey = checkDirection
|
|
|
+ ? `${edge.from},${edge.to}`
|
|
|
+ : `${min},${max}`
|
|
|
+
|
|
|
+ // 检查重复边
|
|
|
+ if (edgeSet.has(edgeKey)) {
|
|
|
+ errors.push(`发现重复边: ${edge.from} → ${edge.to}`)
|
|
|
+ } else {
|
|
|
+ edgeSet.add(edgeKey)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 处理验证结果
|
|
|
+ if (errors.length > 0) {
|
|
|
+ ElMessage.error({
|
|
|
+ message: `发现 ${errors.length} 个错误:<br>${errors.join('<br>')}`,
|
|
|
+ duration: 5000,
|
|
|
+ dangerouslyUseHTMLString: true, // 启用 HTML 解析
|
|
|
+ customClass: 'error-message' // 可选:添加自定义样式类
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构造上传数据
|
|
|
+ const uploadData = {
|
|
|
+ nodes: nodes.value
|
|
|
+ .filter(node => node.type !== '')
|
|
|
+ .map(node => ({
|
|
|
+ id: node.id,
|
|
|
+ type: node.type,
|
|
|
+ name: node.name
|
|
|
+ })),
|
|
|
+ edges: edges.value
|
|
|
+ .filter(edge => edge.from && edge.to)
|
|
|
+ .map(edge => ({
|
|
|
+ source: edge.from,
|
|
|
+ target: edge.to
|
|
|
+ }))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行上传
|
|
|
+ try {
|
|
|
+ // await postData('/api/upload', uploadData)
|
|
|
+ ElMessage.success('数据验证通过,上传成功')
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('上传失败: ' + error.message)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// 上传文件处理
|
|
|
+
|
|
|
const handleNodeFileChange = (file, fileList) => {
|
|
|
if (fileList.length > 1) {
|
|
|
fileList.splice(0, 1);
|
|
@@ -392,4 +638,37 @@ onMounted(() => {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+.online-input {
|
|
|
+ padding: 15px;
|
|
|
+
|
|
|
+ .input-section {
|
|
|
+ border: 1px solid var(--el-border-color);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 15px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+
|
|
|
+ h4 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-row {
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ .node-id {
|
|
|
+ padding: 8px 12px;
|
|
|
+ background: var(--el-fill-color-light);
|
|
|
+ border-radius: 4px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.el-message.error-message {
|
|
|
+ white-space: pre-line;
|
|
|
+ line-height: 1.6;
|
|
|
+ padding: 15px 20px;
|
|
|
+}
|
|
|
</style>
|