From 75e3b934bc68cdeda08242ec4069561c29e7283d Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Thu, 28 May 2026 22:39:04 +0800 Subject: [PATCH] chore: release v1.0.3 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 10 +- backend/internal/handler/finance.go | 2 +- backend/internal/handler/inventory.go | 2 +- backend/internal/handler/number_rule.go | 2 +- backend/internal/handler/partner.go | 2 +- backend/internal/handler/product.go | 2 +- backend/internal/handler/product_option.go | 6 +- backend/internal/handler/stock_in.go | 2 +- backend/internal/handler/stock_out.go | 2 +- backend/internal/handler/user.go | 2 +- backend/internal/handler/warehouse.go | 2 +- backend/internal/middleware/auth.go | 2 +- backend/internal/service/auth.go | 2 +- client/lib/core/models/page_result.dart | 2 +- client/lib/core/utils/print_util_web.dart | 96 +++++++------- .../repositories/number_rule_repository.dart | 2 +- client/lib/repositories/user_repository.dart | 2 +- .../repositories/warehouse_repository.dart | 2 +- client/lib/screens/auth/login_screen.dart | 5 +- .../inventory/inventory_list_screen.dart | 1 - .../lib/screens/settings/settings_screen.dart | 2 - client/pubspec.lock | 40 ++---- scripts/ci/compile.sh | 7 + scripts/ci/deploy.sh | 11 +- scripts/ci/release.sh | 1 + scripts/dev-proxy.js | 98 ++++++++++++++ scripts/dev.sh | 100 +++++++++++++- web/index.njk | 6 +- web/register.njk | 123 ++++++++++++------ 29 files changed, 386 insertions(+), 150 deletions(-) create mode 100644 scripts/dev-proxy.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0746652..8e88daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,18 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.3] - 2026-05-26 +## [1.0.3] - 2026-05-28 ### 新功能 - 新增用户自助注册功能,在官网 /register/ 填写门店信息即可完成注册,系统自动生成门店编码(S000001 格式) -- 支持 Windows 桌面客户端构建与发布(需配置 Windows CI Runner) +- 注册账号默认设为超级管理员权限 +- 支持 Windows 桌面客户端构建与发布 ### 修复 -- 商品标签打印改为竖向 20mm×38mm 布局,修复标签内容旋转 90° 问题 +- 修复新门店首次登录时库存、仓库、用户等列表加载崩溃的问题 +- 修复库存管理页在无数据时页面崩溃(RangeError)的问题 +- 商品标签打印改为横向 38mm×20mm 布局,修复标签内容错位问题 +- 修复数据导入中移除不必要的"商品编码"导入项 ## [1.0.2] - 2026-05-25 diff --git a/backend/internal/handler/finance.go b/backend/internal/handler/finance.go index 5b09906..5294b08 100644 --- a/backend/internal/handler/finance.go +++ b/backend/internal/handler/finance.go @@ -51,7 +51,7 @@ func (h *FinanceHandler) ListRecords(c *gin.Context) { var total int64 base.Count(&total) - var records []model.FinanceRecord + records := make([]model.FinanceRecord, 0) offset := (q.Page - 1) * q.PageSize base.Preload("Partner").Order("record_date DESC, id DESC").Offset(offset).Limit(q.PageSize).Find(&records) diff --git a/backend/internal/handler/inventory.go b/backend/internal/handler/inventory.go index a3ec62a..42ef213 100644 --- a/backend/internal/handler/inventory.go +++ b/backend/internal/handler/inventory.go @@ -138,7 +138,7 @@ func (h *InventoryHandler) Logs(c *gin.Context) { var total int64 query.Count(&total) - var logs []model.InventoryLog + logs := make([]model.InventoryLog, 0) query.Preload("Product").Preload("Warehouse").Offset((page-1)*pageSize).Limit(pageSize).Order("id DESC").Find(&logs) c.JSON(http.StatusOK, gin.H{"data": logs, "total": total, "page": page, "page_size": pageSize}) diff --git a/backend/internal/handler/number_rule.go b/backend/internal/handler/number_rule.go index 6db14be..57092d7 100644 --- a/backend/internal/handler/number_rule.go +++ b/backend/internal/handler/number_rule.go @@ -32,7 +32,7 @@ var defaultRules = []struct { // List GET /api/v1/number-rules func (h *NumberRuleHandler) List(c *gin.Context) { shopID := middleware.GetShopID(c) - var rules []model.NumberRule + rules := make([]model.NumberRule, 0) h.db.Where("shop_id = ?", shopID).Order("id").Find(&rules) if len(rules) == 0 { diff --git a/backend/internal/handler/partner.go b/backend/internal/handler/partner.go index 9a8fc3a..c77f44d 100644 --- a/backend/internal/handler/partner.go +++ b/backend/internal/handler/partner.go @@ -38,7 +38,7 @@ func (h *PartnerHandler) List(c *gin.Context) { var total int64 query.Count(&total) - var partners []model.Partner + partners := make([]model.Partner, 0) query.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&partners) c.JSON(http.StatusOK, gin.H{"data": partners, "total": total, "page": page, "page_size": pageSize}) diff --git a/backend/internal/handler/product.go b/backend/internal/handler/product.go index 1ab399d..9b3175d 100644 --- a/backend/internal/handler/product.go +++ b/backend/internal/handler/product.go @@ -48,7 +48,7 @@ func (h *ProductHandler) List(c *gin.Context) { var total int64 query.Count(&total) - var products []model.Product + products := make([]model.Product, 0) offset := (page - 1) * pageSize query.Preload("Category").Offset(offset).Limit(pageSize). Order("id DESC").Find(&products) diff --git a/backend/internal/handler/product_option.go b/backend/internal/handler/product_option.go index 927a628..7d39467 100644 --- a/backend/internal/handler/product_option.go +++ b/backend/internal/handler/product_option.go @@ -22,7 +22,7 @@ func NewProductOptionHandler(db *gorm.DB) *ProductOptionHandler { func (h *ProductOptionHandler) ListNames(c *gin.Context) { shopID := middleware.GetShopID(c) - var items []model.ProductNameOption + items := make([]model.ProductNameOption, 0) h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items) c.JSON(http.StatusOK, gin.H{"data": items}) } @@ -84,7 +84,7 @@ func (h *ProductOptionHandler) DeleteName(c *gin.Context) { func (h *ProductOptionHandler) ListSeries(c *gin.Context) { shopID := middleware.GetShopID(c) - var items []model.ProductSeriesOption + items := make([]model.ProductSeriesOption, 0) h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items) c.JSON(http.StatusOK, gin.H{"data": items}) } @@ -146,7 +146,7 @@ func (h *ProductOptionHandler) DeleteSeries(c *gin.Context) { func (h *ProductOptionHandler) ListSpecs(c *gin.Context) { shopID := middleware.GetShopID(c) - var items []model.ProductSpecOption + items := make([]model.ProductSpecOption, 0) h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items) c.JSON(http.StatusOK, gin.H{"data": items}) } diff --git a/backend/internal/handler/stock_in.go b/backend/internal/handler/stock_in.go index 57e1e42..810d3e4 100644 --- a/backend/internal/handler/stock_in.go +++ b/backend/internal/handler/stock_in.go @@ -50,7 +50,7 @@ func (h *StockInHandler) List(c *gin.Context) { var total int64 query.Count(&total) - var orders []model.StockInOrder + orders := make([]model.StockInOrder, 0) query.Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer"). Offset((page - 1) * pageSize).Limit(pageSize). Order("id DESC").Find(&orders) diff --git a/backend/internal/handler/stock_out.go b/backend/internal/handler/stock_out.go index b5d0d57..24a5184 100644 --- a/backend/internal/handler/stock_out.go +++ b/backend/internal/handler/stock_out.go @@ -44,7 +44,7 @@ func (h *StockOutHandler) List(c *gin.Context) { var total int64 query.Count(&total) - var orders []model.StockOutOrder + orders := make([]model.StockOutOrder, 0) query.Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer"). Offset((page - 1) * pageSize).Limit(pageSize). Order("id DESC").Find(&orders) diff --git a/backend/internal/handler/user.go b/backend/internal/handler/user.go index 952c4d5..00a80cb 100644 --- a/backend/internal/handler/user.go +++ b/backend/internal/handler/user.go @@ -22,7 +22,7 @@ func NewUserHandler(db *gorm.DB) *UserHandler { // List GET /api/v1/users func (h *UserHandler) List(c *gin.Context) { shopID := middleware.GetShopID(c) - var users []model.User + users := make([]model.User, 0) h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID). Order("id ASC").Find(&users) c.JSON(http.StatusOK, gin.H{"data": users}) diff --git a/backend/internal/handler/warehouse.go b/backend/internal/handler/warehouse.go index d512b2c..97057b6 100644 --- a/backend/internal/handler/warehouse.go +++ b/backend/internal/handler/warehouse.go @@ -20,7 +20,7 @@ func NewWarehouseHandler(db *gorm.DB) *WarehouseHandler { func (h *WarehouseHandler) List(c *gin.Context) { shopID := middleware.GetShopID(c) - var warehouses []model.Warehouse + warehouses := make([]model.Warehouse, 0) h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID).Find(&warehouses) c.JSON(http.StatusOK, gin.H{"data": warehouses}) } diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index c8a23ef..c5e1da5 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -51,7 +51,7 @@ func JWT() gin.HandlerFunc { func AdminOnly() gin.HandlerFunc { return func(c *gin.Context) { role, _ := c.Get(CtxRole) - if role != "admin" { + if role != "admin" && role != "superadmin" { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin only"}) return } diff --git a/backend/internal/service/auth.go b/backend/internal/service/auth.go index 0548f38..1dd4dd9 100644 --- a/backend/internal/service/auth.go +++ b/backend/internal/service/auth.go @@ -116,7 +116,7 @@ func (s *AuthService) Register(in RegisterInput) (*RegisterResult, error) { PasswordHash: string(hash), RealName: in.ManagerName, Phone: in.Phone, - Role: "admin", + Role: "superadmin", IsActive: true, } if err := tx.Create(&user).Error; err != nil { diff --git a/client/lib/core/models/page_result.dart b/client/lib/core/models/page_result.dart index 60aaf2f..072fe1a 100644 --- a/client/lib/core/models/page_result.dart +++ b/client/lib/core/models/page_result.dart @@ -16,7 +16,7 @@ class PageResult { T Function(Map) fromJson, ) { return PageResult( - data: (json['data'] as List) + data: ((json['data'] as List?) ?? []) .map((e) => fromJson(e as Map)) .toList(), total: json['total'] as int, diff --git a/client/lib/core/utils/print_util_web.dart b/client/lib/core/utils/print_util_web.dart index 42520bb..aedcb29 100644 --- a/client/lib/core/utils/print_util_web.dart +++ b/client/lib/core/utils/print_util_web.dart @@ -46,72 +46,76 @@ Future printProductLabelImpl({ - + @@ -123,17 +127,19 @@ body { width: 20mm; height: 38mm; overflow: hidden; background: #fff; } $shopName -
$name
- -
- -
扫码溯源
-
- -
-
$specVal
-
$batchVal
-
$dateVal
+
+
+
$name
+
+
$specVal
+
$batchVal
+
$dateVal
+
+
+
+ +
扫码溯源
+
${contactLine.isNotEmpty ? contactLine : shopName}
diff --git a/client/lib/repositories/number_rule_repository.dart b/client/lib/repositories/number_rule_repository.dart index feb9bd5..785ed00 100644 --- a/client/lib/repositories/number_rule_repository.dart +++ b/client/lib/repositories/number_rule_repository.dart @@ -10,7 +10,7 @@ class NumberRuleRepository { Future> list() async { try { final resp = await _client.get('/number-rules'); - final data = resp.data['data'] as List; + final data = (resp.data['data'] as List?) ?? []; return data .map((e) => NumberRule.fromJson(e as Map)) .toList(); diff --git a/client/lib/repositories/user_repository.dart b/client/lib/repositories/user_repository.dart index 662d0e5..e67e97e 100644 --- a/client/lib/repositories/user_repository.dart +++ b/client/lib/repositories/user_repository.dart @@ -7,7 +7,7 @@ class UserRepository { Future> list() async { final resp = await _client.get('/users'); - final data = resp.data['data'] as List; + final data = (resp.data['data'] as List?) ?? []; return data.map((e) => AppUser.fromJson(e as Map)).toList(); } diff --git a/client/lib/repositories/warehouse_repository.dart b/client/lib/repositories/warehouse_repository.dart index 0f0ab57..5ef356f 100644 --- a/client/lib/repositories/warehouse_repository.dart +++ b/client/lib/repositories/warehouse_repository.dart @@ -12,7 +12,7 @@ class WarehouseRepository { try { final resp = await _client.get('/warehouses'); final body = resp.data as Map; - final raw = body['data'] as List; + final raw = (body['data'] as List?) ?? []; return raw .map((e) => Warehouse.fromJson(e as Map)) .toList(); diff --git a/client/lib/screens/auth/login_screen.dart b/client/lib/screens/auth/login_screen.dart index 7a589bc..ca4b4e2 100644 --- a/client/lib/screens/auth/login_screen.dart +++ b/client/lib/screens/auth/login_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../core/auth/auth_state.dart'; +import '../../core/config/app_config.dart'; import '../../core/theme/app_theme.dart'; import '../../core/storage/login_history.dart'; import '../../providers/connectivity_provider.dart'; @@ -488,13 +489,13 @@ class _LoginScreenState extends ConsumerState { const SizedBox(height: 12), TextButton( onPressed: () => launchUrl( - Uri.parse('https://jiu.51yanmei.com/register/'), + Uri.parse('${AppConfig.publicBaseUrl}/register/'), mode: LaunchMode.externalApplication, ), child: const Text( '还没有门店账号?前往官网注册', style: TextStyle( - fontSize: 13, + fontSize: 15, color: AppTheme.textSecondary, ), ), diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart index 5946a21..71a993f 100644 --- a/client/lib/screens/inventory/inventory_list_screen.dart +++ b/client/lib/screens/inventory/inventory_list_screen.dart @@ -386,7 +386,6 @@ class _InventoryListScreenState extends ConsumerState { DataCell(SizedBox()), DataCell(SizedBox()), DataCell(SizedBox()), - DataCell(SizedBox()), ]) ] : filteredItems diff --git a/client/lib/screens/settings/settings_screen.dart b/client/lib/screens/settings/settings_screen.dart index 7c30b7b..8271627 100644 --- a/client/lib/screens/settings/settings_screen.dart +++ b/client/lib/screens/settings/settings_screen.dart @@ -1578,8 +1578,6 @@ class _BatchImportWidgetState extends ConsumerState<_BatchImportWidget> { '格式:选项编号 | 选项名称 | 备注'), _ImportSlot('商品规格', '/import/product-specs', '格式:选项编号 | 选项名称 | 单品数量 | 备注'), - _ImportSlot('商品编码', '/import/product-codes', - '格式:商品编码:xxxx'), _ImportSlot('库存', '/import/inventory', '格式:商品编号|商品名称|系列|规格|单位|库存数量|单价|金额|生产日期|批次|分类|所在仓库|入库日期|供应商|上次盘点|备注'), ]; diff --git a/client/pubspec.lock b/client/pubspec.lock index f0fbcda..922c788 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.0" collection: dependency: transitive description: @@ -205,14 +205,6 @@ packages: description: flutter source: sdk version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" go_router: dependency: "direct main" description: @@ -225,10 +217,10 @@ packages: dependency: transitive description: name: hooks - sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31 url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.0.0" http: dependency: transitive description: @@ -373,22 +365,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" objective_c: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.4.1" package_config: dependency: transitive description: @@ -694,10 +678,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" url: "https://pub.dev" source: hosted - version: "6.3.29" + version: "6.3.30" url_launcher_ios: dependency: transitive description: @@ -734,10 +718,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: @@ -758,10 +742,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.2.0" web: dependency: "direct main" description: diff --git a/scripts/ci/compile.sh b/scripts/ci/compile.sh index 57afc5a..3ad87e5 100755 --- a/scripts/ci/compile.sh +++ b/scripts/ci/compile.sh @@ -29,10 +29,17 @@ flutter build web --release \ "--dart-define=APP_VERSION=${TAG}" cd .. +# Build marketing site +cd web +npm ci --prefer-offline +npm run build +cd .. + # Package artifacts mkdir -p dist mv backend/jiu-server dist/jiu-server tar -czf dist/web.tar.gz -C client/build web +tar -czf dist/marketing.tar.gz -C web/dist . echo "==> compile: done — dist/ contents:" ls -lh dist/ diff --git a/scripts/ci/deploy.sh b/scripts/ci/deploy.sh index 8406dd6..5f8b928 100755 --- a/scripts/ci/deploy.sh +++ b/scripts/ci/deploy.sh @@ -10,7 +10,7 @@ TAG="$1" echo "==> deploy: tag=${TAG}" # Download from Forgejo if dist/ was not built in this job -if [ ! -f dist/jiu-server ] || [ ! -f dist/web.tar.gz ] || [ ! -f dist/configs.tar.gz ]; then +if [ ! -f dist/jiu-server ] || [ ! -f dist/web.tar.gz ] || [ ! -f dist/configs.tar.gz ] || [ ! -f dist/marketing.tar.gz ]; then echo "==> deploy: dist/ incomplete — downloading assets for ${TAG} from Forgejo" mkdir -p dist @@ -57,6 +57,11 @@ rm -rf /tmp/jiu-web-new mkdir -p /tmp/jiu-web-new tar -xzf dist/web.tar.gz -C /tmp/jiu-web-new --strip-components=1 +# Extract marketing site +rm -rf /tmp/jiu-marketing-new +mkdir -p /tmp/jiu-marketing-new +tar -xzf dist/marketing.tar.gz -C /tmp/jiu-marketing-new + # Setup SSH mkdir -p ~/.ssh printf '%s' "${EC2_SSH_KEY}" > ~/.ssh/ec2_deploy.pem @@ -81,9 +86,9 @@ ${SCP} /tmp/jiu-configs/deploy/nginx-jiu.conf "${EC2_USER}@${EC2_HOST}:/tmp/ngin rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \ /tmp/jiu-web-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-web-new/" -# Upload marketing site (from repo checkout) +# Upload marketing site (built by compile.sh) rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \ - web/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/" + /tmp/jiu-marketing-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/" echo "==> deploy: running remote deploy" diff --git a/scripts/ci/release.sh b/scripts/ci/release.sh index 0bf8e68..85396a3 100755 --- a/scripts/ci/release.sh +++ b/scripts/ci/release.sh @@ -121,6 +121,7 @@ upload_asset() { upload_asset dist/jiu-server upload_asset dist/web.tar.gz +upload_asset dist/marketing.tar.gz upload_asset dist/configs.tar.gz upload_asset dist/jiu-windows-x64.zip diff --git a/scripts/dev-proxy.js b/scripts/dev-proxy.js new file mode 100644 index 0000000..b4874d1 --- /dev/null +++ b/scripts/dev-proxy.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node +// dev-proxy.js — 本地开发反向代理 +// localhost:3000 → 营销站 :3001 / Flutter Web :8888 +// /app/* 路由到 Flutter,其余路由到 Eleventy +// 自动把 Flutter index.html 里的 base href="/" 改写为 /app/,使静态资源请求正确走代理 + +const http = require('http'); +const net = require('net'); + +const PROXY_PORT = parseInt(process.env.PROXY_PORT || '3000', 10); +const ELEVENTY_PORT = parseInt(process.env.ELEVENTY_PORT || '3001', 10); +const FLUTTER_PORT = parseInt(process.env.FLUTTER_PORT || '9090', 10); +const BACKEND_PORT = parseInt(process.env.BACKEND_PORT || '8080', 10); + +function isFlutter(url) { + return url === '/app' || url.startsWith('/app/'); +} + +function isBackend(url) { + return url.startsWith('/api/') || url === '/health' || url.startsWith('/images/'); +} + +function stripApp(url) { + return url.slice(4) || '/'; +} + +function proxyHttp(req, res, port, path) { + const opts = { + hostname: 'localhost', + port, + path, + method: req.method, + headers: { ...req.headers, host: `localhost:${port}` }, + }; + + const pr = http.request(opts, (upstream) => { + const ct = upstream.headers['content-type'] || ''; + + if (port === FLUTTER_PORT && ct.includes('text/html')) { + let body = ''; + upstream.setEncoding('utf8'); + upstream.on('data', (c) => { body += c; }); + upstream.on('end', () => { + // rewrite base href so Flutter asset requests stay under /app/ + body = body.replace(/( res.writeHead(502).end('upstream error')); + req.pipe(pr, { end: true }); +} + +const server = http.createServer((req, res) => { + if (isFlutter(req.url)) { + proxyHttp(req, res, FLUTTER_PORT, stripApp(req.url)); + } else if (isBackend(req.url)) { + proxyHttp(req, res, BACKEND_PORT, req.url); + } else { + proxyHttp(req, res, ELEVENTY_PORT, req.url); + } +}); + +// WebSocket 透传(Eleventy 热重载 / Flutter DevTools) +server.on('upgrade', (req, socket, head) => { + const port = isFlutter(req.url) ? FLUTTER_PORT + : isBackend(req.url) ? BACKEND_PORT + : ELEVENTY_PORT; + const path = isFlutter(req.url) ? stripApp(req.url) : req.url; + + const conn = net.createConnection(port, 'localhost', () => { + conn.write(`${req.method} ${path} HTTP/1.1\r\n`); + for (const [k, v] of Object.entries(req.headers)) { + conn.write(`${k}: ${v}\r\n`); + } + conn.write('\r\n'); + if (head && head.length) conn.write(head); + socket.pipe(conn); + conn.pipe(socket); + }); + + conn.on('error', () => socket.destroy()); + socket.on('error', () => conn.destroy()); +}); + +server.listen(PROXY_PORT, () => { + console.log(`[proxy] http://localhost:${PROXY_PORT}`); + console.log(` / → Eleventy :${ELEVENTY_PORT}`); + console.log(` /app/ → Flutter Web :${FLUTTER_PORT}`); +}); diff --git a/scripts/dev.sh b/scripts/dev.sh index 8b1d673..0f703ae 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -32,6 +32,9 @@ BACKEND_PID_FILE="$LOG_DIR/backend.pid" BACKEND_HASH_FILE="$LOG_DIR/backend.hash" WEB_LOG="$LOG_DIR/web.log" WEB_PID_FILE="$LOG_DIR/web.pid" +ELEVENTY_LOG="$LOG_DIR/eleventy.log" +ELEVENTY_PID_FILE="$LOG_DIR/eleventy.pid" +PROXY_PID_FILE="$LOG_DIR/proxy.pid" mkdir -p "$LOG_DIR" @@ -115,6 +118,79 @@ RUN_BACKEND=true RUN_FRONTEND=true # macOS RUN_WEB=false +cmd_marketing() { + local WEB_DIR="$ROOT/web" + local FLUTTER_WEB_PORT=9090 + local ELEVENTY_PORT=3001 + local PROXY_PORT=3000 + + # 检查依赖 + if ! command -v node &>/dev/null; then + error "未找到 node,请安装 Node.js" + exit 1 + fi + if ! command -v flutter &>/dev/null; then + error "未找到 flutter" + exit 1 + fi + + # 清理旧进程 + for pid_file in "$ELEVENTY_PID_FILE" "$PROXY_PID_FILE" "$WEB_PID_FILE"; do + if [ -f "$pid_file" ]; then + pid=$(cat "$pid_file") + kill "$pid" 2>/dev/null || true + rm -f "$pid_file" + fi + done + for port in $ELEVENTY_PORT $FLUTTER_WEB_PORT $PROXY_PORT; do + pids=$(lsof -ti:"$port" 2>/dev/null || true) + [ -n "$pids" ] && echo "$pids" | xargs kill -9 2>/dev/null || true + done + sleep 0.5 + + # 启动后端(如已运行则跳过) + if ! curl -s -o /dev/null http://localhost:8080/health 2>/dev/null; then + info "启动后端..." + cd "$BACKEND_DIR" + go run main.go >>"$BACKEND_LOG" 2>&1 & + local bp=$! + echo "$bp" > "$BACKEND_PID_FILE" + wait_backend + cd "$ROOT" + else + success "后端已在运行 → http://localhost:8080" + fi + + # 启动 Eleventy(宣传站)在后台,3001 口 + info "启动 Eleventy(宣传站后台)→ :${ELEVENTY_PORT}..." + cd "$WEB_DIR" + npx eleventy --serve --port=$ELEVENTY_PORT >>"$ELEVENTY_LOG" 2>&1 & + echo "$!" > "$ELEVENTY_PID_FILE" + cd "$ROOT" + + # 启动代理(后台) + info "启动代理(后台)→ http://localhost:${PROXY_PORT}..." + PROXY_PORT=$PROXY_PORT ELEVENTY_PORT=$ELEVENTY_PORT FLUTTER_PORT=$FLUTTER_WEB_PORT \ + node "$ROOT/scripts/dev-proxy.js" >>"$LOG_DIR/proxy.log" 2>&1 & + echo "$!" > "$PROXY_PID_FILE" + sleep 1 + + echo "" + success "代理已就绪:http://localhost:${PROXY_PORT}(宣传站)/ http://localhost:${PROXY_PORT}/app/(Flutter)" + echo -e " ${CYAN}Eleventy 日志:tail -f $ELEVENTY_LOG${NC}" + echo "" + info "在前台启动 Flutter Web(支持热重载,按 r 重载,按 q 退出)..." + echo "" + + # Flutter 在前台运行,支持交互热重载 + cd "$CLIENT_DIR" + flutter pub get + flutter run -d chrome --web-port "$FLUTTER_WEB_PORT" \ + "--dart-define=BASE_URL=http://localhost:8080" \ + "--dart-define=PUBLIC_URL=http://localhost:${PROXY_PORT}" \ + "--dart-define=APP_VERSION=dev" +} + show_help() { echo "用法: sh scripts/dev.sh <命令> [选项]" echo "" @@ -126,6 +202,7 @@ show_help() { echo " --backend-only 仅启动后端" echo " --frontend-only 仅启动前端(macOS)" echo " --web-only 仅启动前端(Web)" + echo " marketing 宣传站 + Flutter Web + 代理(localhost:3000,/app/ 路由到 Flutter)" echo "" echo "数据库命令:" echo " seed 写入指定门店测试数据(如 S001)" @@ -139,6 +216,8 @@ show_help() { } case "$COMMAND" in + marketing) + ;; # 在底部所有函数定义完后执行 run) case "${2:-}" in --force) FORCE=true ;; @@ -223,12 +302,13 @@ esac cleanup() { echo "" info "正在关闭服务..." - if [ -f "$WEB_PID_FILE" ]; then - pid=$(cat "$WEB_PID_FILE") - kill "$pid" 2>/dev/null || true - rm -f "$WEB_PID_FILE" - info "Web 服务已停止" - fi + for pid_file in "$WEB_PID_FILE" "$ELEVENTY_PID_FILE" "$PROXY_PID_FILE"; do + if [ -f "$pid_file" ]; then + pid=$(cat "$pid_file") + kill "$pid" 2>/dev/null || true + rm -f "$pid_file" + fi + done info "后端继续在后台运行。如需停止:sh scripts/dev.sh stop" exit 0 } @@ -284,7 +364,7 @@ wait_backend() { info "等待后端启动..." local i=0 while [ $i -lt 30 ]; do - if curl -s -o /dev/null http://localhost:8080/api/v1/auth/login 2>/dev/null; then + if curl -s -o /dev/null http://localhost:8080/health 2>/dev/null; then success "后端已就绪 → http://localhost:8080" return 0 fi @@ -356,6 +436,12 @@ start_web_bg() { } # ════════════════════════════════════════════════════════ + +if [ "$COMMAND" = "marketing" ]; then + cmd_marketing + exit 0 +fi + check_deps if [ "$RUN_BACKEND" = true ]; then diff --git a/web/index.njk b/web/index.njk index f3183c7..05fba6a 100644 --- a/web/index.njk +++ b/web/index.njk @@ -184,7 +184,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }

从入库到出库,
一套系统管到底。

岩美酒库管理系统,为酒行与酒店设计的库存、审核、财务一体化平台。审核驱动业务流,库存与账款自动同步,五端无缝接入。

@@ -537,7 +537,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
  • Web 端 + 移动端
  • 邮件技术支持
  • - 免费开通 + 免费开通
    - 立即开通试用 + 立即开通试用 预约 1 对 1 演示 销售热线 {{ site.support.phone }} · 工作日 9:00-18:00
    diff --git a/web/register.njk b/web/register.njk index 2efb019..0b097e5 100644 --- a/web/register.njk +++ b/web/register.njk @@ -14,10 +14,13 @@ pageStyle: | .form-row { margin-bottom: var(--space-5); } .form-label { display: block; font-size: var(--text-sm); font-weight: 500; color: var(--neutral-700); margin-bottom: var(--space-1-5); } .form-label .req { color: var(--error-500); margin-left: 2px; } - .form-input, .form-textarea { width: 100%; padding: var(--space-2-5) var(--space-3); border: 1px solid var(--neutral-300); border-radius: var(--radius-md); font-size: var(--text-md); font-family: inherit; background: var(--neutral-0); color: var(--neutral-900); transition: border-color .15s, box-shadow .15s; box-sizing: border-box; } + .form-input, .form-textarea { width: 100%; padding: var(--space-2-5) var(--space-3); border: 1.5px solid var(--neutral-300); border-radius: var(--radius-md); font-size: var(--text-md); font-family: inherit; background: var(--neutral-0); color: var(--neutral-900); transition: border-color .15s, box-shadow .15s; box-sizing: border-box; } .form-input:focus, .form-textarea:focus { outline: none; border-color: var(--brand-500); box-shadow: 0 0 0 3px var(--brand-100); } + .form-input.error, .form-textarea.error { border-color: var(--error-500); box-shadow: 0 0 0 3px rgba(239,68,68,.12); } .form-textarea { resize: vertical; min-height: 80px; } .form-helper { font-size: var(--text-xs); color: var(--neutral-400); margin-top: var(--space-1); } + .field-error { font-size: var(--text-xs); color: var(--error-600); margin-top: 4px; display: none; } + .field-error.visible { display: block; } .reg-submit { width: 100%; padding: var(--space-3) var(--space-4); font-size: var(--text-md); font-weight: 600; margin-top: var(--space-2); } .reg-login-hint { text-align: center; margin-top: var(--space-4); font-size: var(--text-sm); color: var(--neutral-500); } .reg-login-hint a { color: var(--brand-600); } @@ -61,24 +64,28 @@ pageStyle: |
    +
    您的酒行正式名称,将显示在所有单据和商品标签上
    +
    实际经营地址,打印标签和单据时展示
    +
    主要负责人真实姓名,也是首个管理员账号的姓名
    +
    对外联系电话,打印商品标签时会展示
    @@ -96,18 +103,21 @@ pageStyle: |
    +
    登录账号(3–30 位,字母/数字/下划线),创建后不可修改
    +
    至少 6 位,建议字母数字组合
    +
    @@ -115,7 +125,7 @@ pageStyle: | 立即注册 - +
    @@ -152,7 +162,7 @@ pageStyle: |

    如忘记门店编码,登录后可在「系统设置 → 关于」中查看。

    - + 前往登录 @@ -162,49 +172,86 @@ pageStyle: |