Ver Fonte

fix(photography): 优化LOGO上传与缓存逻辑

- 更新LOGO加载错误提示文案,明确文件不存在或加载失败原因
- 调整LOGO预览区域UI结构,新增重新上传和删除操作按钮
- 修改LOGO缓存键名,避免缓存冲突问题
- 优化加载缓存时的默认值处理,确保路径为空时正确显示
- 简化保存LOGO到缓存的逻辑,移除冗余条件判断
- 修复删除LOGO功能,清空路径并更新缓存状态
- 增加LOGO加载错误处理判断,防止重复设置错误状态
- 样式调整:为LOGO上传底部操作区添加布局与间距样式
panqiuyao há 1 semana atrás
pai
commit
10fdadbf32

+ 33 - 0
frontend/src/views/components/PictureEditor/mixin/layer/index.js

@@ -19,6 +19,7 @@ export default  {
       layersSort:0,  //用于设定用户层级
       layerId:0,         //唯一标识
       islayerSelect:false,  //设置为true的时候,右侧点击图册的时候,也切换selected
+      openLayerMenuId: null,
 
       //修改数据
       editUrl:null,
@@ -160,6 +161,9 @@ export default  {
         case 'bringToFront':
           sort = this.layersSort+10
           break;
+        case 'sendToBack':
+          sort = item.sort-1000
+          break;
         case 'up':
           sort = item.sort+11
           break;
@@ -173,6 +177,35 @@ export default  {
       })
       this.getLayers()
     },
+    // 显隐切换
+    toggleVisibility(item){
+      if(!item) return
+      item.set({ visible: !item.visible })
+      this.fcanvas.requestRenderAll()
+      this.getLayers(false)
+    },
+    toggleLayerMenu(item){
+      if(!item) return
+      this.openLayerMenuId = this.openLayerMenuId === item.id ? null : item.id
+    },
+    closeLayerMenu(){
+      this.openLayerMenuId = null
+    },
+    // 重命名图层
+    renameLayer(item, name){
+      if(!item) return
+      item.set({ name: name || '' })
+      this.getLayers(false)
+    },
+    // 图层显示名称
+    layerDisplayName(item){
+      if(!item) return ''
+      if(item.name) return item.name
+      if(item.type === 'image') return '图片'
+      if(item.type === 'textbox') return '文字'
+      if(item.type === 'group') return '组合'
+      return '图层'
+    },
     async getImageUrl(item){
 
       if(!(item._element?.currentSrc ||  item.cover)) return ;

+ 156 - 68
frontend/src/views/components/PictureEditor/mixin/layer/index.scss

@@ -1,83 +1,171 @@
-.picture-editor-wrap_layer {
-  position: absolute;
-  top: 0px;
-  left: 100% ;
-  margin-left: 10px;
-  bottom: 0px;
-  z-index: 10;
-  width: 120px;
-  background: #535353;
-  padding: 10px;
-  padding-right: 0px;
-  padding-top: 0px;
-  border-radius: 5px;
-  overflow: auto;
-  &~.picture-editor-wrap_edit {
-    right: -440px;
-  }
-  &:before {
-    content: "";
-    width: 100%;
-    height: 10px;
-    display: block;
-    background: #535353;
-    position:sticky;
-    top:0;
-    z-index: 10;
+.picture-editor-wrap_layer.layer-panel {
+  position: fixed;
+  top: 85px;
+  left: 0;
+  width: 210px;
+  height: 260px;
+  background: #ffffff;
+  color: #222;
+  padding: 8px 10px;
+  box-sizing: border-box;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  z-index: 12;
 
+  .layer-panel__header {
+    display: flex;
+    align-items: baseline;
+    justify-content: space-between;
+    padding: 4px 2px 8px;
+    border-bottom: 1px solid #f0f0f0;
+  }
+  .layer-panel__title {
+    font-size: 13px;
+    font-weight: 600;
+  }
+  .layer-panel__sub {
+    font-size: 11px;
+    color: #999;
   }
 
+  .layer-panel__list {
+    flex: 1;
+    overflow: auto;
+    padding: 6px 0;
+  }
 
-  .item {
-    width: 102px;
-    height: 102px;
-    margin-bottom: 10px;
-    border: 1px solid #ddd;
-
-    &.active {
-      border: 1px solid $primary2;
-      box-shadow: 0 0 5px $primary2 ;
+  .layer-item {
+    display: flex;
+    align-items: center;
+    padding: 6px;
+    border-radius: 4px;
+    background: #fff;
+    border: 1px solid transparent;
+    margin-bottom: 6px;
+    cursor: pointer;
+    transition: all .15s ease;
+  }
+  .layer-item:hover {
+    border-color: #dcdcdc;
+    background: #fafafa;
+  }
+  .layer-item.active {
+    border-color: $primary1;
+    box-shadow: 0 0 0 1px rgba($primary1,0.35);
+    background: #f5fbff;
+  }
 
-    }
+  .layer-item__preview {
+    width: 44px;
+    height: 44px;
+    border-radius: 4px;
+    background: #f5f5f5;
+    border: 1px solid #eee;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    overflow: hidden;
+    margin-right: 8px;
+  }
+  .layer-item__preview img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+  }
 
-    &.save-image {
-      border: 1px #535353 dashed;
-      font-size: 14px;
-      background: url('~@/assets/images/bg-image.png') #535353;
-      background-size: 12px 12px;
-      color: #EEEEEE;
-      cursor: pointer;
-      margin-top: 0;
-      position: sticky;
-      top:10px;
+  .layer-item__meta {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+  }
 
-      z-index: 10;
-      .el-icon-plus {
-        color: #eee;
-      }
+  .layer-item__name {
+    width: 100%;
+    background: #fafafa;
+    border: 1px solid #e2e2e2;
+    border-radius: 4px;
+    color: #333;
+    padding: 5px 6px;
+    outline: none;
+    font-size: 12px;
+  }
+  .layer-item__name:focus {
+    border-color: $primary1;
+    box-shadow: 0 0 0 1px rgba($primary1,0.25);
+  }
 
-      &:hover {
-        border: 1px #6f6f6f dashed;
-      }
+  .layer-item__ops {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    position: relative;
+  }
 
+  .op-text {
+    font-size: 12px;
+    color: #666;
+    cursor: pointer;
+  }
+  .op-text:hover {
+    color: $primary1;
+  }
 
-      &.disabled {
-        opacity: .4;
-        cursor: not-allowed;
-        border: 1px #535353 dashed;
-        box-shadow: none;
-      }
-    }
+  .op-btn.more {
+    min-width: 32px;
+    height: 24px;
+    padding: 0 6px;
+    border-radius: 4px;
+    border: 1px solid #e2e2e2;
+    background: #fff;
+    color: #444;
+    font-size: 14px;
+    line-height: 22px;
+    cursor: pointer;
+  }
+  .op-btn.more:hover {
+    border-color: $primary1;
+    color: $primary1;
+  }
 
-    img {
-      object-fit: contain;
-      width: 100px;
-      height: 100px;
-    }
+  .layer-item__menu {
+    position: absolute;
+    top: 26px;
+    right: 0;
+    min-width: 120px;
+    background: #fff;
+    border: 1px solid #e2e2e2;
+    border-radius: 4px;
+    box-shadow: 0 6px 16px rgba(0,0,0,0.12);
+    padding: 4px 0;
+    z-index: 20;
+  }
+  .menu-item {
+    padding: 6px 12px;
+    font-size: 12px;
+    color: #444;
+    cursor: pointer;
+    white-space: nowrap;
+  }
+  .menu-item:hover {
+    background: #f5f5f5;
+    color: $primary1;
+  }
+  .menu-item.danger {
+    color: #e24b4b;
+  }
+  .menu-item.danger:hover {
+    background: #fff3f3;
+    color: #e24b4b;
   }
 
-  .matting {
-  //  position: absolute;
- //   visibility: hidden;
+  .layer-panel__empty {
+    flex: 1;
+    color: #888;
+    font-size: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
   }
 }

+ 1 - 1
frontend/src/views/components/marketingEdit/index.vue

@@ -559,7 +559,7 @@ export default {
 
   .add-action-wrap {
     position: fixed;
-    top:80px;
+    top:350px;
     overflow: auto;
     left: 0px;
     z-index: 10;

+ 4 - 0
frontend/src/views/components/marketingEdit/tpl/index.js

@@ -2,6 +2,7 @@ import  viewTpl from  './view'
 import  colorTpl from  '@/views/components/PictureEditor/mixin//color/tpl'
 import  editTpl from  './edit'
 import add from "./add";
+import  layerTpl from  './layer'
 import header from "./header";
 
 
@@ -18,6 +19,9 @@ export  default function tpl(){
           </template>
           <template v-else>
           
+                  <!--图层-->
+                  ${layerTpl()}
+                  <!--图层-->
                   <!--新增-->
                   ${add()}
                   <!--新增-->

+ 52 - 0
frontend/src/views/components/marketingEdit/tpl/layer.js

@@ -0,0 +1,52 @@
+export default function() {
+  return `
+    <div class="picture-editor-wrap_layer layer-panel">
+      <div class="layer-panel__header">
+        <div class="layer-panel__title">图层</div>
+        <div class="layer-panel__sub">选中 / 预览 / 排序 / 删除</div>
+      </div>
+      <template v-if="layers.length > 0">
+        <div class="layer-panel__list">
+          <div
+            class="layer-item"
+            v-for="(item,index) in layers"
+            :key="item.id"
+            :id="item.id"
+            :class="{active:selectedIds.includes(item.id)}"
+            @click.stop="select(item)"
+          >
+            <div class="layer-item__preview">
+              <img :src="getLayerCover(item)" alt="" />
+            </div>
+            <div class="layer-item__meta">
+              <input
+                class="layer-item__name"
+                :value="layerDisplayName(item)"
+                @click.stop
+                @change="renameLayer(item, $event.target.value)"
+              />
+              <div class="layer-item__ops">
+                <span class="op-text" @click.stop="toggleVisibility(item)">{{ item.visible === false ? '显示' : '隐藏' }}</span>
+                <button class="op-btn more" title="更多" @click.stop="toggleLayerMenu(item)">···</button>
+                <div
+                  v-if="openLayerMenuId === item.id"
+                  class="layer-item__menu"
+                >
+                  <div class="menu-item" @click.stop="move(item,'up'); closeLayerMenu()">上移一层</div>
+                  <div class="menu-item" @click.stop="move(item,'down'); closeLayerMenu()">下移一层</div>
+                  <div class="menu-item" @click.stop="move(item,'bringToFront'); closeLayerMenu()">置顶</div>
+                  <div class="menu-item" @click.stop="move(item,'sendToBack'); closeLayerMenu()">置底</div>
+                  <div class="menu-item danger" @click.stop="delLayers(item); closeLayerMenu()">删除</div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </template>
+      <template v-else>
+        <div class="layer-panel__empty">暂无图层</div>
+      </template>
+    </div>
+  `
+}
+

+ 2 - 2
public/dist/index.html

@@ -5,8 +5,8 @@
     <link rel="icon" type="image/svg+xml" href="./vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>智惠映AI自动拍照机</title>
-    <script type="module" crossorigin src="./assets/index-CfhKUcBR.js"></script>
-    <link rel="stylesheet" crossorigin href="./assets/index-CO20n6gp.css">
+    <script type="module" crossorigin src="./assets/index-B_6t4weG.js"></script>
+    <link rel="stylesheet" crossorigin href="./assets/index-rncNxO19.css">
   </head>
   <body>
     <div id="app"></div>