标准流程管理+流程创建

This commit is contained in:
Lxq
2026-01-08 11:52:11 +08:00
parent 5f6808b2a4
commit 3c5d854967
8 changed files with 576 additions and 25 deletions

12
rc_autoplc_backend/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>

6
rc_autoplc_backend/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

54
rc_autoplc_backend/.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="d47bc58e-d36b-422c-92c6-393c467a25d3" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="useMavenConfig" value="true" />
</MavenGeneralSettings>
</option>
</component>
<component name="ProjectId" id="37uy9ngFoDUVUddX8Yg9afHWRsn" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="d47bc58e-d36b-422c-92c6-393c467a25d3" name="Changes" comment="" />
<created>1767767108230</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1767767108230</updated>
<workItem from="1767767109651" duration="29000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>北京融创智能仪器管理系统</title>
</head>
<body>
<div id="app"></div>

View File

@@ -8,6 +8,14 @@ export function stepInfoadd(data: any) {
})
}
export function stepInfoaddlist(data: any) {
return request({
url: '/stepInfo/batchAdd',
method: 'post',
data,
})
}
export function stepInfodel(id: string | number) {
return request({
url: `/stepInfo/del/${id}`,
@@ -15,6 +23,14 @@ export function stepInfodel(id: string | number) {
})
}
export function stepInfodellist(data: any[]) {
return request({
url: '/stepInfo/batchDel',
method: 'delete',
data
})
}
export function stepInfoupd(data: any) {
return request({
url: '/stepInfo/update',
@@ -23,6 +39,14 @@ export function stepInfoupd(data: any) {
})
}
export function stepInfoupdlist(data: any) {
return request({
url: '/stepInfo/batchUpdate',
method: 'post',
data,
})
}
export function stepInfolist(data: any) {
return request({
url: '/stepInfo/listPage',

View File

@@ -91,6 +91,14 @@
/>
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button
:type="scope.row.hasWorkflow ? 'success' : 'warning'"
link
:loading="scope.row.workflowLoading"
@click="handleConfigWorkflow(scope.row)"
>
{{ scope.row.hasWorkflow ? '编辑流程' : '配置流程' }}
</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
@@ -203,6 +211,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
@@ -213,6 +222,7 @@ import {
flowInfolist,
flowInfobyid,
} from '@/api/tb/flowinfo'
import { stepInfolist } from '@/api/tb/stepinfo'
interface FlowInfoItem {
id?: number | string
@@ -226,8 +236,11 @@ interface FlowInfoItem {
testMethod?: string
scanNum?: string
islandIdList?: string
hasWorkflow?: boolean
workflowLoading?: boolean
}
const router = useRouter()
const loading = ref(false)
const submitting = ref(false)
const tableData = ref<FlowInfoItem[]>([])
@@ -334,8 +347,40 @@ const getFlowInfoList = async () => {
const totalValue =
data.total ?? data.count ?? data.totalCount ?? records.length ?? 0
tableData.value = records
tableData.value = records.map((item: any) => ({
...item,
hasWorkflow: false,
workflowLoading: false,
}))
total.value = Number(totalValue) || 0
// 查询每条标准流程是否已配置流程
await Promise.all(
tableData.value.map(async (row) => {
if (!row.id) return
row.workflowLoading = true
try {
const res: any = await stepInfolist({
flowId: row.id,
pageNum: 1,
pageSize: 1,
})
const data = res?.data ?? res ?? {}
const records =
data.records ||
data.list ||
data.rows ||
data.items ||
(Array.isArray(data.data) ? data.data : [])
row.hasWorkflow = Array.isArray(records) && records.length > 0
} catch (error) {
console.error(`查询流程配置状态失败(${row.id}):`, error)
row.hasWorkflow = false
} finally {
row.workflowLoading = false
}
})
)
} catch (error) {
console.error('获取流程信息列表失败:', error)
ElMessage.error('获取流程信息列表失败')
@@ -432,6 +477,23 @@ const handleEdit = async (row: FlowInfoItem) => {
}
}
// 跳转到流程创建/编辑
const handleConfigWorkflow = (row: FlowInfoItem) => {
if (!row.id) {
ElMessage.warning('缺少流程信息ID')
return
}
router.push({
name: 'step-info',
query: {
flowId: row.id,
flowIndex: row.flowIndex ?? '',
flowName: row.flowName ?? '',
mode: row.hasWorkflow ? 'edit' : 'create',
},
})
}
// 删除流程信息
const handleDelete = async (row: FlowInfoItem) => {
if (!row.id) {

View File

@@ -37,6 +37,7 @@
v-for="(item, index) in islandList"
:key="item.id"
class="island-card"
:style="{ animationDelay: `${index * 50}ms` }"
>
<!-- 左侧步骤编号和过滤器图标 -->
<div class="card-left">
@@ -945,6 +946,8 @@ onMounted(() => {
min-height: 95px;
height: 95px;
border: 1px solid rgba(64, 158, 255, 0.1);
animation: slideUpIn 0.4s ease-out forwards;
opacity: 0;
&:hover {
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
@@ -1100,6 +1103,18 @@ onMounted(() => {
justify-content: flex-end;
gap: 10px;
}
// 功能岛卡片动画:从下往上滑动
@keyframes slideUpIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -11,6 +11,7 @@
v-for="(item, index) in islandList"
:key="item.id"
class="component-item"
:style="{ animationDelay: `${index * 50}ms` }"
draggable="true"
@dragstart="handleDragStart($event, item)"
>
@@ -24,7 +25,29 @@
</div>
<!-- 中间流程设计画布 -->
<div class="workflow-canvas" @dragover="handleDragOver" @drop="handleDrop">
<div
class="workflow-canvas"
v-loading="workflowLoading"
@dragover="handleDragOver"
@drop="handleDrop"
>
<div class="canvas-header">
<div class="canvas-title">
<template v-if="flowIndex && flowName">
{{ flowIndex }}{{ flowName }}
</template>
<template v-else-if="flowName">
{{ flowName }}
</template>
<template v-else>
未选择标准流程
</template>
</div>
<div class="canvas-status">
<el-tag v-if="isEditMode" type="success" effect="plain">编辑流程</el-tag>
<el-tag v-else type="warning" effect="plain">配置流程</el-tag>
</div>
</div>
<div v-if="workflowItems.length === 0" class="empty-canvas">
拖拽左侧功能岛到此处创建流程步骤
</div>
@@ -35,6 +58,7 @@
:key="index"
class="workflow-item"
:class="{ 'is-selected': selectedItemIndex === index }"
:style="{ animationDelay: `${index * 100}ms` }"
draggable="true"
@dragstart="handleWorkflowItemDragStart($event, index)"
@dragover.stop="handleWorkflowItemDragOver($event, index)"
@@ -84,9 +108,8 @@
</div>
<div class="action-buttons">
<el-button @click="clearWorkflow">清空</el-button>
<el-button type="primary" plain @click="createWorkflow">新建</el-button>
<el-button type="primary" @click="saveWorkflow">保存</el-button>
<el-button type="primary" plain @click="clearWorkflow">清空</el-button>
<el-button type="primary" @click="saveWorkflow" :loading="savingWorkflow">保存</el-button>
</div>
</div>
@@ -175,6 +198,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
MoreFilled,
@@ -198,16 +222,50 @@ import { devselect } from '@/api/tb/devinfo'
import { devparamselect } from '@/api/tb/devparam'
import { dictypelist } from '@/api/system/dictype'
import { dicdatabydicid } from '@/api/system/dicdata'
import {
stepInfoaddlist,
stepInfodel,
stepInfodellist,
stepInfoupdlist,
stepInfolist,
} from '@/api/tb/stepinfo'
const route = useRoute()
const router = useRouter()
// 路由携带的标准流程信息
const flowId = computed(() => {
const raw = route.query.flowId
const num = Number(raw)
return Number.isNaN(num) ? undefined : num
})
const flowName = computed(() => (route.query.flowName as string) || '')
const flowIndex = computed(() => (route.query.flowIndex as string) || '')
const hasRemoteWorkflow = ref(false)
const isEditMode = computed(() => {
const mode = (route.query.mode as string) || ''
return mode === 'edit' || hasRemoteWorkflow.value
})
// 功能岛列表
const islandList = ref<any[]>([])
const loading = ref(false)
const workflowLoading = ref(false)
// 当前流程中的组件
const workflowItems = ref<any[]>([])
const selectedItemIndex = ref(-1)
const workflowGridRef = ref<HTMLElement | null>(null)
const generateInstanceId = (base?: number | string) =>
`island-${base ?? 'unknown'}-${Date.now()}-${Math.random().toString(16).slice(2)}`
const getInstanceId = (item: any) => {
if (!item.instanceId) {
item.instanceId = generateInstanceId(item.id)
}
return item.instanceId
}
// 计算网格样式
const gridStyle = computed(() => {
const count = workflowItems.value.length
@@ -238,6 +296,7 @@ const paramList = ref<any[]>([])
const paramFormData = reactive<Record<string, any>>({})
const paramLoading = ref(false)
const savingParams = ref(false)
const savingWorkflow = ref(false)
// 字典类型和字典数据缓存
const dicTypeCache = ref<Map<string, any>>(new Map())
@@ -245,6 +304,24 @@ const dicDataCache = ref<Map<number, any[]>>(new Map())
// 参数选项映射paramId -> options[]
const paramOptionsMap = reactive<Record<string | number, Array<{ label: string; value: string }>>>({})
const paramCache = reactive<Record<string, any[]>>({})
const clearParamCache = () => {
Object.keys(paramCache).forEach((key) => {
delete paramCache[key]
})
}
const normalizeListResponse = (res: any) => {
const data = res?.data ?? res ?? {}
return (
data.records ||
data.list ||
data.rows ||
data.items ||
(Array.isArray(data.data) ? data.data : []) ||
[]
)
}
// 中文数字
const chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
@@ -372,7 +449,12 @@ const getIslandList = async () => {
return idA - idB
})
islandList.value = records
islandList.value = records
// 若带有标准流程信息则尝试加载已保存的流程
if (flowId.value) {
await loadWorkflowFromServer()
}
} catch (error) {
console.error('获取功能岛列表失败:', error)
ElMessage.error('获取功能岛列表失败')
@@ -381,6 +463,88 @@ const getIslandList = async () => {
}
}
// 从后端加载已有流程并反填
const loadWorkflowFromServer = async () => {
if (!flowId.value) return
try {
workflowLoading.value = true
const res: any = await stepInfolist({
flowId: flowId.value,
pageNum: 1,
pageSize: 10000,
})
const records = normalizeListResponse(res)
if (!Array.isArray(records) || records.length === 0) {
hasRemoteWorkflow.value = false
workflowItems.value = []
clearParamCache()
return
}
const sortedRecords = [...records].sort(
(a: any, b: any) => (a?.stepOrder ?? 0) - (b?.stepOrder ?? 0)
)
workflowItems.value = []
clearParamCache()
const islandCountMap: Record<string, number> = {}
let lastIslandId: any = null
let currentInstanceId = ''
sortedRecords.forEach((record: any) => {
const islandId = record.islandId ?? record.islandID ?? record.islandid
const islandNameFromRecord = record.islandName || record.islandname || ''
if (lastIslandId !== islandId) {
const islandMeta = islandList.value.find((it) => it.id === islandId) || {}
const islandName = islandMeta.islandName || islandMeta.name || islandNameFromRecord || '功能岛'
const descriptionKey = getIslandDescriptionKey(islandName)
const count = islandCountMap[descriptionKey] ?? 0
islandCountMap[descriptionKey] = count + 1
currentInstanceId = generateInstanceId(islandId)
workflowItems.value.push({
...islandMeta,
id: islandId,
islandName,
name: islandMeta.name || islandName,
description: `${chineseNumbers[count] || count + 1}${descriptionKey || islandName}`,
sort: count,
instanceId: currentInstanceId,
})
paramCache[currentInstanceId] = []
lastIslandId = islandId
}
if (!currentInstanceId) return
const cacheArr = paramCache[currentInstanceId] || []
paramCache[currentInstanceId] = cacheArr
cacheArr.push({
stepId: record.id,
flowId: flowId.value,
islandId,
islandName: islandNameFromRecord,
devId: record.devId ?? record.devid,
devName: record.devName ?? record.devname,
paramId: record.paramId ?? record.paramid,
paramName: record.paramName,
paramType: record.paramType,
paramUnit: record.paramUnit,
paramValue: record.paramValue,
formType: record.formType,
stepOrder: record.stepOrder,
})
})
hasRemoteWorkflow.value = workflowItems.value.length > 0
selectedItemIndex.value = -1
} catch (error) {
console.error('加载流程数据失败:', error)
ElMessage.error('加载流程数据失败')
} finally {
workflowLoading.value = false
}
}
// 拖拽开始
const handleDragStart = (event: DragEvent, item: any) => {
// 检查是否超过添加次数限制
@@ -423,6 +587,7 @@ const handleDrop = (event: DragEvent) => {
...dragItemData,
description: `${chineseNumbers[count]}${descriptionKey}`,
sort: 0,
instanceId: generateInstanceId(dragItemData.id),
}
workflowItems.value.push(newItem)
@@ -462,6 +627,7 @@ const handleContainerDrop = (event: DragEvent) => {
...dragItemData,
description: `${chineseNumbers[count]}${descriptionKey}`,
sort: count,
instanceId: generateInstanceId(dragItemData.id),
}
const items = document.querySelectorAll('.workflow-item')
@@ -607,6 +773,8 @@ const handleEdit = async (item: any) => {
try {
paramLoading.value = true
paramList.value = []
const instanceId = getInstanceId(item)
const cachedParams = paramCache[instanceId] || []
// 获取功能岛绑定的设备
const devRes: any = await devselect({})
@@ -644,10 +812,20 @@ const handleEdit = async (item: any) => {
})
// 给每个参数添加设备信息
params.forEach((param: any) => {
const cached = cachedParams.find(
(cacheItem: any) =>
cacheItem.paramId === param.id ||
cacheItem.paramName === param.paramName
)
allParams.push({
...param,
devId: device.id,
devName: device.devName || `设备${device.id}`,
paramValue: cached?.paramValue ?? param.paramValue,
paramType: cached?.paramType ?? param.paramType,
paramUnit: cached?.paramUnit ?? param.paramUnit,
formType: cached?.formType ?? param.formType,
stepId: cached?.stepId,
})
})
}
@@ -664,7 +842,7 @@ const handleEdit = async (item: any) => {
})
allParams.forEach((param) => {
if (param.id) {
paramFormData[param.id] = param.paramValue || ''
paramFormData[param.id] = param.paramValue ?? ''
}
})
@@ -763,9 +941,30 @@ const loadParamOptions = async (params: any[]) => {
const handleSaveParams = async () => {
try {
savingParams.value = true
// TODO: 调用API保存参数值
// 这里需要根据实际的API接口来保存参数值
ElMessage.success('保存成功')
if (!editingItem.value) {
ElMessage.warning('未选中功能岛')
return
}
const instanceId = getInstanceId(editingItem.value)
const paramsForCache =
paramList.value.map((param) => ({
stepId: param.stepId,
flowId: flowId.value,
islandId: editingItem.value.id,
islandName: editingItem.value.islandName || editingItem.value.name,
devId: param.devId,
devName: param.devName,
paramId: param.id,
paramName: param.paramName,
paramType: param.paramType,
paramUnit: param.paramUnit,
paramValue: paramFormData[param.id],
formType: param.formType,
})) || []
paramCache[instanceId] = paramsForCache
ElMessage.success('参数保存成功')
drawerVisible.value = false
} catch (error) {
console.error('保存参数失败:', error)
@@ -782,7 +981,26 @@ const handleDelete = (index: number, item: any) => {
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
.then(async () => {
const cacheKey = getInstanceId(item)
const cachedParams = paramCache[cacheKey] || []
// 删除已保存的步骤数据
if (hasRemoteWorkflow.value && cachedParams.some((p) => p.stepId)) {
try {
await Promise.all(
cachedParams
.map((p) => p.stepId)
.filter(Boolean)
.map((id) => stepInfodel(id))
)
} catch (error) {
console.error('删除功能岛关联步骤失败:', error)
ElMessage.error('删除功能岛关联步骤失败')
}
}
delete paramCache[cacheKey]
workflowItems.value.splice(index, 1)
if (selectedItemIndex.value === index) {
selectedItemIndex.value = -1
@@ -810,23 +1028,131 @@ const handleDelete = (index: number, item: any) => {
}
// 清空
const clearWorkflow = () => {
workflowItems.value = []
selectedItemIndex.value = -1
ElMessage.info('已清空当前流程')
}
const clearWorkflow = async () => {
// 如果是编辑模式(从编辑流程按钮跳转过来),需要调用删除接口
if (isEditMode.value && flowId.value) {
try {
await ElMessageBox.confirm(
`您确定要清除[${flowName.value || '该'}]的完整流程吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
// 新建
const createWorkflow = () => {
workflowItems.value = []
selectedItemIndex.value = -1
ElMessage.success('已创建新的流程')
// 先获取所有需要删除的数据 ID
try {
const res: any = await stepInfolist({
flowId: flowId.value,
pageNum: 1,
pageSize: 10000,
})
const records = normalizeListResponse(res)
if (Array.isArray(records) && records.length > 0) {
const ids = records.map((r: any) => r.id).filter(Boolean)
if (ids.length > 0) {
await stepInfodellist(ids)
ElMessage.success('流程已清空,可重新配置流程')
} else {
ElMessage.info('没有可删除的数据')
}
} else {
ElMessage.info('没有可删除的数据')
}
} catch (error) {
console.error('删除流程数据失败:', error)
ElMessage.error('删除流程数据失败')
return
}
// 清空画布和缓存
workflowItems.value = []
selectedItemIndex.value = -1
clearParamCache()
hasRemoteWorkflow.value = false
} catch (error: any) {
if (error !== 'cancel') {
console.error('清空流程失败:', error)
}
}
} else {
// 配置模式或从菜单栏直接进入,仅清除画布卡片
workflowItems.value = []
selectedItemIndex.value = -1
clearParamCache()
ElMessage.info('已清空当前流程')
}
}
// 保存
const saveWorkflow = () => {
// 暂时留空,后续实现
ElMessage.info('保存功能待实现')
const saveWorkflow = async () => {
if (!flowId.value) {
ElMessage.warning('请先选择标准流程!')
return
}
if (!flowName.value) {
ElMessage.warning('缺少标准流程名称stepName请返回列表补全')
return
}
if (workflowItems.value.length === 0) {
ElMessage.warning('请先在画布上添加功能岛')
return
}
let stepOrder = 1
const payload: any[] = []
for (const item of workflowItems.value) {
const cacheKey = getInstanceId(item)
const params = paramCache[cacheKey] || []
if (!params.length) {
ElMessage.warning(`请先为${item.islandName || item.name || '功能岛'}配置参数并保存`)
return
}
params.forEach((param) => {
payload.push({
id: param.stepId,
flowId: flowId.value,
islandId: item.id,
devId: param.devId,
paramName: param.paramName,
paramType: param.paramType,
paramUnit: param.paramUnit,
paramValue: param.paramValue ?? '',
formType: param.formType,
stepName: flowName.value,
stepOrder: stepOrder++,
})
})
}
if (!payload.length) {
ElMessage.warning('暂无可保存的参数数据')
return
}
try {
savingWorkflow.value = true
const apiAction = hasRemoteWorkflow.value ? stepInfoupdlist : stepInfoaddlist
const res: any = await apiAction(payload)
if (res?.code === 0 || res?.code === '0') {
ElMessage.success(hasRemoteWorkflow.value ? '流程更新成功!' : '流程保存成功')
hasRemoteWorkflow.value = true
clearParamCache()
await loadWorkflowFromServer()
} else {
ElMessage.error(res?.message || res?.msg || '保存失败')
}
} catch (error) {
console.error('保存流程失败:', error)
ElMessage.error('保存流程失败')
} finally {
savingWorkflow.value = false
}
}
// 关闭抽屉
@@ -921,6 +1247,8 @@ onMounted(() => {
cursor: move;
user-select: none;
transition: all 0.3s;
animation: slideUpIn 0.4s ease-out forwards;
opacity: 0;
&:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
@@ -961,6 +1289,30 @@ onMounted(() => {
position: relative;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
.canvas-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #d9ecff;
}
.canvas-title {
font-size: 16px;
font-weight: 600;
color: #303133;
display: flex;
gap: 6px;
align-items: center;
}
.canvas-status {
display: flex;
align-items: center;
gap: 8px;
}
.empty-canvas {
flex: 1;
display: flex;
@@ -1019,6 +1371,8 @@ onMounted(() => {
box-sizing: border-box;
transition: all 0.3s;
cursor: pointer;
animation: fadeInUp 0.5s ease-out forwards;
opacity: 0;
&.is-selected {
border-color: #409eff;
@@ -1273,5 +1627,29 @@ onMounted(() => {
padding: 20px;
border-top: 1px solid #e6e6e6;
}
// 功能岛卡片动画:从下往上滑动
@keyframes slideUpIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 流程卡片动画:淡入+向上移动
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>