Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front

This commit is contained in:
2026-03-25 17:28:33 +08:00
10 changed files with 445 additions and 49 deletions

View File

@@ -79,3 +79,20 @@ body,
--el-color-primary-light-9: #fff2ec;
--el-color-primary-dark-2: #cc6241;
}
.el-select,
.el-popper {
--el-color-primary: #6c6c6c;
/* 主灰色 */
--el-color-primary-light-3: #8a8a8a;
/* 较浅的灰色混合20%白) */
--el-color-primary-light-5: #a8a8a8;
/* 更浅的灰色混合33%白) */
--el-color-primary-light-7: #c6c6c6;
/* 浅灰色混合47%白) */
--el-color-primary-light-8: #d4d4d4;
/* 很浅的灰色混合53%白) */
--el-color-primary-light-9: #e3e3e3;
/* 极浅的灰色混合60%白) */
--el-color-primary-dark-2: #565656;
/* 深灰色加深20% */
}

View File

@@ -95,4 +95,13 @@ body,
--el-color-primary-light-8: #ffe8df; // 很浅的橙红混合53%白)
--el-color-primary-light-9: #fff2ec; // 极浅的橙红混合60%白)
--el-color-primary-dark-2: #cc6241; // 深橙红加深20%
}
}
.el-select, .el-popper{
--el-color-primary: #6c6c6c; /* 主灰色 */
--el-color-primary-light-3: #8a8a8a; /* 较浅的灰色混合20%白) */
--el-color-primary-light-5: #a8a8a8; /* 更浅的灰色混合33%白) */
--el-color-primary-light-7: #c6c6c6; /* 浅灰色混合47%白) */
--el-color-primary-light-8: #d4d4d4; /* 很浅的灰色混合53%白) */
--el-color-primary-light-9: #e3e3e3; /* 极浅的灰色混合60%白) */
--el-color-primary-dark-2: #565656; /* 深灰色加深20% */
}

View File

@@ -0,0 +1,9 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="8.0041" height="8.0041" fill="url(#pattern0_2958_107309)"/>
<defs>
<pattern id="pattern0_2958_107309" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_2958_107309" transform="scale(0.0078125)"/>
</pattern>
<image id="image0_2958_107309" width="128" height="128" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAAN2AAADdgF91YLMAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAThQTFRF////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAR/EwCQAAAGd0Uk5TAAECBAUGBwoLDA8QEhUXGRocIiUnLC03PEBCRkdJS0xOVFZXWV1hYmdobnF1enyAgYKDhoeLjY+QkpSVmp6go6anr7S1uLq7wcTGx8jKy8zN0NPV19zg5+rr7vDx8vP09vf6+/z9/seyqosAAAGbSURBVHja7dhVUwNBEATgJri7uwR3lwDBggZ3OZz5//8Aqd17DpLqKqr7KbszV9+83GYTQFEURVH+eRpiiZkqHh+Zs488dtD8RfvKTSnXN+sh+zZG9q2T7J/lcv2gluzXyZcvX758+fLly5cvX758+fLly5cvX758+fLl/y/fJsm+PdZwfbPTPK5vtkT2zaJk34Iqih/Ewo9HOZz3fz1cLHDOn+KLcFnP8IHGN7/epvjAVLjTRPGRfeC3khQfqLz3m20UH5gIX8UMio+iW7/fTvGBEV84iVB8FFz7UhfFB4Z8bYPjI//KFZ8KKT4wmNav5RTun0XPrr7K8YFN1/CQx/HRl7ajIMX7f8mr64lzfGDLNd3lcHz0+7YWjo9yfzEZ5fjAoWtcIfnwzYckPzwMA5KPVt9cxvFR8bfX8+///xB5cO29HB84cv3TJB9x98Da3/q2PZti9t0Dl7M/z0x/9ecAMePlZRzoNGqi2OcOkMQ9d4AA59wBjjHAHWAYWGb6iUwgq3l+Z+9H2d37XXbj3Wn9ha0oiqIoqeUdwmrKD1GdTTcAAAAASUVORK5CYII="/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -13,6 +13,8 @@
<div class="label">Rotation</div>
<div class="value">
<depth-input
icon="dc-angle"
size="8"
v-model="angle"
type="number"
@input="inputFillTransform"

View File

@@ -10,7 +10,7 @@
<div class="content" v-if="isShow" v-show="show">
<!-- <basic-info :object="activeObject" /> -->
<fill-repeat :object="activeObject" v-if="isRepeat" />
<!-- <shape-setting :object="activeObject" v-if="isShape && !isRepeat" /> -->
<shape-setting :object="activeObject" v-if="isShape && !isRepeat" />
</div>
</div>
</template>
@@ -27,10 +27,10 @@
const layers = computed(() => layerManager.layers.value)
const activeObject = ref(null)
// const shapes = ['rect', 'line', 'path', 'triangle', 'polygon', 'ellipse']
// const isShape = computed(() => shapes.includes(activeObject.value?.type))
const shapes = ['rect', 'line', 'path', 'triangle', 'polygon', 'ellipse']
const isShape = computed(() => shapes.includes(activeObject.value?.type))
const isRepeat = computed(() => activeObject.value?.fill?.repeat === 'repeat')
const isShow = computed(() => isRepeat.value)
const isShow = computed(() => isRepeat.value || isShape.value)
const updateActiveObject = () => {
const layer = layerManager.getActiveLayer()
@@ -104,6 +104,10 @@
width: 100%;
height: auto;
padding: 0 1.4rem;
margin-bottom: 1.6rem;
&:last-child {
margin-bottom: 0;
}
}
}
&.v > div {

View File

@@ -1,21 +1,121 @@
<template>
<div class="shape-setting v">
<!-- <div>
<div class="label">填充颜色</div>
<div class="value">
<el-color-picker
v-model="data.fill"
show-alpha
:predefine="['transparent', '#000', '#f00', '#0f0', '#00f']"
@change="onChange"
/>
<div class="shape-setting h">
<div>
<div class="title">Position</div>
<div class="content">
<div>
<depth-input
before="X"
type="number"
v-model="data.left"
@input="onInpot"
@change="onChange"
/>
</div>
<div>
<depth-input
before="Y"
type="number"
v-model="data.top"
@input="onInpot"
@change="onChange"
/>
</div>
</div>
</div> -->
<div class="content">
<div>
<depth-input
before="W"
type="number"
v-model="data.width"
@input="onInpot"
@change="onChange"
/>
</div>
<div>
<depth-input
before="H"
type="number"
v-model="data.height"
@input="onInpot"
@change="onChange"
/>
</div>
</div>
</div>
<div>
<div class="title">Scale</div>
<div class="content">
<div>
<depth-input
type="number"
before="X"
after="%"
v-model="data.scaleX"
@input="onInpot"
@change="onChange"
/>
</div>
<div>
<depth-input
type="number"
before="Y"
after="%"
v-model="data.scaleY"
@input="onInpot"
@change="onChange"
/>
</div>
</div>
</div>
<div>
<div class="title">Appearance</div>
<div class="content">
<div>
<span class="label">Opacity</span>
<depth-input
type="number"
after="%"
min="0"
max="100"
step="1"
v-model="data.opacity"
@input="onInpot"
@change="onChange"
/>
</div>
<div v-if="object.type === 'rect'">
<span class="label">Corner Radius</span>
<depth-input
type="number"
v-model="data.radius"
min="0"
step="1"
@input="onInpot"
@change="onChange"
/>
</div>
<div v-else></div>
</div>
<div class="content">
<div>
<span class="label">Color</span>
<depth-input
type="color"
v-model="data.fill"
after="%"
@input="onInpot"
@change="onChange"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, inject, computed, nextTick, onBeforeUnmount, reactive } from 'vue'
import { ref, inject, computed, nextTick, onBeforeUnmount, reactive, watch } from 'vue'
import { ElColorPicker } from 'element-plus'
import { getTransformScaleAngle } from '../../manager/ObjectManager'
import DepthOffsetTool from '../tools/depth-offset-tool.vue'
@@ -32,22 +132,50 @@
const id = computed(() => props.object.info.id)
const data = reactive({
fill: '',
stroke: '',
strokeWidth: 0
top: 0,
left: 0,
width: 0,
height: 0,
scaleX: 1,
scaleY: 1,
opacity: 100,
radius: 0,
fill: ''
// stroke: '',
// strokeWidth: 0
})
const updateData = async () => {
await nextTick()
data.top = Math.round(props.object.top)
data.left = Math.round(props.object.left)
data.width = Math.round(props.object.width)
data.height = Math.round(props.object.height)
data.scaleX = Math.round(props.object.scaleX * 100)
data.scaleY = Math.round(props.object.scaleY * 100)
data.opacity = Math.round(props.object.opacity * 100)
data.radius = Math.round(props.object.rx)
data.fill = props.object.fill
data.stroke = props.object.stroke
data.strokeWidth = props.object.strokeWidth
// data.stroke = props.object.stroke
// data.strokeWidth = props.object.strokeWidth
}
updateData()
watch(() => props.object, updateData)
const onInpot = () => setPriority(false)
const onChange = () => setPriority(true)
const setPriority = (isRecord: boolean) => {
const options = { ...data }
const options = {
...data,
opacity: data.opacity / 100,
scaleX: data.scaleX / 100,
scaleY: data.scaleY / 100
}
if (props.object.type === 'rect') {
options['rx'] = data.radius
options['ry'] = data.radius
}
delete options.radius
objectManager.updateProperty(id.value, options, isRecord)
}
@@ -61,8 +189,30 @@
<style lang="less" scoped>
.shape-setting {
--details-item-margin-bottom: 1rem;
--details-item-margin-bottom: 1.6rem;
> div {
> .content {
display: flex;
align-items: center;
justify-content: center;
> div {
flex: 1;
margin-right: 3rem;
display: flex;
flex-direction: column;
--depth-input-height: 2.4rem;
--depth-input-bg-color: rgba(249, 249, 250, 1);
--depth-input-border-color: rgba(230, 230, 231, 1);
--depth-input-decorate-color: rgba(69, 71, 84, 0.1);
&:last-child {
margin-right: 0;
}
> .label {
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
}
}
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="depth-input">
<div class="depth-input" :class="{ color: isColor }">
<span class="decorate"></span>
<span v-show="icon" class="icon">
<svg-icon :name="icon" :size="iconSize" size-unit="px" />
@@ -13,12 +13,34 @@
@copy.stop
@keydown.stop
/>
<input
v-if="isColor"
readonly
type="text"
:value="colorObj.color"
@copy.stop
@keydown.stop
/>
<template v-if="isColor">
<span class="decorate marginl"></span>
<input
class="alpha"
type="number"
:value="Math.round(colorObj.alpha * 100)"
min="0"
max="100"
@input="(e) => onInput(e, 'alpha')"
@change="(e) => onChange(e, 'alpha')"
@copy.stop
@keydown.stop
/>
</template>
<span v-show="after" class="after">{{ after }}</span>
</div>
</template>
<script setup lang="ts">
import { ref, useAttrs, watch } from 'vue'
import { ref, useAttrs, watch, computed } from 'vue'
const props = defineProps({
modelValue: { type: [String, Number] },
icon: { default: '', type: String },
@@ -28,17 +50,169 @@
})
const attrs = useAttrs()
const emit = defineEmits(['update:modelValue', 'input', 'change'])
const onInput = (e) => {
var value = e.target.value
if (attrs.type === 'number') value = Number(value)
emit('update:modelValue', value)
emit('input', value)
const isColor = computed(() => attrs.type === 'color')
const colorObj = computed(() => {
if (isColor.value && props.modelValue) {
let color = parseColor(props.modelValue)
color.color = `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`
return color
}
return { color: '#fff', alpha: 1 }
})
const inputtime = ref(null)
const onInput = (e, type?: string) => {
clearTimeout(inputtime.value)
inputtime.value = setTimeout(() => {
const value = handleInputChange(e, type)
emit('update:modelValue', value)
emit('input', value)
}, 10)
}
const onChange = (e) => {
const changetime = ref(null)
const onChange = (e, type?: string) => {
clearTimeout(changetime.value)
changetime.value = setTimeout(() => {
const value = handleInputChange(e, type)
emit('update:modelValue', value)
emit('change', value)
}, 50)
}
// 处理输入事件
const handleInputChange = (e, type?: string) => {
var value = e.target.value
if (attrs.type === 'number') value = Number(value)
emit('update:modelValue', value)
emit('change', value)
if (isColor.value) {
const rgba = { ...colorObj.value }
if (type === 'alpha') {
let a = value / 100
value = `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${a})`
} else {
let { r, g, b } = parseColor(value)
value = `rgba(${r}, ${g}, ${b}, ${rgba.a})`
}
}
return value
}
function parseColor(colorString) {
// 处理 rgba/rgb
const rgbaMatch = colorString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i)
if (rgbaMatch) {
return {
type: 'rgb',
r: parseInt(rgbaMatch[1]),
g: parseInt(rgbaMatch[2]),
b: parseInt(rgbaMatch[3]),
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1,
color: `#${toHex(rgbaMatch[1])}${toHex(rgbaMatch[2])}${toHex(rgbaMatch[3])}`,
alpha: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
}
}
// 处理十六进制
const hexMatch = colorString.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)
if (hexMatch) {
let hex = hexMatch[1]
if (hex.length === 3) {
hex = hex
.split('')
.map((c) => c + c)
.join('')
}
return {
type: 'hex',
r: parseInt(hex.substring(0, 2), 16),
g: parseInt(hex.substring(2, 4), 16),
b: parseInt(hex.substring(4, 6), 16),
a: 1,
color: `#${hex}`,
alpha: 1
}
}
// 处理 hsl/hsla
const hslMatch = colorString.match(/hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%(?:,\s*([\d.]+))?\)/i)
if (hslMatch) {
const h = parseInt(hslMatch[1])
const s = parseInt(hslMatch[2])
const l = parseInt(hslMatch[3])
const a = hslMatch[4] ? parseFloat(hslMatch[4]) : 1
const rgb = hslToRgb(h, s, l)
return {
type: 'hsl',
h,
s,
l,
r: rgb.r,
g: rgb.g,
b: rgb.b,
a: a,
color: `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`,
alpha: a
}
}
// 处理颜色名称
const namedColors = {
red: '#ff0000',
green: '#00ff00',
blue: '#0000ff',
black: '#000000',
white: '#ffffff',
yellow: '#ffff00',
cyan: '#00ffff',
magenta: '#ff00ff',
transparent: 'rgba(0,0,0,0)'
}
if (namedColors[colorString.toLowerCase()]) {
return parseColor(namedColors[colorString.toLowerCase()])
}
return parseColor('#fff')
}
// 将 0-255 的数字转换为十六进制
function toHex(num) {
// 确保数字在 0-255 范围内
num = Math.min(255, Math.max(0, num))
// 转换为十六进制,并确保两位数
return num.toString(16).padStart(2, '0').toUpperCase()
}
// HSL 转 RGB
function hslToRgb(h, s, l) {
h = h / 360
s = s / 100
l = l / 100
let r, g, b
if (s === 0) {
r = g = b = l
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1
if (t > 1) t -= 1
if (t < 1 / 6) return p + (q - p) * 6 * t
if (t < 1 / 2) return q
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
return p
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
r = hue2rgb(p, q, h + 1 / 3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1 / 3)
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
}
}
</script>
<style scoped lang="less">
@@ -46,37 +220,46 @@
display: flex;
align-items: center;
width: 100%;
border: 1px solid rgba(230, 230, 231, 1);
border-radius: 1.7px;
height: 17px;
border: 1px solid var(--depth-input-border-color, rgba(230, 230, 231, 1));
border-radius: 2px;
height: var(--depth-input-height, 20px);
background-color: var(--depth-input-bg-color, #fff);
padding: 0 4px 0 2px;
&.color {
--depth-input-decorate-margin-right: 10px;
--depth-input-input-margin-right: 10px;
--depth-input-input-font-align: left;
--depth-input-after-color: rgba(181, 181, 181, 1);
}
> .decorate {
width: 2px;
background-color: rgba(230, 230, 231, 1);
border-radius: 3px;
height: 85%;
margin-right: 4px;
background-color: var(--depth-input-decorate-color, rgba(230, 230, 231, 1));
border-radius: 2px;
height: 75%;
margin-right: var(--depth-input-decorate-margin-right, 4px);
}
> .iconfont {
font-size: 12px;
color: #000;
margin-right: 2px;
margin-right: 4px;
}
> .before {
font-size: 12px;
color: #000;
margin-right: 2px;
margin-right: 4px;
}
> .after {
font-size: 12px;
color: #000;
margin-left: 1px;
color: var(--depth-input-after-color, #000);
margin-left: 2px;
}
> input {
font-size: 12px;
width: 0;
height: 100%;
flex: 1;
text-align: right;
margin-right: var(--depth-input-input-margin-right, 0);
text-align: var(--depth-input-input-font-align, right);
outline: none;
border: none;
background-color: transparent;
@@ -87,6 +270,28 @@
-webkit-appearance: none;
margin: 0;
}
&.alpha {
flex: 0.4;
text-align: right;
margin-right: 0;
}
&[type='color'] {
flex: 0.5;
border-radius: 2px;
height: 70%;
border: 1px solid rgb(42, 42, 42);
display: block;
&::-webkit-color-swatch-wrapper {
padding: 0;
}
&::-webkit-color-swatch {
border: none;
border-radius: 0;
width: 100%;
height: 100%;
}
}
}
}
</style>

View File

@@ -44,7 +44,7 @@
const tools = ref([
{ name: TOOLS.SELECT, icon: 'c-select', iconSize: 16, disabled: ref(false) },
{ name: TOOLS.MOVE, icon: 'c-move', iconSize: 18, disabled: ref(false) },
{ name: TOOLS.TEXT, icon: 'c-text', iconSize: 18, disabled: ref(false) },
{ name: TOOLS.TEXT, icon: 'c-text', iconSize: 22, disabled: ref(false) },
{ type: 'line' },
{
name: TOOLS.UNDO,

View File

@@ -12,7 +12,7 @@
@paste.prevent
@keydown.stop
></div>
<div class="tools" @mousedown="onMouseDown">
<div class="tools" @mousedown="onMouseDown" v-if="active">
<myTextTools :textStyle="data.textStyle"></myTextTools>
</div>
<span class="delete" @mousedown.stop @click="emit('delete-node')" v-show="active">

View File

@@ -121,7 +121,7 @@
}
.fontFamily{
width: 103px;
--el-text-color-regular: #000
}
.color{
width: 55px;