Compare commits

..

7 Commits

Author SHA1 Message Date
wangjia bf36f7aa3f fix(client): 标签字体细化:阈值 128,店名细体
Deploy / build-linux-web (push) Successful in 46s
Deploy / build-windows (push) Successful in 1m43s
Deploy / build-macos (push) Successful in 1m24s
Deploy / build-android (push) Successful in 59s
Deploy / build-ios (push) Successful in 7s
Deploy / release-deploy (push) Successful in 1m53s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:04:32 +08:00
wangjia 097e466372 fix(client): 标签打印 2× 超采样 + 阈值 160,消除热敏字体模糊
Deploy / build-linux-web (push) Successful in 51s
Deploy / build-windows (push) Successful in 1m46s
Deploy / build-macos (push) Successful in 1m30s
Deploy / build-android (push) Successful in 1m0s
Deploy / build-ios (push) Successful in 7s
Deploy / release-deploy (push) Successful in 1m54s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 18:20:42 +08:00
wangjia c1ba6c6d97 feat(backend): 二维码链接统一改为 /app/product/ 路径,消除 URL 跳变
Deploy / build-linux-web (push) Successful in 46s
Deploy / build-windows (push) Successful in 1m41s
Deploy / build-macos (push) Successful in 1m27s
Deploy / build-android (push) Successful in 58s
Deploy / build-ios (push) Successful in 6s
Deploy / release-deploy (push) Successful in 1m53s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:55:24 +08:00
wangjia 2b327cd365 fix(backend): 修复 /app/product/ 分享无 OG 标签,描述分两行显示
Deploy / build-linux-web (push) Successful in 49s
Deploy / build-windows (push) Successful in 1m45s
Deploy / build-macos (push) Successful in 1m24s
Deploy / build-android (push) Successful in 58s
Deploy / build-ios (push) Failing after 10m6s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:52:41 +08:00
wangjia 07f8973477 fix(backend): 分享卡片描述补充系列(度数)信息
Deploy / build-linux-web (push) Successful in 47s
Deploy / build-windows (push) Successful in 1m44s
Deploy / build-macos (push) Successful in 1m26s
Deploy / build-android (push) Successful in 58s
Deploy / build-ios (push) Successful in 7s
Deploy / release-deploy (push) Successful in 1m50s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:36:04 +08:00
wangjia 3b02a848c0 fix(client): 商品公开页加载后更新浏览器标题为商品名称
Deploy / build-linux-web (push) Successful in 49s
Deploy / build-windows (push) Successful in 1m43s
Deploy / build-macos (push) Successful in 1m26s
Deploy / build-android (push) Successful in 58s
Deploy / build-ios (push) Successful in 7s
Deploy / release-deploy (push) Successful in 1m46s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:26:22 +08:00
wangjia bde2d35756 fix(backend): 修复分享卡片 <title> 因缩进导致替换失效的问题
Deploy / build-linux-web (push) Successful in 47s
Deploy / build-windows (push) Successful in 1m45s
Deploy / build-macos (push) Successful in 1m27s
Deploy / build-android (push) Successful in 1m0s
Deploy / build-ios (push) Successful in 7s
Deploy / release-deploy (push) Successful in 1m48s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:37:20 +08:00
6 changed files with 95 additions and 18 deletions
+37
View File
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.51] - 2026-06-15
### 改进
- 标签打印字体细化:阈值调回 128,店名改细体,商品名保持粗体
## [1.0.50] - 2026-06-15
### 改进
- 标签打印字体更清晰:采用 2× 超采样渲染,消除热敏打印时反锯齿导致的模糊笔划
## [1.0.49] - 2026-06-15
### 改进
- 二维码链接统一使用 `/app/product/` 路径,扫码后 URL 不再跳变,分享与打开同一个链接
## [1.0.48] - 2026-06-15
### 修复
- 修复分享 `/app/product/` 路径时微信爬虫拿不到 OG 标签的问题(nginx 补充该路径的代理规则)
- 分享卡片描述规格/系列与门店名分两行显示
## [1.0.47] - 2026-06-15
### 修复
- 微信分享卡片描述补充度数/系列信息,不再只显示规格
## [1.0.46] - 2026-06-15
### 修复
- 微信内打开商品页时顶部标题栏显示商品名称,不再显示"岩美"
## [1.0.45] - 2026-06-15
### 修复
- 修复微信分享卡片标题仍显示"岩美"的问题(HTML 缩进导致字符串匹配失败)
- 微信分享卡片描述去掉"扫码验真"后缀
## [1.0.44] - 2026-06-15 ## [1.0.44] - 2026-06-15
### 改进 ### 改进
+1 -1
View File
@@ -198,7 +198,7 @@ func (h *ProductHandler) QRCode(c *gin.Context) {
h.db.Model(&product).Update("public_id", product.PublicID) h.db.Model(&product).Update("public_id", product.PublicID)
} }
url := config.C.Storage.PublicURL + "/product/" + product.PublicID url := config.C.Storage.PublicURL + "/app/product/" + product.PublicID
if product.Code != "" { if product.Code != "" {
url += "?code=" + product.Code url += "?code=" + product.Code
} }
+15 -5
View File
@@ -6,6 +6,7 @@ import (
"html" "html"
"net/http" "net/http"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
@@ -315,7 +316,9 @@ func (h *PublicHandler) ProductPage(c *gin.Context) {
ogTags := buildProductOG(product, shopName, config.C.Storage.PublicURL) ogTags := buildProductOG(product, shopName, config.C.Storage.PublicURL)
ogTitle := buildOGTitle(product) ogTitle := buildOGTitle(product)
// 同时替换 <title> 标签,微信/飞书等平台有时优先读 <title> 而非 og:title // 同时替换 <title> 标签,微信/飞书等平台有时优先读 <title> 而非 og:title
out := strings.Replace(idxHTML, "<title>岩美</title>", "<title>"+html.EscapeString(ogTitle)+"</title>", 1) // 用正则替换避免缩进空格导致字符串不匹配
titleRe := regexp.MustCompile(`<title>[^<]*</title>`)
out := titleRe.ReplaceAllString(idxHTML, "<title>"+html.EscapeString(ogTitle)+"</title>")
out = strings.Replace(out, "</head>", ogTags+"</head>", 1) out = strings.Replace(out, "</head>", ogTags+"</head>", 1)
// 禁止缓存,确保爬虫每次都能拿到最新 OG 标签 // 禁止缓存,确保爬虫每次都能拿到最新 OG 标签
c.Header("Cache-Control", "no-store") c.Header("Cache-Control", "no-store")
@@ -340,14 +343,21 @@ func buildOGTitle(product model.Product) string {
func buildProductOG(product model.Product, shopName, publicURL string) string { func buildProductOG(product model.Product, shopName, publicURL string) string {
title := buildOGTitle(product) title := buildOGTitle(product)
// og:description规格(度数/香型/容量| 门店名正品 // og:description系列(度数/香型)· 规格(容量/单位| 门店名正品
desc := product.Spec var specParts []string
if product.Series != "" {
specParts = append(specParts, product.Series)
}
if product.Spec != "" {
specParts = append(specParts, product.Spec)
}
desc := strings.Join(specParts, " · ")
suffix := "正品" suffix := "正品"
if shopName != "" { if shopName != "" {
suffix = shopName + suffix suffix = shopName + suffix
} }
if desc != "" { if desc != "" {
desc = desc + " | " + suffix desc = desc + "\n" + suffix
} else { } else {
desc = suffix desc = suffix
} }
@@ -359,7 +369,7 @@ func buildProductOG(product model.Product, shopName, publicURL string) string {
} }
// og:url // og:url
pageURL := publicURL + "/product/" + product.PublicID pageURL := publicURL + "/app/product/" + product.PublicID
var sb strings.Builder var sb strings.Builder
sb.WriteString("\n") sb.WriteString("\n")
+24 -12
View File
@@ -189,7 +189,8 @@ double _fitFont(String s, double maxW, double maxH, bool bold, double cap) {
return size; return size;
} }
/// 用 dart:ui 把一张标签绘制成 320×160 位图(热敏标签实际尺寸 40×20mm @203dpi)。 /// 用 dart:ui 把一张标签绘制成位图(热敏标签实际尺寸 40×20mm @203dpi)。
/// 内部以 2× 超采样(640×320)绘制,转 TSPL 时再降采样,消除反锯齿模糊。
/// 供 TSPL 裸发和预览渲染共用同一套画布逻辑,确保预览即实际输出。 /// 供 TSPL 裸发和预览渲染共用同一套画布逻辑,确保预览即实际输出。
Future<ui.Image> _renderLabelBitmap({ Future<ui.Image> _renderLabelBitmap({
required String shop, required String shop,
@@ -199,10 +200,13 @@ Future<ui.Image> _renderLabelBitmap({
required String date, required String date,
required Uint8List qrBytes, required Uint8List qrBytes,
}) async { }) async {
const w = 320, h = 160; // 40×20mm @203dpi const w = 320, h = 160; // 40×20mm @203dpi(逻辑尺寸)
const scale = 2; // 2× 超采样,内部以 640×320 绘制
const sw = w * scale, sh = h * scale;
final recorder = ui.PictureRecorder(); final recorder = ui.PictureRecorder();
final canvas = ui.Canvas( final canvas = ui.Canvas(
recorder, ui.Rect.fromLTWH(0, 0, w.toDouble(), h.toDouble())); recorder, ui.Rect.fromLTWH(0, 0, sw.toDouble(), sh.toDouble()));
canvas.scale(scale.toDouble(), scale.toDouble()); // 坐标系放大,后续坐标不变
canvas.drawRect(ui.Rect.fromLTWH(0, 0, w.toDouble(), h.toDouble()), canvas.drawRect(ui.Rect.fromLTWH(0, 0, w.toDouble(), h.toDouble()),
ui.Paint()..color = const ui.Color(0xFFFFFFFF)); ui.Paint()..color = const ui.Color(0xFFFFFFFF));
@@ -236,8 +240,8 @@ Future<ui.Image> _renderLabelBitmap({
final codeText = code.isNotEmpty ? '编号:$code' : ''; final codeText = code.isNotEmpty ? '编号:$code' : '';
final rows = <(String, double, bool)>[ final rows = <(String, double, bool)>[
if (shop.isNotEmpty) (shop, kShopSize, true), if (shop.isNotEmpty) (shop, kShopSize, false), // 店名细体,突出商品名
if (name.isNotEmpty) (name, nameSize, true), if (name.isNotEmpty) (name, nameSize, true), // 商品名粗体
if (codeText.isNotEmpty) (codeText, kSmallSize, false), if (codeText.isNotEmpty) (codeText, kSmallSize, false),
if (degSpec.isNotEmpty) (degSpec, kSmallSize, false), if (degSpec.isNotEmpty) (degSpec, kSmallSize, false),
if (date.isNotEmpty) (date, kSmallSize, false), if (date.isNotEmpty) (date, kSmallSize, false),
@@ -258,7 +262,7 @@ Future<ui.Image> _renderLabelBitmap({
yy += r.$2 * kLineH + gap; yy += r.$2 * kLineH + gap;
} }
return recorder.endRecording().toImage(w, h); return recorder.endRecording().toImage(sw, sh); // 返回 2× 图(640×320
} }
/// 桌面端把「扁平标签」直接画成单色位图,TSPL BITMAP 裸发到热敏机。 /// 桌面端把「扁平标签」直接画成单色位图,TSPL BITMAP 裸发到热敏机。
@@ -279,11 +283,13 @@ Future<bool> _printFlatLabelThermal({
debugPrint('[label] thermal printer = $printer'); debugPrint('[label] thermal printer = $printer');
if (printer == null) return false; if (printer == null) return false;
// img 是 2× 超采样图(640×320);TSPL 输出仍是 320×160
const w = 320, h = 160; const w = 320, h = 160;
final img = await _renderLabelBitmap( final img = await _renderLabelBitmap(
shop: shop, name: name, code: code, degSpec: degSpec, date: date, qrBytes: qrBytes); shop: shop, name: name, code: code, degSpec: degSpec, date: date, qrBytes: qrBytes);
final bd = await img.toByteData(format: ui.ImageByteFormat.rawRgba); final bd = await img.toByteData(format: ui.ImageByteFormat.rawRgba);
final rgba = bd!.buffer.asUint8List(); final rgba = bd!.buffer.asUint8List();
final sw = img.width; // 6402×宽)
// 打包 TSPL(SIZE 用整数,避免固件不认小数导致多走纸) // 打包 TSPL(SIZE 用整数,避免固件不认小数导致多走纸)
const wbytes = (w + 7) >> 3; const wbytes = (w + 7) >> 3;
@@ -294,12 +300,18 @@ Future<bool> _printFlatLabelThermal({
for (int yy = 0; yy < h; yy++) { for (int yy = 0; yy < h; yy++) {
final row = Uint8List(wbytes)..fillRange(0, wbytes, 0xFF); // 默认白 bit=1 final row = Uint8List(wbytes)..fillRange(0, wbytes, 0xFF); // 默认白 bit=1
for (int xx = 0; xx < w; xx++) { for (int xx = 0; xx < w; xx++) {
final i = (yy * w + xx) * 4; // 2×2 四像素平均后阈值,消除反锯齿模糊(阈值 160 = 偏灰的边缘算黑)
final a = rgba[i + 3]; double lum = 0;
final lum = a < 128 for (int dy = 0; dy < 2; dy++) {
? 255.0 for (int dx = 0; dx < 2; dx++) {
: 0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2]; final i = ((yy * 2 + dy) * sw + (xx * 2 + dx)) * 4;
if (lum < 128) row[xx >> 3] &= ~(0x80 >> (xx & 7)); // 黑点 -> bit0 final a = rgba[i + 3];
lum += a < 128
? 255.0
: 0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2];
}
}
if (lum / 4 < 128) row[xx >> 3] &= ~(0x80 >> (xx & 7)); // 黑点 -> bit0
} }
out.add(row); out.add(row);
} }
@@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../core/config/app_config.dart'; import '../../core/config/app_config.dart';
@@ -54,6 +55,13 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
); );
final data = (resp.data as Map<String, dynamic>)['data'] as Map<String, dynamic>; final data = (resp.data as Map<String, dynamic>)['data'] as Map<String, dynamic>;
setState(() { _data = data; _loading = false; }); setState(() { _data = data; _loading = false; });
// 更新浏览器标签标题(Flutter Web 会写 document.title
final name = data['name'] as String? ?? '';
if (name.isNotEmpty) {
SystemChrome.setApplicationSwitcherDescription(
ApplicationSwitcherDescription(label: name, primaryColor: 0xFF8B2331),
);
}
} catch (e) { } catch (e) {
setState(() { _error = e.toString(); _loading = false; }); setState(() { _error = e.toString(); _loading = false; });
} }
+10
View File
@@ -55,6 +55,16 @@ server {
proxy_read_timeout 30s; proxy_read_timeout 30s;
} }
# Flutter 路由会把 /product/:id 重写为 /app/product/:idbase-href=/app/
# 分享此 URL 时微信爬虫也需要 OG 标签 → 去掉 /app 前缀后转发后端
location ~ ^/app/product/ {
rewrite ^/app(/product/.+)$ $1 break;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
# 桌面客户端安装包下载(Windows .exe / macOS .zip # 桌面客户端安装包下载(Windows .exe / macOS .zip
location /downloads/ { location /downloads/ {
alias /opt/jiu/downloads/; alias /opt/jiu/downloads/;