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}} - 统一认证中心 -
-
+
+ +
@@ -41,6 +46,7 @@ import { ref } from "vue"; import platformSettings from "./components/platformSettings.vue"; import storageSettings from "./components/storageSettings.vue"; import notificationSettings from "./components/notificationSettings.vue"; +import loginBrandSettings from "./components/loginBrandSettings.vue"; const activeTab = ref("platform"); diff --git a/uniapp/pages/oa/dailyreport/index.vue b/uniapp/pages/oa/dailyreport/index.vue index 7577723..4896369 100644 --- a/uniapp/pages/oa/dailyreport/index.vue +++ b/uniapp/pages/oa/dailyreport/index.vue @@ -391,9 +391,11 @@ function cellClass(cell) { return cell.isFuture ? 'is-future' : 'is-none' } -// 补写窗口内、尚未写日报的日期标注「补」,点击直接进入补写 +// 补写窗口内、往日尚未写日报的日期标注「补」,点击直接进入补写 +// 今天属于正常填写,未写时不标「补」 function cellBackfill(cell) { if (!cell || cell.isFuture || cell.record) return false + if (cell.date === today) return false return inWindow(cell.date) } @@ -403,7 +405,7 @@ function onCalendarCellTap(cell) { openDetail(cell.record.id) return } - if (cellBackfill(cell)) { + if (cell.date === today || cellBackfill(cell)) { uni.navigateTo({ url: `/pages/oa/dailyreport/edit?date=${cell.date}` }) return }