feat(canvas): 画布 Memphis 化并补全编辑功能
- 后端新增 POST /admin/api/my-images 上传接口:图片存入用户 owner 目录并生成缩略图,复用 /images 服务与 my-images picker - 本地图片三种入画布路径:上传按钮 / 拖入舞台(落点即拖放 位置) / Ctrl+V 粘贴 - 节点选中后显示悬浮工具条(预览/复制/置顶/删除)与右下角 缩放手柄;图片等比缩放,便签自由缩放;Ctrl+D 复制选中(含 相互连线);双击空白建笔记;按 1 / ⤢ 按钮适应全部内容 - 整套画布 UI 换 Memphis:纸张底+墨点网格、黑边硬影节点、 黄色贴纸便签、橙色连线/选中态、虚线框选、顶栏/生成面板/ 预览灯箱/图片选择器全部换墨边 chunky 样式,随站点明暗主题 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// SaveOwnedImage stores a user-uploaded image under their owner directory so it
|
||||
// behaves like a generated output: it shows up in the 我的图片 picker, can be
|
||||
// dropped onto the infinite canvas, and is served by /images/<owner>/<name>.
|
||||
// format must be a decoder-registered image format ("png", "jpeg", ...).
|
||||
func (s *AdminReadService) SaveOwnedImage(ctx context.Context, owner, format string, data []byte) (string, error) {
|
||||
owner = strings.TrimSpace(owner)
|
||||
if owner == "" || s.store == nil || !s.store.Configured() {
|
||||
return "", errors.New("storage not configured")
|
||||
}
|
||||
ext := strings.ToLower(strings.TrimSpace(format))
|
||||
contentType := ""
|
||||
switch ext {
|
||||
case "jpeg":
|
||||
ext = "jpg"
|
||||
contentType = "image/jpeg"
|
||||
case "png", "gif", "webp":
|
||||
contentType = "image/" + ext
|
||||
default:
|
||||
return "", errors.New("unsupported image format")
|
||||
}
|
||||
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err != nil {
|
||||
return "", errors.New("invalid image data")
|
||||
}
|
||||
key := fmt.Sprintf("%s/up_%d%s.%s", owner, time.Now().Unix(), randomUpper(6), ext)
|
||||
if err := s.store.Put(ctx, key, data, contentType); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Best-effort thumbnail, same convention as generated outputs.
|
||||
if thumb, terr := makeThumbnail(data); terr == nil {
|
||||
_ = s.store.Put(ctx, ThumbKey(key), thumb, "image/jpeg")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
Reference in New Issue
Block a user