|
@@ -2,10 +2,9 @@
|
|
|
<div class="template-design-container">
|
|
<div class="template-design-container">
|
|
|
<!-- 左侧工具栏 -->
|
|
<!-- 左侧工具栏 -->
|
|
|
<div class="toolbar">
|
|
<div class="toolbar">
|
|
|
- <!-- 可滚动区域:标签页 + 属性 -->
|
|
|
|
|
- <div class="toolbar-scroll">
|
|
|
|
|
- <div class="toolbar-tabs">
|
|
|
|
|
- <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
|
|
|
|
|
|
+ <!-- 标签页 + 可滚动内容(保存按钮固定在底部) -->
|
|
|
|
|
+ <div class="toolbar-tabs">
|
|
|
|
|
+ <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
|
|
<el-tab-pane label="模版设计" name="design">
|
|
<el-tab-pane label="模版设计" name="design">
|
|
|
<el-upload
|
|
<el-upload
|
|
|
class="custom-upload"
|
|
class="custom-upload"
|
|
@@ -114,12 +113,36 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div class="property-item">
|
|
<div class="property-item">
|
|
|
<span>字体:</span>
|
|
<span>字体:</span>
|
|
|
- <el-select v-model="selectedLayer.fontFamily" size="small" style="flex: 1;">
|
|
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="selectedLayer.fontFamily"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ style="flex: 1;"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ clearable
|
|
|
|
|
+ placeholder="搜索字体"
|
|
|
|
|
+ >
|
|
|
<el-option label="Arial" value="Arial" />
|
|
<el-option label="Arial" value="Arial" />
|
|
|
|
|
+ <el-option label="Helvetica" value="Helvetica" />
|
|
|
|
|
+ <el-option label="Verdana" value="Verdana" />
|
|
|
|
|
+ <el-option label="Tahoma" value="Tahoma" />
|
|
|
|
|
+ <el-option label="Georgia" value="Georgia" />
|
|
|
|
|
+ <el-option label="Courier New" value="Courier New" />
|
|
|
|
|
+ <el-option label="思源黑体(Source Han Sans SC)" value="Source Han Sans SC" />
|
|
|
|
|
+ <el-option label="思源黑体 CN(Source Han Sans CN)" value="Source Han Sans CN" />
|
|
|
|
|
+ <el-option label="思源宋体(Source Han Serif SC)" value="Source Han Serif SC" />
|
|
|
|
|
+ <el-option label="思源宋体 CN(Source Han Serif CN)" value="Source Han Serif CN" />
|
|
|
|
|
+ <el-option label="Noto Sans CJK SC" value="Noto Sans CJK SC" />
|
|
|
|
|
+ <el-option label="Noto Serif CJK SC" value="Noto Serif CJK SC" />
|
|
|
<el-option label="宋体" value="SimSun" />
|
|
<el-option label="宋体" value="SimSun" />
|
|
|
<el-option label="黑体" value="SimHei" />
|
|
<el-option label="黑体" value="SimHei" />
|
|
|
<el-option label="微软雅黑" value="Microsoft YaHei" />
|
|
<el-option label="微软雅黑" value="Microsoft YaHei" />
|
|
|
|
|
+ <el-option label="微软雅黑 UI" value="Microsoft YaHei UI" />
|
|
|
<el-option label="楷体" value="KaiTi" />
|
|
<el-option label="楷体" value="KaiTi" />
|
|
|
|
|
+ <el-option label="仿宋" value="FangSong" />
|
|
|
|
|
+ <el-option label="华文中宋" value="STZhongsong" />
|
|
|
|
|
+ <el-option label="华文宋体" value="STSong" />
|
|
|
|
|
+ <el-option label="华文黑体" value="STHeiti" />
|
|
|
|
|
+ <el-option label="华文楷体" value="STKaiti" />
|
|
|
<el-option label="Times New Roman" value="Times New Roman" />
|
|
<el-option label="Times New Roman" value="Times New Roman" />
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</div>
|
|
</div>
|
|
@@ -165,46 +188,139 @@
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <!-- 保存模版按钮 -->
|
|
|
|
|
- <el-divider />
|
|
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
- <el-tab-pane label="素材选择" name="material" :label-class="'material-tab'">
|
|
|
|
|
-
|
|
|
|
|
- <div class="materials-list-full">
|
|
|
|
|
- <el-skeleton v-if="materialsLoading" :rows="5" animated />
|
|
|
|
|
- <div v-else-if="materials.length === 0" class="empty-materials">
|
|
|
|
|
- 暂无素材
|
|
|
|
|
|
|
+ <el-tab-pane label="素材库" name="material" :label-class="'material-tab'">
|
|
|
|
|
+ <div class="materials-panel">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="materialSearch"
|
|
|
|
|
+ class="materials-search"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ :prefix-icon="Search"
|
|
|
|
|
+ placeholder="输入相关商品类型/背景素材/元素"
|
|
|
|
|
+ @input="handleMaterialSearchInput"
|
|
|
|
|
+ @clear="handleMaterialSearchInput"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="materials-type-chips">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="materials-chip"
|
|
|
|
|
+ :class="{ active: activeMaterialType === '全部' }"
|
|
|
|
|
+ @click="setActiveMaterialType('全部')"
|
|
|
|
|
+ >
|
|
|
|
|
+ 全部
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="t in materialTypeStats"
|
|
|
|
|
+ :key="t.type"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="materials-chip"
|
|
|
|
|
+ :class="{ active: activeMaterialType === t.type }"
|
|
|
|
|
+ @click="setActiveMaterialType(t.type)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ t.type }}
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
- <div
|
|
|
|
|
- v-else
|
|
|
|
|
- class="material-item-full"
|
|
|
|
|
- v-for="material in materials"
|
|
|
|
|
- :key="material.id"
|
|
|
|
|
- @click="addMaterialToCanvas(material)"
|
|
|
|
|
- >
|
|
|
|
|
- <img :src="material.material_url" :alt="material.id" />
|
|
|
|
|
- <div class="material-overlay">
|
|
|
|
|
- <el-icon><Plus /></el-icon>
|
|
|
|
|
- <span>添加</span>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <el-skeleton v-if="materialsLoading" :rows="5" animated />
|
|
|
|
|
+
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <!-- 搜索时:直接展示结果列表 -->
|
|
|
|
|
+ <div v-if="materialSearch.trim()" class="materials-list-full">
|
|
|
|
|
+ <div v-if="materialsAfterSearch.length === 0" class="empty-materials">
|
|
|
|
|
+ 暂无素材
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-else
|
|
|
|
|
+ class="material-item-full"
|
|
|
|
|
+ v-for="material in materialsAfterSearch"
|
|
|
|
|
+ :key="material.id"
|
|
|
|
|
+ @click="addMaterialToCanvas(material)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img :src="material.material_url" :alt="material.id" />
|
|
|
|
|
+ <div class="material-overlay">
|
|
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
|
|
+ <span>添加</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 分类明细:点击“更多”或顶部标签进入 -->
|
|
|
|
|
+ <div v-else-if="materialViewMode === 'detail'" class="materials-detail">
|
|
|
|
|
+ <div class="materials-detail-header">
|
|
|
|
|
+ <button type="button" class="materials-back" @click="backToMaterialCategory">
|
|
|
|
|
+ <el-icon><ArrowLeft /></el-icon>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <span class="materials-detail-title">{{ activeMaterialType }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="materials-list-full">
|
|
|
|
|
+ <div v-if="detailMaterials.length === 0" class="empty-materials">
|
|
|
|
|
+ 暂无素材
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-else
|
|
|
|
|
+ class="material-item-full"
|
|
|
|
|
+ v-for="material in detailMaterials"
|
|
|
|
|
+ :key="material.id"
|
|
|
|
|
+ @click="addMaterialToCanvas(material)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img :src="material.material_url" :alt="material.id" />
|
|
|
|
|
+ <div class="material-overlay">
|
|
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
|
|
+ <span>添加</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 分类总览:按分类分组展示(每组预览部分 + 更多) -->
|
|
|
|
|
+ <div v-else class="materials-groups">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="g in materialTypeStats"
|
|
|
|
|
+ :key="g.type"
|
|
|
|
|
+ class="materials-group"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="materials-group-header">
|
|
|
|
|
+ <span class="materials-group-title">{{ g.type }}</span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="(materialsByType.get(g.type)?.length || 0) > 0"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="materials-more"
|
|
|
|
|
+ @click="openMaterialTypeDetail(g.type)"
|
|
|
|
|
+ >
|
|
|
|
|
+ 更多 >
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="materials-list-full materials-list-preview">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="material-item-full"
|
|
|
|
|
+ v-for="material in (materialsByType.get(g.type) || []).slice(0, PREVIEW_LIMIT)"
|
|
|
|
|
+ :key="material.id"
|
|
|
|
|
+ @click="addMaterialToCanvas(material)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img :src="material.material_url" :alt="material.id" />
|
|
|
|
|
+
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
</div>
|
|
</div>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
</el-tabs>
|
|
</el-tabs>
|
|
|
- </div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 固定在底部的保存模版按钮 -->
|
|
|
|
|
<el-divider class="save-template-divider" />
|
|
<el-divider class="save-template-divider" />
|
|
|
<el-button
|
|
<el-button
|
|
|
type="primary"
|
|
type="primary"
|
|
|
- :icon="Document"
|
|
|
|
|
class="save-template-btn"
|
|
class="save-template-btn"
|
|
|
@click="saveTemplate"
|
|
@click="saveTemplate"
|
|
|
>
|
|
>
|
|
|
- 保存模版
|
|
|
|
|
|
|
+ 生成模版
|
|
|
</el-button>
|
|
</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -335,7 +451,7 @@
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref, computed, reactive, onMounted, onUnmounted } from 'vue'
|
|
import { ref, computed, reactive, onMounted, onUnmounted } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
-import { Pointer, Rank, ArrowUp, ArrowDown, Delete, View, Hide, Lock, Unlock, Plus, Document, Picture, Refresh, Upload } from '@element-plus/icons-vue'
|
|
|
|
|
|
|
+import { Pointer, Rank, ArrowUp, ArrowDown, Delete, View, Hide, Lock, Unlock, Plus, Document, Picture, Refresh, Upload, Search, ArrowLeft } from '@element-plus/icons-vue'
|
|
|
import { Material_List } from '@/api/mes/job'
|
|
import { Material_List } from '@/api/mes/job'
|
|
|
|
|
|
|
|
const canvasRef = ref(null)
|
|
const canvasRef = ref(null)
|
|
@@ -442,7 +558,7 @@ const handleUpload = (options) => {
|
|
|
|
|
|
|
|
layers.value.push(newLayer)
|
|
layers.value.push(newLayer)
|
|
|
selectedLayerId.value = newLayer.id
|
|
selectedLayerId.value = newLayer.id
|
|
|
- ElMessage.success('素材上传成功!')
|
|
|
|
|
|
|
+ // ElMessage.success('素材上传成功!')
|
|
|
}
|
|
}
|
|
|
img.src = e.target.result
|
|
img.src = e.target.result
|
|
|
}
|
|
}
|
|
@@ -480,7 +596,7 @@ const addTextLayer = () => {
|
|
|
layers.value.push(newLayer)
|
|
layers.value.push(newLayer)
|
|
|
selectedLayerId.value = newLayer.id
|
|
selectedLayerId.value = newLayer.id
|
|
|
textLayerCount.value++
|
|
textLayerCount.value++
|
|
|
- ElMessage.success('文字图层已添加')
|
|
|
|
|
|
|
+ // ElMessage.success('文字图层已添加')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const textLayerCount = ref(0)
|
|
const textLayerCount = ref(0)
|
|
@@ -488,6 +604,54 @@ const textLayerCount = ref(0)
|
|
|
// 素材库状态
|
|
// 素材库状态
|
|
|
const materials = ref([])
|
|
const materials = ref([])
|
|
|
const materialsLoading = ref(false)
|
|
const materialsLoading = ref(false)
|
|
|
|
|
+const materialSearch = ref('')
|
|
|
|
|
+const activeMaterialType = ref('全部') // 顶部标签选中的分类("全部" / 某个type)
|
|
|
|
|
+const materialViewMode = ref('category') // category: 按分类展示;detail: 单分类明细
|
|
|
|
|
+
|
|
|
|
|
+const PREVIEW_LIMIT = 4
|
|
|
|
|
+
|
|
|
|
|
+const materialsAfterSearch = computed(() => {
|
|
|
|
|
+ // 接口已支持 search,但仍做前端兜底过滤(避免接口不返回预期)
|
|
|
|
|
+ const kw = (materialSearch.value || '').trim().toLowerCase()
|
|
|
|
|
+ const list = materials.value || []
|
|
|
|
|
+ if (!kw) return list
|
|
|
|
|
+ return list.filter(m => {
|
|
|
|
|
+ const hay = `${m?.type || ''} ${m?.material_url || ''} ${m?.sys_id || ''}`.toLowerCase()
|
|
|
|
|
+ return hay.includes(kw)
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 红框:按 type 汇总 count,并按数量从大到小排序
|
|
|
|
|
+const materialTypeStats = computed(() => {
|
|
|
|
|
+ const map = new Map()
|
|
|
|
|
+ for (const m of materialsAfterSearch.value) {
|
|
|
|
|
+ const t = m?.type || '未分类'
|
|
|
|
|
+ const c = Number(m?.count ?? 1) || 1
|
|
|
|
|
+ map.set(t, (map.get(t) || 0) + c)
|
|
|
|
|
+ }
|
|
|
|
|
+ return Array.from(map.entries())
|
|
|
|
|
+ .map(([type, totalCount]) => ({ type, totalCount }))
|
|
|
|
|
+ .sort((a, b) => b.totalCount - a.totalCount)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const materialsByType = computed(() => {
|
|
|
|
|
+ const map = new Map()
|
|
|
|
|
+ for (const m of materialsAfterSearch.value) {
|
|
|
|
|
+ const t = m?.type || '未分类'
|
|
|
|
|
+ if (!map.has(t)) map.set(t, [])
|
|
|
|
|
+ map.get(t).push(m)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 每个分类内部也按 count 降序,保证热门在前
|
|
|
|
|
+ for (const [t, list] of map.entries()) {
|
|
|
|
|
+ list.sort((a, b) => (Number(b?.count ?? 1) || 1) - (Number(a?.count ?? 1) || 1))
|
|
|
|
|
+ }
|
|
|
|
|
+ return map
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const detailMaterials = computed(() => {
|
|
|
|
|
+ if (activeMaterialType.value === '全部') return materialsAfterSearch.value
|
|
|
|
|
+ return (materialsByType.value.get(activeMaterialType.value) || [])
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
// 获取素材库数据
|
|
// 获取素材库数据
|
|
|
const fetchMaterials = async () => {
|
|
const fetchMaterials = async () => {
|
|
@@ -495,7 +659,9 @@ const fetchMaterials = async () => {
|
|
|
try {
|
|
try {
|
|
|
materialsLoading.value = true
|
|
materialsLoading.value = true
|
|
|
console.log('开始获取素材库数据')
|
|
console.log('开始获取素材库数据')
|
|
|
- const response = await Material_List()
|
|
|
|
|
|
|
+ const response = await Material_List({
|
|
|
|
|
+ search: (materialSearch.value || '').trim() || undefined
|
|
|
|
|
+ })
|
|
|
console.log('素材库数据:', response)
|
|
console.log('素材库数据:', response)
|
|
|
if (response.code === 0) {
|
|
if (response.code === 0) {
|
|
|
materials.value = response.data
|
|
materials.value = response.data
|
|
@@ -513,6 +679,29 @@ const fetchMaterials = async () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+let materialSearchTimer = null
|
|
|
|
|
+const handleMaterialSearchInput = () => {
|
|
|
|
|
+ if (materialSearchTimer) clearTimeout(materialSearchTimer)
|
|
|
|
|
+ materialSearchTimer = setTimeout(() => {
|
|
|
|
|
+ fetchMaterials()
|
|
|
|
|
+ }, 250)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const setActiveMaterialType = (t) => {
|
|
|
|
|
+ activeMaterialType.value = t
|
|
|
|
|
+ materialViewMode.value = t === '全部' ? 'category' : 'detail'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const openMaterialTypeDetail = (t) => {
|
|
|
|
|
+ activeMaterialType.value = t
|
|
|
|
|
+ materialViewMode.value = 'detail'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const backToMaterialCategory = () => {
|
|
|
|
|
+ activeMaterialType.value = '全部'
|
|
|
|
|
+ materialViewMode.value = 'category'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 标签页点击事件
|
|
// 标签页点击事件
|
|
|
const handleTabClick = (tab) => {
|
|
const handleTabClick = (tab) => {
|
|
|
console.log('Tab clicked:', tab.props.name)
|
|
console.log('Tab clicked:', tab.props.name)
|
|
@@ -553,7 +742,7 @@ const addMaterialToCanvas = (material) => {
|
|
|
|
|
|
|
|
layers.value.push(newLayer)
|
|
layers.value.push(newLayer)
|
|
|
selectedLayerId.value = newLayer.id
|
|
selectedLayerId.value = newLayer.id
|
|
|
- ElMessage.success('素材已添加到画布!')
|
|
|
|
|
|
|
+ // ElMessage.success('素材已添加到画布!')
|
|
|
}
|
|
}
|
|
|
img.src = material.material_url
|
|
img.src = material.material_url
|
|
|
}
|
|
}
|
|
@@ -642,8 +831,9 @@ const getLayerStyle = (layer) => {
|
|
|
return {
|
|
return {
|
|
|
left: layer.x + 'px',
|
|
left: layer.x + 'px',
|
|
|
top: layer.y + 'px',
|
|
top: layer.y + 'px',
|
|
|
- width: layer.type === 'text' ? 'auto' : layer.width + 'px',
|
|
|
|
|
- height: layer.type === 'text' ? 'auto' : layer.height + 'px',
|
|
|
|
|
|
|
+ // 文字和图片图层都使用显式宽高,方便通过属性和拖拽进行拉伸
|
|
|
|
|
+ width: layer.width + 'px',
|
|
|
|
|
+ height: layer.height + 'px',
|
|
|
transform: `rotate(${layer.rotation}deg)`,
|
|
transform: `rotate(${layer.rotation}deg)`,
|
|
|
opacity: layer.opacity / 100
|
|
opacity: layer.opacity / 100
|
|
|
}
|
|
}
|
|
@@ -663,7 +853,7 @@ const getTextStyle = (layer) => {
|
|
|
lineHeight: layer.lineHeight,
|
|
lineHeight: layer.lineHeight,
|
|
|
letterSpacing: layer.letterSpacing + 'px',
|
|
letterSpacing: layer.letterSpacing + 'px',
|
|
|
padding: '4px 8px',
|
|
padding: '4px 8px',
|
|
|
- whiteSpace: 'nowrap',
|
|
|
|
|
|
|
+ whiteSpace: 'pre-wrap',
|
|
|
userSelect: 'none'
|
|
userSelect: 'none'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -674,10 +864,10 @@ const editingTextLayer = ref(null)
|
|
|
const handleLayerDblClick = (e, layer) => {
|
|
const handleLayerDblClick = (e, layer) => {
|
|
|
if (layer.type === 'text' && !layer.locked) {
|
|
if (layer.type === 'text' && !layer.locked) {
|
|
|
editingTextLayer.value = layer
|
|
editingTextLayer.value = layer
|
|
|
- // 创建输入框进行编辑
|
|
|
|
|
- const input = document.createElement('input')
|
|
|
|
|
- input.value = layer.text
|
|
|
|
|
- input.style.cssText = `
|
|
|
|
|
|
|
+ // 创建多行输入框进行编辑
|
|
|
|
|
+ const textarea = document.createElement('textarea')
|
|
|
|
|
+ textarea.value = layer.text
|
|
|
|
|
+ textarea.style.cssText = `
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
left: ${layer.x}px;
|
|
left: ${layer.x}px;
|
|
|
top: ${layer.y}px;
|
|
top: ${layer.y}px;
|
|
@@ -689,27 +879,25 @@ const handleLayerDblClick = (e, layer) => {
|
|
|
background: white;
|
|
background: white;
|
|
|
border: 2px solid #409eff;
|
|
border: 2px solid #409eff;
|
|
|
padding: 4px 8px;
|
|
padding: 4px 8px;
|
|
|
|
|
+ min-width: 100px;
|
|
|
|
|
+ min-height: 30px;
|
|
|
|
|
+ resize: none;
|
|
|
z-index: 1000;
|
|
z-index: 1000;
|
|
|
outline: none;
|
|
outline: none;
|
|
|
`
|
|
`
|
|
|
|
|
|
|
|
const canvas = canvasRef.value
|
|
const canvas = canvasRef.value
|
|
|
- canvas.appendChild(input)
|
|
|
|
|
- input.focus()
|
|
|
|
|
- input.select()
|
|
|
|
|
|
|
+ canvas.appendChild(textarea)
|
|
|
|
|
+ textarea.focus()
|
|
|
|
|
+ textarea.select()
|
|
|
|
|
|
|
|
const saveEdit = () => {
|
|
const saveEdit = () => {
|
|
|
- layer.text = input.value || '双击编辑文字'
|
|
|
|
|
- canvas.removeChild(input)
|
|
|
|
|
|
|
+ layer.text = textarea.value || '双击编辑文字'
|
|
|
|
|
+ canvas.removeChild(textarea)
|
|
|
editingTextLayer.value = null
|
|
editingTextLayer.value = null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- input.addEventListener('blur', saveEdit)
|
|
|
|
|
- input.addEventListener('keydown', (e) => {
|
|
|
|
|
- if (e.key === 'Enter') {
|
|
|
|
|
- saveEdit()
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ textarea.addEventListener('blur', saveEdit)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -873,7 +1061,7 @@ const deleteLayer = () => {
|
|
|
if (index !== -1) {
|
|
if (index !== -1) {
|
|
|
layers.value.splice(index, 1)
|
|
layers.value.splice(index, 1)
|
|
|
selectedLayerId.value = layers.value.length > 0 ? layers.value[0].id : null
|
|
selectedLayerId.value = layers.value.length > 0 ? layers.value[0].id : null
|
|
|
- ElMessage.success('图层已删除')
|
|
|
|
|
|
|
+ // ElMessage.success('图层已删除')
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1063,14 +1251,27 @@ const handleCanvasWheel = (e) => {
|
|
|
.toolbar-tabs {
|
|
.toolbar-tabs {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-tabs :deep(.el-tabs) {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.toolbar-tabs :deep(.el-tabs__header) {
|
|
.toolbar-tabs :deep(.el-tabs__header) {
|
|
|
|
|
+ order: 0;
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
margin-bottom: 8px;
|
|
margin-bottom: 8px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ z-index: 2;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.toolbar-tabs :deep(.el-tabs__content) {
|
|
.toolbar-tabs :deep(.el-tabs__content) {
|
|
|
|
|
+ order: 1;
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
min-height: 0;
|
|
min-height: 0;
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
@@ -1078,9 +1279,7 @@ const handleCanvasWheel = (e) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.toolbar-tabs :deep(.el-tab-pane) {
|
|
.toolbar-tabs :deep(.el-tab-pane) {
|
|
|
- height: 100%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
|
|
+ min-height: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.toolbar-tabs :deep(.material-tab) {
|
|
.toolbar-tabs :deep(.material-tab) {
|
|
@@ -1089,6 +1288,107 @@ const handleCanvasWheel = (e) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 完整素材库样式 */
|
|
/* 完整素材库样式 */
|
|
|
|
|
+.materials-panel {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-search :deep(.el-input__wrapper) {
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-type-chips {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-chip {
|
|
|
|
|
+ padding: 4px 10px;
|
|
|
|
|
+ border-radius: 14px;
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ transition: all 0.15s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-chip:hover {
|
|
|
|
|
+ background: #eef2f7;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-chip.active {
|
|
|
|
|
+ background: #ecf5ff;
|
|
|
|
|
+ border-color: #b3d8ff;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-groups {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-group-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 2px 2px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-group-title {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-more {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-more:hover {
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-detail-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ padding: 2px 2px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-back {
|
|
|
|
|
+ width: 26px;
|
|
|
|
|
+ height: 26px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-back:hover {
|
|
|
|
|
+ background: #eef2f7;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.materials-detail-title {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.materials-library-full {
|
|
.materials-library-full {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -1124,6 +1424,15 @@ const handleCanvasWheel = (e) => {
|
|
|
min-height: 0;
|
|
min-height: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.materials-list-preview {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ padding: 4px;
|
|
|
|
|
+ background-color: #f9f9f9;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.material-item-full {
|
|
.material-item-full {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
aspect-ratio: 1;
|
|
aspect-ratio: 1;
|