mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
feat: unify asset/rule interop and add toolbar asset manager
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
<el-button icon="View" type="success" @click="handlePreviewData">数据预览</el-button>
|
||||
<el-button icon="Share" type="primary" @click="prepareCapture">{{ t('prepareCapture') }}</el-button>
|
||||
<el-button icon="Setting" type="primary" @click="state.showWatermarkDialog = true">{{ t('setWatermark') }}</el-button>
|
||||
<el-button icon="Picture" type="primary" plain @click="openAssetManager">素材管理</el-button>
|
||||
<el-button v-if="!props.isEmbed" type="info" @click="loadExample">{{ t('loadExample') }}</el-button>
|
||||
<el-button v-if="!props.isEmbed" type="info" @click="showUpdateLog">{{ t('updateLog') }}</el-button>
|
||||
<el-button v-if="!props.isEmbed" type="warning" @click="showFeedbackForm">{{ t('feedback') }}</el-button>
|
||||
@@ -111,11 +112,62 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 素材管理对话框 -->
|
||||
<el-dialog v-model="state.showAssetManagerDialog" title="素材管理" width="70%">
|
||||
<div class="asset-manager-actions">
|
||||
<input
|
||||
ref="assetUploadInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="asset-upload-input"
|
||||
@change="handleAssetManagerUpload"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="triggerAssetManagerUpload">
|
||||
上传当前分类素材
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="assetManagerLibrary" class="asset-manager-tabs">
|
||||
<el-tab-pane
|
||||
v-for="library in assetLibraries"
|
||||
:key="library.id"
|
||||
:label="library.label"
|
||||
:name="library.id"
|
||||
>
|
||||
<div class="asset-manager-grid">
|
||||
<div
|
||||
v-for="item in getManagedAssets(library.id)"
|
||||
:key="item.id || `${item.name}-${item.avatar}`"
|
||||
class="asset-manager-item"
|
||||
>
|
||||
<div
|
||||
class="asset-manager-image"
|
||||
:style="{ backgroundImage: `url('${resolveAssetUrl(item.avatar)}')` }"
|
||||
/>
|
||||
<div class="asset-manager-name">{{ item.name }}</div>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="danger"
|
||||
@click="removeManagedAsset(library.id, item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="getManagedAssets(library.id).length === 0"
|
||||
:description="`暂无${library.label}`"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue';
|
||||
import { reactive, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import updateLogs from "../data/updateLog.json"
|
||||
import { useFilesStore } from "@/ts/useStore";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
@@ -123,8 +175,16 @@ import { useGlobalMessage } from "@/ts/useGlobalMessage";
|
||||
import { getLogicFlowInstance } from "@/ts/useLogicFlow";
|
||||
import { useCanvasSettings } from '@/ts/useCanvasSettings';
|
||||
import { useSafeI18n } from '@/ts/useSafeI18n';
|
||||
import { ASSET_LIBRARIES } from '@/types/nodeTypes';
|
||||
import type { Pinia } from 'pinia';
|
||||
import { resolveAssetUrl } from '@/utils/assetUrl';
|
||||
import {
|
||||
createCustomAssetFromFile,
|
||||
deleteCustomAsset,
|
||||
listCustomAssets,
|
||||
subscribeCustomAssetStore,
|
||||
type CustomAssetItem
|
||||
} from '@/utils/customAssets';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
isEmbed?: boolean;
|
||||
@@ -156,8 +216,68 @@ const state = reactive({
|
||||
showUpdateLogDialog: false, // 控制更新日志对话框的显示状态
|
||||
showFeedbackFormDialog: false, // 控制反馈表单对话框的显示状态
|
||||
showDataPreviewDialog: false, // 控制数据预览对话框的显示状态
|
||||
showAssetManagerDialog: false, // 控制素材管理对话框的显示状态
|
||||
previewDataContent: '', // 存储预览的数据内容
|
||||
});
|
||||
const assetLibraries = ASSET_LIBRARIES.map((item) => ({
|
||||
id: item.id,
|
||||
label: `${item.label}素材`
|
||||
}));
|
||||
const assetManagerLibrary = ref(assetLibraries[0]?.id || 'shikigami');
|
||||
const assetUploadInputRef = ref<HTMLInputElement | null>(null);
|
||||
const managedAssets = reactive<Record<string, CustomAssetItem[]>>({});
|
||||
assetLibraries.forEach((item) => {
|
||||
managedAssets[item.id] = [];
|
||||
});
|
||||
let unsubscribeAssetStore: (() => void) | null = null;
|
||||
|
||||
const refreshManagedAssets = (library?: string) => {
|
||||
if (library) {
|
||||
managedAssets[library] = listCustomAssets(library);
|
||||
return;
|
||||
}
|
||||
assetLibraries.forEach((item) => {
|
||||
managedAssets[item.id] = listCustomAssets(item.id);
|
||||
});
|
||||
};
|
||||
|
||||
const openAssetManager = () => {
|
||||
refreshManagedAssets();
|
||||
state.showAssetManagerDialog = true;
|
||||
};
|
||||
|
||||
const getManagedAssets = (libraryId: string) => {
|
||||
return managedAssets[libraryId] || [];
|
||||
};
|
||||
|
||||
const triggerAssetManagerUpload = () => {
|
||||
assetUploadInputRef.value?.click();
|
||||
};
|
||||
|
||||
const handleAssetManagerUpload = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
const file = target?.files?.[0];
|
||||
if (!file) {
|
||||
if (target) target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await createCustomAssetFromFile(assetManagerLibrary.value, file);
|
||||
refreshManagedAssets(assetManagerLibrary.value);
|
||||
showMessage('success', '素材上传成功');
|
||||
} catch (error) {
|
||||
console.error('素材上传失败:', error);
|
||||
showMessage('error', '素材上传失败');
|
||||
} finally {
|
||||
if (target) target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const removeManagedAsset = (libraryId: string, item: CustomAssetItem) => {
|
||||
deleteCustomAsset(libraryId, item);
|
||||
refreshManagedAssets(libraryId);
|
||||
};
|
||||
|
||||
// 重新渲染 LogicFlow 画布的通用方法
|
||||
const refreshLogicFlowCanvas = (message?: string) => {
|
||||
@@ -224,6 +344,11 @@ const showUpdateLog = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
refreshManagedAssets();
|
||||
unsubscribeAssetStore = subscribeCustomAssetStore(() => {
|
||||
refreshManagedAssets();
|
||||
});
|
||||
|
||||
if (props.isEmbed) {
|
||||
return;
|
||||
}
|
||||
@@ -238,6 +363,11 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribeAssetStore?.();
|
||||
unsubscribeAssetStore = null;
|
||||
});
|
||||
|
||||
|
||||
const showFeedbackForm = () => {
|
||||
state.showFeedbackFormDialog = !state.showFeedbackFormDialog;
|
||||
@@ -541,4 +671,51 @@ const handleClose = (done) => {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.asset-manager-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.asset-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.asset-manager-tabs {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.asset-manager-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.asset-manager-item {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.asset-manager-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.asset-manager-name {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -74,10 +74,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import type { SelectorConfig, GroupConfig } from '@/types/selector'
|
||||
import { resolveAssetUrl } from '@/utils/assetUrl'
|
||||
import { createCustomAssetFromFile } from '@/utils/customAssets'
|
||||
import { createCustomAssetFromFile, listCustomAssets, subscribeCustomAssetStore } from '@/utils/customAssets'
|
||||
|
||||
const props = defineProps<{
|
||||
config: SelectorConfig
|
||||
@@ -100,15 +100,50 @@ const imageSize = computed(() => props.config.itemRender.imageSize || 100)
|
||||
const imageField = computed(() => props.config.itemRender.imageField)
|
||||
const uploadInputRef = ref<HTMLInputElement | null>(null)
|
||||
const dataSource = ref<any[]>([])
|
||||
let unsubscribeCustomAssets: (() => void) | null = null
|
||||
|
||||
const refreshDataSource = () => {
|
||||
const source = Array.isArray(props.config.dataSource) ? props.config.dataSource : []
|
||||
const staticAssets = source.filter((item) => !item?.__userAsset)
|
||||
const library = props.config.assetLibrary
|
||||
if (!library) {
|
||||
dataSource.value = [...source]
|
||||
return
|
||||
}
|
||||
const customAssets = listCustomAssets(library)
|
||||
dataSource.value = [...staticAssets, ...customAssets]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.config.dataSource,
|
||||
(value) => {
|
||||
dataSource.value = Array.isArray(value) ? [...value] : []
|
||||
() => [props.config.dataSource, props.config.assetLibrary],
|
||||
() => {
|
||||
refreshDataSource()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
refreshDataSource()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
unsubscribeCustomAssets = subscribeCustomAssetStore(() => {
|
||||
if (props.modelValue) {
|
||||
refreshDataSource()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribeCustomAssets?.()
|
||||
unsubscribeCustomAssets = null
|
||||
})
|
||||
|
||||
// 过滤逻辑
|
||||
const filteredItems = (group: GroupConfig) => {
|
||||
let items = dataSource.value
|
||||
@@ -168,8 +203,8 @@ const handleUploadAsset = async (event: Event) => {
|
||||
|
||||
try {
|
||||
const createdAsset = await createCustomAssetFromFile(props.config.assetLibrary, file)
|
||||
dataSource.value = [createdAsset, ...dataSource.value]
|
||||
props.config.onUserAssetUploaded?.(createdAsset)
|
||||
refreshDataSource()
|
||||
} finally {
|
||||
if (target) {
|
||||
target.value = ''
|
||||
@@ -178,11 +213,8 @@ const handleUploadAsset = async (event: Event) => {
|
||||
}
|
||||
|
||||
const removeUserAsset = (item: any) => {
|
||||
if (!item?.id) {
|
||||
return
|
||||
}
|
||||
props.config.onDeleteUserAsset?.(item)
|
||||
dataSource.value = dataSource.value.filter((entry) => entry.id !== item.id)
|
||||
refreshDataSource()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
<div class="editor-layout" :style="{ height }">
|
||||
<!-- 中间流程图区域 -->
|
||||
<div ref="flowHostRef" class="flow-container" :class="{ 'snapline-disabled': !snaplineEnabled }">
|
||||
<div class="flow-controls">
|
||||
<div class="flow-controls" :class="{ 'flow-controls--collapsed': flowControlsCollapsed }">
|
||||
<div class="control-row control-header">
|
||||
<button class="control-button" type="button" @click="flowControlsCollapsed = !flowControlsCollapsed">
|
||||
{{ flowControlsCollapsed ? `显示画布控制${groupRuleWarnings.length ? `(${groupRuleWarnings.length})` : ''}` : '收起画布控制' }}
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="!flowControlsCollapsed">
|
||||
<div class="control-row toggles">
|
||||
<label class="control-toggle">
|
||||
<input type="checkbox" v-model="selectionEnabled" />
|
||||
@@ -57,6 +63,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="container" ref="containerRef" :style="{ height: '100%' }"></div>
|
||||
</div>
|
||||
@@ -90,6 +97,7 @@ import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlo
|
||||
import { normalizePropertiesWithStyle, normalizeNodeStyle, styleEquals } from '@/ts/nodeStyle';
|
||||
import { useCanvasSettings } from '@/ts/useCanvasSettings';
|
||||
import { validateGraphGroupRules, type GroupRuleWarning } from '@/utils/groupRules';
|
||||
import { subscribeSharedGroupRulesConfig } from '@/utils/groupRulesConfigSource';
|
||||
|
||||
type AlignType = 'left' | 'right' | 'top' | 'bottom' | 'hcenter' | 'vcenter';
|
||||
type DistributeType = 'horizontal' | 'vertical';
|
||||
@@ -128,9 +136,11 @@ const { showMessage } = useGlobalMessage();
|
||||
const selectedNode = ref<any>(null);
|
||||
const copyBuffer = ref<GraphData | null>(null);
|
||||
const groupRuleWarnings = ref<GroupRuleWarning[]>([]);
|
||||
const flowControlsCollapsed = ref(true);
|
||||
let nextPasteDistance = COPY_TRANSLATION;
|
||||
let containerResizeObserver: ResizeObserver | null = null;
|
||||
let groupRuleValidationTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let unsubscribeSharedGroupRules: (() => void) | null = null;
|
||||
|
||||
const resolveResizeHost = () => {
|
||||
const container = containerRef.value;
|
||||
@@ -1074,6 +1084,9 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
unsubscribeSharedGroupRules = subscribeSharedGroupRulesConfig(() => {
|
||||
scheduleGroupRuleValidation(0);
|
||||
});
|
||||
});
|
||||
|
||||
watch(selectionEnabled, (enabled) => {
|
||||
@@ -1114,6 +1127,8 @@ onBeforeUnmount(() => {
|
||||
clearTimeout(groupRuleValidationTimer);
|
||||
groupRuleValidationTimer = null;
|
||||
}
|
||||
unsubscribeSharedGroupRules?.();
|
||||
unsubscribeSharedGroupRules = null;
|
||||
lf.value?.destroy();
|
||||
lf.value = null;
|
||||
destroyLogicFlowInstance();
|
||||
@@ -1158,6 +1173,10 @@ onBeforeUnmount(() => {
|
||||
max-width: 460px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.flow-controls--collapsed {
|
||||
padding: 6px;
|
||||
max-width: 220px;
|
||||
}
|
||||
.control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1165,6 +1184,9 @@ onBeforeUnmount(() => {
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.control-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.control-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,10 @@ const currentAssetLibrary = computed({
|
||||
</div>
|
||||
|
||||
<div v-if="!hasNodeSelected" class="no-selection">
|
||||
<p>请选择一个节点以编辑其属性</p>
|
||||
<div class="no-selection-text">
|
||||
<p>请选择一个节点以编辑其属性</p>
|
||||
<p class="no-selection-tip">素材入口:添加并选中 assetSelector 节点后,点击“选择资产”。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="property-content">
|
||||
@@ -155,6 +158,18 @@ const currentAssetLibrary = computed({
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-selection-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.no-selection-tip {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.property-content {
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
|
||||
@@ -59,10 +59,7 @@ const handleOpenSelector = () => {
|
||||
assetLibrary: library,
|
||||
allowUserAssetUpload: true,
|
||||
onDeleteUserAsset: (item: any) => {
|
||||
if (!item?.id) {
|
||||
return;
|
||||
}
|
||||
deleteCustomAsset(library, item.id);
|
||||
deleteCustomAsset(library, item);
|
||||
},
|
||||
onUserAssetUploaded: () => {
|
||||
// 上传后的数据刷新由选择器内部完成,这里保留扩展钩子。
|
||||
|
||||
Reference in New Issue
Block a user