From 68cdaf38fa09f34096913977a0ad330ccd4da5c8 Mon Sep 17 00:00:00 2001
From: lzq <357099073@qq.com>
Date: Thu, 22 Oct 2026 17:55:50 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=97=A5=E6=8A=A5=E7=9B=B8?=
=?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/components.d.ts | 96 ++
backend/public/auth-callback.html | 33 +-
backend/public/loading.gif | Bin 0 -> 272136 bytes
.../oa/dailyreport/components/ReportView.vue | 77 +-
.../src/views/apps/oa/dailyreport/index.vue | 5 +-
backend/src/views/auth/appLogin.vue | 85 +-
backend/src/views/auth/callback.vue | 59 +-
backend/src/views/auth/mpLogin.vue | 85 +-
backend/src/views/dashboard/index.vue | 1411 +++++++++--------
go/controllers/app_erp.go | 2 +-
go/controllers/auth/login.go | 17 +
go/controllers/backend_erp_report.go | 2 +-
go/controllers/backend_reimburse.go | 15 +-
go/controllers/erp_report_core.go | 186 ++-
go/controllers/platform_website_setting.go | 108 ++
go/models/platform_site_setting.go | 6 +
go/routers/platform/platform.go | 4 +
go/services/software_upgrade_url.go | 43 +-
go/views/auth/login.tpl | 629 +++++---
platform/src/api/sitesettings.js | 24 +
.../components/loginBrandSettings.vue | 470 ++++++
.../views/system/platformsettings/index.vue | 6 +
uniapp/pages/oa/dailyreport/index.vue | 6 +-
23 files changed, 2313 insertions(+), 1056 deletions(-)
create mode 100644 backend/public/loading.gif
create mode 100644 platform/src/views/system/platformsettings/components/loginBrandSettings.vue
diff --git a/backend/components.d.ts b/backend/components.d.ts
index e69de29..9091e5c 100644
--- a/backend/components.d.ts
+++ b/backend/components.d.ts
@@ -0,0 +1,96 @@
+/* eslint-disable */
+// @ts-nocheck
+// biome-ignore lint: disable
+// oxlint-disable
+// ------
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+ export interface GlobalComponents {
+ CommonAside: typeof import('./src/components/CommonAside.vue')['default']
+ CommonHeader: typeof import('./src/components/CommonHeader.vue')['default']
+ ElAlert: typeof import('element-plus/es')['ElAlert']
+ ElAside: typeof import('element-plus/es')['ElAside']
+ ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
+ ElAvatar: typeof import('element-plus/es')['ElAvatar']
+ ElBacktop: typeof import('element-plus/es')['ElBacktop']
+ ElBadge: typeof import('element-plus/es')['ElBadge']
+ ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
+ ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
+ ElButton: typeof import('element-plus/es')['ElButton']
+ ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
+ ElCard: typeof import('element-plus/es')['ElCard']
+ ElCascader: typeof import('element-plus/es')['ElCascader']
+ ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+ ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
+ ElCol: typeof import('element-plus/es')['ElCol']
+ ElCollapse: typeof import('element-plus/es')['ElCollapse']
+ ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
+ ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
+ ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
+ ElContainer: typeof import('element-plus/es')['ElContainer']
+ ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+ ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+ ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
+ ElDialog: typeof import('element-plus/es')['ElDialog']
+ ElDivider: typeof import('element-plus/es')['ElDivider']
+ ElDrawer: typeof import('element-plus/es')['ElDrawer']
+ ElDropdown: typeof import('element-plus/es')['ElDropdown']
+ ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+ ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
+ ElEmpty: typeof import('element-plus/es')['ElEmpty']
+ ElForm: typeof import('element-plus/es')['ElForm']
+ ElFormItem: typeof import('element-plus/es')['ElFormItem']
+ ElHeader: typeof import('element-plus/es')['ElHeader']
+ ElIcon: typeof import('element-plus/es')['ElIcon']
+ ElImage: typeof import('element-plus/es')['ElImage']
+ ElInput: typeof import('element-plus/es')['ElInput']
+ ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+ ElLink: typeof import('element-plus/es')['ElLink']
+ ElMain: typeof import('element-plus/es')['ElMain']
+ ElMenu: typeof import('element-plus/es')['ElMenu']
+ ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+ ElOption: typeof import('element-plus/es')['ElOption']
+ ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
+ ElPagination: typeof import('element-plus/es')['ElPagination']
+ ElPopover: typeof import('element-plus/es')['ElPopover']
+ ElProgress: typeof import('element-plus/es')['ElProgress']
+ ElRadio: typeof import('element-plus/es')['ElRadio']
+ ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
+ ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+ ElResult: typeof import('element-plus/es')['ElResult']
+ ElRow: typeof import('element-plus/es')['ElRow']
+ ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+ ElSelect: typeof import('element-plus/es')['ElSelect']
+ ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
+ ElSlider: typeof import('element-plus/es')['ElSlider']
+ ElStep: typeof import('element-plus/es')['ElStep']
+ ElSteps: typeof import('element-plus/es')['ElSteps']
+ ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+ ElSwitch: typeof import('element-plus/es')['ElSwitch']
+ ElTable: typeof import('element-plus/es')['ElTable']
+ ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+ ElTabPane: typeof import('element-plus/es')['ElTabPane']
+ ElTabs: typeof import('element-plus/es')['ElTabs']
+ ElTag: typeof import('element-plus/es')['ElTag']
+ ElTimeline: typeof import('element-plus/es')['ElTimeline']
+ ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
+ ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
+ ElTooltip: typeof import('element-plus/es')['ElTooltip']
+ ElTree: typeof import('element-plus/es')['ElTree']
+ ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
+ ElUpload: typeof import('element-plus/es')['ElUpload']
+ FlowCanvas: typeof import('./src/components/approval/FlowCanvas.vue')['default']
+ MessageDetailDialog: typeof import('./src/components/MessageDetailDialog.vue')['default']
+ RouterLink: typeof import('vue-router')['RouterLink']
+ RouterView: typeof import('vue-router')['RouterView']
+ TenantSwitcher: typeof import('./src/components/TenantSwitcher.vue')['default']
+ }
+ export interface GlobalDirectives {
+ vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+ }
+}
diff --git a/backend/public/auth-callback.html b/backend/public/auth-callback.html
index ed2e046..8031617 100644
--- a/backend/public/auth-callback.html
+++ b/backend/public/auth-callback.html
@@ -3,22 +3,41 @@
+
正在完成登录…
+
- 正在完成登录,请稍候…
+
+
+

+
登录中,请稍后…
+
diff --git a/go/controllers/app_erp.go b/go/controllers/app_erp.go
index 981abf9..2518973 100644
--- a/go/controllers/app_erp.go
+++ b/go/controllers/app_erp.go
@@ -144,7 +144,7 @@ func (c *AppErpController) Dashboard() {
if c.erpDenied(err) {
return
}
- c.erpOk(erpDashboardCore(c.actor(cl)))
+ c.erpOk(erpDashboardCore(c.actor(cl), c.erpQuery("period")))
}
// Meta GET /api/erp/meta 单据类型、状态、收付款方式等下拉字典
diff --git a/go/controllers/auth/login.go b/go/controllers/auth/login.go
index 0633897..a33bec2 100644
--- a/go/controllers/auth/login.go
+++ b/go/controllers/auth/login.go
@@ -72,6 +72,23 @@ func (c *AuthLoginController) LoginPage() {
// 自助账户流程提示:registered=注册成功 / registered_warn=注册成功但加入企业失败 / reset=密码已重置
c.Data["Notice"] = c.GetString("notice")
c.Data["WarnMsg"] = c.GetString("warn")
+
+ // 登录页品牌:platform「平台设置 → 登录页设置」里的配置,未配置时用默认文案。
+ // 读取失败不阻断登录页,GetPlatformSiteTagMap 会返回空值。
+ brand := models.GetPlatformSiteTagMap()
+ loginTitle := strings.TrimSpace(brand["login_title"])
+ if loginTitle == "" {
+ loginTitle = "一次登录,全平台通行"
+ }
+ loginSubtitle := strings.TrimSpace(brand["login_subtitle"])
+ if loginSubtitle == "" {
+ loginSubtitle = "基于 OIDC / OAuth 2.0 的统一身份底座,为所有接入应用提供安全、一致的登录体验。"
+ }
+ c.Data["LoginTitle"] = loginTitle
+ c.Data["LoginSubtitle"] = loginSubtitle
+ c.Data["LoginLogo"] = strings.TrimSpace(brand["login_logo"])
+ c.Data["LoginBg"] = strings.TrimSpace(brand["login_bg"])
+
c.TplName = "auth/login.tpl"
}
diff --git a/go/controllers/backend_erp_report.go b/go/controllers/backend_erp_report.go
index 52a6ce5..dbf3bbb 100644
--- a/go/controllers/backend_erp_report.go
+++ b/go/controllers/backend_erp_report.go
@@ -250,7 +250,7 @@ func (c *BackendErpReportController) Dashboard() {
if c.denied(err) {
return
}
- c.ok(erpDashboardCore(erpNewActor(cl)))
+ c.ok(erpDashboardCore(erpNewActor(cl), c.GetString("period")))
}
// ---- 汇总报表共用:聚合实现见 erp_report_core.go ----
diff --git a/go/controllers/backend_reimburse.go b/go/controllers/backend_reimburse.go
index 1d0f134..57e2376 100644
--- a/go/controllers/backend_reimburse.go
+++ b/go/controllers/backend_reimburse.go
@@ -187,6 +187,19 @@ func remapReceiptAmounts(oldIDs []uint64, oldAmounts []string, newIDs []uint64)
return joinReceiptAmounts(parts)
}
+// normalizeReceiptURLs 规范化逗号分隔的票据地址(历史七牛 HTTP 域名改写为存储配置入口),
+// 保证 HTTPS 页面不会因 Mixed Content 拦截发票预览。
+func normalizeReceiptURLs(scheme, host, raw string) string {
+ if strings.TrimSpace(raw) == "" {
+ return ""
+ }
+ parts := strings.Split(raw, ",")
+ for i, part := range parts {
+ parts[i] = services.ResolvePublicURL(scheme, host, strings.TrimSpace(part))
+ }
+ return strings.Join(parts, ",")
+}
+
func resolveReceiptFiles(scheme, host string, item models.BackendReimbursementItem) []map[string]interface{} {
ids := parseReceiptFileIDs(item.ReceiptFileIDs)
if len(ids) == 0 {
@@ -282,7 +295,7 @@ func applyReceiptToItem(item *models.BackendReimbursementItem, payload reimburse
func reimbursementItemDTO(item models.BackendReimbursementItem, scheme, host string) map[string]interface{} {
receiptFiles := resolveReceiptFiles(scheme, host, item)
- receiptURLs := strings.TrimSpace(item.ReceiptURLs)
+ receiptURLs := normalizeReceiptURLs(scheme, host, item.ReceiptURLs)
receiptFlag := item.ReceiptFlag
if len(receiptFiles) > 0 {
receiptFlag = 1
diff --git a/go/controllers/erp_report_core.go b/go/controllers/erp_report_core.go
index 94382b4..33a09a7 100644
--- a/go/controllers/erp_report_core.go
+++ b/go/controllers/erp_report_core.go
@@ -68,33 +68,84 @@ func erpDocAmount(tid string, docTypes []string, from, to time.Time) (float64, i
return round2f(sum), int64(len(docs))
}
-// erpDashboardTrend 近 6 个月采购/销售趋势
+// erpDashboardTrend 看板趋势点。Month 保留旧字段(兼容旧页面),Label 为图表 x 轴文案
type erpDashboardTrend struct {
Month string `json:"month"`
+ Label string `json:"label"`
Buy float64 `json:"buy"`
Sale float64 `json:"sale"`
}
-// erpDashboardCore 经营看板:本月购销/毛利、今日销售、库存价值、往来余额、待办、趋势、预警
-func erpDashboardCore(a erpActor) map[string]interface{} {
- tid := a.Tid
- monthFrom, monthTo := erpMonthRange(0)
- // 不能用 time.Now().Truncate(24*time.Hour):它按「自零时起算的绝对时长」取整,
- // 在东八区得到的是 UTC 零点(本地 08:00),会把当天 00:00–08:00 的单据漏掉。
- now := time.Now()
- todayFrom := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
+// erpDashboardPeriod 解析看板统计周期:week|month|quarter|year,缺省或非法值回落 month
+func erpDashboardPeriod(raw string) string {
+ switch p := strings.ToLower(strings.TrimSpace(raw)); p {
+ case "week", "quarter", "year":
+ return p
+ default:
+ return "month"
+ }
+}
- buyAmt, buyCnt := erpDocAmount(tid, []string{models.ErpDocPurchaseIn}, monthFrom, monthTo)
- saleAmt, saleCnt := erpDocAmount(tid, []string{models.ErpDocSalesOut}, monthFrom, monthTo)
- backAmt, _ := erpDocAmount(tid, []string{models.ErpDocSalesReturn}, monthFrom, monthTo)
- todaySale, todayCnt := erpDocAmount(tid, []string{models.ErpDocSalesOut}, todayFrom, todayFrom.AddDate(0, 0, 1))
+// erpPeriodSpan 趋势分桶:Key 用于把单据日期映射到桶,Label 是图表 x 轴文案
+type erpPeriodSpan struct {
+ Key string
+ Label string
+}
- // 本月毛利(出库流水成本口径)
+// erpPeriodRange 返回周期区间 [from, to)、中文标签与趋势分桶。
+// 周/月按天分桶,季度按 3 个月分桶,年度按 12 个月分桶。
+func erpPeriodRange(period string, now time.Time) (time.Time, time.Time, string, []erpPeriodSpan) {
+ switch period {
+ case "week":
+ // 周一为一周起点;Go 的 Sunday=0,需要换算成「距本周一的天数」
+ offset := (int(now.Weekday()) + 6) % 7
+ from := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, -offset)
+ names := []string{"周一", "周二", "周三", "周四", "周五", "周六", "周日"}
+ spans := make([]erpPeriodSpan, 0, 7)
+ for i := 0; i < 7; i++ {
+ d := from.AddDate(0, 0, i)
+ spans = append(spans, erpPeriodSpan{Key: d.Format("2006-01-02"), Label: names[i]})
+ }
+ return from, from.AddDate(0, 0, 7), "本周", spans
+ case "quarter":
+ q := (int(now.Month()) - 1) / 3
+ from := time.Date(now.Year(), time.Month(q*3+1), 1, 0, 0, 0, 0, time.Local)
+ spans := make([]erpPeriodSpan, 0, 3)
+ for i := 0; i < 3; i++ {
+ m := from.AddDate(0, i, 0)
+ spans = append(spans, erpPeriodSpan{Key: m.Format("2006-01"), Label: fmt.Sprintf("%d月", int(m.Month()))})
+ }
+ return from, from.AddDate(0, 3, 0), "本季度", spans
+ case "year":
+ from := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.Local)
+ spans := make([]erpPeriodSpan, 0, 12)
+ for i := 1; i <= 12; i++ {
+ spans = append(spans, erpPeriodSpan{Key: fmt.Sprintf("%04d-%02d", now.Year(), i), Label: fmt.Sprintf("%d月", i)})
+ }
+ return from, from.AddDate(1, 0, 0), "本年", spans
+ default: // month
+ from := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local)
+ to := from.AddDate(0, 1, 0)
+ spans := make([]erpPeriodSpan, 0, 31)
+ for d := from; d.Before(to); d = d.AddDate(0, 0, 1) {
+ spans = append(spans, erpPeriodSpan{Key: d.Format("2006-01-02"), Label: fmt.Sprintf("%d日", d.Day())})
+ }
+ return from, to, "本月", spans
+ }
+}
+
+// erpDashboardStats 区间内购销/毛利统计(只认已生效单据与库存流水)
+func erpDashboardStats(tid string, from, to time.Time) map[string]interface{} {
+ buyAmt, buyCnt := erpDocAmount(tid, []string{models.ErpDocPurchaseIn}, from, to)
+ saleAmt, saleCnt := erpDocAmount(tid, []string{models.ErpDocSalesOut}, from, to)
+ backAmt, _ := erpDocAmount(tid, []string{models.ErpDocSalesReturn}, from, to)
+
+ // 毛利按出库流水成本口径
var ledgers []models.ErpStockLedger
_, _ = models.Orm.QueryTable(new(models.ErpStockLedger)).
Filter("tenant_id", tid).
Filter("doc_type__in", []string{models.ErpDocSalesOut, models.ErpDocSalesReturn}).
- Filter("create_time__gte", monthFrom).Filter("create_time__lt", monthTo).
+ Filter("create_time__gte", from).Filter("create_time__lt", to).
All(&ledgers, "direction", "total_cost")
var cost float64
for _, l := range ledgers {
@@ -105,6 +156,82 @@ func erpDashboardCore(a erpActor) map[string]interface{} {
}
}
profit := saleAmt - backAmt - cost
+ rate := 0.0
+ if saleAmt-backAmt > 0.005 {
+ rate = round2f(profit / (saleAmt - backAmt) * 100)
+ }
+ return map[string]interface{}{
+ "purchase_amount": buyAmt, "purchase_count": buyCnt,
+ "sales_amount": saleAmt, "sales_count": saleCnt,
+ "return_amount": backAmt, "cost": round2f(cost),
+ "profit": round2f(profit), "profit_rate": rate,
+ }
+}
+
+// erpDashboardPeriodTrend 当前周期内的购销分布:一次查出单据后按分桶汇总,
+// 避免「每个点都打一次库」(年度 12 点、月度 31 点)。
+func erpDashboardPeriodTrend(tid string, from, to time.Time, period string, spans []erpPeriodSpan) []erpDashboardTrend {
+ var docs []models.ErpDocument
+ _, _ = models.Orm.QueryTable(new(models.ErpDocument)).
+ Filter("tenant_id", tid).
+ Filter("doc_type__in", []string{models.ErpDocPurchaseIn, models.ErpDocSalesOut}).
+ Filter("status", models.ErpDocStatusApproved).
+ Filter("delete_time__isnull", true).
+ Filter("biz_date__gte", from).Filter("biz_date__lt", to).
+ All(&docs, "doc_type", "biz_date", "actual_amount")
+
+ index := make(map[string]int, len(spans))
+ for i, s := range spans {
+ index[s.Key] = i
+ }
+ buy := make([]float64, len(spans))
+ sale := make([]float64, len(spans))
+ for _, d := range docs {
+ key := d.BizDate.Format("2006-01-02")
+ if period == "quarter" || period == "year" {
+ key = d.BizDate.Format("2006-01")
+ }
+ i, ok := index[key]
+ if !ok {
+ continue
+ }
+ if d.DocType == models.ErpDocPurchaseIn {
+ buy[i] += d.ActualAmount
+ } else {
+ sale[i] += d.ActualAmount
+ }
+ }
+ list := make([]erpDashboardTrend, 0, len(spans))
+ for i, s := range spans {
+ list = append(list, erpDashboardTrend{
+ Month: s.Key, Label: s.Label,
+ Buy: round2f(buy[i]), Sale: round2f(sale[i]),
+ })
+ }
+ return list
+}
+
+// erpDashboardCore 经营看板:所选周期的购销/毛利、今日销售、库存价值、往来余额、
+// 待办、近 6 个月趋势、当前周期趋势与预警。period 取 week|month|quarter|year,缺省 month。
+// month 字段保持「本月」口径不变,供旧页面与小程序继续使用;current 为所选周期。
+func erpDashboardCore(a erpActor, periodRaw string) map[string]interface{} {
+ tid := a.Tid
+ period := erpDashboardPeriod(periodRaw)
+ now := time.Now()
+
+ monthFrom, monthTo := erpMonthRange(0)
+ monthStats := erpDashboardStats(tid, monthFrom, monthTo)
+
+ periodFrom, periodTo, periodLabel, spans := erpPeriodRange(period, now)
+ current := monthStats
+ if period != "month" {
+ current = erpDashboardStats(tid, periodFrom, periodTo)
+ }
+
+ // 不能用 time.Now().Truncate(24*time.Hour):它按「自零时起算的绝对时长」取整,
+ // 在东八区得到的是 UTC 零点(本地 08:00),会把当天 00:00–08:00 的单据漏掉。
+ todayFrom := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
+ todaySale, todayCnt := erpDocAmount(tid, []string{models.ErpDocSalesOut}, todayFrom, todayFrom.AddDate(0, 0, 1))
var stocks []models.ErpStock
_, _ = models.Orm.QueryTable(new(models.ErpStock)).Filter("tenant_id", tid).
@@ -142,30 +269,29 @@ func erpDashboardCore(a erpActor) map[string]interface{} {
f, t := erpMonthRange(-i)
b, _ := erpDocAmount(tid, []string{models.ErpDocPurchaseIn}, f, t)
s, _ := erpDocAmount(tid, []string{models.ErpDocSalesOut}, f, t)
- trendList = append(trendList, erpDashboardTrend{Month: f.Format("2006-01"), Buy: b, Sale: s})
+ month := f.Format("2006-01")
+ trendList = append(trendList, erpDashboardTrend{Month: month, Label: month, Buy: b, Sale: s})
}
return map[string]interface{}{
- "month": map[string]interface{}{
- "purchase_amount": buyAmt, "purchase_count": buyCnt,
- "sales_amount": saleAmt, "sales_count": saleCnt,
- "return_amount": backAmt, "cost": round2f(cost),
- "profit": round2f(profit),
- "profit_rate": func() float64 {
- if saleAmt-backAmt > 0.005 {
- return round2f(profit / (saleAmt - backAmt) * 100)
- }
- return 0
- }(),
+ "period": period,
+ "range": map[string]interface{}{
+ "from": periodFrom.Format("2006-01-02"),
+ "to": periodTo.AddDate(0, 0, -1).Format("2006-01-02"),
+ "label": periodLabel,
},
+ "current": current,
+ // 兼容旧调用方:month 始终是本月口径
+ "month": monthStats,
"today": map[string]interface{}{"sales_amount": todaySale, "sales_count": todayCnt},
"stock": map[string]interface{}{"value": round2f(stockValue), "qty": round4f(stockQty)},
"partner_bill": map[string]interface{}{
"receivable": round2f(recv), "payable": round2f(pay), "net": round2f(recv - pay),
},
- "todo": map[string]interface{}{"approval_pending": pendingTasks, "draft_docs": draftCnt},
- "trend": trendList,
- "warning": erpWarningCounts(tid),
+ "todo": map[string]interface{}{"approval_pending": pendingTasks, "draft_docs": draftCnt},
+ "trend": trendList,
+ "period_trend": erpDashboardPeriodTrend(tid, periodFrom, periodTo, period, spans),
+ "warning": erpWarningCounts(tid),
}
}
diff --git a/go/controllers/platform_website_setting.go b/go/controllers/platform_website_setting.go
index 13f55fe..72eee95 100644
--- a/go/controllers/platform_website_setting.go
+++ b/go/controllers/platform_website_setting.go
@@ -7,6 +7,7 @@ import (
"server/models"
"server/pkg/identity"
+ "server/services"
beego "github.com/beego/beego/v2/server/web"
)
@@ -108,3 +109,110 @@ func (c *PlatformWebsiteSettingController) Save() {
c.Data["json"] = map[string]any{"code": 200, "msg": "保存成功"}
_ = c.ServeJSON()
}
+
+// loginBrandTags 登录页品牌配置项,存放于 yz_platform_site_setting,
+// 统一认证中心 /auth/login 渲染时读取。
+var loginBrandTags = []string{"login_title", "login_subtitle", "login_logo", "login_bg"}
+
+// LoginBrandInfo GET /platform/loginBrand
+// 返回登录页品牌配置(主标题 / 副标题 / Logo / 背景图)。
+func (c *PlatformWebsiteSettingController) LoginBrandInfo() {
+ if _, err := requirePlatform(&c.Controller); err != nil {
+ code := identity.Status(err)
+ c.jsonErr(code, code, err.Error())
+ return
+ }
+
+ all := models.GetPlatformSiteTagMap()
+ data := make(map[string]string, len(loginBrandTags))
+ for _, tag := range loginBrandTags {
+ data[tag] = all[tag]
+ }
+
+ c.Data["json"] = map[string]any{"code": 200, "msg": "success", "data": data}
+ _ = c.ServeJSON()
+}
+
+// SaveLoginBrand POST /platform/saveLoginBrand
+// 入参 { login_title, login_subtitle, login_logo, login_bg }。
+// 只接受这四个标签,避免与「站点设置」共用的 yz_platform_site_setting 被写入无关项。
+func (c *PlatformWebsiteSettingController) SaveLoginBrand() {
+ if _, err := requirePlatform(&c.Controller); err != nil {
+ code := identity.Status(err)
+ c.jsonErr(code, code, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var payload map[string]string
+ if uerr := json.Unmarshal(raw, &payload); uerr != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ values := make(map[string]string, len(loginBrandTags))
+ for _, tag := range loginBrandTags {
+ if v, ok := payload[tag]; ok {
+ values[tag] = strings.TrimSpace(v)
+ }
+ }
+ if len(values) == 0 {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ if len([]rune(values["login_title"])) > 60 {
+ c.jsonErr(400, 400, "主标题不能超过 60 个字符")
+ return
+ }
+ if len([]rune(values["login_subtitle"])) > 120 {
+ c.jsonErr(400, 400, "副标题不能超过 120 个字符")
+ return
+ }
+ for _, tag := range []string{"login_logo", "login_bg"} {
+ img, ok := normalizeLoginBrandImage(values[tag])
+ if !ok {
+ c.jsonErr(400, 400, "图片地址必须是 HTTPS 或站内相对路径")
+ return
+ }
+ values[tag] = img
+ }
+
+ if err = models.SavePlatformSiteSettings(values); err != nil {
+ c.jsonErr(500, 500, "保存失败: "+err.Error())
+ return
+ }
+
+ c.Data["json"] = map[string]any{"code": 200, "msg": "保存成功"}
+ _ = c.ServeJSON()
+}
+
+// normalizeLoginBrandImage 校验登录页图片地址:
+// - 站内相对路径(/uploads/...)直接放行;
+// - HTTPS 完整地址放行;
+// - HTTP 历史七牛域名尝试改写为存储配置的 HTTPS 基址,改不动则拒绝,
+// 否则登录页会被浏览器按 Mixed Content 拦截。
+func normalizeLoginBrandImage(raw string) (string, bool) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return "", true
+ }
+ if strings.HasPrefix(raw, "/") {
+ return raw, true
+ }
+ lower := strings.ToLower(raw)
+ if strings.HasPrefix(lower, "https://") {
+ return raw, true
+ }
+ if strings.HasPrefix(lower, "http://") {
+ rewritten := services.ResolvePublicURL("", "", raw)
+ if strings.HasPrefix(strings.ToLower(rewritten), "https://") {
+ return rewritten, true
+ }
+ return "", false
+ }
+ return "", false
+}
diff --git a/go/models/platform_site_setting.go b/go/models/platform_site_setting.go
index 536371d..45ff9b0 100644
--- a/go/models/platform_site_setting.go
+++ b/go/models/platform_site_setting.go
@@ -65,6 +65,12 @@ var PlatformSiteSettingDefs = []PlatformSiteSettingDef{
// 政策条款
{Tag: "privacy", Title: "隐私政策", Remark: "隐私政策链接,如 /page/privacy | group:政策条款"},
{Tag: "terms", Title: "使用条款", Remark: "使用条款链接,如 /page/terms | group:政策条款"},
+
+ // 登录页品牌(统一认证中心 /auth/login 左侧展示)
+ {Tag: "login_title", Title: "登录页主标题", Remark: "登录页左侧主标题,留空则使用默认文案 | group:登录页设置"},
+ {Tag: "login_subtitle", Title: "登录页副标题", Remark: "登录页左侧副标题,留空则使用默认文案 | group:登录页设置"},
+ {Tag: "login_logo", Title: "登录页 Logo", Remark: "显示在登录页左上角,建议透明背景 PNG | group:登录页设置 | type:image"},
+ {Tag: "login_bg", Title: "登录页背景图", Remark: "登录页左侧背景图,建议浅色系、竖版 1200×1600 以上 | group:登录页设置 | type:image"},
}
// rePlatformSiteTag 标签名校验:与 tagengine 全局标签正则保持一致
diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go
index 8b42d0c..6e6f581 100644
--- a/go/routers/platform/platform.go
+++ b/go/routers/platform/platform.go
@@ -400,6 +400,10 @@ func Register() {
beego.Router("/platform/website/setting/info", &controllers.PlatformWebsiteSettingController{}, "get:Info")
beego.Router("/platform/website/setting/save", &controllers.PlatformWebsiteSettingController{}, "post:Save")
+ // 统一登录页品牌(Logo / 背景图 / 主副标题,与站点设置共用 yz_platform_site_setting)
+ beego.Router("/platform/loginBrand", &controllers.PlatformWebsiteSettingController{}, "get:LoginBrandInfo")
+ beego.Router("/platform/saveLoginBrand", &controllers.PlatformWebsiteSettingController{}, "post:SaveLoginBrand")
+
// ===== 平台官网管理(docs/sql/create_platform_website.sql)=====
// 文章管理 yz_platform_website_article
beego.Router("/platform/website/article/list", &controllers.PlatformWebsiteArticleController{}, "get:List")
diff --git a/go/services/software_upgrade_url.go b/go/services/software_upgrade_url.go
index 79800e4..f8b2c55 100644
--- a/go/services/software_upgrade_url.go
+++ b/go/services/software_upgrade_url.go
@@ -44,14 +44,20 @@ func ResolveSoftwareDownloadURLs(scheme, host string, downloadURLs *string) map[
return result
}
-// ResolvePublicURL 把相对路径拼成公开 URL;http(s) 地址原样返回。
+// legacyStorageHosts 历史七牛 CDN 域名:这两个域名只支持 HTTP,HTTPS 页面直接引用会被
+// 浏览器以 Mixed Content 拦截,需在返回给前端前改写为当前存储配置的访问入口。
+var legacyStorageHosts = []string{"7colud.yunzer.cn", "7cloud.yunzer.cn"}
+
+// ResolvePublicURL 把相对路径拼成公开 URL;http(s) 地址原样返回,
+// 但历史七牛域名会改写为当前存储配置的访问基址(如 https://yunzer.com.cn/qiniu)。
func ResolvePublicURL(scheme, host, src string) string {
src = strings.TrimSpace(src)
if src == "" {
return ""
}
- if strings.HasPrefix(strings.ToLower(src), "http://") || strings.HasPrefix(strings.ToLower(src), "https://") {
- return src
+ lowerSrc := strings.ToLower(src)
+ if strings.HasPrefix(lowerSrc, "http://") || strings.HasPrefix(lowerSrc, "https://") {
+ return rewriteLegacyStorageURL(src)
}
if host == "" {
return src
@@ -65,6 +71,37 @@ func ResolvePublicURL(scheme, host, src string) string {
return scheme + "://" + host + src
}
+// rewriteLegacyStorageURL 把历史七牛域名的 scheme+host 换成当前存储配置域名,
+// 保留对象路径不变;非历史域名或配置缺失时原样返回。
+func rewriteLegacyStorageURL(src string) string {
+ cfg, err := models.GetStorageConfig()
+ if err != nil {
+ return src
+ }
+ base := strings.TrimRight(strings.TrimSpace(cfg.QiniuDomain), "/")
+ if base == "" {
+ return src
+ }
+ if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") {
+ base = "https://" + base
+ }
+ lowerSrc := strings.ToLower(src)
+ for _, legacyHost := range legacyStorageHosts {
+ for _, scheme := range []string{"http://", "https://"} {
+ prefix := scheme + legacyHost
+ if !strings.HasPrefix(lowerSrc, prefix) {
+ continue
+ }
+ rest := src[len(prefix):]
+ // 防止 7colud.yunzer.cn.example.com 这类前缀误匹配
+ if rest == "" || strings.HasPrefix(rest, "/") {
+ return base + rest
+ }
+ }
+ }
+ return src
+}
+
// ResolveSoftwareDownloadURL 优先使用自定义 download_url;否则根据 file_id 读附件 src 拼完整 URL
func ResolveSoftwareDownloadURL(scheme, host string, downloadURL *string, fileID *uint64) string {
if downloadURL != nil {
diff --git a/go/views/auth/login.tpl b/go/views/auth/login.tpl
index a4418d0..acb4e5d 100644
--- a/go/views/auth/login.tpl
+++ b/go/views/auth/login.tpl
@@ -4,7 +4,7 @@
- 统一认证中心 - 登录
+ {{.LoginTitle}} - 统一认证中心
-
-
+
+
+