合并画布代码
This commit is contained in:
@@ -1,338 +1,350 @@
|
||||
<template>
|
||||
<div class="brush-panel">
|
||||
<div class="brush-panel-content">
|
||||
<!-- 笔刷类型选择 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷类型</span>
|
||||
</div>
|
||||
<div class="brush-type-grid">
|
||||
<div
|
||||
v-for="brush in brushStore.state.availableBrushes"
|
||||
:key="brush.id"
|
||||
@click="setBrushTypeWithCommand(brush.id)"
|
||||
:class="[
|
||||
'brush-type-item',
|
||||
{ active: brushStore.state.type === brush.id },
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="getBrushPreviewStyle(brush)"
|
||||
></div>
|
||||
<span class="brush-name">{{ brush.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 动态渲染笔刷可配置属性 -->
|
||||
<template
|
||||
v-for="(properties, category) in propertiesByCategory"
|
||||
:key="category"
|
||||
>
|
||||
<div class="brush-panel" @click.stop="">
|
||||
<div class="brush-panel-wrapper">
|
||||
<div class="brush-panel-content">
|
||||
<!-- 笔刷类型选择 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>{{ category }}</span>
|
||||
<div class="section-actions" v-if="category === '材质'">
|
||||
<button class="action-btn" @click="showLibrary = !showLibrary">
|
||||
<i class="icon-library">📚</i> 材质库
|
||||
</button>
|
||||
<span>笔刷类型</span>
|
||||
</div>
|
||||
<div class="brush-type-grid">
|
||||
<div
|
||||
v-for="brush in brushStore.state.availableBrushes"
|
||||
:key="brush.id"
|
||||
@click="setBrushTypeWithCommand(brush.id)"
|
||||
:class="[
|
||||
'brush-type-item',
|
||||
{ active: brushStore.state.type === brush.id },
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="getBrushPreviewStyle(brush)"
|
||||
></div>
|
||||
<span class="brush-name">{{ brush.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 针对每个属性,根据其类型渲染合适的控件 -->
|
||||
<div class="property-list">
|
||||
<div
|
||||
v-for="prop in properties"
|
||||
:key="prop.id"
|
||||
class="property-item"
|
||||
>
|
||||
<!-- 滑块控件 -->
|
||||
<template v-if="prop.type === 'slider'">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<span class="slider-value">
|
||||
{{ formatPropertyValue(prop) }}
|
||||
</span>
|
||||
<!-- 动态渲染笔刷可配置属性 -->
|
||||
<template
|
||||
v-for="(properties, category) in propertiesByCategory"
|
||||
:key="category"
|
||||
>
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>{{ category }}</span>
|
||||
<div class="section-actions" v-if="category === '材质'">
|
||||
<button class="action-btn" @click="showLibrary = !showLibrary">
|
||||
<i class="icon-library">📚</i> 材质库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 针对每个属性,根据其类型渲染合适的控件 -->
|
||||
<div class="property-list">
|
||||
<div
|
||||
v-for="prop in properties"
|
||||
:key="prop.id"
|
||||
class="property-item"
|
||||
>
|
||||
<!-- 滑块控件 -->
|
||||
<template v-if="prop.type === 'slider'">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<span class="slider-value">
|
||||
{{ formatPropertyValue(prop) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) =>
|
||||
handlePropertyChange(
|
||||
prop.id,
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="prop.min || 0"
|
||||
:max="prop.max || 100"
|
||||
:step="prop.step || 1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<!-- 预设值按钮,如果定义了预设 -->
|
||||
<div v-if="prop.presets" class="property-presets">
|
||||
<button
|
||||
v-for="preset in prop.presets"
|
||||
:key="preset"
|
||||
@click="handlePropertyChange(prop.id, preset)"
|
||||
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
</template>
|
||||
|
||||
<!-- 颜色选择器 -->
|
||||
<template v-else-if="prop.type === 'color'">
|
||||
<div class="color-property">
|
||||
<div class="color-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: prop.value }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="color-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="color-picker"
|
||||
/>
|
||||
<!-- 如果是主颜色,显示最近使用的颜色 -->
|
||||
<div v-if="prop.id === 'color'" class="recent-colors">
|
||||
<div
|
||||
v-for="(color, index) in brushStore.state
|
||||
.recentColors"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="handlePropertyChange(prop.id, color)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 复选框 -->
|
||||
<template v-else-if="prop.type === 'checkbox'">
|
||||
<div class="checkbox-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.checked)
|
||||
"
|
||||
:id="`toggle-${prop.id}`"
|
||||
/>
|
||||
<label :for="`toggle-${prop.id}`"></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 选择器 -->
|
||||
<template v-else-if="prop.type === 'select'">
|
||||
<div class="select-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<select
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) =>
|
||||
handlePropertyChange(
|
||||
prop.id,
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
:min="prop.min || 0"
|
||||
:max="prop.max || 100"
|
||||
:step="prop.step || 1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<!-- 预设值按钮,如果定义了预设 -->
|
||||
<div v-if="prop.presets" class="property-presets">
|
||||
<button
|
||||
v-for="preset in prop.presets"
|
||||
:key="preset"
|
||||
@click="handlePropertyChange(prop.id, preset)"
|
||||
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
|
||||
class="property-select"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
<option
|
||||
v-for="option in prop.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 颜色选择器 -->
|
||||
<template v-else-if="prop.type === 'color'">
|
||||
<div class="color-property">
|
||||
<div class="color-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<!-- 文件选择器(用于材质) -->
|
||||
<template v-else-if="prop.type === 'file'">
|
||||
<div class="file-property">
|
||||
<div class="file-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: prop.value }"
|
||||
></div>
|
||||
class="file-preview"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
|
||||
<div v-else class="no-file">点击上传材质图片</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button
|
||||
class="select-file-btn"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
上传图片
|
||||
</button>
|
||||
<button
|
||||
class="clear-file-btn"
|
||||
@click="handlePropertyChange(prop.id, '')"
|
||||
v-if="prop.value"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-row">
|
||||
</template>
|
||||
|
||||
<!-- 材质网格选择器 -->
|
||||
<template v-else-if="prop.type === 'texture-grid'">
|
||||
<div class="texture-grid-property">
|
||||
<div class="texture-grid-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="texture-grid">
|
||||
<div
|
||||
v-for="texture in prop.options"
|
||||
:key="texture.value"
|
||||
class="texture-item"
|
||||
:class="{ active: prop.value === texture.value }"
|
||||
@click="handleTextureSelect(texture.value)"
|
||||
>
|
||||
<img
|
||||
:src="texture.preview || texture.value"
|
||||
:alt="texture.label"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-label">{{ texture.label }}</span>
|
||||
</div>
|
||||
<!-- 自定义纹理上传按钮 -->
|
||||
<div
|
||||
class="texture-item upload-item"
|
||||
@click="triggerTextureUpload"
|
||||
>
|
||||
<div class="upload-icon">
|
||||
<span>+</span>
|
||||
</div>
|
||||
<span class="texture-label">上传纹理</span>
|
||||
</div>
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleTextureUpload"
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 其他类型的属性 -->
|
||||
<template v-else>
|
||||
<div class="generic-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<input
|
||||
type="color"
|
||||
type="text"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="color-picker"
|
||||
/>
|
||||
<!-- 如果是主颜色,显示最近使用的颜色 -->
|
||||
<div v-if="prop.id === 'color'" class="recent-colors">
|
||||
<div
|
||||
v-for="(color, index) in brushStore.state.recentColors"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="handlePropertyChange(prop.id, color)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 复选框 -->
|
||||
<template v-else-if="prop.type === 'checkbox'">
|
||||
<div class="checkbox-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.checked)
|
||||
"
|
||||
:id="`toggle-${prop.id}`"
|
||||
/>
|
||||
<label :for="`toggle-${prop.id}`"></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 选择器 -->
|
||||
<template v-else-if="prop.type === 'select'">
|
||||
<div class="select-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<select
|
||||
:value="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="property-select"
|
||||
>
|
||||
<option
|
||||
v-for="option in prop.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 文件选择器(用于材质) -->
|
||||
<template v-else-if="prop.type === 'file'">
|
||||
<div class="file-property">
|
||||
<div class="file-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="file-preview" @click="handleFileSelect(prop.id)">
|
||||
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
|
||||
<div v-else class="no-file">点击上传材质图片</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button
|
||||
class="select-file-btn"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
上传图片
|
||||
</button>
|
||||
<button
|
||||
class="clear-file-btn"
|
||||
@click="handlePropertyChange(prop.id, '')"
|
||||
v-if="prop.value"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 材质网格选择器 -->
|
||||
<template v-else-if="prop.type === 'texture-grid'">
|
||||
<div class="texture-grid-property">
|
||||
<div class="texture-grid-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="texture-grid">
|
||||
<div
|
||||
v-for="texture in prop.options"
|
||||
:key="texture.value"
|
||||
class="texture-item"
|
||||
:class="{ active: prop.value === texture.value }"
|
||||
@click="handleTextureSelect(texture.value)"
|
||||
>
|
||||
<img
|
||||
:src="texture.preview || texture.value"
|
||||
:alt="texture.label"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-label">{{ texture.label }}</span>
|
||||
</div>
|
||||
<!-- 自定义纹理上传按钮 -->
|
||||
<div class="texture-item upload-item" @click="triggerTextureUpload">
|
||||
<div class="upload-icon">
|
||||
<span>+</span>
|
||||
</div>
|
||||
<span class="texture-label">上传纹理</span>
|
||||
</div>
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleTextureUpload"
|
||||
style="display: none;"
|
||||
class="property-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 其他类型的属性 -->
|
||||
<template v-else>
|
||||
<div class="generic-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="property-input"
|
||||
/>
|
||||
<!-- 属性描述提示 -->
|
||||
<div v-if="prop.description" class="property-description">
|
||||
{{ prop.description }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 属性描述提示 -->
|
||||
<div v-if="prop.description" class="property-description">
|
||||
{{ prop.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 材质库弹窗 -->
|
||||
<div
|
||||
v-if="showLibrary"
|
||||
class="texture-library-overlay"
|
||||
@click.self="showLibrary = false"
|
||||
>
|
||||
<div class="texture-library-modal">
|
||||
<div class="modal-header">
|
||||
<h3>材质库</h3>
|
||||
<button class="close-btn" @click="showLibrary = false">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="texture-categories">
|
||||
<button
|
||||
v-for="(category, index) in textureCategories"
|
||||
:key="index"
|
||||
:class="[
|
||||
'category-btn',
|
||||
{ active: selectedCategory === category },
|
||||
]"
|
||||
@click="selectedCategory = category"
|
||||
>
|
||||
{{ category }}
|
||||
<!-- 材质库弹窗 -->
|
||||
<div
|
||||
v-if="showLibrary"
|
||||
class="texture-library-overlay"
|
||||
@click.self="showLibrary = false"
|
||||
>
|
||||
<div class="texture-library-modal">
|
||||
<div class="modal-header">
|
||||
<h3>材质库</h3>
|
||||
<button class="close-btn" @click="showLibrary = false">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="texture-list">
|
||||
<div
|
||||
v-for="(texture, index) in filteredTextures"
|
||||
:key="index"
|
||||
class="library-texture-item"
|
||||
@click="selectLibraryTexture(texture.path)"
|
||||
>
|
||||
<img
|
||||
:src="texture.thumbnail"
|
||||
:alt="texture.name"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-name">{{ texture.name }}</span>
|
||||
<div class="modal-content">
|
||||
<div class="texture-categories">
|
||||
<button
|
||||
v-for="(category, index) in textureCategories"
|
||||
:key="index"
|
||||
:class="[
|
||||
'category-btn',
|
||||
{ active: selectedCategory === category },
|
||||
]"
|
||||
@click="selectedCategory = category"
|
||||
>
|
||||
{{ category }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="texture-list">
|
||||
<div
|
||||
v-for="(texture, index) in filteredTextures"
|
||||
:key="index"
|
||||
class="library-texture-item"
|
||||
@click="selectLibraryTexture(texture.path)"
|
||||
>
|
||||
<img
|
||||
:src="texture.thumbnail"
|
||||
:alt="texture.name"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-name">{{ texture.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="upload-btn"
|
||||
@click="handleFileSelect('texturePath')"
|
||||
>
|
||||
上传新材质
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="upload-btn" @click="handleFileSelect('texturePath')">
|
||||
上传新材质
|
||||
</div>
|
||||
|
||||
<!-- 笔刷预设 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷预设</span>
|
||||
<button
|
||||
class="save-preset-btn"
|
||||
@click="saveCurrentAsPreset"
|
||||
title="保存当前设置为预设"
|
||||
>
|
||||
<i class="save-icon">+</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 笔刷预设 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷预设</span>
|
||||
<button
|
||||
class="save-preset-btn"
|
||||
@click="saveCurrentAsPreset"
|
||||
title="保存当前设置为预设"
|
||||
>
|
||||
<i class="save-icon">+</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="presets-container">
|
||||
<div
|
||||
v-for="(preset, index) in brushStore.state.presets"
|
||||
:key="index"
|
||||
class="preset-item"
|
||||
@click="applyPresetWithCommand(index)"
|
||||
>
|
||||
<div class="presets-container">
|
||||
<div
|
||||
class="preset-color"
|
||||
:style="{
|
||||
backgroundColor: preset.color,
|
||||
width: preset.size + 'px',
|
||||
height: preset.size + 'px',
|
||||
opacity: preset.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="preset-name">{{ preset.name }}</span>
|
||||
v-for="(preset, index) in brushStore.state.presets"
|
||||
:key="index"
|
||||
class="preset-item"
|
||||
@click="applyPresetWithCommand(index)"
|
||||
>
|
||||
<div
|
||||
class="preset-color"
|
||||
:style="{
|
||||
backgroundColor: preset.color,
|
||||
width: preset.size + 'px',
|
||||
height: preset.size + 'px',
|
||||
opacity: preset.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="preset-name">{{ preset.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -448,10 +460,7 @@ function handleTextureSelect(textureId) {
|
||||
|
||||
// 触发纹理上传文件选择
|
||||
function triggerTextureUpload() {
|
||||
const fileInput = textureFileInput.value;
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
handleFileSelect("texturePath");
|
||||
}
|
||||
|
||||
// 处理纹理文件上传
|
||||
@@ -461,15 +470,15 @@ async function handleTextureUpload(event) {
|
||||
|
||||
try {
|
||||
// 验证文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('请选择图片文件');
|
||||
if (!file.type.startsWith("image/")) {
|
||||
alert("请选择图片文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件大小 (限制为 5MB)
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
alert('文件大小不能超过 5MB');
|
||||
alert("文件大小不能超过 5MB");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -478,8 +487,8 @@ async function handleTextureUpload(event) {
|
||||
const brushManager = toolManager?.brushManager;
|
||||
|
||||
if (!texturePresetManager || !brushManager) {
|
||||
console.error('缺少必要的管理器实例');
|
||||
alert('系统错误:无法上传纹理');
|
||||
console.error("缺少必要的管理器实例");
|
||||
alert("系统错误:无法上传纹理");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -493,12 +502,11 @@ async function handleTextureUpload(event) {
|
||||
});
|
||||
|
||||
await commandManager.execute(command);
|
||||
|
||||
|
||||
// 清空文件输入,允许重复上传同一文件
|
||||
event.target.value = '';
|
||||
|
||||
event.target.value = "";
|
||||
} catch (error) {
|
||||
console.error('纹理上传失败:', error);
|
||||
console.error("纹理上传失败:", error);
|
||||
alert(`上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -673,29 +681,44 @@ onMounted(() => {
|
||||
const brushStore = BrushStore;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="less">
|
||||
.brush-panel {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 62px;
|
||||
padding: 0;
|
||||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
max-height: 85vh;
|
||||
width: 30%;
|
||||
/* overflow-y: auto; */
|
||||
min-width: 280px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
position: relative;
|
||||
animation: panelFadeIn 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
|
||||
backdrop-filter: blur(2px); /* 添加模糊效果 */
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||||
z-index: 1000; /* 确保面板在最上层 */
|
||||
|
||||
.brush-panel-wrapper {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
max-height: 85vh; /* 限制最大高度 */
|
||||
/*优化ios上的滚动效果*/
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.brush-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加指向整个面板的倒三角 */
|
||||
.brush-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 20px;
|
||||
top: -9px;
|
||||
right: 58px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
@@ -716,12 +739,6 @@ const brushStore = BrushStore;
|
||||
}
|
||||
}
|
||||
|
||||
.brush-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brush-section {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
@@ -746,9 +763,10 @@ const brushStore = BrushStore;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
|
||||
}
|
||||
|
||||
.section-header::after {
|
||||
/* .section-header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
@@ -757,18 +775,18 @@ const brushStore = BrushStore;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid #999; /* 更柔和的颜色 */
|
||||
border-top: 6px solid #999;
|
||||
transform: translateY(-50%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
} */
|
||||
|
||||
.section-header:hover {
|
||||
/* .section-header:hover {
|
||||
background-color: rgba(248, 249, 250, 1);
|
||||
}
|
||||
|
||||
.section-header:hover::after {
|
||||
border-top-color: #666;
|
||||
}
|
||||
} */
|
||||
|
||||
.section-actions {
|
||||
display: flex;
|
||||
@@ -1114,6 +1132,12 @@ const brushStore = BrushStore;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.property-select {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.property-select:focus {
|
||||
border-color: #4285f4;
|
||||
outline: none;
|
||||
|
||||
Reference in New Issue
Block a user