mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
feat: unify node style schema and add full style editing panel
This commit is contained in:
@@ -74,6 +74,7 @@ import ImageNode from './nodes/common/ImageNode.vue';
|
||||
import PropertyPanel from './PropertyPanel.vue';
|
||||
import { useGlobalMessage } from '@/ts/useGlobalMessage';
|
||||
import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||
import { normalizePropertiesWithStyle, normalizeNodeStyle, styleEquals } from '@/ts/nodeStyle';
|
||||
|
||||
type AlignType = 'left' | 'right' | 'top' | 'bottom' | 'hcenter' | 'vcenter';
|
||||
type DistributeType = 'horizontal' | 'vertical';
|
||||
@@ -152,22 +153,41 @@ function applyMetaToModel(model: BaseNodeModel, metaInput?: Record<string, any>)
|
||||
}
|
||||
}
|
||||
|
||||
function applyStyleToModel(model: BaseNodeModel, styleInput?: Record<string, any>) {
|
||||
const style = normalizeNodeStyle(styleInput, { width: model.width, height: model.height });
|
||||
if (style.width && model.width !== style.width) {
|
||||
model.width = style.width;
|
||||
}
|
||||
if (style.height && model.height !== style.height) {
|
||||
model.height = style.height;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeNodeModel(model: BaseNodeModel) {
|
||||
const lfInstance = lf.value;
|
||||
if (!lfInstance) return;
|
||||
|
||||
const props = (model.getProperties?.() as any) ?? (model as any)?.properties ?? {};
|
||||
const incomingMeta = ensureMeta(props.meta);
|
||||
const normalized = normalizePropertiesWithStyle(
|
||||
{ ...props, meta: incomingMeta },
|
||||
{ width: props.width ?? model.width, height: props.height ?? model.height }
|
||||
);
|
||||
const currentMeta = ensureMeta((model as any)?.properties?.meta);
|
||||
const needPersist =
|
||||
const metaChanged =
|
||||
currentMeta.visible !== incomingMeta.visible ||
|
||||
currentMeta.locked !== incomingMeta.locked ||
|
||||
currentMeta.groupId !== incomingMeta.groupId;
|
||||
const styleChanged =
|
||||
!styleEquals(props.style, normalized.style) ||
|
||||
props.width !== normalized.width ||
|
||||
props.height !== normalized.height;
|
||||
|
||||
if (needPersist) {
|
||||
lfInstance.setProperties(model.id, { ...props, meta: incomingMeta });
|
||||
if (metaChanged || styleChanged) {
|
||||
lfInstance.setProperties(model.id, normalized);
|
||||
}
|
||||
applyMetaToModel(model, incomingMeta);
|
||||
applyMetaToModel(model, normalized.meta);
|
||||
applyStyleToModel(model, normalized.style);
|
||||
}
|
||||
|
||||
function normalizeAllNodes() {
|
||||
@@ -688,9 +708,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
const model = lfInstance.getNodeModelById(nodeId);
|
||||
if (model && data.properties?.meta) {
|
||||
applyMetaToModel(model, data.properties.meta);
|
||||
}
|
||||
if (model) normalizeNodeModel(model);
|
||||
});
|
||||
|
||||
lfInstance.on('selection:selected', () => updateSelectedCount());
|
||||
|
||||
@@ -5,6 +5,7 @@ import YuhunPanel from './panels/YuhunPanel.vue';
|
||||
import PropertyRulePanel from './panels/PropertyRulePanel.vue';
|
||||
import ImagePanel from './panels/ImagePanel.vue';
|
||||
import TextPanel from './panels/TextPanel.vue';
|
||||
import StylePanel from './panels/StylePanel.vue';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
@@ -59,6 +60,8 @@ const panelComponent = computed(() => panelMap[nodeType.value] || null);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StylePanel :node="selectedNode" />
|
||||
|
||||
<component v-if="panelComponent" :is="panelComponent" :node="selectedNode" />
|
||||
<div v-else class="property-section">
|
||||
<div class="section-header">暂无特定属性</div>
|
||||
|
||||
@@ -1,44 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { EventType } from '@logicflow/core';
|
||||
import { ref } from 'vue';
|
||||
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||
|
||||
type FitMode = 'contain' | 'cover' | 'fill';
|
||||
|
||||
const getNode = inject('getNode') as (() => any) | undefined;
|
||||
const getGraph = inject('getGraph') as (() => any) | undefined;
|
||||
|
||||
const imageUrl = ref('');
|
||||
const fit = ref<FitMode>('contain');
|
||||
|
||||
const refreshFromProps = (props?: any, node?: any) => {
|
||||
const targetProps = props ?? node?.properties ?? {};
|
||||
imageUrl.value = targetProps.image?.url ?? targetProps.url ?? '';
|
||||
fit.value = targetProps.image?.fit ?? targetProps.fit ?? 'contain';
|
||||
};
|
||||
|
||||
let offChange: (() => void) | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
const node = getNode?.();
|
||||
refreshFromProps(node?.properties, node);
|
||||
|
||||
const graph = getGraph?.();
|
||||
const handler = (eventData: any) => {
|
||||
if (node && eventData.id === node.id) {
|
||||
refreshFromProps(eventData.properties, node);
|
||||
}
|
||||
};
|
||||
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, handler);
|
||||
offChange = () => graph?.eventCenter.off(EventType.NODE_PROPERTIES_CHANGE, handler);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
offChange?.();
|
||||
const { containerStyle } = useNodeAppearance({
|
||||
onPropsChange(props) {
|
||||
imageUrl.value = props.image?.url ?? props.url ?? '';
|
||||
fit.value = props.image?.fit ?? props.fit ?? 'contain';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-node">
|
||||
<div class="image-node" :style="containerStyle">
|
||||
<div class="image-wrapper">
|
||||
<img v-if="imageUrl" :src="imageUrl" alt="图片节点" :style="{ objectFit: fit }" draggable="false" />
|
||||
<div v-else class="placeholder">
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, inject } from 'vue';
|
||||
import { EventType } from '@logicflow/core';
|
||||
import { ref } from 'vue';
|
||||
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||
|
||||
const currentProperty = ref({ type: '未选择', priority: '可选' });
|
||||
|
||||
const getNode = inject('getNode') as (() => any) | undefined;
|
||||
const getGraph = inject('getGraph') as (() => any) | undefined;
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const node = getNode?.();
|
||||
const graph = getGraph?.();
|
||||
|
||||
if (node?.properties?.property) {
|
||||
currentProperty.value = node.properties.property;
|
||||
}
|
||||
|
||||
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData: any) => {
|
||||
if (eventData.id === node.id && eventData.properties?.property) {
|
||||
currentProperty.value = eventData.properties.property;
|
||||
const { containerStyle, textStyle } = useNodeAppearance({
|
||||
onPropsChange(props) {
|
||||
if (props.property) {
|
||||
currentProperty.value = props.property;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 辅助函数
|
||||
@@ -50,19 +39,19 @@ const getPriorityName = () => {
|
||||
|
||||
<template>
|
||||
<div class="property-node" :class="[currentProperty.priority ? `priority-${currentProperty.priority}` : '']">
|
||||
<div class="node-content">
|
||||
<div class="node-content" :style="containerStyle">
|
||||
<div class="node-header">
|
||||
<div class="node-title">属性要求</div>
|
||||
<div class="node-title" :style="textStyle">属性要求</div>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
<div class="property-main">
|
||||
<div class="property-type">{{ getPropertyTypeName() }}</div>
|
||||
<div v-if="currentProperty.type !== '未选择'" class="property-value">{{ currentProperty.value }}</div>
|
||||
<div v-else class="property-placeholder">点击设置属性</div>
|
||||
<div class="property-type" :style="textStyle">{{ getPropertyTypeName() }}</div>
|
||||
<div v-if="currentProperty.type !== '未选择'" class="property-value" :style="textStyle">{{ currentProperty.value }}</div>
|
||||
<div v-else class="property-placeholder" :style="textStyle">点击设置属性</div>
|
||||
</div>
|
||||
<div class="property-details" v-if="currentProperty.type !== '未选择'">
|
||||
<div class="property-priority">优先级: {{ getPriorityName() }}</div>
|
||||
<div class="property-description" v-if="currentProperty.description">
|
||||
<div class="property-priority" :style="textStyle">优先级: {{ getPriorityName() }}</div>
|
||||
<div class="property-description" v-if="currentProperty.description" :style="textStyle">
|
||||
{{ currentProperty.description }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,4 +146,4 @@ const getPriorityName = () => {
|
||||
padding-top: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,41 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, inject, watch} from 'vue';
|
||||
import { EventType } from '@logicflow/core';
|
||||
import { computed, ref } from 'vue';
|
||||
import { toTextStyle } from '@/ts/nodeStyle';
|
||||
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||
|
||||
const currentShikigami = ref({ name: '未选择式神', avatar: '', rarity: '' });
|
||||
|
||||
const getNode = inject('getNode') as (() => any) | undefined;
|
||||
const getGraph = inject('getGraph') as (() => any) | undefined;
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const node = getNode?.();
|
||||
const graph = getGraph?.();
|
||||
|
||||
// 初始化
|
||||
if (node?.properties?.shikigami) {
|
||||
currentShikigami.value = node.properties.shikigami;
|
||||
}
|
||||
|
||||
// 监听属性变化
|
||||
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData: any) => {
|
||||
if (eventData.id === node.id && eventData.properties?.shikigami) {
|
||||
currentShikigami.value = eventData.properties.shikigami;
|
||||
const { containerStyle, textStyle } = useNodeAppearance({
|
||||
onPropsChange(props) {
|
||||
if (props.shikigami) {
|
||||
currentShikigami.value = props.shikigami;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const mergedContainerStyle = computed(() => ({ ...containerStyle.value, boxSizing: 'border-box' }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="node-content"
|
||||
:style="{
|
||||
boxSizing: 'border-box',
|
||||
background: '#fff',
|
||||
borderRadius: '8px'
|
||||
}"
|
||||
>
|
||||
<div class="node-content" :style="mergedContainerStyle">
|
||||
<img
|
||||
v-if="currentShikigami.avatar"
|
||||
:src="currentShikigami.avatar"
|
||||
@@ -43,8 +25,8 @@ onMounted(() => {
|
||||
class="shikigami-image"
|
||||
draggable="false"
|
||||
/>
|
||||
<div v-else class="placeholder-text">点击选择式神</div>
|
||||
<div class="name-text">{{ currentShikigami.name }}</div>
|
||||
<div v-else class="placeholder-text" :style="textStyle">点击选择式神</div>
|
||||
<div class="name-text" :style="textStyle">{{ currentShikigami.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -69,4 +51,4 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, inject } from 'vue';
|
||||
import { EventType } from '@logicflow/core';
|
||||
import { ref } from 'vue';
|
||||
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||
|
||||
const currentYuhun = ref({ name: '未选择御魂', avatar: '', type: '' });
|
||||
|
||||
const getNode = inject('getNode') as (() => any) | undefined;
|
||||
const getGraph = inject('getGraph') as (() => any) | undefined;
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const node = getNode?.();
|
||||
const graph = getGraph?.();
|
||||
|
||||
if (node?.properties?.yuhun) {
|
||||
currentYuhun.value = node.properties.yuhun;
|
||||
}
|
||||
|
||||
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData: any) => {
|
||||
if (eventData.id === node.id && eventData.properties?.yuhun) {
|
||||
currentYuhun.value = eventData.properties.yuhun;
|
||||
const { containerStyle, textStyle } = useNodeAppearance({
|
||||
onPropsChange(props) {
|
||||
if (props.yuhun) {
|
||||
currentYuhun.value = props.yuhun;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-content">
|
||||
<div class="node-content" :style="containerStyle">
|
||||
<img
|
||||
v-if="currentYuhun.avatar"
|
||||
:src="currentYuhun.avatar"
|
||||
@@ -34,9 +22,9 @@ onMounted(() => {
|
||||
class="yuhun-image"
|
||||
draggable="false"
|
||||
/>
|
||||
<div v-else class="placeholder-text">点击选择御魂</div>
|
||||
<div class="name-text">{{ currentYuhun.name }}</div>
|
||||
<div v-if="currentYuhun.type" class="type-text">{{ currentYuhun.type }}</div>
|
||||
<div v-else class="placeholder-text" :style="textStyle">点击选择御魂</div>
|
||||
<div class="name-text" :style="textStyle">{{ currentYuhun.name }}</div>
|
||||
<div v-if="currentYuhun.type" class="type-text" :style="textStyle">{{ currentYuhun.type }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||
import { normalizeNodeStyle } from '@/ts/nodeStyle';
|
||||
|
||||
type FitMode = 'contain' | 'cover' | 'fill';
|
||||
|
||||
@@ -29,12 +30,12 @@ const parseNumber = (value: any, fallback: number) => {
|
||||
|
||||
const getImageProps = (node?: any): ImageForm => {
|
||||
const props = node?.properties ?? {};
|
||||
const style = props.style ?? {};
|
||||
const style = normalizeNodeStyle(props.style, { width: props.width ?? node?.width, height: props.height ?? node?.height });
|
||||
return {
|
||||
url: props.image?.url ?? props.url ?? '',
|
||||
fit: (props.image?.fit ?? props.fit ?? 'contain') as FitMode,
|
||||
width: parseNumber(props.width ?? style.width ?? node?.width, 180),
|
||||
height: parseNumber(props.height ?? style.height ?? node?.height, 120)
|
||||
width: parseNumber(style.width, 180),
|
||||
height: parseNumber(style.height, 120)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -64,17 +65,18 @@ const applyImageChanges = (partial: Partial<ImageForm>) => {
|
||||
|
||||
const baseProps = node.properties || {};
|
||||
const merged = { ...getImageProps(node), ...partial };
|
||||
const currentStyle = normalizeNodeStyle(baseProps.style, { width: baseProps.width ?? node.width, height: baseProps.height ?? node.height });
|
||||
const nextStyle = normalizeNodeStyle(
|
||||
{ ...currentStyle, width: merged.width, height: merged.height },
|
||||
{ width: merged.width, height: merged.height }
|
||||
);
|
||||
|
||||
const nextProps = {
|
||||
...baseProps,
|
||||
...merged,
|
||||
width: merged.width,
|
||||
height: merged.height,
|
||||
style: {
|
||||
...(baseProps.style || {}),
|
||||
width: merged.width,
|
||||
height: merged.height
|
||||
},
|
||||
style: nextStyle,
|
||||
width: nextStyle.width,
|
||||
height: nextStyle.height,
|
||||
image: {
|
||||
...(baseProps.image || {}),
|
||||
url: merged.url,
|
||||
|
||||
262
src/components/flow/panels/StylePanel.vue
Normal file
262
src/components/flow/panels/StylePanel.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||
import { normalizeNodeStyle, type NodeStyle } from '@/ts/nodeStyle';
|
||||
|
||||
const props = defineProps<{
|
||||
node: any;
|
||||
}>();
|
||||
|
||||
type StyleForm = {
|
||||
fill: string;
|
||||
stroke: string;
|
||||
strokeWidth: number;
|
||||
radius: number;
|
||||
shadowColor: string;
|
||||
shadowBlur: number;
|
||||
shadowOffsetX: number;
|
||||
shadowOffsetY: number;
|
||||
opacity: number;
|
||||
textAlign: 'left' | 'center' | 'right';
|
||||
lineHeight: number;
|
||||
fontWeight: number | string;
|
||||
};
|
||||
|
||||
const form = reactive<StyleForm>({
|
||||
fill: '#ffffff',
|
||||
stroke: '#dcdfe6',
|
||||
strokeWidth: 1,
|
||||
radius: 4,
|
||||
shadowColor: 'rgba(0,0,0,0.1)',
|
||||
shadowBlur: 4,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
opacity: 1,
|
||||
textAlign: 'left',
|
||||
lineHeight: 1.4,
|
||||
fontWeight: 400
|
||||
});
|
||||
|
||||
const syncFromNode = (node?: any) => {
|
||||
if (!node) return;
|
||||
const baseProps = node.properties ?? {};
|
||||
const style = normalizeNodeStyle(baseProps.style, { width: baseProps.width ?? node.width, height: baseProps.height ?? node.height });
|
||||
form.fill = style.fill ?? form.fill;
|
||||
form.stroke = style.stroke ?? form.stroke;
|
||||
form.strokeWidth = style.strokeWidth ?? form.strokeWidth;
|
||||
form.radius = typeof style.radius === 'number' ? style.radius : style.radius?.[0] ?? form.radius;
|
||||
form.shadowColor = style.shadow?.color ?? form.shadowColor;
|
||||
form.shadowBlur = style.shadow?.blur ?? form.shadowBlur;
|
||||
form.shadowOffsetX = style.shadow?.offsetX ?? form.shadowOffsetX;
|
||||
form.shadowOffsetY = style.shadow?.offsetY ?? form.shadowOffsetY;
|
||||
form.opacity = style.opacity ?? form.opacity;
|
||||
form.textAlign = (style.textStyle?.align as StyleForm['textAlign']) ?? form.textAlign;
|
||||
form.lineHeight = style.textStyle?.lineHeight ?? form.lineHeight;
|
||||
form.fontWeight = style.textStyle?.fontWeight ?? form.fontWeight;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.node,
|
||||
(node) => {
|
||||
if (node) {
|
||||
syncFromNode(node);
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const getShadowFromForm = () => ({
|
||||
color: form.shadowColor,
|
||||
blur: form.shadowBlur,
|
||||
offsetX: form.shadowOffsetX,
|
||||
offsetY: form.shadowOffsetY
|
||||
});
|
||||
|
||||
const applyStyle = (partial: Partial<NodeStyle>) => {
|
||||
const lf = getLogicFlowInstance();
|
||||
const node = props.node;
|
||||
if (!lf || !node) return;
|
||||
|
||||
const baseProps = node.properties || {};
|
||||
const currentStyle = normalizeNodeStyle(baseProps.style, { width: baseProps.width ?? node.width, height: baseProps.height ?? node.height });
|
||||
const mergedStyleInput: Partial<NodeStyle> = {
|
||||
...currentStyle,
|
||||
...partial,
|
||||
shadow: partial.shadow ? { ...currentStyle.shadow, ...partial.shadow } : currentStyle.shadow,
|
||||
textStyle: partial.textStyle ? { ...currentStyle.textStyle, ...partial.textStyle } : currentStyle.textStyle
|
||||
};
|
||||
const nextStyle = normalizeNodeStyle(mergedStyleInput, { width: currentStyle.width, height: currentStyle.height });
|
||||
|
||||
lf.setProperties(node.id, {
|
||||
...baseProps,
|
||||
style: nextStyle,
|
||||
width: nextStyle.width,
|
||||
height: nextStyle.height
|
||||
});
|
||||
};
|
||||
|
||||
const applyShadow = (override?: Partial<NodeStyle['shadow']>) => {
|
||||
applyStyle({ shadow: { ...getShadowFromForm(), ...(override || {}) } });
|
||||
};
|
||||
|
||||
const applyTextStyle = (override?: Partial<NodeStyle['textStyle']>) => {
|
||||
applyStyle({
|
||||
textStyle: {
|
||||
...((props.node?.properties?.style?.textStyle as NodeStyle['textStyle']) || {}),
|
||||
align: form.textAlign,
|
||||
lineHeight: form.lineHeight,
|
||||
fontWeight: form.fontWeight,
|
||||
...(override || {})
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="property-section">
|
||||
<div class="section-header">样式</div>
|
||||
|
||||
<div class="property-item">
|
||||
<div class="property-label">填充</div>
|
||||
<div class="property-value">
|
||||
<el-color-picker v-model="form.fill" size="small" @change="(val: string) => applyStyle({ fill: val })" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-item">
|
||||
<div class="property-label">描边</div>
|
||||
<div class="property-value row">
|
||||
<el-color-picker v-model="form.stroke" size="small" @change="(val: string) => applyStyle({ stroke: val })" />
|
||||
<el-input-number
|
||||
v-model="form.strokeWidth"
|
||||
:min="0"
|
||||
:max="20"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="(val) => applyStyle({ stroke: form.stroke, strokeWidth: Number(val) || 0 })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-item">
|
||||
<div class="property-label">圆角</div>
|
||||
<div class="property-value">
|
||||
<el-input-number
|
||||
v-model="form.radius"
|
||||
:min="0"
|
||||
:max="200"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="(val) => applyStyle({ radius: Number(val) || 0 })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-item">
|
||||
<div class="property-label">阴影</div>
|
||||
<div class="property-value shadow-grid">
|
||||
<el-color-picker v-model="form.shadowColor" size="small" @change="(val: string) => applyShadow({ color: val })" />
|
||||
<el-input-number
|
||||
v-model="form.shadowBlur"
|
||||
:min="0"
|
||||
:max="50"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="(val) => applyShadow({ blur: Number(val) || 0 })"
|
||||
/>
|
||||
<el-input-number
|
||||
v-model="form.shadowOffsetX"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="(val) => applyShadow({ offsetX: Number(val) || 0 })"
|
||||
/>
|
||||
<el-input-number
|
||||
v-model="form.shadowOffsetY"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="(val) => applyShadow({ offsetY: Number(val) || 0 })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-item">
|
||||
<div class="property-label">透明度</div>
|
||||
<div class="property-value slider-row">
|
||||
<el-slider
|
||||
v-model="form.opacity"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
show-input
|
||||
:show-input-controls="false"
|
||||
@change="(val) => applyStyle({ opacity: Number(val) || 0 })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-item">
|
||||
<div class="property-label">文字对齐</div>
|
||||
<div class="property-value">
|
||||
<el-select
|
||||
v-model="form.textAlign"
|
||||
size="small"
|
||||
style="width: 100%;"
|
||||
@change="(val) => applyTextStyle({ align: val as StyleForm['textAlign'] })"
|
||||
>
|
||||
<el-option label="左对齐" value="left" />
|
||||
<el-option label="居中" value="center" />
|
||||
<el-option label="右对齐" value="right" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-item">
|
||||
<div class="property-label">行高 / 字重</div>
|
||||
<div class="property-value row">
|
||||
<el-input-number
|
||||
v-model="form.lineHeight"
|
||||
:min="0.8"
|
||||
:max="3"
|
||||
:step="0.1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="(val) => applyTextStyle({ lineHeight: Number(val) || 1 })"
|
||||
/>
|
||||
<el-select
|
||||
v-model="form.fontWeight"
|
||||
size="small"
|
||||
style="flex: 1;"
|
||||
@change="(val) => applyTextStyle({ fontWeight: val as number | string })"
|
||||
>
|
||||
<el-option label="细(300)" :value="300" />
|
||||
<el-option label="常规(400)" :value="400" />
|
||||
<el-option label="中等(500)" :value="500" />
|
||||
<el-option label="半粗(600)" :value="600" />
|
||||
<el-option label="粗体(700)" :value="700" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shadow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.slider-row {
|
||||
padding-right: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user