From 9f8c0f9cb2a186728679651100ebd67413b1433e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 16 Oct 2026 16:02:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(home-banner):=20=E8=BD=AE=E6=92=AD?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20PC/=E7=A7=BB=E5=8A=A8=E4=B8=A4=E7=AB=AF?= =?UTF-8?q?=E5=88=86=E5=9B=BE=E4=B8=8E=E7=83=AD=E5=8C=BA=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=EF=BC=8C=E7=83=AD=E5=8C=BA=E5=9B=BE=E6=82=AC=E6=B5=AE=E6=94=BE?= =?UTF-8?q?=E5=A4=A7=EF=BC=9B=E4=BF=AE=E5=A4=8D=E4=B8=8A=E4=BC=A0=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9=E5=8E=8B=E7=99=BD=E9=80=8F=E6=98=8E=20PNG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/systemconfig/dto.go | 45 +- .../modules/systemconfig/home_banners_test.go | 122 ++++ .../modules/systemconfig/normalize.go | 97 ++- docs/首页轮播图配置说明.md | 115 +++ .../admin/components/BannerActionEditor.vue | 80 ++ .../admin/components/BannerVisualEditor.vue | 689 ++++++++++++++++++ .../admin/components/HomeBannersDialog.vue | 290 ++++---- .../features/listings/api/homeConfig.spec.ts | 144 ++++ .../src/features/listings/api/homeConfig.ts | 213 +++++- .../components/BannerHotspotLayer.vue | 126 ++++ .../listings/components/HomeBanner.vue | 83 ++- .../listings/composables/useBannerAction.ts | 29 + .../src/features/listings/views/HomeView.vue | 12 +- .../listings/views/MobileHomeView.css | 22 +- .../listings/views/MobileHomeView.vue | 63 +- .../shared/utils/imageTransparency.spec.ts | 68 ++ .../src/shared/utils/imageTransparency.ts | 106 +++ frontend/src/shared/utils/imageUpload.spec.ts | 32 + frontend/src/shared/utils/imageUpload.ts | 44 +- .../src/stores/distributionHomeBanner.spec.ts | 38 +- frontend/src/stores/distributionSite.ts | 71 +- 21 files changed, 2217 insertions(+), 272 deletions(-) create mode 100644 backend/internal/modules/systemconfig/home_banners_test.go create mode 100644 docs/首页轮播图配置说明.md create mode 100644 frontend/src/features/admin/components/BannerActionEditor.vue create mode 100644 frontend/src/features/admin/components/BannerVisualEditor.vue create mode 100644 frontend/src/features/listings/api/homeConfig.spec.ts create mode 100644 frontend/src/features/listings/components/BannerHotspotLayer.vue create mode 100644 frontend/src/features/listings/composables/useBannerAction.ts create mode 100644 frontend/src/shared/utils/imageTransparency.spec.ts create mode 100644 frontend/src/shared/utils/imageTransparency.ts create mode 100644 frontend/src/shared/utils/imageUpload.spec.ts diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index 4211b3f..4980fde 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -73,13 +73,48 @@ type AgreementContentDTO struct { Content string `json:"content"` } +// HomeBannerItem 首页轮播项:文案 + PC/移动两端视觉配置(图片、整图点击、热区)。 type HomeBannerItem struct { - Eyebrow string `json:"eyebrow"` - Title string `json:"title"` - Badge string `json:"badge"` - Pill string `json:"pill"` - Tone string `json:"tone"` + Eyebrow string `json:"eyebrow"` + Title string `json:"title"` + Badge string `json:"badge"` + Pill string `json:"pill"` + Tone string `json:"tone"` + Enabled *bool `json:"enabled,omitempty"` + // ImageURL 早期扁平结构的图片字段,仅用于兼容读取:归一化时并入 PC/移动两端。 + ImageURL string `json:"image_url,omitempty"` + PC HomeBannerVisual `json:"pc"` + Mobile HomeBannerVisual `json:"mobile"` +} + +// HomeBannerVisual 单端展示配置:图片 + 整图点击行为 + 热区。 +type HomeBannerVisual struct { + ImageURL string `json:"image_url,omitempty"` + Alt string `json:"alt,omitempty"` + Action HomeBannerAction `json:"action"` + Hotspots []HomeBannerHotspot `json:"hotspots,omitempty"` +} + +// HomeBannerAction 点击行为:none(不跳转)/ route(站内路径)/ link(外部链接)/ preview(查看大图)。 +type HomeBannerAction struct { + Type string `json:"type"` + Target string `json:"target"` + OpenNewTab bool `json:"open_new_tab"` +} + +// HomeBannerHotspot 热区:相对图片的百分比坐标(0-100),与出图分辨率解耦。 +type HomeBannerHotspot struct { + ID string `json:"id"` + Label string `json:"label"` + X float64 `json:"x"` + Y float64 `json:"y"` + Width float64 `json:"width"` + Height float64 `json:"height"` + // ImageURL 热区按钮图(透明 PNG),留空则前台用半透明高亮框提示可点击。 ImageURL string `json:"image_url,omitempty"` + // HoverScale 悬浮放大倍数:1 表示不放大,建议 1.04-1.08。 + HoverScale float64 `json:"hover_scale,omitempty"` + Action HomeBannerAction `json:"action"` } type PublishOptionsDTO struct { diff --git a/backend/internal/modules/systemconfig/home_banners_test.go b/backend/internal/modules/systemconfig/home_banners_test.go new file mode 100644 index 0000000..7b15598 --- /dev/null +++ b/backend/internal/modules/systemconfig/home_banners_test.go @@ -0,0 +1,122 @@ +package systemconfig + +import "testing" + +func TestNormalizeHomeBannersMigratesLegacyImageURL(t *testing.T) { + items := normalizeHomeBanners([]HomeBannerItem{ + {Title: "旧结构", ImageURL: " https://cdn.example/legacy.png "}, + }) + if len(items) != 1 { + t.Fatalf("期望保留 1 条轮播,实际 %d", len(items)) + } + if items[0].PC.ImageURL != "https://cdn.example/legacy.png" { + t.Fatalf("旧图未迁移到 PC 端:%q", items[0].PC.ImageURL) + } + if items[0].Mobile.ImageURL != "https://cdn.example/legacy.png" { + t.Fatalf("旧图未迁移到移动端:%q", items[0].Mobile.ImageURL) + } + if items[0].ImageURL != "" { + t.Fatalf("旧字段应被清空,实际 %q", items[0].ImageURL) + } +} + +func TestNormalizeHomeBannersKeepsPerPlatformVisual(t *testing.T) { + items := normalizeHomeBanners([]HomeBannerItem{{ + Title: "两端分图", + PC: HomeBannerVisual{ImageURL: " https://cdn.example/pc.png "}, + Mobile: HomeBannerVisual{ImageURL: " https://cdn.example/mobile.png "}, + }}) + if items[0].PC.ImageURL != "https://cdn.example/pc.png" { + t.Fatalf("PC 图被改动:%q", items[0].PC.ImageURL) + } + if items[0].Mobile.ImageURL != "https://cdn.example/mobile.png" { + t.Fatalf("移动端图被改动:%q", items[0].Mobile.ImageURL) + } +} + +func TestNormalizeHomeBannersClampsHotspots(t *testing.T) { + items := normalizeHomeBanners([]HomeBannerItem{{ + Title: "热区", + PC: HomeBannerVisual{Hotspots: []HomeBannerHotspot{ + {ID: " h1 ", X: 90, Y: 95, Width: 20, Height: 20, Action: HomeBannerAction{Type: "route", Target: " /listings "}}, + {ID: "zero", Width: 0, Height: 10, Action: HomeBannerAction{Type: "route", Target: "/x"}}, + }}, + }}) + hotspots := items[0].PC.Hotspots + if len(hotspots) != 1 { + t.Fatalf("宽高为 0 的热区应被丢弃,剩余 %d 个", len(hotspots)) + } + if hotspots[0].ID != "h1" || hotspots[0].X != 80 || hotspots[0].Y != 80 { + t.Fatalf("热区未收敛到合法范围:%+v", hotspots[0]) + } + if hotspots[0].Action.Type != "route" || hotspots[0].Action.Target != "/listings" { + t.Fatalf("热区跳转未归一化:%+v", hotspots[0].Action) + } +} + +func TestNormalizeHomeBannersHotspotImageAndHoverScale(t *testing.T) { + items := normalizeHomeBanners([]HomeBannerItem{{ + Title: "热区按钮图", + PC: HomeBannerVisual{Hotspots: []HomeBannerHotspot{ + {ID: "a", Width: 10, Height: 10, ImageURL: " https://cdn.example/btn.png "}, + {ID: "b", Width: 10, Height: 10, HoverScale: 9}, + {ID: "c", Width: 10, Height: 10, HoverScale: 0.5}, + }}, + }}) + hotspots := items[0].PC.Hotspots + if hotspots[0].ImageURL != "https://cdn.example/btn.png" { + t.Fatalf("热区按钮图未清理空白:%q", hotspots[0].ImageURL) + } + if hotspots[0].HoverScale != defaultBannerHotspotHoverScale { + t.Fatalf("未配置悬浮倍数应回落默认值,实际 %v", hotspots[0].HoverScale) + } + if hotspots[1].HoverScale != maxBannerHotspotHoverScale { + t.Fatalf("超上限的悬浮倍数应被收敛,实际 %v", hotspots[1].HoverScale) + } + if hotspots[2].HoverScale != 1 { + t.Fatalf("小于 1 的悬浮倍数应收敛为 1,实际 %v", hotspots[2].HoverScale) + } +} + +func TestNormalizeBannerActionFallsBackToNone(t *testing.T) { + cases := []struct { + name string + input HomeBannerAction + expect HomeBannerAction + }{ + { + name: "缺失目标的站内跳转退化为不跳转", + input: HomeBannerAction{Type: "route", Target: " "}, + expect: HomeBannerAction{Type: "none"}, + }, + { + name: "未知类型退化为不跳转", + input: HomeBannerAction{Type: "unknown", Target: "/x"}, + expect: HomeBannerAction{Type: "none"}, + }, + { + name: "查看大图不保留目标与新窗口", + input: HomeBannerAction{Type: "preview", Target: "/x", OpenNewTab: true}, + expect: HomeBannerAction{Type: "preview"}, + }, + { + name: "外链保留新窗口标记", + input: HomeBannerAction{Type: "link", Target: " https://cdn.example/a ", OpenNewTab: true}, + expect: HomeBannerAction{Type: "link", Target: "https://cdn.example/a", OpenNewTab: true}, + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + if got := normalizeBannerAction(testCase.input); got != testCase.expect { + t.Fatalf("期望 %+v,实际 %+v", testCase.expect, got) + } + }) + } +} + +func TestNormalizeHomeBannersFallsBackToDefaults(t *testing.T) { + items := normalizeHomeBanners([]HomeBannerItem{{Title: " "}}) + if len(items) != len(defaultHomeBanners()) { + t.Fatalf("全部无效时应回落默认轮播,实际 %d 条", len(items)) + } +} diff --git a/backend/internal/modules/systemconfig/normalize.go b/backend/internal/modules/systemconfig/normalize.go index ccb914e..6dd3a3f 100644 --- a/backend/internal/modules/systemconfig/normalize.go +++ b/backend/internal/modules/systemconfig/normalize.go @@ -1,9 +1,14 @@ package systemconfig import ( + "math" "strings" ) +// 热区悬浮放大倍数:与前端 BANNER_HOTSPOT_DEFAULT_HOVER_SCALE 保持一致。 +const defaultBannerHotspotHoverScale = 1.06 +const maxBannerHotspotHoverScale = 1.5 + func normalizeAnnouncements(items []string) []string { normalized := make([]string, 0, len(items)) for _, item := range items { @@ -26,11 +31,15 @@ func normalizeHomeBanners(items []HomeBannerItem) []HomeBannerItem { item.Badge = strings.TrimSpace(item.Badge) item.Pill = strings.TrimSpace(item.Pill) item.Tone = strings.TrimSpace(item.Tone) - item.ImageURL = strings.TrimSpace(item.ImageURL) - if item.Title == "" && item.ImageURL == "" { + // 旧结构只有一张图,两端共用,避免历史配置升级后丢图 + legacyImageURL := strings.TrimSpace(item.ImageURL) + item.ImageURL = "" + item.PC = normalizeBannerVisual(item.PC, legacyImageURL) + item.Mobile = normalizeBannerVisual(item.Mobile, legacyImageURL) + if item.Title == "" && item.PC.ImageURL == "" && item.Mobile.ImageURL == "" { continue } - if item.Tone != "green" && item.Tone != "orange" { + if item.Tone != "green" && item.Tone != "orange" && item.Tone != "gold" { item.Tone = "blue" } normalized = append(normalized, item) @@ -41,6 +50,88 @@ func normalizeHomeBanners(items []HomeBannerItem) []HomeBannerItem { return normalized } +func normalizeBannerVisual(visual HomeBannerVisual, legacyImageURL string) HomeBannerVisual { + visual.ImageURL = strings.TrimSpace(visual.ImageURL) + if visual.ImageURL == "" { + visual.ImageURL = legacyImageURL + } + visual.Alt = strings.TrimSpace(visual.Alt) + visual.Action = normalizeBannerAction(visual.Action) + hotspots := make([]HomeBannerHotspot, 0, len(visual.Hotspots)) + for _, hotspot := range visual.Hotspots { + item, ok := normalizeBannerHotspot(hotspot) + if !ok { + continue + } + hotspots = append(hotspots, item) + } + visual.Hotspots = hotspots + return visual +} + +// normalizeBannerHotspot 收敛热区坐标:宽高为 0 的热区无意义直接丢弃,其余保证不越界。 +func normalizeBannerHotspot(hotspot HomeBannerHotspot) (HomeBannerHotspot, bool) { + width := clampBannerPercent(hotspot.Width, 100) + height := clampBannerPercent(hotspot.Height, 100) + if width <= 0 || height <= 0 { + return HomeBannerHotspot{}, false + } + hotspot.ID = strings.TrimSpace(hotspot.ID) + hotspot.Label = strings.TrimSpace(hotspot.Label) + hotspot.ImageURL = strings.TrimSpace(hotspot.ImageURL) + hotspot.HoverScale = normalizeHotspotHoverScale(hotspot.HoverScale) + hotspot.Width = width + hotspot.Height = height + hotspot.X = clampBannerPercent(hotspot.X, 100-width) + hotspot.Y = clampBannerPercent(hotspot.Y, 100-height) + hotspot.Action = normalizeBannerAction(hotspot.Action) + return hotspot, true +} + +// normalizeHotspotHoverScale 悬浮放大倍数:缺失/非法回落默认值,上限 1.5 防止热区盖住整张图。 +func normalizeHotspotHoverScale(value float64) float64 { + if value <= 0 { + return defaultBannerHotspotHoverScale + } + if value > maxBannerHotspotHoverScale { + value = maxBannerHotspotHoverScale + } else if value < 1 { + value = 1 + } + return math.Round(value*100) / 100 +} + +// normalizeBannerAction 目标缺失的跳转会变成「点了没反应」的死热区,统一退化为 none。 +func normalizeBannerAction(action HomeBannerAction) HomeBannerAction { + action.Type = strings.TrimSpace(action.Type) + action.Target = strings.TrimSpace(action.Target) + switch action.Type { + case "route", "link": + if action.Target == "" { + return HomeBannerAction{Type: "none"} + } + case "preview": + action.Target = "" + action.OpenNewTab = false + case "none": + action.Target = "" + action.OpenNewTab = false + default: + return HomeBannerAction{Type: "none"} + } + return action +} + +func clampBannerPercent(value float64, max float64) float64 { + if value < 0 { + value = 0 + } + if value > max { + value = max + } + return math.Round(value*100) / 100 +} + func normalizeOrderAgreements(agreements *OrderAgreementsDTO) { defaults := DefaultOrderAgreements() agreements.VirtualAssetPurchase.Title = strings.TrimSpace(agreements.VirtualAssetPurchase.Title) diff --git a/docs/首页轮播图配置说明.md b/docs/首页轮播图配置说明.md new file mode 100644 index 0000000..1e8f4df --- /dev/null +++ b/docs/首页轮播图配置说明.md @@ -0,0 +1,115 @@ +# 首页轮播图配置说明 + +> 适用范围:PC 首页 Hero 轮播(`HomeBanner.vue`)、移动端首页 Banner 轮播(`MobileHomeView.vue`)、后台「首页轮播图」弹窗(`HomeBannersDialog.vue`)。 +> 配置存储位置:系统配置表 `mobile.home_banners`(JSON 字符串),前台接口 `GET /api/mobile-home-config`。 + +## 一、出图规范(两端容器严格等比,按规范出图不裁切) + +| 端 | 展示容器比例 | 建议出图尺寸 | 说明 | +| --- | --- | --- | --- | +| PC | 4:1 | **1600×400** | Hero 卡片宽度自适应,高度由 `aspect-ratio: 1600 / 400` 推导 | +| 移动端 | 2:1 | **750×375**(或 720×360) | 只要有一条配图,整条轮播统一按 `aspect-ratio: 2 / 1` 展示 | + +两端使用**不同的图片字段**,必须分别出图: + +- PC 端只用 `pc.image_url`; +- 移动端只用 `mobile.image_url`; +- 若该端未配图,会退回文字卡片(眉标 + 主标题 + 胶囊文案),不会用另一端的图硬撑(避免严重裁切)。 + +历史数据兼容:早期只有扁平 `image_url` 的轮播,读取时会自动并入 `pc.image_url` 与 `mobile.image_url`,不会丢图。 + +## 二、数据结构 + +```json +[ + { + "eyebrow": "三角洲行动账号专区", + "title": "高哈夫币 · 安全交接 · 随租随玩", + "badge": "HOT", + "pill": "新人领券最高减 20 元", + "tone": "blue", + "enabled": true, + "pc": { + "image_url": "https://cdn.example/banner-pc.jpg", + "alt": "首页活动横幅", + "action": { "type": "route", "target": "/listings", "open_new_tab": false }, + "hotspots": [ + { + "id": "hotspot-1", + "label": "租币教程", + "x": 6, + "y": 20, + "width": 20, + "height": 24, + "action": { "type": "route", "target": "/m/add-home-guide", "open_new_tab": false } + } + ] + }, + "mobile": { "image_url": "...", "action": {}, "hotspots": [] } + } +] +``` + +字段说明: + +- `enabled`:`false` 时前台不展示该条轮播(后台仍保留配置)。 +- `pc` / `mobile`:两端独立配置,结构相同(图片、整图点击行为、热区)。 +- `action.type`:`none` 不跳转 / `route` 站内路径 / `link` 外部链接 / `preview` 查看大图。 +- `action.open_new_tab`:仅 `route`、`link` 有意义。 +- `hotspots[].x/y/width/height`:**相对图片的百分比(0-100)**,与出图分辨率解耦,改分辨率不会跑位。 +- `hotspots[].image_url`:热区按钮图(透明 PNG)。配了图就渲染成海报上的「胶囊按钮」,留空则用半透明高亮框提示可点击。 +- `hotspots[].hover_scale`:鼠标悬浮时按钮图的放大倍数,默认 `1.06`,范围 `1-1.5`(`1` = 不放大)。 + +## 三、热区按钮图导出踩坑(重要) + +热区按钮图**必须是带 alpha 透明通道的 PNG**,否则整个矩形会盖住海报。最常见的坑: + +- 把设计工具/Figma/PS 里显示「透明区域」的**灰白棋盘格预览截图或另存**,棋盘格就成了真实像素,前台会照实显示成一块格子; +- 从 AI 出图工具导出时选了不透明背景(或导出成 JPG),透明区域变成白底; +- 直接把整张海报(含按钮)当热区图用,等于又盖了一层。 + +自查方法: + +1. 把 PNG 拖到纯深色背景的窗口里看,如果还能看到灰白格子,就是图里带的; +2. macOS 可用 `sips -g hasAlpha 图片.png` 检查是否真有 alpha 通道(`hasAlpha: yes` 才算透明图)。 + +后台已经加了兜底提示:上传「热区按钮图」时会做一次像素体检,若**没有任何透明像素**会立刻提示(「只有白/浅灰底色」还会明确点出疑似棋盘格);上传海报底图时也会提示疑似棋盘格的情况。提示只是提醒,不会阻止保存。 + +修复办法:回到设计源文件,隐藏背景后重新导出为 PNG(不要截图预览图);若只有一张带底纹的图,可在 PS 里用「色彩范围」选中浅灰网格后删除再导出。 + +> 已修复的历史坑:后台上传前会做一次「压缩优化」,旧实现先给 canvas 铺白底再编码,会把透明区域压成白底方块(透明按钮图上传后变白色矩形)。现在改为:检测到原图带透明像素时**不铺白底**并输出 `image/png`(无损保留 alpha),不透明图仍走 WebP 压缩。代码:`frontend/src/shared/utils/imageUpload.ts`。修复前上传过的图需要重新上传。 + +## 四、点击行为与悬浮反馈 + +1. 命中热区(且热区配置了真实跳转)→ 执行该热区的跳转; +2. 未命中热区 → 执行该端「整图点击」行为(`none` 时不跳转); +3. 完全没有配置跳转(整图 `none` 且无有效热区)→ 保留原有「点击查看大图」能力。 + +`preview` 类型会打开大图预览:PC 用 Element Plus `el-image-viewer`,移动端用内置 lightbox(2:1 框 + `object-fit: contain`,整图完整可见)。 + +悬浮反馈分两种: + +- 热区配了 `image_url`:渲染透明 PNG 按钮图,鼠标悬浮时按 `hover_scale` 轻微放大(`transform: scale()` + `0.2s` 过渡)并加投影,按下时轻微回缩; +- 热区没配图:悬浮显示半透明高亮框,提示「这里可点」。 + +这正是海报型轮播的常见做法:把海报上的按钮单独切透明 PNG 叠在底图上,既能点击跳转,又有「按钮浮起来」的手感。运营操作要点: + +1. 底图(PC 1600×400 / 移动 750×375)里**不要**烘焙按钮,按钮留空; +2. 把按钮单独导出为透明 PNG(尺寸与海报上的位置匹配); +3. 后台在同一端的预览图上拖框对齐,再填该按钮的跳转。 + +## 五、后台怎么配 + +后台 → 系统配置 → `mobile.home_banners` → 「编辑首页轮播图」: + +1. 每条轮播是一张卡片,可启用/停用、上移/下移、删除;卡片内先填文案(眉标/主标题/角标/胶囊/色调),无图时按文字卡片展示。 +2. 切到「PC 端 1600×400(4:1)」或「移动端 750×375(2:1)」页签,上传/填写该端图片,并配置「整图点击」行为。 +3. 在预览图上**单击**新增热区 → **拖动**移动 → 拖右下角小方块缩放;下方可为选中热区设置名称、精确坐标、跳转,以及**热区按钮图**与**悬浮放大倍数**(后台预览图会把按钮图半透明铺在框内,所见即所得)。 +4. 保存时写入 `mobile.home_banners`,后台会顺带把旧的扁平 `image_url` 结构升级为 `pc` / `mobile` 结构。 + +## 六、校验与兜底 + +- 前后端都会归一化配置:坐标收敛到 `0-100` 且不越界,宽高为 0 的热区直接丢弃;`route` / `link` 缺少目标时退化为 `none`(避免「点了没反应」的死热区)。 +- 后端归一化实现:`backend/internal/modules/systemconfig/normalize.go`(`normalizeHomeBanners`)。 +- 前端归一化实现:`frontend/src/features/listings/api/homeConfig.ts`(`normalizeHomeBanner`)。 +- 相关测试:`frontend/src/features/listings/api/homeConfig.spec.ts`、`backend/internal/modules/systemconfig/home_banners_test.go`。 diff --git a/frontend/src/features/admin/components/BannerActionEditor.vue b/frontend/src/features/admin/components/BannerActionEditor.vue new file mode 100644 index 0000000..20c0313 --- /dev/null +++ b/frontend/src/features/admin/components/BannerActionEditor.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/frontend/src/features/admin/components/BannerVisualEditor.vue b/frontend/src/features/admin/components/BannerVisualEditor.vue new file mode 100644 index 0000000..3429645 --- /dev/null +++ b/frontend/src/features/admin/components/BannerVisualEditor.vue @@ -0,0 +1,689 @@ + + + + + diff --git a/frontend/src/features/admin/components/HomeBannersDialog.vue b/frontend/src/features/admin/components/HomeBannersDialog.vue index bf3dfed..c9bdc39 100644 --- a/frontend/src/features/admin/components/HomeBannersDialog.vue +++ b/frontend/src/features/admin/components/HomeBannersDialog.vue @@ -1,16 +1,19 @@ + + + + diff --git a/frontend/src/features/listings/components/HomeBanner.vue b/frontend/src/features/listings/components/HomeBanner.vue index bb9237c..8f22552 100644 --- a/frontend/src/features/listings/components/HomeBanner.vue +++ b/frontend/src/features/listings/components/HomeBanner.vue @@ -1,14 +1,13 @@