update
This commit is contained in:
@ -207,9 +207,56 @@
|
||||
if (focusMain) setExpanded(focusMain, true);
|
||||
}
|
||||
|
||||
// ── 编辑页:实时翻译建议 + 季节码预览 ──────────────────────────────
|
||||
// 与后端 pkg/season.Derive 同规则;仅用于只读预览,服务端保存时仍按同规则重算 season_code。
|
||||
function deriveSeasonCode(year, collectionType, season) {
|
||||
if (!year) return '';
|
||||
var y = String(year).slice(-2);
|
||||
var prefix = 'SS';
|
||||
switch (collectionType) {
|
||||
case 'resort': prefix = 'RES'; break;
|
||||
case 'pre_fall': prefix = 'PF'; break;
|
||||
case 'couture': case 'menswear': prefix = 'FW'; break;
|
||||
default:
|
||||
if (season === 'fall' || season === 'winter') prefix = 'FW';
|
||||
else prefix = 'SS';
|
||||
}
|
||||
return prefix + y;
|
||||
}
|
||||
|
||||
function initEditForm() {
|
||||
var en = document.getElementById('rw-title-en') || document.getElementById('snap-title-en');
|
||||
if (!en) return;
|
||||
var cn = document.getElementById('rw-title-cn') || document.getElementById('snap-title-cn');
|
||||
|
||||
// 英文标题失焦且中文为空时,拉建议回填(不覆盖手填)。
|
||||
en.addEventListener('blur', function () {
|
||||
if (cn && cn.value.trim() === '') {
|
||||
fetch('/admin/api/suggest-title-cn?title_en=' + encodeURIComponent(en.value))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) { if (d && d.title_cn) cn.value = d.title_cn; })
|
||||
.catch(function () {});
|
||||
}
|
||||
});
|
||||
|
||||
// 走秀:年份/季节/系列变动即时算季节码(只读预览;服务端保存时仍按同规则重算)。
|
||||
var year = document.getElementById('rw-year');
|
||||
var season = document.getElementById('rw-season');
|
||||
var collection = document.getElementById('rw-collection');
|
||||
var code = document.getElementById('rw-season-code');
|
||||
function refreshCode() {
|
||||
if (!year || !code) return;
|
||||
code.value = deriveSeasonCode(year.value, collection ? collection.value : '', season ? season.value : '');
|
||||
}
|
||||
if (year) year.addEventListener('input', refreshCode);
|
||||
if (season) season.addEventListener('input', refreshCode);
|
||||
if (collection) collection.addEventListener('input', refreshCode);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initZoom();
|
||||
if (document.getElementById('sel-all') || document.getElementById('batchbar')) initArticleList();
|
||||
if (document.getElementById('batch')) initArticleDetail();
|
||||
initEditForm();
|
||||
});
|
||||
})();
|
||||
|
||||
@ -5,14 +5,16 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fashionapi/internal/dto"
|
||||
"fashionapi/internal/middleware"
|
||||
"fashionapi/internal/model"
|
||||
"fashionapi/internal/pkg/hashid"
|
||||
"fashionapi/internal/repository"
|
||||
"fashionapi/internal/pkg/pagination"
|
||||
"fashionapi/internal/pkg/season"
|
||||
"fashionapi/internal/repository"
|
||||
"fashionapi/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -34,11 +36,12 @@ type BackstageHandler struct {
|
||||
review service.ReviewService
|
||||
ingest *service.IngestService
|
||||
adminRepo repository.AdminRepository
|
||||
translate *service.TranslateService // 标题翻译(可为 nil:未启用时跳过记忆写入/建议)
|
||||
}
|
||||
|
||||
// NewBackstageHandler 创建后台 handler。
|
||||
func NewBackstageHandler(auth service.AuthService, brand service.BrandService, article service.ArticleService, street service.StreetSnapService, review service.ReviewService, ingest *service.IngestService, adminRepo repository.AdminRepository) *BackstageHandler {
|
||||
return &BackstageHandler{auth: auth, brand: brand, article: article, street: street, review: review, ingest: ingest, adminRepo: adminRepo}
|
||||
func NewBackstageHandler(auth service.AuthService, brand service.BrandService, article service.ArticleService, street service.StreetSnapService, review service.ReviewService, ingest *service.IngestService, adminRepo repository.AdminRepository, translate *service.TranslateService) *BackstageHandler {
|
||||
return &BackstageHandler{auth: auth, brand: brand, article: article, street: street, review: review, ingest: ingest, adminRepo: adminRepo, translate: translate}
|
||||
}
|
||||
|
||||
// 模板渲染机制见 templates.go:模板已抽成 templates/ 下的独立 .html 文件(go:embed),
|
||||
@ -534,6 +537,23 @@ type ingestJobView struct {
|
||||
|
||||
// IngestJobs 分页列出入库任务,展示状态 / 重试次数 / 来源 / 错误 / 时间。
|
||||
// 支持 ?page(从 1 起)与 ?pageSize(默认 50,上限 200);其余查询串原样忽略。
|
||||
// SuggestTitleCn 后台编辑页实时翻译建议:给定英文标题,返回建议中文标题(JSON)。
|
||||
// 仅对已登录后台暴露;翻译服务未注入时返回空 title_cn(前端据此不回填)。
|
||||
func (h *BackstageHandler) SuggestTitleCn(c *gin.Context) {
|
||||
en := c.Query("title_en")
|
||||
if strings.TrimSpace(en) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing title_en"})
|
||||
return
|
||||
}
|
||||
cn := ""
|
||||
if h.translate != nil {
|
||||
if s, err := h.translate.SuggestTitleCn(c.Request.Context(), en); err == nil {
|
||||
cn = s
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"title_cn": cn})
|
||||
}
|
||||
|
||||
func (h *BackstageHandler) IngestJobs(c *gin.Context) {
|
||||
p := pagination.Parse(c, "page", "pageSize", 1, 50, 200)
|
||||
offset := p.Offset()
|
||||
@ -754,15 +774,24 @@ func (h *BackstageHandler) RunwayUpdate(c *gin.Context) {
|
||||
year = uint16(y)
|
||||
}
|
||||
}
|
||||
seasonVal := c.PostForm("season")
|
||||
collectionType := c.PostForm("collection_type")
|
||||
titleEn := c.PostForm("title_en")
|
||||
titleCn := strings.TrimSpace(c.PostForm("title_cn"))
|
||||
fields := map[string]any{
|
||||
"title_en": c.PostForm("title_en"),
|
||||
"title_cn": c.PostForm("title_cn"),
|
||||
"title_en": titleEn,
|
||||
"title_cn": titleCn,
|
||||
"description_en": c.PostForm("description_en"),
|
||||
"description_cn": c.PostForm("description_cn"),
|
||||
"season": c.PostForm("season"),
|
||||
"collection_type": c.PostForm("collection_type"),
|
||||
"season_code": c.PostForm("season_code"),
|
||||
"year": year,
|
||||
"season": seasonVal,
|
||||
"collection_type": collectionType,
|
||||
// season_code 一律由「年份 + 系列 + 季节」推导,忽略表单原值,杜绝错码(见 pkg/season)。
|
||||
"season_code": season.Derive(year, collectionType, seasonVal),
|
||||
"year": year,
|
||||
}
|
||||
// 人工填写的中文标题写入翻译记忆,供后续入库/编辑优先命中(永久精确);翻译服务未注入时跳过。
|
||||
if titleCn != "" && h.translate != nil {
|
||||
_ = h.translate.Remember(c.Request.Context(), titleEn, titleCn)
|
||||
}
|
||||
if err := h.article.UpdateRunway(c.Request.Context(), numeric, fields); err != nil {
|
||||
renderPage(c, http.StatusInternalServerError, "error", gin.H{"Error": err.Error()})
|
||||
@ -999,12 +1028,18 @@ func (h *BackstageHandler) StreetSnapUpdate(c *gin.Context) {
|
||||
year = uint16(y)
|
||||
}
|
||||
}
|
||||
titleEn := c.PostForm("title")
|
||||
titleCn := strings.TrimSpace(c.PostForm("title_cn"))
|
||||
fields := map[string]any{
|
||||
"title": c.PostForm("title"),
|
||||
"title_cn": c.PostForm("title_cn"),
|
||||
"title": titleEn,
|
||||
"title_cn": titleCn,
|
||||
"city": c.PostForm("city"),
|
||||
"year": year,
|
||||
}
|
||||
// 人工填写的中文标题写入翻译记忆,供后续入库/编辑优先命中(永久精确);翻译服务未注入时跳过。
|
||||
if titleCn != "" && h.translate != nil {
|
||||
_ = h.translate.Remember(c.Request.Context(), titleEn, titleCn)
|
||||
}
|
||||
if err := h.street.UpdateSnap(c.Request.Context(), numeric, fields); err != nil {
|
||||
renderPage(c, http.StatusInternalServerError, "error", gin.H{"Error": err.Error()})
|
||||
return
|
||||
|
||||
@ -7,10 +7,16 @@
|
||||
<p class="muted">品牌 {{.BrandName}}{{if .IsDown}} · <span class="down">已下架</span>{{end}}</p>
|
||||
<p class="muted">原图总大小:{{formatBytes .ImageTotalSize}}</p>
|
||||
<form method="post" action="/admin/runways/{{.UID}}/edit">
|
||||
<div class="row"><div><label>英文标题</label><input name="title_en" value="{{.Runway.TitleEn}}"></div><div><label>中文标题</label><input name="title_cn" value="{{.Runway.TitleCn}}"></div></div>
|
||||
<div class="row"><div><label>英文标题</label><input name="title_en" id="rw-title-en" value="{{.Runway.TitleEn}}"></div><div><label>中文标题</label><input name="title_cn" id="rw-title-cn" value="{{.Runway.TitleCn}}"></div></div>
|
||||
<div class="row"><div><label>英文描述</label><textarea name="description_en">{{.Runway.DescriptionEn}}</textarea></div><div><label>中文描述</label><textarea name="description_cn">{{.Runway.DescriptionCn}}</textarea></div></div>
|
||||
<div class="row"><div><label>年份</label><input name="year" value="{{.Runway.Year}}"></div><div><label>季节</label><input name="season" value="{{.Runway.Season}}"></div></div>
|
||||
<div class="row"><div><label>系列 (collection_type)</label><input name="collection_type" value="{{.Runway.CollectionType}}"></div><div><label>季节码 (season_code)</label><input name="season_code" value="{{.Runway.SeasonCode}}"></div></div>
|
||||
<div class="row"><div><label>年份</label><input name="year" id="rw-year" value="{{.Runway.Year}}"></div><div><label>季节</label><input name="season" id="rw-season" list="season-list" value="{{.Runway.Season}}"></div></div>
|
||||
<div class="row"><div><label>系列 (collection_type)</label><input name="collection_type" id="rw-collection" list="collection-list" value="{{.Runway.CollectionType}}"></div><div><label>季节码(自动)</label><input name="season_code" id="rw-season-code" value="{{.Runway.SeasonCode}}" readonly></div></div>
|
||||
<datalist id="season-list">
|
||||
<option value="Spring"><option value="Summer"><option value="Fall"><option value="Winter"><option value="Resort"><option value="Pre-Spring"><option value="Pre-Fall">
|
||||
</datalist>
|
||||
<datalist id="collection-list">
|
||||
<option value="rtw"><option value="menswear"><option value="couture"><option value="resort"><option value="pre_fall">
|
||||
</datalist>
|
||||
<button type="submit">保存字段</button>
|
||||
</form>
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<p class="muted">{{if .IsDown}}<span class="down">已下架</span>{{end}}</p>
|
||||
<p class="muted">原图总大小:{{formatBytes .ImageTotalSize}}</p>
|
||||
<form method="post" action="/admin/street-snaps/{{.UID}}/edit">
|
||||
<div class="row"><div><label>英文标题</label><input name="title" value="{{.Snap.Title}}"></div><div><label>中文标题</label><input name="title_cn" value="{{.Snap.TitleCn}}"></div></div>
|
||||
<div class="row"><div><label>英文标题</label><input name="title" id="snap-title-en" value="{{.Snap.Title}}"></div><div><label>中文标题</label><input name="title_cn" id="snap-title-cn" value="{{.Snap.TitleCn}}"></div></div>
|
||||
<div class="row"><div><label>年份</label><input name="year" value="{{.Snap.Year}}"></div><div><label>城市</label><input name="city" value="{{.Snap.City}}"></div></div>
|
||||
<button type="submit">保存字段</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user