feat: 扫码商品详情页重设计 + 宣传主页 + nginx 路由完善
- public_product_screen: 1:1 复刻设计稿,新增商品介绍、批次防伪(自定义圆形印章)、完整商品参数表、门店营业时间、页脚链接 - app_theme: 完整品牌色系(brand/gray/accent/semantic) - shop model: 新增 business_hours 字段,public 接口同步返回 - nginx: 新增 /product/ → Flutter app 扫码入口,修复 /docs /download html 后缀兼容 - web/: 新增宣传主页、功能页、文档页、扫码 HTML 及设计资源 - app_shell: 界面优化 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- name: Build Flutter Web
|
||||
working-directory: client
|
||||
run: flutter build web --release --dart-define=BASE_URL=https://jiu.51yanmei.com --dart-define=PUBLIC_URL=https://jiu.51yanmei.com
|
||||
run: flutter build web --release --base-href=/app/ --dart-define=BASE_URL=https://jiu.51yanmei.com --dart-define=PUBLIC_URL=https://jiu.51yanmei.com
|
||||
|
||||
- name: Package & Create Forgejo Release
|
||||
env:
|
||||
@@ -75,6 +75,8 @@ ${RECENT_LOGS}"
|
||||
scp -O -i ~/.ssh/ec2_deploy.pem backend/jiu-server ${EC2_USER}@${EC2_HOST}:/tmp/jiu-server
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem" \
|
||||
client/build/web/ ${EC2_USER}@${EC2_HOST}:/tmp/jiu-web-new/
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem" \
|
||||
web/ ${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/
|
||||
ssh -i ~/.ssh/ec2_deploy.pem ${EC2_USER}@${EC2_HOST} << 'ENDSSH'
|
||||
sudo systemctl stop jiu
|
||||
cp /tmp/jiu-server /opt/jiu/backend/jiu-server
|
||||
@@ -87,6 +89,9 @@ ${RECENT_LOGS}"
|
||||
rm -rf /opt/jiu/web-old
|
||||
mv /opt/jiu/web /opt/jiu/web-old 2>/dev/null || true
|
||||
mv /tmp/jiu-web-new /opt/jiu/web
|
||||
mkdir -p /opt/jiu/marketing
|
||||
rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/
|
||||
rm -rf /tmp/jiu-marketing-new
|
||||
sudo nginx -s reload
|
||||
ENDSSH
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -29,7 +31,37 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Return only public-safe fields (no price/stock info)
|
||||
// Fetch shop public info
|
||||
var shop model.Shop
|
||||
shopData := gin.H{}
|
||||
if err := h.db.Where("id = ?", product.ShopID).First(&shop).Error; err == nil {
|
||||
shopData = gin.H{
|
||||
"name": shop.Name,
|
||||
"code": shop.Code,
|
||||
"address": shop.Address,
|
||||
"phone": shop.Phone,
|
||||
"business_hours": shop.BusinessHours,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch latest inventory batch for this product
|
||||
var inv model.Inventory
|
||||
batchData := gin.H(nil)
|
||||
if err := h.db.Where("product_id = ? AND quantity > 0 AND deleted_at IS NULL", product.ID).
|
||||
Order("created_at DESC").
|
||||
First(&inv).Error; err == nil {
|
||||
var pdStr *string
|
||||
if inv.ProductionDate != nil {
|
||||
s := inv.ProductionDate.Time.Format("2006-01-02")
|
||||
pdStr = &s
|
||||
}
|
||||
batchData = gin.H{
|
||||
"production_date": pdStr,
|
||||
"batch_no": inv.BatchNo,
|
||||
"in_stock_date": inv.CreatedAt.Format("2006-01-02"),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": gin.H{
|
||||
"id": product.ID,
|
||||
@@ -40,6 +72,31 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
|
||||
"unit": product.Unit,
|
||||
"description": product.Description,
|
||||
"images": product.Images,
|
||||
"shop": shopData,
|
||||
"batch": batchData,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetRelease GET /api/v1/public/release (no auth)
|
||||
func (h *PublicHandler) GetRelease(c *gin.Context) {
|
||||
version := os.Getenv("APP_VERSION")
|
||||
if version == "" {
|
||||
version = "1.0.0"
|
||||
}
|
||||
buildDate := os.Getenv("BUILD_DATE")
|
||||
if buildDate == "" {
|
||||
buildDate = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": version,
|
||||
"build_date": buildDate,
|
||||
"download_urls": gin.H{
|
||||
"macos": os.Getenv("DOWNLOAD_URL_MACOS"),
|
||||
"windows": os.Getenv("DOWNLOAD_URL_WINDOWS"),
|
||||
"web": "https://jiu.51yanmei.com/app",
|
||||
},
|
||||
"changelog": []gin.H{},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ type Shop struct {
|
||||
Code string `gorm:"size:50;uniqueIndex" json:"code"`
|
||||
Address string `gorm:"size:255" json:"address"`
|
||||
Phone string `gorm:"size:30" json:"phone"`
|
||||
BusinessHours string `gorm:"size:100" json:"business_hours"`
|
||||
ManagerName string `gorm:"size:50" json:"manager_name"`
|
||||
BusinessLicense string `gorm:"size:500" json:"business_license"`
|
||||
ShopPhotos JSON `gorm:"type:json" json:"shop_photos,omitempty"`
|
||||
|
||||
@@ -51,10 +51,11 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
auth.POST("/refresh", authH.Refresh)
|
||||
}
|
||||
|
||||
// 商品公开详情(无需登录)
|
||||
// 公开接口(无需登录)
|
||||
public := v1.Group("/public")
|
||||
{
|
||||
public.GET("/products/:public_id", publicH.GetProduct)
|
||||
public.GET("/release", publicH.GetRelease)
|
||||
}
|
||||
|
||||
// 需要 JWT 的路由(ReadOnly 中间件:只读用户不可执行写操作)
|
||||
|
||||
@@ -37,16 +37,17 @@ class _RouterNotifier extends ChangeNotifier {
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
final authState = _ref.read(authStateProvider);
|
||||
final isLoggedIn = authState.isLoggedIn;
|
||||
final isLoginRoute = state.matchedLocation == '/login';
|
||||
final isPublicRoute = state.matchedLocation.startsWith('/product/');
|
||||
final loc = state.matchedLocation;
|
||||
final isPublicRoute = loc == '/login' ||
|
||||
loc.startsWith('/product/');
|
||||
final result = !authState.initialized
|
||||
? null
|
||||
: (!isLoggedIn && !isLoginRoute && !isPublicRoute)
|
||||
: (!isLoggedIn && !isPublicRoute)
|
||||
? '/login'
|
||||
: (isLoggedIn && isLoginRoute)
|
||||
: (isLoggedIn && loc == '/login')
|
||||
? '/stock-in'
|
||||
: null;
|
||||
debugPrint('[Router] redirect: location=${state.matchedLocation}'
|
||||
debugPrint('[Router] redirect: location=$loc'
|
||||
' initialized=${authState.initialized}'
|
||||
' isLoggedIn=$isLoggedIn'
|
||||
' → ${result ?? "null (no redirect)"}');
|
||||
@@ -71,7 +72,7 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
refreshListenable: notifier,
|
||||
redirect: notifier.redirect,
|
||||
routes: [
|
||||
// Public route — no auth, no shell nav bar
|
||||
// Public product scan — no auth, no shell nav bar
|
||||
GoRoute(
|
||||
path: '/product/:public_id',
|
||||
builder: (context, state) =>
|
||||
|
||||
@@ -1,17 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
static const Color primary = Color(0xFF1565C0);
|
||||
static const Color primaryDark = Color(0xFF0D47A1);
|
||||
static const Color primaryLight = Color(0xFF1976D2);
|
||||
static const Color accent = Color(0xFFFF6F00);
|
||||
static const Color success = Color(0xFF2E7D32);
|
||||
static const Color danger = Color(0xFFC62828);
|
||||
static const Color background = Color(0xFFF5F5F5);
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// LEGACY API — field names preserved, values updated to 岩美 brand.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
static const Color primary = Color(0xFF2563AC);
|
||||
static const Color primaryDark = Color(0xFF154072);
|
||||
static const Color primaryLight = Color(0xFF4F86C6);
|
||||
|
||||
/// Bordeaux accent — wine context cue. Was warm orange #FF6F00.
|
||||
static const Color accent = Color(0xFF8B2331);
|
||||
|
||||
static const Color success = Color(0xFF2E8B57);
|
||||
static const Color danger = Color(0xFFD14343);
|
||||
static const Color background = Color(0xFFF5F7FA);
|
||||
static const Color surface = Color(0xFFFFFFFF);
|
||||
static const Color border = Color(0xFFE0E0E0);
|
||||
static const Color textPrimary = Color(0xFF212121);
|
||||
static const Color textSecondary = Color(0xFF757575);
|
||||
static const Color border = Color(0xFFDCE2EB);
|
||||
static const Color textPrimary = Color(0xFF232934);
|
||||
static const Color textSecondary = Color(0xFF6E7888);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Full brand scale (50..900)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color brand50 = Color(0xFFEEF4FB);
|
||||
static const Color brand100 = Color(0xFFD6E5F5);
|
||||
static const Color brand200 = Color(0xFFADC9EA);
|
||||
static const Color brand300 = Color(0xFF7FA8DA);
|
||||
static const Color brand400 = Color(0xFF4F86C6);
|
||||
static const Color brand500 = Color(0xFF2563AC);
|
||||
static const Color brand600 = Color(0xFF1B4F8E);
|
||||
static const Color brand700 = Color(0xFF154072);
|
||||
static const Color brand800 = Color(0xFF0F3057);
|
||||
static const Color brand900 = Color(0xFF0A1F3B);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Full neutral (gray) scale
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color gray0 = Color(0xFFFFFFFF);
|
||||
static const Color gray25 = Color(0xFFFBFCFD);
|
||||
static const Color gray50 = Color(0xFFF5F7FA);
|
||||
static const Color gray100 = Color(0xFFECEFF4);
|
||||
static const Color gray200 = Color(0xFFDCE2EB);
|
||||
static const Color gray300 = Color(0xFFC2CAD6);
|
||||
static const Color gray400 = Color(0xFF99A3B3);
|
||||
static const Color gray500 = Color(0xFF6E7888);
|
||||
static const Color gray600 = Color(0xFF4F5867);
|
||||
static const Color gray700 = Color(0xFF353C48);
|
||||
static const Color gray800 = Color(0xFF232934);
|
||||
static const Color gray900 = Color(0xFF141821);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Accent (bordeaux) scale
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color accent50 = Color(0xFFFAEEF0);
|
||||
static const Color accent100 = Color(0xFFF1D2D7);
|
||||
static const Color accent500 = Color(0xFF8B2331);
|
||||
static const Color accent700 = Color(0xFF5F1621);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Semantic scale
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color success50 = Color(0xFFE8F5EE);
|
||||
static const Color success500 = Color(0xFF2E8B57);
|
||||
static const Color success700 = Color(0xFF1F6B41);
|
||||
|
||||
static const Color warning50 = Color(0xFFFFF4DB);
|
||||
static const Color warning500 = Color(0xFFE08E00);
|
||||
static const Color warning700 = Color(0xFFA66700);
|
||||
|
||||
static const Color danger50 = Color(0xFFFDECEC);
|
||||
static const Color danger500 = Color(0xFFD14343);
|
||||
static const Color danger700 = Color(0xFF9E2A2A);
|
||||
|
||||
static const Color info50 = Color(0xFFE5F1FB);
|
||||
static const Color info500 = Color(0xFF2F7BD0);
|
||||
static const Color info700 = Color(0xFF1F5C9F);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// State colors
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const Color rowHover = Color(0xFFEEF4FB);
|
||||
static const Color tableHeader = Color(0xFFF5F7FA);
|
||||
static const Color borderSubtle = Color(0xFFECEFF4);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Radii
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const double radiusSm = 4.0;
|
||||
static const double radiusMd = 6.0;
|
||||
static const double radiusLg = 10.0;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Spacing (4px base)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
static const double space1 = 4.0;
|
||||
static const double space2 = 8.0;
|
||||
static const double space3 = 12.0;
|
||||
static const double space4 = 16.0;
|
||||
static const double space5 = 20.0;
|
||||
static const double space6 = 24.0;
|
||||
static const double space8 = 32.0;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ThemeData
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
static ThemeData light() {
|
||||
return ThemeData(
|
||||
@@ -21,78 +114,172 @@ class AppTheme {
|
||||
brightness: Brightness.light,
|
||||
).copyWith(
|
||||
primary: primary,
|
||||
surface: surface,
|
||||
onPrimary: Colors.white,
|
||||
secondary: brand400,
|
||||
surface: surface,
|
||||
onSurface: textPrimary,
|
||||
error: danger,
|
||||
outline: border,
|
||||
outlineVariant: borderSubtle,
|
||||
),
|
||||
scaffoldBackgroundColor: background,
|
||||
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
toolbarHeight: 56,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: surface,
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
side: const BorderSide(color: border, width: 0.5),
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
|
||||
cardTheme: CardThemeData(
|
||||
color: surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusLg),
|
||||
side: const BorderSide(color: border, width: 1),
|
||||
),
|
||||
margin: const EdgeInsets.all(0),
|
||||
),
|
||||
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primary,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(0, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
minimumSize: const Size(0, 40),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
elevation: 0,
|
||||
).copyWith(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) return primaryDark;
|
||||
if (states.contains(WidgetState.hovered)) return brand600;
|
||||
return primary;
|
||||
}),
|
||||
overlayColor:
|
||||
WidgetStateProperty.all(Colors.white.withValues(alpha: 0.08)),
|
||||
),
|
||||
),
|
||||
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: primary,
|
||||
minimumSize: const Size(0, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
side: const BorderSide(color: primary),
|
||||
foregroundColor: brand700,
|
||||
minimumSize: const Size(0, 40),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
side: const BorderSide(color: border, width: 1),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).copyWith(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) return gray100;
|
||||
if (states.contains(WidgetState.hovered)) return gray50;
|
||||
return surface;
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: primary,
|
||||
minimumSize: const Size(0, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusSm),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: primary, width: 1.5),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: danger),
|
||||
),
|
||||
hoverColor: gray50,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: surface,
|
||||
labelStyle: const TextStyle(fontSize: 13, color: textSecondary),
|
||||
hintStyle: TextStyle(fontSize: 13, color: gray400),
|
||||
),
|
||||
dividerTheme: const DividerThemeData(color: border, thickness: 0.5),
|
||||
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: borderSubtle,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: gray100,
|
||||
labelStyle: const TextStyle(fontSize: 12, color: textPrimary),
|
||||
side: BorderSide.none,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
),
|
||||
|
||||
tooltipTheme: TooltipThemeData(
|
||||
decoration: BoxDecoration(
|
||||
color: gray900,
|
||||
borderRadius: BorderRadius.circular(radiusSm),
|
||||
),
|
||||
textStyle: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
),
|
||||
|
||||
textTheme: const TextTheme(
|
||||
bodyLarge: TextStyle(fontSize: 14, color: textPrimary),
|
||||
bodyMedium: TextStyle(fontSize: 14, color: textPrimary),
|
||||
bodySmall: TextStyle(fontSize: 12, color: textSecondary),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textPrimary),
|
||||
labelMedium: TextStyle(fontSize: 12, color: textSecondary),
|
||||
displayLarge: TextStyle(fontSize: 36, fontWeight: FontWeight.w600, color: brand900, height: 1.2, letterSpacing: 0.4),
|
||||
displayMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.w600, color: brand900, height: 1.2, letterSpacing: 0.4),
|
||||
headlineSmall: TextStyle(fontSize: 22, fontWeight: FontWeight.w600, color: gray900, height: 1.3),
|
||||
titleLarge: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: gray900, height: 1.35),
|
||||
titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: textPrimary, height: 1.4),
|
||||
titleSmall: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: textPrimary, height: 1.4),
|
||||
bodyLarge: TextStyle(fontSize: 14, color: textPrimary, height: 1.55),
|
||||
bodyMedium: TextStyle(fontSize: 14, color: textPrimary, height: 1.55),
|
||||
bodySmall: TextStyle(fontSize: 12, color: textSecondary, height: 1.5),
|
||||
labelLarge: TextStyle(fontSize: 13, color: textPrimary, fontWeight: FontWeight.w500),
|
||||
labelMedium: TextStyle(fontSize: 12, color: textSecondary, letterSpacing: 0.4),
|
||||
labelSmall: TextStyle(fontSize: 11, color: textSecondary, letterSpacing: 0.4),
|
||||
),
|
||||
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: FadeUpwardsPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class _JiuAppState extends ConsumerState<JiuApp> {
|
||||
Widget build(BuildContext context) {
|
||||
final router = ref.watch(appRouterProvider);
|
||||
return MaterialApp.router(
|
||||
title: '酒库管理系统',
|
||||
title: '岩美',
|
||||
theme: AppTheme.light(),
|
||||
routerConfig: router,
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -591,10 +591,10 @@ class _ShopButton extends StatelessWidget {
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.wine_bar, color: Colors.white, size: 22),
|
||||
SizedBox(width: 8),
|
||||
_YanmeiMark(size: 28),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'酒库管理系统',
|
||||
'岩美',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
@@ -608,6 +608,70 @@ class _ShopButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Brand mark widget — approximates the 岩美 logo SVG without flutter_svg.
|
||||
/// Dark blue rounded rect, white mountain/wave strokes, bordeaux dot.
|
||||
class _YanmeiMark extends StatelessWidget {
|
||||
final double size;
|
||||
const _YanmeiMark({this.size = 32});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F3057),
|
||||
borderRadius: BorderRadius.circular(size * 0.19),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: _YanmeiMarkPainter(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _YanmeiMarkPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final w = size.width;
|
||||
final h = size.height;
|
||||
final paint = Paint()
|
||||
..color = Colors.white
|
||||
..strokeWidth = w * 0.055
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
|
||||
// Mountain/wave path: M14 38 L22 22 L32 32 L42 22 L50 38 (on 64px grid)
|
||||
final path = Path();
|
||||
path.moveTo(w * 0.219, h * 0.594);
|
||||
path.lineTo(w * 0.344, h * 0.344);
|
||||
path.lineTo(w * 0.500, h * 0.500);
|
||||
path.lineTo(w * 0.656, h * 0.344);
|
||||
path.lineTo(w * 0.781, h * 0.594);
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
// Horizontal baseline: M12 46 L52 46 (on 64px grid)
|
||||
canvas.drawLine(
|
||||
Offset(w * 0.1875, h * 0.719),
|
||||
Offset(w * 0.8125, h * 0.719),
|
||||
paint,
|
||||
);
|
||||
|
||||
// Bordeaux dot: circle cx=32 cy=52 r=2.4 (on 64px grid)
|
||||
canvas.drawCircle(
|
||||
Offset(w * 0.500, h * 0.859),
|
||||
w * 0.042,
|
||||
Paint()
|
||||
..color = const Color(0xFFC97B86)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
@@ -18,18 +18,18 @@
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
<meta name="description" content="岩美酒库管理系统 — 酒店饮品库存与采购管理平台">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="jiu_client">
|
||||
<meta name="apple-mobile-web-app-title" content="岩美">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>jiu_client</title>
|
||||
<title>岩美</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jiu_client",
|
||||
"short_name": "jiu_client",
|
||||
"name": "岩美",
|
||||
"short_name": "岩美",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"background_color": "#2563AC",
|
||||
"theme_color": "#2563AC",
|
||||
"description": "岩美酒库管理系统 — 酒店饮品库存与采购管理平台",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
|
||||
+39
-7
@@ -7,10 +7,7 @@ server {
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
root /opt/jiu/web;
|
||||
index index.html;
|
||||
|
||||
# 商品图片静态文件(^~ 阻止正则 location 拦截 .jpg 等后缀请求)
|
||||
# 商品图片静态文件
|
||||
location ^~ /images/ {
|
||||
alias /opt/jiu/images/;
|
||||
expires 30d;
|
||||
@@ -25,9 +22,44 @@ server {
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# Flutter SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
# Flutter 管理端(/app/ 子路径)
|
||||
location /app/ {
|
||||
alias /opt/jiu/web/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /app/index.html;
|
||||
}
|
||||
|
||||
# Flutter 公开商品扫码页(/product/:id → Flutter app)
|
||||
location /product/ {
|
||||
alias /opt/jiu/web/;
|
||||
try_files $uri /app/index.html;
|
||||
}
|
||||
|
||||
# 营销站点 — 精确路径
|
||||
location = / {
|
||||
root /opt/jiu/marketing;
|
||||
try_files /index.html =404;
|
||||
}
|
||||
location ~ ^/(docs|download)(\.html)?$ {
|
||||
root /opt/jiu/marketing;
|
||||
try_files /$1.html =404;
|
||||
}
|
||||
location /features/ {
|
||||
root /opt/jiu/marketing;
|
||||
try_files $uri $uri.html =404;
|
||||
}
|
||||
|
||||
# 营销站点 — 扫码 HTML 页(保留旧链接兼容)
|
||||
location /scan/ {
|
||||
root /opt/jiu/marketing;
|
||||
try_files /scan.html =404;
|
||||
}
|
||||
|
||||
# 营销静态资源(CSS / SVG / 图片等)
|
||||
location /assets/ {
|
||||
root /opt/jiu/marketing;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
/* =========================================================================
|
||||
岩美 Design System — Foundations
|
||||
Tokens for color, type, spacing, radius, shadow.
|
||||
Import once at root: <link rel="stylesheet" href="colors_and_type.css">
|
||||
========================================================================= */
|
||||
|
||||
:root {
|
||||
/* ---------- Brand: Primary (Slate Blue) ---------- */
|
||||
/* Trustworthy enterprise blue with a slight slate cast. */
|
||||
--brand-50: #EEF4FB;
|
||||
--brand-100: #D6E5F5;
|
||||
--brand-200: #ADC9EA;
|
||||
--brand-300: #7FA8DA;
|
||||
--brand-400: #4F86C6;
|
||||
--brand-500: #2563AC; /* Primary action */
|
||||
--brand-600: #1B4F8E; /* Hover */
|
||||
--brand-700: #154072; /* Pressed */
|
||||
--brand-800: #0F3057;
|
||||
--brand-900: #0A1F3B; /* Brand ink — headers, logo on light bg */
|
||||
|
||||
/* ---------- Neutrals (cool slate gray) ---------- */
|
||||
--gray-0: #FFFFFF;
|
||||
--gray-25: #FBFCFD;
|
||||
--gray-50: #F5F7FA;
|
||||
--gray-100: #ECEFF4;
|
||||
--gray-200: #DCE2EB;
|
||||
--gray-300: #C2CAD6;
|
||||
--gray-400: #99A3B3;
|
||||
--gray-500: #6E7888;
|
||||
--gray-600: #4F5867;
|
||||
--gray-700: #353C48;
|
||||
--gray-800: #232934;
|
||||
--gray-900: #141821;
|
||||
|
||||
/* ---------- Accent (bordeaux / 酒红) ---------- */
|
||||
/* Used sparingly: brand context cue (wine), key highlights, marketing only. */
|
||||
--accent-50: #FAEEF0;
|
||||
--accent-100: #F1D2D7;
|
||||
--accent-300: #C97B86;
|
||||
--accent-500: #8B2331;
|
||||
--accent-700: #5F1621;
|
||||
|
||||
/* ---------- Semantic ---------- */
|
||||
--success-50: #E8F5EE;
|
||||
--success-500: #2E8B57;
|
||||
--success-700: #1F6B41;
|
||||
|
||||
--warning-50: #FFF4DB;
|
||||
--warning-500: #E08E00;
|
||||
--warning-700: #A66700;
|
||||
|
||||
--danger-50: #FDECEC;
|
||||
--danger-500: #D14343;
|
||||
--danger-700: #9E2A2A;
|
||||
|
||||
--info-50: #E5F1FB;
|
||||
--info-500: #2F7BD0;
|
||||
--info-700: #1F5C9F;
|
||||
|
||||
/* ---------- Semantic foreground / background ---------- */
|
||||
--bg-app: var(--gray-50);
|
||||
--bg-surface: var(--gray-0);
|
||||
--bg-raised: var(--gray-0);
|
||||
--bg-sunken: var(--gray-100);
|
||||
--bg-overlay: rgba(20, 24, 33, 0.45);
|
||||
|
||||
--fg-default: var(--gray-800);
|
||||
--fg-muted: var(--gray-600);
|
||||
--fg-subtle: var(--gray-500);
|
||||
--fg-disabled: var(--gray-400);
|
||||
--fg-on-brand: #FFFFFF;
|
||||
--fg-link: var(--brand-500);
|
||||
|
||||
--border-subtle: var(--gray-100);
|
||||
--border-default: var(--gray-200);
|
||||
--border-strong: var(--gray-300);
|
||||
--border-focus: var(--brand-500);
|
||||
|
||||
/* ---------- Typography ---------- */
|
||||
/* Chinese-primary stack with PingFang on macOS/iOS, Microsoft YaHei on Windows,
|
||||
Noto Sans SC as web fallback (loaded via Google Fonts in index files). */
|
||||
--font-sans: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
||||
"Source Han Sans CN", "Noto Sans SC", -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-display: var(--font-sans);
|
||||
--font-mono: "JetBrains Mono", "SF Mono", "Roboto Mono", Menlo, Consolas,
|
||||
"Microsoft YaHei", monospace;
|
||||
|
||||
/* Type scale — mobile-first, scales up for desktop dashboards */
|
||||
--text-2xs: 11px;
|
||||
--text-xs: 12px;
|
||||
--text-sm: 13px;
|
||||
--text-md: 14px; /* dashboard body default */
|
||||
--text-lg: 16px;
|
||||
--text-xl: 18px;
|
||||
--text-2xl: 22px;
|
||||
--text-3xl: 28px;
|
||||
--text-4xl: 36px;
|
||||
--text-5xl: 48px;
|
||||
|
||||
--leading-tight: 1.25;
|
||||
--leading-snug: 1.4;
|
||||
--leading-normal: 1.55;
|
||||
--leading-loose: 1.75;
|
||||
|
||||
--weight-regular: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-semibold: 600;
|
||||
--weight-bold: 700;
|
||||
|
||||
/* Tracking — Chinese reads better with subtle positive tracking */
|
||||
--tracking-tight: -0.01em;
|
||||
--tracking-normal: 0;
|
||||
--tracking-wide: 0.02em;
|
||||
--tracking-cn-display: 0.04em; /* Chinese display headers */
|
||||
|
||||
/* ---------- Spacing (4px base) ---------- */
|
||||
--space-0: 0;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
--space-20: 80px;
|
||||
--space-24: 96px;
|
||||
|
||||
/* ---------- Radius — restrained, enterprise-grade ---------- */
|
||||
--radius-xs: 2px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px; /* default for inputs, buttons, badges */
|
||||
--radius-lg: 10px; /* cards */
|
||||
--radius-xl: 14px;
|
||||
--radius-pill: 999px;
|
||||
|
||||
/* ---------- Elevation — soft, neutral, no colored shadows ---------- */
|
||||
--shadow-xs: 0 1px 2px rgba(20, 24, 33, 0.04);
|
||||
--shadow-sm: 0 1px 2px rgba(20, 24, 33, 0.06), 0 1px 3px rgba(20, 24, 33, 0.04);
|
||||
--shadow-md: 0 2px 4px rgba(20, 24, 33, 0.06), 0 4px 8px rgba(20, 24, 33, 0.05);
|
||||
--shadow-lg: 0 4px 12px rgba(20, 24, 33, 0.08), 0 12px 24px rgba(20, 24, 33, 0.06);
|
||||
--shadow-xl: 0 8px 20px rgba(20, 24, 33, 0.10), 0 20px 40px rgba(20, 24, 33, 0.08);
|
||||
--shadow-inset: inset 0 1px 0 rgba(255,255,255,0.6), inset 0 -1px 0 rgba(20,24,33,0.04);
|
||||
--ring-focus: 0 0 0 3px rgba(37, 99, 172, 0.22);
|
||||
|
||||
/* ---------- Motion ---------- */
|
||||
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
||||
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1.2);
|
||||
--ease-decelerate: cubic-bezier(0, 0, 0.2, 1);
|
||||
--duration-fast: 120ms;
|
||||
--duration-base: 180ms;
|
||||
--duration-slow: 240ms;
|
||||
|
||||
/* ---------- Layout ---------- */
|
||||
--layout-sidebar: 240px;
|
||||
--layout-sidebar-collapsed: 64px;
|
||||
--layout-topbar: 56px;
|
||||
--layout-content-max: 1440px;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
Semantic element styles — apply to base elements within design system docs.
|
||||
These do NOT bleed into ui_kits / pages with their own styles.
|
||||
========================================================================= */
|
||||
.ds-typography {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--fg-default);
|
||||
font-feature-settings: "tnum" 1, "ss01" 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.ds-typography h1,
|
||||
.ds-h1 {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: var(--weight-semibold);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: var(--tracking-cn-display);
|
||||
color: var(--brand-900);
|
||||
margin: 0 0 var(--space-4);
|
||||
}
|
||||
.ds-typography h2,
|
||||
.ds-h2 {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--weight-semibold);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: var(--tracking-cn-display);
|
||||
color: var(--brand-900);
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
.ds-typography h3,
|
||||
.ds-h3 {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--weight-semibold);
|
||||
line-height: var(--leading-snug);
|
||||
color: var(--gray-900);
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
.ds-typography h4,
|
||||
.ds-h4 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--weight-semibold);
|
||||
line-height: var(--leading-snug);
|
||||
color: var(--gray-900);
|
||||
margin: 0 0 var(--space-2);
|
||||
}
|
||||
.ds-typography h5,
|
||||
.ds-h5 {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--weight-semibold);
|
||||
line-height: var(--leading-snug);
|
||||
color: var(--gray-800);
|
||||
margin: 0 0 var(--space-2);
|
||||
}
|
||||
.ds-typography p,
|
||||
.ds-body {
|
||||
font-size: var(--text-md);
|
||||
font-weight: var(--weight-regular);
|
||||
line-height: var(--leading-normal);
|
||||
color: var(--fg-default);
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
.ds-typography small,
|
||||
.ds-caption {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--fg-muted);
|
||||
line-height: var(--leading-snug);
|
||||
}
|
||||
.ds-typography code,
|
||||
.ds-mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.93em;
|
||||
background: var(--gray-100);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--gray-800);
|
||||
}
|
||||
.ds-num {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: "tnum" 1;
|
||||
}
|
||||
.ds-label {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--weight-medium);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: none; /* Chinese never uppercases */
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
/* Focus ring shared across the system */
|
||||
.ds-focusable:focus-visible,
|
||||
:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--ring-focus);
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 64" fill="none">
|
||||
<rect width="64" height="64" rx="12" fill="#FFFFFF"></rect>
|
||||
<g stroke="#0F3057" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 38 L22 22 L32 32 L42 22 L50 38" fill="none"></path>
|
||||
<path d="M12 46 L52 46" fill="none"></path>
|
||||
</g>
|
||||
<circle cx="32" cy="52" r="2.4" fill="#C97B86"></circle>
|
||||
<text x="82" y="38" font-family="'PingFang SC','Hiragino Sans GB','Microsoft YaHei','Noto Sans SC',sans-serif" font-size="28" font-weight="700" letter-spacing="2" fill="#FFFFFF">岩美</text>
|
||||
<text x="83" y="55" font-family="'PingFang SC','Hiragino Sans GB','Microsoft YaHei','Noto Sans SC',sans-serif" font-size="10" font-weight="500" letter-spacing="2.4" fill="#ADC9EA">酒库管理系统</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 878 B |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 64" fill="none">
|
||||
|
||||
<rect width="64" height="64" rx="12" fill="#0F3057"></rect>
|
||||
<g stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 38 L22 22 L32 32 L42 22 L50 38" fill="none"></path>
|
||||
<path d="M12 46 L52 46" fill="none"></path>
|
||||
</g>
|
||||
<circle cx="32" cy="52" r="2.4" fill="#C97B86"></circle>
|
||||
|
||||
|
||||
<text x="82" y="38" font-family="'PingFang SC','Hiragino Sans GB','Microsoft YaHei','Noto Sans SC',sans-serif" font-size="28" font-weight="700" letter-spacing="2" fill="#0F3057">岩美</text>
|
||||
|
||||
|
||||
<text x="83" y="55" font-family="'PingFang SC','Hiragino Sans GB','Microsoft YaHei','Noto Sans SC',sans-serif" font-size="10" font-weight="500" letter-spacing="2.4" fill="#6E7888">酒库管理系统</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 889 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
|
||||
<rect width="64" height="64" rx="12" fill="#0F3057"></rect>
|
||||
<g stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
<path d="M14 38 L22 22 L32 32 L42 22 L50 38" fill="none"></path>
|
||||
|
||||
<path d="M12 46 L52 46" fill="none"></path>
|
||||
</g>
|
||||
|
||||
<circle cx="32" cy="52" r="2.4" fill="#C97B86"></circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
+204
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>文档 — 岩美酒库管理系统</title>
|
||||
<link rel="stylesheet" href="assets/colors_and_type.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; font-family: var(--font-sans); color: var(--fg-default); background: var(--bg-surface); -webkit-font-smoothing: antialiased; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.btn { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 18px; border-radius: var(--radius-md); font-size: 14px; font-weight: 500; border: 1px solid transparent; cursor: pointer; transition: background 120ms; text-decoration: none; }
|
||||
.btn-primary { background: var(--brand-500); color: #fff; }
|
||||
.btn-primary:hover { background: var(--brand-600); }
|
||||
.icon { width: 18px; height: 18px; }
|
||||
|
||||
.nav { position: sticky; top: 0; z-index: 50; background: rgba(255,255,255,0.88); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border-subtle); }
|
||||
.nav-inner { height: 64px; display: flex; align-items: center; justify-content: space-between; max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.nav-brand { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 700; color: var(--brand-900); }
|
||||
.nav-brand img { height: 30px; }
|
||||
.nav-links { display: flex; gap: 2px; }
|
||||
.nav-links a { padding: 7px 14px; border-radius: var(--radius-md); font-size: 14px; color: var(--gray-700); transition: background 120ms; }
|
||||
.nav-links a:hover { background: var(--gray-100); }
|
||||
.nav-cta { display: flex; gap: 10px; }
|
||||
|
||||
.docs-layout { display: grid; grid-template-columns: 220px 1fr; gap: 0; min-height: calc(100vh - 64px); }
|
||||
.docs-sidebar { border-right: 1px solid var(--border-default); padding: 32px 0; position: sticky; top: 64px; height: calc(100vh - 64px); overflow-y: auto; }
|
||||
.sidebar-group { margin-bottom: 28px; }
|
||||
.sidebar-group-title { font-size: 11px; font-weight: 600; letter-spacing: 0.1em; color: var(--fg-subtle); text-transform: uppercase; padding: 0 20px; margin-bottom: 8px; }
|
||||
.sidebar-link { display: block; padding: 6px 20px; font-size: 14px; color: var(--gray-700); border-radius: 0; transition: background 100ms; }
|
||||
.sidebar-link:hover { background: var(--gray-50); color: var(--brand-900); }
|
||||
.sidebar-link.active { color: var(--brand-500); font-weight: 500; background: var(--brand-50); }
|
||||
|
||||
.docs-content { padding: 48px 64px; max-width: 800px; }
|
||||
.docs-content h1 { font-size: 32px; font-weight: 700; color: var(--brand-900); margin: 0 0 8px; }
|
||||
.docs-content .meta { font-size: 13px; color: var(--fg-muted); margin-bottom: 36px; }
|
||||
.docs-content h2 { font-size: 22px; font-weight: 600; color: var(--brand-900); margin: 48px 0 16px; padding-top: 48px; border-top: 1px solid var(--border-subtle); }
|
||||
.docs-content h2:first-of-type { border-top: none; }
|
||||
.docs-content h3 { font-size: 16px; font-weight: 600; color: var(--gray-800); margin: 28px 0 10px; }
|
||||
.docs-content p { font-size: 14px; line-height: 1.75; color: var(--fg-default); margin: 0 0 16px; }
|
||||
.docs-content ul, .docs-content ol { padding-left: 20px; margin: 0 0 16px; }
|
||||
.docs-content li { font-size: 14px; line-height: 1.75; color: var(--fg-default); margin-bottom: 6px; }
|
||||
.docs-content code { font-family: var(--font-mono); font-size: 12px; background: var(--gray-100); padding: 2px 5px; border-radius: 3px; color: var(--brand-700); }
|
||||
.tip { background: var(--brand-50); border-left: 3px solid var(--brand-400); border-radius: 0 6px 6px 0; padding: 12px 16px; margin: 20px 0; }
|
||||
.tip p { margin: 0; font-size: 13px; color: var(--brand-800); }
|
||||
.warn { background: var(--warning-50); border-left: 3px solid var(--warning-500); border-radius: 0 6px 6px 0; padding: 12px 16px; margin: 20px 0; }
|
||||
.warn p { margin: 0; font-size: 13px; color: var(--warning-700); }
|
||||
table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 13px; }
|
||||
th { background: var(--gray-50); padding: 8px 12px; text-align: left; font-weight: 600; color: var(--gray-700); border: 1px solid var(--border-default); }
|
||||
td { padding: 8px 12px; border: 1px solid var(--border-default); color: var(--fg-default); vertical-align: top; }
|
||||
|
||||
footer { background: var(--gray-50); border-top: 1px solid var(--border-default); padding: 40px 0; }
|
||||
.footer-inner { display: flex; align-items: center; justify-content: space-between; }
|
||||
.footer-brand { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; color: var(--brand-900); }
|
||||
.footer-brand img { height: 24px; }
|
||||
.footer-copy { font-size: 12px; color: var(--fg-subtle); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-brand"><img src="assets/logo-full.svg" alt="岩美" /></a>
|
||||
<div class="nav-links">
|
||||
<a href="/features/inventory.html">功能</a>
|
||||
<a href="/docs.html">文档</a>
|
||||
<a href="/download.html">下载</a>
|
||||
</div>
|
||||
<div class="nav-cta">
|
||||
<a href="/app/" class="btn btn-primary">进入管理端 <i data-lucide="arrow-right" class="icon"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="docs-layout">
|
||||
<aside class="docs-sidebar">
|
||||
<div class="sidebar-group">
|
||||
<div class="sidebar-group-title">快速开始</div>
|
||||
<a href="#intro" class="sidebar-link active">系统介绍</a>
|
||||
<a href="#login" class="sidebar-link">登录与权限</a>
|
||||
<a href="#roles" class="sidebar-link">角色说明</a>
|
||||
</div>
|
||||
<div class="sidebar-group">
|
||||
<div class="sidebar-group-title">核心模块</div>
|
||||
<a href="#stock-in" class="sidebar-link">入库管理</a>
|
||||
<a href="#stock-out" class="sidebar-link">出库管理</a>
|
||||
<a href="#inventory" class="sidebar-link">库存管理</a>
|
||||
<a href="#finance" class="sidebar-link">财务管理</a>
|
||||
<a href="#products" class="sidebar-link">基础数据</a>
|
||||
</div>
|
||||
<div class="sidebar-group">
|
||||
<div class="sidebar-group-title">其他功能</div>
|
||||
<a href="#scan" class="sidebar-link">防伪溯源</a>
|
||||
<a href="#settings" class="sidebar-link">系统设置</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="docs-content">
|
||||
<h1>使用手册</h1>
|
||||
<p class="meta">适用版本:v1.x · 更新于 2025 年</p>
|
||||
|
||||
<h2 id="intro">系统介绍</h2>
|
||||
<p>岩美酒库管理系统是专为酒行、酒店 F&B 部门设计的一体化库存管理平台。系统涵盖入库、出库、库存盘点、财务对账、往来单位管理以及商品防伪溯源等核心功能。</p>
|
||||
<p>系统采用多租户架构,每个门店(<code>shop</code>)的数据完全隔离,支持 Web 端与桌面端(macOS / Windows)同时使用。</p>
|
||||
|
||||
<div class="tip"><p><strong>提示:</strong>如果是初次使用,请让管理员先创建您的账号,然后使用门店编号 + 用户名 + 密码登录。</p></div>
|
||||
|
||||
<h2 id="login">登录与权限</h2>
|
||||
<h3>登录方式</h3>
|
||||
<p>在登录页输入:</p>
|
||||
<ul>
|
||||
<li><strong>门店编号</strong>(由管理员提供,如 <code>S001</code>)</li>
|
||||
<li><strong>用户名</strong></li>
|
||||
<li><strong>密码</strong></li>
|
||||
</ul>
|
||||
<p>登录成功后,系统会保存 Token,关闭浏览器后再次打开无需重新登录(Token 有效期 7 天)。</p>
|
||||
|
||||
<h2 id="roles">角色说明</h2>
|
||||
<table>
|
||||
<thead><tr><th>角色</th><th>权限范围</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>管理员 (admin)</td><td>所有功能,包括用户管理、审批、系统设置</td></tr>
|
||||
<tr><td>普通用户 (user)</td><td>创建/编辑单据,无法审批,无系统设置权限</td></tr>
|
||||
<tr><td>只读用户 (readonly)</td><td>仅查看,不可创建或修改任何数据</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="warn"><p><strong>注意:</strong>只读用户尝试任何写操作时,系统将返回 403 拒绝提示。</p></div>
|
||||
|
||||
<h2 id="stock-in">入库管理</h2>
|
||||
<h3>创建入库单</h3>
|
||||
<ol>
|
||||
<li>点击左侧「入库管理」,然后点击「新建入库单」</li>
|
||||
<li>选择仓库、供应商(往来单位),填写入库日期</li>
|
||||
<li>逐行添加商品,填写数量、单价、批次号、生产日期</li>
|
||||
<li>保存为「草稿」或直接「提交审核」</li>
|
||||
</ol>
|
||||
<h3>审批入库</h3>
|
||||
<p>状态为「待审核」的入库单,管理员可在列表中点击「审核通过」。审核通过后库存自动增加,且操作不可逆。</p>
|
||||
|
||||
<h2 id="stock-out">出库管理</h2>
|
||||
<p>出库流程与入库类似:创建出库单 → 选择仓库和商品 → 提交审核 → 管理员审批。</p>
|
||||
<p>系统在审批出库时会自动校验库存充足性,如果库存不足,审批将失败并给出提示。</p>
|
||||
<table>
|
||||
<thead><tr><th>出库类型</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>销售</td><td>对外销售,关联往来单位(客户)</td></tr>
|
||||
<tr><td>领用</td><td>内部消耗,无对外客户</td></tr>
|
||||
<tr><td>损耗</td><td>破损/过期等非正常消耗</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 id="inventory">库存管理</h2>
|
||||
<p>库存列表按批次展示,每次入库产生一条库存记录。支持按商品名称、仓库、供应商筛选,并可导出 Excel。</p>
|
||||
<h3>批次追溯</h3>
|
||||
<p>每条库存记录保留了入库批次号、生产日期、供应商等信息,可完整追溯商品来源。</p>
|
||||
|
||||
<h2 id="finance">财务管理</h2>
|
||||
<p>财务模块记录与入库/出库单据关联的应收应付账款。支持按往来单位、日期区间、状态(已收/待收)筛选,并可导出汇总报表。</p>
|
||||
|
||||
<h2 id="products">基础数据</h2>
|
||||
<p>基础数据包含:</p>
|
||||
<ul>
|
||||
<li><strong>商品管理</strong>:SKU 信息、规格、品牌、系列、单位,以及商品图片上传</li>
|
||||
<li><strong>仓库管理</strong>:门店下可配置多个仓库</li>
|
||||
<li><strong>往来单位</strong>:供应商与客户统一管理</li>
|
||||
<li><strong>编号规则</strong>:自定义入库单/出库单号前缀和格式</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="scan">防伪溯源</h2>
|
||||
<p>为每件商品生成唯一防伪码(<code>public_id</code>),打印贴附于商品。消费者扫描二维码后跳转至 <code>jiu.51yanmei.com/scan/<id></code>,可查看:</p>
|
||||
<ul>
|
||||
<li>商品基本信息(名称、品牌、规格)</li>
|
||||
<li>批次信息(生产日期、批次号、入库日期)</li>
|
||||
<li>出售门店信息</li>
|
||||
<li>岩美防伪验证标记</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="settings">系统设置</h2>
|
||||
<p>系统设置仅管理员可见,包括:</p>
|
||||
<ul>
|
||||
<li>用户管理:创建/删除用户,重置密码,调整角色</li>
|
||||
<li>门店信息:更新门店名称、地址、电话等公开信息</li>
|
||||
<li>数据导入:批量导入商品、历史库存数据(Excel 格式)</li>
|
||||
</ul>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-brand"><img src="assets/logo-mark.svg" alt="岩美" /> 岩美酒库管理系统</div>
|
||||
<div class="footer-copy">© 2025 岩美。保留所有权利。</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>下载 — 岩美酒库管理系统</title>
|
||||
<meta name="description" content="下载岩美酒库管理系统客户端,支持 macOS 和 Windows。" />
|
||||
<link rel="stylesheet" href="assets/colors_and_type.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; font-family: var(--font-sans); color: var(--fg-default); background: var(--bg-surface); -webkit-font-smoothing: antialiased; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.btn { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 18px; border-radius: var(--radius-md); font-size: 14px; font-weight: 500; border: 1px solid transparent; cursor: pointer; transition: background 120ms; white-space: nowrap; text-decoration: none; }
|
||||
.btn-primary { background: var(--brand-500); color: #fff; }
|
||||
.btn-primary:hover { background: var(--brand-600); }
|
||||
.btn-secondary { background: var(--gray-0); color: var(--brand-900); border-color: var(--border-default); }
|
||||
.btn-secondary:hover { background: var(--gray-50); }
|
||||
.btn-lg { height: 48px; padding: 0 24px; font-size: 15px; }
|
||||
.btn-disabled { opacity: 0.5; pointer-events: none; }
|
||||
.icon { width: 18px; height: 18px; }
|
||||
|
||||
/* NAV */
|
||||
.nav { position: sticky; top: 0; z-index: 50; background: rgba(255,255,255,0.88); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border-subtle); }
|
||||
.nav-inner { height: 64px; display: flex; align-items: center; justify-content: space-between; max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.nav-brand { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 700; color: var(--brand-900); }
|
||||
.nav-brand img { height: 30px; }
|
||||
.nav-links { display: flex; gap: 2px; }
|
||||
.nav-links a { padding: 7px 14px; border-radius: var(--radius-md); font-size: 14px; color: var(--gray-700); transition: background 120ms; }
|
||||
.nav-links a:hover { background: var(--gray-100); }
|
||||
.nav-cta { display: flex; gap: 10px; }
|
||||
|
||||
/* PAGE */
|
||||
.page-hero { padding: 72px 0 56px; text-align: center; border-bottom: 1px solid var(--border-subtle); }
|
||||
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.12em; color: var(--brand-500); text-transform: uppercase; margin-bottom: 14px; }
|
||||
.page-title { font-size: 40px; font-weight: 700; color: var(--brand-900); margin: 0 0 14px; }
|
||||
.page-sub { font-size: 16px; color: var(--fg-muted); margin: 0; line-height: 1.6; }
|
||||
|
||||
/* LOADING / ERROR */
|
||||
.state-loading { padding: 60px 0; text-align: center; color: var(--fg-muted); font-size: 14px; }
|
||||
.state-error { padding: 60px 0; text-align: center; color: var(--danger-500); font-size: 14px; }
|
||||
|
||||
/* RELEASE */
|
||||
.release-section { padding: 64px 0; }
|
||||
.version-badge { display: inline-flex; align-items: center; gap: 8px; background: var(--brand-50); color: var(--brand-700); padding: 5px 14px; border-radius: var(--radius-pill); font-size: 13px; font-weight: 600; margin-bottom: 8px; }
|
||||
.build-date { font-size: 13px; color: var(--fg-muted); margin-bottom: 40px; }
|
||||
.platform-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
||||
.platform-card { background: var(--gray-0); border: 1px solid var(--border-default); border-radius: 10px; padding: 28px 24px; }
|
||||
.platform-card h3 { font-size: 18px; font-weight: 600; color: var(--brand-900); margin: 0 0 6px; display: flex; align-items: center; gap: 10px; }
|
||||
.platform-card h3 i { width: 20px; height: 20px; color: var(--brand-500); }
|
||||
.platform-card p { font-size: 13px; color: var(--fg-muted); margin: 0 0 24px; line-height: 1.6; }
|
||||
.web-badge { display: inline-flex; align-items: center; gap: 6px; background: var(--success-50); color: var(--success-700); padding: 3px 10px; border-radius: var(--radius-pill); font-size: 11px; font-weight: 600; margin-bottom: 6px; }
|
||||
|
||||
/* CHANGELOG */
|
||||
.changelog-section { padding: 0 0 64px; }
|
||||
.changelog-title { font-size: 20px; font-weight: 600; color: var(--brand-900); margin: 0 0 20px; }
|
||||
.changelog-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
|
||||
.changelog-item { display: flex; gap: 14px; background: var(--gray-0); border: 1px solid var(--border-default); border-radius: 8px; padding: 16px 20px; }
|
||||
.changelog-ver { font-size: 13px; font-weight: 600; color: var(--brand-700); font-family: var(--font-mono); min-width: 60px; padding-top: 2px; }
|
||||
.changelog-content { flex: 1; }
|
||||
.changelog-date { font-size: 11px; color: var(--fg-subtle); margin-bottom: 6px; }
|
||||
.changelog-notes { font-size: 13px; color: var(--fg-default); margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
||||
.changelog-notes li::before { content: '· '; color: var(--brand-400); }
|
||||
|
||||
/* FOOTER */
|
||||
footer { background: var(--gray-50); border-top: 1px solid var(--border-default); padding: 40px 0; }
|
||||
.footer-inner { display: flex; align-items: center; justify-content: space-between; }
|
||||
.footer-brand { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; color: var(--brand-900); }
|
||||
.footer-brand img { height: 24px; }
|
||||
.footer-copy { font-size: 12px; color: var(--fg-subtle); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-brand"><img src="assets/logo-full.svg" alt="岩美" /></a>
|
||||
<div class="nav-links">
|
||||
<a href="/features/inventory.html">功能</a>
|
||||
<a href="/docs.html">文档</a>
|
||||
<a href="/download.html">下载</a>
|
||||
</div>
|
||||
<div class="nav-cta">
|
||||
<a href="/app/" class="btn btn-primary">进入管理端 <i data-lucide="arrow-right" class="icon"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page-hero">
|
||||
<div class="container">
|
||||
<div class="eyebrow">Download</div>
|
||||
<h1 class="page-title">下载岩美客户端</h1>
|
||||
<p class="page-sub">支持 macOS、Windows 桌面端,以及通过浏览器直接使用的 Web 版本。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="release-section">
|
||||
<div class="container" id="release-container">
|
||||
<div class="state-loading">正在获取版本信息...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-brand"><img src="assets/logo-mark.svg" alt="岩美" /> 岩美酒库管理系统</div>
|
||||
<div class="footer-copy">© 2025 岩美。保留所有权利。</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
const API_BASE = location.hostname === 'localhost' ? 'http://localhost:8080/api/v1' : '/api/v1';
|
||||
|
||||
function fmt(d) {
|
||||
if (!d) return '—';
|
||||
return d.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1 年 $2 月 $3 日');
|
||||
}
|
||||
|
||||
function renderRelease(r) {
|
||||
const dl = r.download_urls || {};
|
||||
const container = document.getElementById('release-container');
|
||||
container.innerHTML = `
|
||||
<div class="version-badge">
|
||||
<i data-lucide="tag" style="width:14px;height:14px"></i>
|
||||
v${r.version || '—'}
|
||||
</div>
|
||||
<div class="build-date">发布日期:${fmt(r.build_date)}</div>
|
||||
<div class="platform-grid">
|
||||
<div class="platform-card">
|
||||
<h3><i data-lucide="monitor"></i> macOS</h3>
|
||||
<p>适用于 Apple Silicon / Intel Mac。支持本地打印与快捷键操作。</p>
|
||||
${dl.macos
|
||||
? `<a href="${dl.macos}" class="btn btn-primary btn-lg"><i data-lucide="download" class="icon"></i> 下载 macOS</a>`
|
||||
: `<span class="btn btn-secondary btn-lg btn-disabled"><i data-lucide="clock" class="icon"></i> 暂未发布</span>`}
|
||||
</div>
|
||||
<div class="platform-card">
|
||||
<h3><i data-lucide="monitor"></i> Windows</h3>
|
||||
<p>适用于 Windows 10 及以上版本,支持本地打印机直连。</p>
|
||||
${dl.windows
|
||||
? `<a href="${dl.windows}" class="btn btn-primary btn-lg"><i data-lucide="download" class="icon"></i> 下载 Windows</a>`
|
||||
: `<span class="btn btn-secondary btn-lg btn-disabled"><i data-lucide="clock" class="icon"></i> 暂未发布</span>`}
|
||||
</div>
|
||||
<div class="platform-card">
|
||||
<h3><i data-lucide="globe"></i> Web 版本</h3>
|
||||
<span class="web-badge"><i data-lucide="zap" style="width:11px;height:11px"></i> 无需安装</span>
|
||||
<p>直接在浏览器中使用全功能管理端,推荐 Chrome / Edge。</p>
|
||||
<a href="/app/" class="btn btn-primary btn-lg"><i data-lucide="external-link" class="icon"></i> 打开 Web 版</a>
|
||||
</div>
|
||||
</div>
|
||||
${renderChangelog(r.changelog)}
|
||||
`;
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderChangelog(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
const items = list.map(v => `
|
||||
<li class="changelog-item">
|
||||
<div class="changelog-ver">v${v.version}</div>
|
||||
<div class="changelog-content">
|
||||
<div class="changelog-date">${fmt(v.date)}</div>
|
||||
<ul class="changelog-notes">${(v.items||[]).map(i=>`<li>${i}</li>`).join('')}</ul>
|
||||
</div>
|
||||
</li>
|
||||
`).join('');
|
||||
return `
|
||||
<div class="changelog-section" style="margin-top:48px">
|
||||
<h2 class="changelog-title">更新日志</h2>
|
||||
<ul class="changelog-list">${items}</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
fetch(`${API_BASE}/public/release`)
|
||||
.then(r => r.json())
|
||||
.then(renderRelease)
|
||||
.catch(err => {
|
||||
document.getElementById('release-container').innerHTML =
|
||||
`<div class="state-error">无法获取版本信息,请稍后再试。<br/><small>${err.message}</small></div>`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>审批流程功能 — 岩美酒库管理系统</title>
|
||||
<link rel="stylesheet" href="../assets/colors_and_type.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; font-family: var(--font-sans); color: var(--fg-default); background: var(--bg-surface); -webkit-font-smoothing: antialiased; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.btn { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 18px; border-radius: var(--radius-md); font-size: 14px; font-weight: 500; border: 1px solid transparent; cursor: pointer; transition: background 120ms; text-decoration: none; }
|
||||
.btn-primary { background: var(--brand-500); color: #fff; }
|
||||
.btn-primary:hover { background: var(--brand-600); }
|
||||
.btn-secondary { background: var(--gray-0); color: var(--brand-900); border-color: var(--border-default); }
|
||||
.btn-secondary:hover { background: var(--gray-50); }
|
||||
.btn-lg { height: 48px; padding: 0 24px; font-size: 15px; }
|
||||
.icon { width: 18px; height: 18px; }
|
||||
|
||||
.nav { position: sticky; top: 0; z-index: 50; background: rgba(255,255,255,0.88); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border-subtle); }
|
||||
.nav-inner { height: 64px; display: flex; align-items: center; justify-content: space-between; max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.nav-brand { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 700; color: var(--brand-900); }
|
||||
.nav-brand img { height: 30px; }
|
||||
.nav-links { display: flex; gap: 2px; }
|
||||
.nav-links a { padding: 7px 14px; border-radius: var(--radius-md); font-size: 14px; color: var(--gray-700); transition: background 120ms; }
|
||||
.nav-links a:hover { background: var(--gray-100); }
|
||||
.nav-links a.active { color: var(--brand-500); font-weight: 500; }
|
||||
.nav-cta { display: flex; gap: 10px; }
|
||||
|
||||
.page-hero { padding: 80px 0 64px; }
|
||||
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.12em; color: var(--brand-500); text-transform: uppercase; margin-bottom: 12px; }
|
||||
.page-hero h1 { font-size: 44px; font-weight: 700; color: var(--brand-900); margin: 0 0 18px; line-height: 1.15; }
|
||||
.page-hero .lead { font-size: 18px; line-height: 1.65; color: var(--gray-600); margin: 0 0 36px; max-width: 600px; }
|
||||
.hero-actions { display: flex; gap: 12px; }
|
||||
|
||||
/* Workflow steps */
|
||||
.workflow-section { padding: 72px 0; background: var(--gray-50); }
|
||||
.section-title { font-size: 28px; font-weight: 700; color: var(--brand-900); margin: 0 0 14px; }
|
||||
.section-sub { font-size: 15px; color: var(--fg-muted); margin: 0 0 48px; line-height: 1.6; max-width: 560px; }
|
||||
.steps { display: flex; flex-direction: column; gap: 0; max-width: 800px; }
|
||||
.step { display: flex; gap: 24px; padding: 28px 0; border-bottom: 1px solid var(--border-subtle); }
|
||||
.step:last-child { border-bottom: none; }
|
||||
.step-num { width: 36px; height: 36px; border-radius: 50%; background: var(--brand-500); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 700; flex-shrink: 0; }
|
||||
.step-content { flex: 1; }
|
||||
.step-content h3 { font-size: 16px; font-weight: 600; color: var(--brand-900); margin: 0 0 6px; margin-top: 6px; }
|
||||
.step-content p { font-size: 14px; line-height: 1.65; color: var(--fg-muted); margin: 0; }
|
||||
.step-badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; margin-top: 8px; }
|
||||
.step-badge.blue { background: var(--brand-50); color: var(--brand-700); }
|
||||
.step-badge.amber { background: var(--warning-50); color: var(--warning-700); }
|
||||
.step-badge.green { background: var(--success-50); color: var(--success-700); }
|
||||
.step-badge.red { background: var(--danger-50); color: var(--danger-700); }
|
||||
|
||||
/* Role matrix */
|
||||
.matrix-section { padding: 72px 0; }
|
||||
.matrix-table { width: 100%; border-collapse: collapse; margin-top: 32px; }
|
||||
.matrix-table th { background: var(--gray-50); border: 1px solid var(--border-default); padding: 10px 16px; text-align: left; font-size: 13px; font-weight: 600; color: var(--gray-700); }
|
||||
.matrix-table td { border: 1px solid var(--border-default); padding: 10px 16px; font-size: 13px; vertical-align: middle; }
|
||||
.perm-yes { display: inline-flex; align-items: center; gap: 5px; color: var(--success-700); font-weight: 500; }
|
||||
.perm-yes i { width: 14px; height: 14px; }
|
||||
.perm-no { color: var(--gray-400); }
|
||||
|
||||
/* Audit log */
|
||||
.audit-section { padding: 72px 0; background: var(--gray-50); }
|
||||
.audit-log { background: var(--gray-0); border: 1px solid var(--border-default); border-radius: 10px; overflow: hidden; max-width: 720px; }
|
||||
.audit-header { padding: 14px 20px; border-bottom: 1px solid var(--border-default); background: var(--gray-50); display: flex; align-items: center; gap: 8px; }
|
||||
.audit-header-title { font-size: 13px; font-weight: 600; color: var(--gray-700); }
|
||||
.audit-row { display: flex; gap: 16px; padding: 12px 20px; border-bottom: 1px solid var(--border-subtle); align-items: flex-start; }
|
||||
.audit-row:last-child { border-bottom: none; }
|
||||
.audit-time { font-size: 11px; color: var(--fg-subtle); font-family: var(--font-mono); min-width: 80px; padding-top: 2px; }
|
||||
.audit-user { font-size: 12px; font-weight: 500; color: var(--brand-700); min-width: 64px; padding-top: 2px; }
|
||||
.audit-action { font-size: 13px; color: var(--fg-default); }
|
||||
.audit-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; flex-shrink: 0; }
|
||||
.dot-green { background: var(--success-500); }
|
||||
.dot-amber { background: var(--warning-500); }
|
||||
.dot-blue { background: var(--brand-400); }
|
||||
.dot-red { background: var(--danger-500); }
|
||||
|
||||
.cta-section { padding: 80px 0; text-align: center; border-top: 1px solid var(--border-default); }
|
||||
.cta-section h2 { font-size: 32px; font-weight: 700; color: var(--brand-900); margin: 0 0 14px; }
|
||||
.cta-section p { font-size: 16px; color: var(--fg-muted); margin: 0 0 32px; }
|
||||
.cta-actions { display: flex; justify-content: center; gap: 14px; }
|
||||
|
||||
footer { background: var(--gray-50); border-top: 1px solid var(--border-default); padding: 40px 0; }
|
||||
.footer-inner { display: flex; align-items: center; justify-content: space-between; }
|
||||
.footer-brand { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; color: var(--brand-900); }
|
||||
.footer-brand img { height: 24px; }
|
||||
.footer-copy { font-size: 12px; color: var(--fg-subtle); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-brand"><img src="../assets/logo-full.svg" alt="岩美" /></a>
|
||||
<div class="nav-links">
|
||||
<a href="/features/inventory.html" class="active">功能</a>
|
||||
<a href="/docs.html">文档</a>
|
||||
<a href="/download.html">下载</a>
|
||||
</div>
|
||||
<div class="nav-cta">
|
||||
<a href="/app/" class="btn btn-primary">进入管理端 <i data-lucide="arrow-right" class="icon"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page-hero">
|
||||
<div class="container">
|
||||
<div class="eyebrow">审批流程</div>
|
||||
<h1>每笔操作都有<br/>经手记录</h1>
|
||||
<p class="lead">岩美的审批机制确保每张入库单和出库单在生效前经过明确的负责人审核,防止未经授权的库存变动。</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/app/" class="btn btn-primary btn-lg">立即体验 <i data-lucide="arrow-right" class="icon"></i></a>
|
||||
<a href="/docs.html#stock-in" class="btn btn-secondary btn-lg">查看文档</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workflow -->
|
||||
<section class="workflow-section">
|
||||
<div class="container">
|
||||
<div class="section-title">审批流程步骤</div>
|
||||
<p class="section-sub">以入库单为例,完整的操作流程如下:</p>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-content">
|
||||
<h3>创建入库单</h3>
|
||||
<p>库管或采购员录入入库信息:供应商、仓库、商品明细(含批次号、生产日期)。保存为草稿,随时修改。</p>
|
||||
<span class="step-badge blue"><i data-lucide="file-edit" style="width:11px;height:11px"></i> 草稿</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-content">
|
||||
<h3>提交审核</h3>
|
||||
<p>确认无误后提交,单据状态变为「待审核」,管理员收到待办提示。提交后不可编辑,防止信息篡改。</p>
|
||||
<span class="step-badge amber"><i data-lucide="clock" style="width:11px;height:11px"></i> 待审核</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-content">
|
||||
<h3>管理员审批</h3>
|
||||
<p>管理员核实单据后选择「审批通过」或「驳回」。驳回时可填写驳回原因,由经办人修改后重新提交。</p>
|
||||
<div style="display:flex;gap:8px;margin-top:8px;">
|
||||
<span class="step-badge green"><i data-lucide="check" style="width:11px;height:11px"></i> 审批通过</span>
|
||||
<span class="step-badge red"><i data-lucide="x" style="width:11px;height:11px"></i> 已驳回</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">4</div>
|
||||
<div class="step-content">
|
||||
<h3>库存自动更新</h3>
|
||||
<p>审批通过后,系统在同一事务中完成库存增加与财务记录写入,保证数据一致性。操作不可逆,留存完整记录。</p>
|
||||
<span class="step-badge green"><i data-lucide="package-check" style="width:11px;height:11px"></i> 已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Role matrix -->
|
||||
<section class="matrix-section">
|
||||
<div class="container">
|
||||
<div class="section-title">角色权限矩阵</div>
|
||||
<p class="section-sub">不同角色在审批流程中的权限一览。</p>
|
||||
<table class="matrix-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>操作</th>
|
||||
<th>管理员</th>
|
||||
<th>普通用户</th>
|
||||
<th>只读用户</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>创建入库/出库单(草稿)</td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
<td><span class="perm-no">禁止</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>提交审核</td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
<td><span class="perm-no">禁止</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>审批通过 / 驳回</td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
<td><span class="perm-no">禁止</span></td>
|
||||
<td><span class="perm-no">禁止</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>查看单据列表</td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>用户管理</td>
|
||||
<td><span class="perm-yes"><i data-lucide="check-circle"></i> 允许</span></td>
|
||||
<td><span class="perm-no">禁止</span></td>
|
||||
<td><span class="perm-no">禁止</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Audit log -->
|
||||
<section class="audit-section">
|
||||
<div class="container">
|
||||
<div class="section-title">完整操作日志</div>
|
||||
<p class="section-sub">每笔库存变动都有对应的操作记录,明确到具体操作人和时间点。</p>
|
||||
<div class="audit-log">
|
||||
<div class="audit-header">
|
||||
<i data-lucide="clock" style="width:14px;height:14px;color:var(--brand-500)"></i>
|
||||
<span class="audit-header-title">库存流水记录 — 麦卡伦 12年</span>
|
||||
</div>
|
||||
<div class="audit-row">
|
||||
<div class="audit-dot dot-green"></div>
|
||||
<div class="audit-time">05-23 14:32</div>
|
||||
<div class="audit-user">王经理</div>
|
||||
<div class="audit-action">审批通过入库单 <strong>RK20250523000001</strong>,库存 +24 瓶(批次 L-2026-01)</div>
|
||||
</div>
|
||||
<div class="audit-row">
|
||||
<div class="audit-dot dot-blue"></div>
|
||||
<div class="audit-time">05-23 11:08</div>
|
||||
<div class="audit-user">李库管</div>
|
||||
<div class="audit-action">提交入库单 <strong>RK20250523000001</strong> 审核,共 24 瓶</div>
|
||||
</div>
|
||||
<div class="audit-row">
|
||||
<div class="audit-dot dot-red"></div>
|
||||
<div class="audit-time">05-22 16:45</div>
|
||||
<div class="audit-user">王经理</div>
|
||||
<div class="audit-action">驳回出库单 <strong>CK20250522000003</strong>,原因:数量与实际不符</div>
|
||||
</div>
|
||||
<div class="audit-row">
|
||||
<div class="audit-dot dot-amber"></div>
|
||||
<div class="audit-time">05-22 15:30</div>
|
||||
<div class="audit-user">张吧台</div>
|
||||
<div class="audit-action">提交出库申请 <strong>CK20250522000003</strong>,领用 12 瓶</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<h2>查看所有功能</h2>
|
||||
<p>了解库存管理的完整能力,包括批次追溯和盘点功能。</p>
|
||||
<div class="cta-actions">
|
||||
<a href="/features/inventory.html" class="btn btn-primary btn-lg">库存管理功能 <i data-lucide="arrow-right" class="icon"></i></a>
|
||||
<a href="/app/" class="btn btn-secondary btn-lg">进入管理端</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-brand"><img src="../assets/logo-mark.svg" alt="岩美" /> 岩美酒库管理系统</div>
|
||||
<div class="footer-copy">© 2025 岩美。保留所有权利。</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,285 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>库存管理功能 — 岩美酒库管理系统</title>
|
||||
<link rel="stylesheet" href="../assets/colors_and_type.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; font-family: var(--font-sans); color: var(--fg-default); background: var(--bg-surface); -webkit-font-smoothing: antialiased; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.btn { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 18px; border-radius: var(--radius-md); font-size: 14px; font-weight: 500; border: 1px solid transparent; cursor: pointer; transition: background 120ms; text-decoration: none; }
|
||||
.btn-primary { background: var(--brand-500); color: #fff; }
|
||||
.btn-primary:hover { background: var(--brand-600); }
|
||||
.btn-secondary { background: var(--gray-0); color: var(--brand-900); border-color: var(--border-default); }
|
||||
.btn-secondary:hover { background: var(--gray-50); }
|
||||
.btn-lg { height: 48px; padding: 0 24px; font-size: 15px; }
|
||||
.icon { width: 18px; height: 18px; }
|
||||
|
||||
.nav { position: sticky; top: 0; z-index: 50; background: rgba(255,255,255,0.88); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border-subtle); }
|
||||
.nav-inner { height: 64px; display: flex; align-items: center; justify-content: space-between; max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
||||
.nav-brand { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 700; color: var(--brand-900); }
|
||||
.nav-brand img { height: 30px; }
|
||||
.nav-links { display: flex; gap: 2px; }
|
||||
.nav-links a { padding: 7px 14px; border-radius: var(--radius-md); font-size: 14px; color: var(--gray-700); transition: background 120ms; }
|
||||
.nav-links a:hover { background: var(--gray-100); }
|
||||
.nav-links a.active { color: var(--brand-500); font-weight: 500; }
|
||||
.nav-cta { display: flex; gap: 10px; }
|
||||
|
||||
.page-hero { padding: 80px 0 64px; }
|
||||
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.12em; color: var(--brand-500); text-transform: uppercase; margin-bottom: 12px; }
|
||||
.page-hero h1 { font-size: 44px; font-weight: 700; color: var(--brand-900); margin: 0 0 18px; line-height: 1.15; }
|
||||
.page-hero .lead { font-size: 18px; line-height: 1.65; color: var(--gray-600); margin: 0 0 36px; max-width: 600px; }
|
||||
.hero-actions { display: flex; gap: 12px; }
|
||||
|
||||
.feature-list { padding: 72px 0; }
|
||||
.feature-row { display: grid; grid-template-columns: 1fr 1fr; gap: 80px; align-items: center; margin-bottom: 96px; }
|
||||
.feature-row:last-child { margin-bottom: 0; }
|
||||
.feature-row.reverse { }
|
||||
.feature-text .tag { display: inline-block; background: var(--brand-50); color: var(--brand-700); padding: 3px 10px; border-radius: var(--radius-pill); font-size: 11px; font-weight: 600; margin-bottom: 14px; }
|
||||
.feature-text h2 { font-size: 28px; font-weight: 700; color: var(--brand-900); margin: 0 0 14px; }
|
||||
.feature-text p { font-size: 15px; line-height: 1.7; color: var(--fg-muted); margin: 0 0 20px; }
|
||||
.feature-text ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.feature-text li { display: flex; align-items: flex-start; gap: 10px; font-size: 14px; color: var(--fg-default); }
|
||||
.feature-text li i { width: 16px; height: 16px; color: var(--brand-500); flex-shrink: 0; margin-top: 2px; }
|
||||
.feature-visual { background: var(--gray-0); border: 1px solid var(--border-default); border-radius: 12px; overflow: hidden; box-shadow: var(--shadow-lg); }
|
||||
.visual-header { background: var(--gray-50); border-bottom: 1px solid var(--border-default); padding: 10px 16px; display: flex; align-items: center; gap: 10px; }
|
||||
.visual-title { font-size: 13px; font-weight: 600; color: var(--gray-700); }
|
||||
.mock-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; }
|
||||
.mock-badge.green { background: var(--success-50); color: var(--success-700); }
|
||||
.mock-badge.amber { background: var(--warning-50); color: var(--warning-700); }
|
||||
.mock-badge.blue { background: var(--brand-50); color: var(--brand-700); }
|
||||
.mock-table-wrap { padding: 0; }
|
||||
.mock-thead { display: grid; background: var(--gray-50); border-bottom: 1px solid var(--border-default); padding: 7px 16px; font-size: 11px; color: var(--fg-muted); font-weight: 500; gap: 8px; }
|
||||
.mock-row-item { display: grid; border-bottom: 1px solid var(--border-subtle); padding: 9px 16px; font-size: 12px; color: var(--fg-default); gap: 8px; align-items: center; }
|
||||
.mock-row-item:last-child { border-bottom: none; }
|
||||
.cols-inv { grid-template-columns: 2fr 1fr 1fr 1fr 1fr; }
|
||||
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border-default); border-bottom: 1px solid var(--border-default); }
|
||||
.stat-block { background: var(--gray-0); padding: 16px 20px; }
|
||||
.stat-label { font-size: 11px; color: var(--fg-muted); margin-bottom: 4px; }
|
||||
.stat-val { font-size: 22px; font-weight: 700; color: var(--brand-900); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.cta-section { padding: 80px 0; background: var(--gray-50); text-align: center; border-top: 1px solid var(--border-default); }
|
||||
.cta-section h2 { font-size: 32px; font-weight: 700; color: var(--brand-900); margin: 0 0 14px; }
|
||||
.cta-section p { font-size: 16px; color: var(--fg-muted); margin: 0 0 32px; }
|
||||
.cta-actions { display: flex; justify-content: center; gap: 14px; }
|
||||
|
||||
footer { background: var(--gray-50); border-top: 1px solid var(--border-default); padding: 40px 0; }
|
||||
.footer-inner { display: flex; align-items: center; justify-content: space-between; }
|
||||
.footer-brand { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; color: var(--brand-900); }
|
||||
.footer-brand img { height: 24px; }
|
||||
.footer-copy { font-size: 12px; color: var(--fg-subtle); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-brand"><img src="../assets/logo-full.svg" alt="岩美" /></a>
|
||||
<div class="nav-links">
|
||||
<a href="/features/inventory.html" class="active">功能</a>
|
||||
<a href="/docs.html">文档</a>
|
||||
<a href="/download.html">下载</a>
|
||||
</div>
|
||||
<div class="nav-cta">
|
||||
<a href="/app/" class="btn btn-primary">进入管理端 <i data-lucide="arrow-right" class="icon"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page-hero">
|
||||
<div class="container">
|
||||
<div class="eyebrow">库存管理</div>
|
||||
<h1>批次级精准库存,<br/>全程可追溯</h1>
|
||||
<p class="lead">岩美的库存模块以入库批次为最小单元,每瓶酒的来源、成本、去向都有据可查。从采购到销售,一条完整的追溯链条。</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/app/" class="btn btn-primary btn-lg">立即体验 <i data-lucide="arrow-right" class="icon"></i></a>
|
||||
<a href="/docs.html#inventory" class="btn btn-secondary btn-lg">查看文档</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="feature-list">
|
||||
<div class="container">
|
||||
|
||||
<!-- Feature 1: 实时库存 -->
|
||||
<div class="feature-row">
|
||||
<div class="feature-text">
|
||||
<span class="tag">实时库存</span>
|
||||
<h2>入库即更新,所见即所得</h2>
|
||||
<p>每张入库单审批通过后,库存立即更新。支持多仓库分区管理,库存不足时自动预警。</p>
|
||||
<ul>
|
||||
<li><i data-lucide="check-circle"></i> 多仓库独立计数,库存不串仓</li>
|
||||
<li><i data-lucide="check-circle"></i> 低库存预警,提前安排采购</li>
|
||||
<li><i data-lucide="check-circle"></i> 任意时点的库存快照,支持历史查询</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature-visual">
|
||||
<div class="visual-header">
|
||||
<i data-lucide="package" style="width:14px;height:14px;color:var(--brand-500)"></i>
|
||||
<span class="visual-title">库存列表</span>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-block"><div class="stat-label">库存 SKU 数</div><div class="stat-val">356</div></div>
|
||||
<div class="stat-block"><div class="stat-label">本月入库量</div><div class="stat-val">1,284</div></div>
|
||||
<div class="stat-block"><div class="stat-label">库存总价值</div><div class="stat-val">¥84.2万</div></div>
|
||||
</div>
|
||||
<div class="mock-table-wrap">
|
||||
<div class="mock-thead cols-inv"><span>商品名称</span><span>批次号</span><span>库存量</span><span>仓库</span><span>状态</span></div>
|
||||
<div class="mock-row-item cols-inv">
|
||||
<span>麦卡伦 12年</span><span style="font-family:var(--font-mono);font-size:11px">L-2026-01</span><span>24 瓶</span><span>主仓</span>
|
||||
<span><span class="mock-badge green">在库</span></span>
|
||||
</div>
|
||||
<div class="mock-row-item cols-inv">
|
||||
<span>芝华士 18年</span><span style="font-family:var(--font-mono);font-size:11px">L-2025-12</span><span>8 瓶</span><span>主仓</span>
|
||||
<span><span class="mock-badge amber">偏低</span></span>
|
||||
</div>
|
||||
<div class="mock-row-item cols-inv">
|
||||
<span>百龄坛特醇</span><span style="font-family:var(--font-mono);font-size:11px">L-2026-02</span><span>36 瓶</span><span>备用库</span>
|
||||
<span><span class="mock-badge green">在库</span></span>
|
||||
</div>
|
||||
<div class="mock-row-item cols-inv">
|
||||
<span>格兰威特 15年</span><span style="font-family:var(--font-mono);font-size:11px">L-2025-11</span><span>0 瓶</span><span>主仓</span>
|
||||
<span><span class="mock-badge" style="background:var(--gray-100);color:var(--gray-500)">已清空</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2: 出入库审批 -->
|
||||
<div class="feature-row reverse">
|
||||
<div class="feature-visual">
|
||||
<div class="visual-header">
|
||||
<i data-lucide="package-plus" style="width:14px;height:14px;color:var(--brand-500)"></i>
|
||||
<span class="visual-title">入库单详情</span>
|
||||
</div>
|
||||
<div style="padding:16px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--fg-muted)">单据编号</div>
|
||||
<div style="font-size:14px;font-weight:600;font-family:var(--font-mono)">RK20250523000001</div>
|
||||
</div>
|
||||
<span class="mock-badge blue">待审核</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px;">
|
||||
<div style="background:var(--gray-50);border-radius:6px;padding:10px 12px;">
|
||||
<div style="font-size:10px;color:var(--fg-muted)">供应商</div>
|
||||
<div style="font-size:13px;margin-top:2px;">百川酒业有限公司</div>
|
||||
</div>
|
||||
<div style="background:var(--gray-50);border-radius:6px;padding:10px 12px;">
|
||||
<div style="font-size:10px;color:var(--fg-muted)">入库仓库</div>
|
||||
<div style="font-size:13px;margin-top:2px;">主仓</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:var(--gray-50);border:1px solid var(--border-default);border-radius:6px;overflow:hidden;">
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;padding:6px 10px;background:var(--gray-100);font-size:10px;color:var(--fg-muted);font-weight:500;gap:8px;">
|
||||
<span>商品</span><span>数量</span><span>单价</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;padding:8px 10px;font-size:12px;gap:8px;border-bottom:1px solid var(--border-subtle);">
|
||||
<span>麦卡伦 12年</span><span>24 瓶</span><span>¥980</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;padding:8px 10px;font-size:12px;gap:8px;">
|
||||
<span>百龄坛特醇</span><span>48 瓶</span><span>¥280</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:14px;">
|
||||
<button style="flex:1;height:32px;background:var(--brand-500);color:#fff;border:none;border-radius:var(--radius-md);font-size:13px;cursor:pointer;">审核通过</button>
|
||||
<button style="height:32px;padding:0 14px;background:var(--gray-0);color:var(--danger-500);border:1px solid var(--danger-500);border-radius:var(--radius-md);font-size:13px;cursor:pointer;">驳回</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-text">
|
||||
<span class="tag">审批流程</span>
|
||||
<h2>操作留痕,审批有序</h2>
|
||||
<p>入库单与出库单均需经过审批才能更新库存,杜绝随意修改。每张单据记录经办人与审批人,操作全程可追溯。</p>
|
||||
<ul>
|
||||
<li><i data-lucide="check-circle"></i> 草稿 → 待审核 → 已审批完整状态流转</li>
|
||||
<li><i data-lucide="check-circle"></i> 出库前自动校验库存充足性</li>
|
||||
<li><i data-lucide="check-circle"></i> 审批记录不可修改,符合内控要求</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3: 盘点 -->
|
||||
<div class="feature-row">
|
||||
<div class="feature-text">
|
||||
<span class="tag">库存盘点</span>
|
||||
<h2>定期盘点,差异一目了然</h2>
|
||||
<p>发起盘点后,系统自动生成基于当前库存的盘点表。录入实盘数量后,系统自动计算差异并归因。</p>
|
||||
<ul>
|
||||
<li><i data-lucide="check-circle"></i> 按仓库或全仓发起盘点</li>
|
||||
<li><i data-lucide="check-circle"></i> 系统数 vs 实盘数,差异高亮显示</li>
|
||||
<li><i data-lucide="check-circle"></i> 盘点结果自动调整库存,生成调账记录</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature-visual">
|
||||
<div class="visual-header">
|
||||
<i data-lucide="clipboard-list" style="width:14px;height:14px;color:var(--brand-500)"></i>
|
||||
<span class="visual-title">盘点差异报告</span>
|
||||
</div>
|
||||
<div style="padding:14px 16px;">
|
||||
<div style="display:flex;gap:8px;margin-bottom:12px;">
|
||||
<div style="flex:1;background:var(--success-50);border-radius:6px;padding:10px 12px;text-align:center;">
|
||||
<div style="font-size:10px;color:var(--success-700)">盘盈</div>
|
||||
<div style="font-size:20px;font-weight:700;color:var(--success-700)">+6</div>
|
||||
</div>
|
||||
<div style="flex:1;background:var(--danger-50);border-radius:6px;padding:10px 12px;text-align:center;">
|
||||
<div style="font-size:10px;color:var(--danger-700)">盘亏</div>
|
||||
<div style="font-size:20px;font-weight:700;color:var(--danger-700)">-14</div>
|
||||
</div>
|
||||
<div style="flex:1;background:var(--gray-50);border-radius:6px;padding:10px 12px;text-align:center;">
|
||||
<div style="font-size:10px;color:var(--fg-muted)">无差异</div>
|
||||
<div style="font-size:20px;font-weight:700;color:var(--brand-900)">284</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:var(--gray-50);border:1px solid var(--border-default);border-radius:6px;overflow:hidden;">
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;padding:6px 10px;background:var(--gray-100);font-size:10px;color:var(--fg-muted);font-weight:500;gap:8px;">
|
||||
<span>商品</span><span>系统数</span><span>实盘数</span><span>差异</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;padding:8px 10px;font-size:12px;gap:8px;border-bottom:1px solid var(--border-subtle);">
|
||||
<span>麦卡伦 12年</span><span>24</span><span>22</span><span style="color:var(--danger-500)">-2</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;padding:8px 10px;font-size:12px;gap:8px;border-bottom:1px solid var(--border-subtle);">
|
||||
<span>野格利口酒</span><span>48</span><span>51</span><span style="color:var(--success-500)">+3</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;padding:8px 10px;font-size:12px;gap:8px;">
|
||||
<span>百龄坛特醇</span><span>36</span><span>36</span><span style="color:var(--fg-muted)">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<h2>准备好了解更多功能?</h2>
|
||||
<p>查看审批流程功能,或直接进入管理端体验。</p>
|
||||
<div class="cta-actions">
|
||||
<a href="/features/approval.html" class="btn btn-primary btn-lg">审批流程功能 <i data-lucide="arrow-right" class="icon"></i></a>
|
||||
<a href="/app/" class="btn btn-secondary btn-lg">进入管理端</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-brand"><img src="../assets/logo-mark.svg" alt="岩美" /> 岩美酒库管理系统</div>
|
||||
<div class="footer-copy">© 2025 岩美。保留所有权利。</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
+1024
File diff suppressed because it is too large
Load Diff
+379
@@ -0,0 +1,379 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>商品验真 — 岩美</title>
|
||||
<meta name="description" content="岩美防伪验证 — 扫码查看商品详情与防伪信息" />
|
||||
<link rel="stylesheet" href="/assets/colors_and_type.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; font-family: var(--font-sans); color: #1A1612; background: #F7F3EE; -webkit-font-smoothing: antialiased; min-height: 100vh; }
|
||||
img { display: block; max-width: 100%; }
|
||||
|
||||
/* STATE SCREENS */
|
||||
.state-screen { display: flex; align-items: center; justify-content: center; min-height: 100vh; flex-direction: column; gap: 16px; padding: 32px; text-align: center; }
|
||||
.spinner { width: 36px; height: 36px; border: 3px solid #E8E2D9; border-top-color: var(--brand-500); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.state-text { font-size: 14px; color: #7A7068; }
|
||||
.error-icon { width: 48px; height: 48px; background: var(--danger-50); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; }
|
||||
.error-title { font-size: 16px; font-weight: 600; color: #1A1612; }
|
||||
.error-sub { font-size: 13px; color: #7A7068; max-width: 260px; }
|
||||
.retry-btn { margin-top: 4px; padding: 10px 24px; background: var(--brand-500); color: #fff; border: none; border-radius: 6px; font-size: 14px; font-family: inherit; cursor: pointer; }
|
||||
.retry-btn:active { background: var(--brand-700); }
|
||||
|
||||
/* MAIN CONTENT */
|
||||
#content { display: none; }
|
||||
|
||||
/* IMAGE GALLERY */
|
||||
.gallery { position: relative; background: #EDE8E2; }
|
||||
.gallery-main { position: relative; overflow: hidden; }
|
||||
.gallery-main img { width: 100%; aspect-ratio: 1; object-fit: cover; display: none; }
|
||||
.gallery-main img.active { display: block; }
|
||||
.gallery-placeholder { width: 100%; aspect-ratio: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
|
||||
.placeholder-mark { width: 56px; height: 56px; background: #0F3057; border-radius: 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.placeholder-mark::after { content: '岩'; font-size: 24px; font-weight: 700; color: rgba(255,255,255,0.9); }
|
||||
.placeholder-text { font-size: 13px; color: #9E9580; }
|
||||
.gallery-counter { position: absolute; right: 12px; bottom: 12px; background: rgba(0,0,0,0.5); color: #fff; font-size: 12px; padding: 3px 10px; border-radius: 12px; }
|
||||
.thumbnails { display: flex; gap: 8px; padding: 10px 14px; overflow-x: auto; background: #EDE8E2; }
|
||||
.thumbnails::-webkit-scrollbar { display: none; }
|
||||
.thumb { width: 52px; height: 52px; border-radius: 4px; object-fit: cover; border: 2px solid transparent; flex-shrink: 0; cursor: pointer; }
|
||||
.thumb.active { border-color: var(--brand-500); }
|
||||
|
||||
/* VERIFIED RIBBON */
|
||||
.ribbon { background: #EAF4ED; padding: 10px 16px; display: flex; align-items: center; gap: 10px; }
|
||||
.ribbon-check { width: 22px; height: 22px; background: #2E8B57; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.ribbon-check::after { content: '✓'; color: #fff; font-size: 13px; font-weight: 700; }
|
||||
.ribbon-text { flex: 1; }
|
||||
.ribbon-title { font-size: 13px; font-weight: 600; color: #1F6B41; }
|
||||
.ribbon-sub { font-size: 11px; color: #4A7A5C; margin-top: 1px; }
|
||||
.ribbon-icon { font-size: 18px; color: #2E8B57; }
|
||||
|
||||
/* TITLE */
|
||||
.title-section { background: #fff; padding: 20px 16px 18px; }
|
||||
.product-name { font-size: 22px; font-weight: 700; color: #1A1612; line-height: 1.3; margin: 0 0 6px; }
|
||||
.product-subtitle { font-size: 14px; color: #7A7068; margin: 0 0 14px; line-height: 1.4; }
|
||||
.tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.tag { display: inline-block; padding: 3px 9px; border-radius: 4px; font-size: 12px; font-weight: 500; }
|
||||
.tag-brand { background: #EEF4FB; color: #154072; }
|
||||
.tag-unit { background: #F0ECE6; color: #6B6259; }
|
||||
|
||||
/* QUICK SPECS */
|
||||
.quick-specs { background: #fff; display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid #F0ECE6; }
|
||||
.quick-spec-item { padding: 16px 8px; text-align: center; border-left: 1px solid #F5F1EB; }
|
||||
.quick-spec-item:first-child { border-left: none; }
|
||||
.quick-spec-val { font-size: 20px; font-weight: 700; color: #1A1612; line-height: 1; font-variant-numeric: tabular-nums; }
|
||||
.quick-spec-unit { font-size: 9px; color: #9E9580; font-weight: 500; margin-left: 1px; }
|
||||
.quick-spec-label { font-size: 11px; color: #9E9580; margin-top: 4px; letter-spacing: 0.04em; }
|
||||
|
||||
/* PARAMS */
|
||||
.params-card { background: #fff; margin-top: 10px; }
|
||||
.card-header { padding: 14px 16px 12px; border-bottom: 1px solid #F0ECE6; font-size: 14px; font-weight: 600; color: #1A1612; display: flex; align-items: center; gap: 8px; }
|
||||
.card-header::before { content: ''; display: block; width: 3px; height: 14px; background: #8B2331; border-radius: 2px; }
|
||||
.param-row { display: flex; align-items: flex-start; padding: 10px 16px; border-bottom: 1px solid #F5F1EB; }
|
||||
.param-row:last-child { border-bottom: none; }
|
||||
.param-label { font-size: 13px; color: #9E9580; min-width: 72px; flex-shrink: 0; padding-top: 1px; }
|
||||
.param-val { font-size: 13px; color: #1A1612; flex: 1; line-height: 1.5; }
|
||||
|
||||
/* AUTHENTICITY */
|
||||
.auth-card { background: #FAF8F5; margin-top: 10px; padding: 20px 16px; }
|
||||
.auth-inner { background: #fff; border: 1px solid #E8E4DC; border-radius: 8px; padding: 16px; display: flex; gap: 14px; align-items: center; }
|
||||
.auth-rows { flex: 1; display: grid; grid-template-columns: 64px 1fr; gap: 8px; font-size: 12px; line-height: 1.7; }
|
||||
.auth-lbl { color: #9E9580; }
|
||||
.auth-val { color: #1A1612; font-weight: 500; }
|
||||
.auth-mono { font-family: 'JetBrains Mono', monospace; color: var(--brand-700); font-weight: 600; letter-spacing: 0.04em; }
|
||||
.auth-stamp { width: 72px; height: 72px; border-radius: 50%; border: 2px solid #8B2331; background: rgba(139,35,49,0.04); display: grid; place-items: center; flex-shrink: 0; position: relative; transform: rotate(-8deg); overflow: hidden; }
|
||||
.auth-stamp-text { font-size: 8px; color: #8B2331; font-weight: 700; text-align: center; letter-spacing: 0.02em; line-height: 1.4; }
|
||||
.auth-note { font-size: 11px; color: #A89E92; margin-top: 12px; text-align: center; line-height: 1.6; }
|
||||
|
||||
/* SHOP CARD */
|
||||
.shop-card { background: #fff; margin-top: 10px; }
|
||||
.shop-name-row { display: flex; align-items: center; gap: 8px; padding: 14px 16px 12px; border-bottom: 1px solid #F0ECE6; }
|
||||
.shop-icon { font-size: 16px; }
|
||||
.shop-name-text { font-size: 14px; font-weight: 600; color: #1A1612; }
|
||||
|
||||
/* FOOTER */
|
||||
.footer { padding: 20px 16px 32px; display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
.footer-mark { width: 18px; height: 18px; background: #0F3057; border-radius: 3px; display: flex; align-items: center; justify-content: center; }
|
||||
.footer-mark::after { content: '岩'; color: rgba(255,255,255,0.9); font-size: 10px; font-weight: 700; }
|
||||
.footer-text { font-size: 11px; color: #A89E92; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="state-loading" class="state-screen">
|
||||
<div class="spinner"></div>
|
||||
<div class="state-text">正在查询商品信息…</div>
|
||||
</div>
|
||||
|
||||
<div id="state-error" class="state-screen" style="display:none">
|
||||
<div class="error-icon">✗</div>
|
||||
<div class="error-title">商品不存在或已下架</div>
|
||||
<div class="error-sub">请检查二维码是否完整,或联系出售方确认。</div>
|
||||
<button class="retry-btn" onclick="load()">重试</button>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<div class="gallery" id="gallery">
|
||||
<div class="gallery-main" id="gallery-main">
|
||||
<div class="gallery-placeholder" id="gallery-placeholder">
|
||||
<div class="placeholder-mark"></div>
|
||||
<div class="placeholder-text">暂无图片</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="thumbnails" id="thumbnails" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="ribbon">
|
||||
<div class="ribbon-check"></div>
|
||||
<div class="ribbon-text">
|
||||
<div class="ribbon-title">已通过岩美防伪验证</div>
|
||||
<div class="ribbon-sub" id="ribbon-shop"></div>
|
||||
</div>
|
||||
<div class="ribbon-icon">✓</div>
|
||||
</div>
|
||||
|
||||
<div class="title-section">
|
||||
<h1 class="product-name" id="product-name">—</h1>
|
||||
<p class="product-subtitle" id="product-subtitle"></p>
|
||||
<div class="tags" id="product-tags"></div>
|
||||
</div>
|
||||
|
||||
<div class="quick-specs" id="quick-specs" style="display:none">
|
||||
<!-- filled by JS -->
|
||||
</div>
|
||||
|
||||
<div class="params-card" id="params-card">
|
||||
<div class="card-header">商品参数</div>
|
||||
<div id="params-rows"></div>
|
||||
</div>
|
||||
|
||||
<div class="auth-card" id="auth-card" style="display:none">
|
||||
<div class="card-header">批次与防伪</div>
|
||||
<div class="auth-inner">
|
||||
<div class="auth-rows" id="auth-rows"></div>
|
||||
<div class="auth-stamp">
|
||||
<div class="auth-stamp-text">岩美<br/>防伪<br/>✓</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-note">如发现可疑商品请联系出售方确认</div>
|
||||
</div>
|
||||
|
||||
<div class="shop-card" id="shop-card" style="display:none">
|
||||
<div class="shop-name-row">
|
||||
<span class="shop-icon">🏪</span>
|
||||
<span class="shop-name-text" id="shop-display-name">门店信息</span>
|
||||
</div>
|
||||
<div id="shop-rows"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="footer-mark"></div>
|
||||
<div class="footer-text">由岩美酒库管理系统提供防伪验证</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = location.hostname === 'localhost' ? 'http://localhost:8080/api/v1' : '/api/v1';
|
||||
|
||||
function getPublicId() {
|
||||
// URL pattern: /scan/<public_id>
|
||||
const parts = location.pathname.split('/').filter(Boolean);
|
||||
const idx = parts.indexOf('scan');
|
||||
return idx >= 0 ? parts[idx + 1] : null;
|
||||
}
|
||||
|
||||
let _imgs = [];
|
||||
let _current = 0;
|
||||
|
||||
function showImg(i) {
|
||||
_current = i;
|
||||
document.querySelectorAll('#gallery-main img').forEach((el, j) => {
|
||||
el.classList.toggle('active', j === i);
|
||||
});
|
||||
document.querySelectorAll('.thumb').forEach((el, j) => {
|
||||
el.classList.toggle('active', j === i);
|
||||
});
|
||||
const counter = document.getElementById('gallery-counter');
|
||||
if (counter) counter.textContent = `${i + 1} / ${_imgs.length}`;
|
||||
}
|
||||
|
||||
function renderGallery(imageUrls) {
|
||||
const main = document.getElementById('gallery-main');
|
||||
const thumbsEl = document.getElementById('thumbnails');
|
||||
const placeholder = document.getElementById('gallery-placeholder');
|
||||
|
||||
if (!imageUrls || imageUrls.length === 0) return;
|
||||
|
||||
_imgs = imageUrls;
|
||||
placeholder.style.display = 'none';
|
||||
|
||||
imageUrls.forEach((url, i) => {
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.alt = '';
|
||||
if (i === 0) img.classList.add('active');
|
||||
img.onclick = () => openFullscreen(i);
|
||||
main.appendChild(img);
|
||||
});
|
||||
|
||||
if (imageUrls.length > 1) {
|
||||
const counter = document.createElement('div');
|
||||
counter.className = 'gallery-counter';
|
||||
counter.id = 'gallery-counter';
|
||||
counter.textContent = `1 / ${imageUrls.length}`;
|
||||
main.appendChild(counter);
|
||||
|
||||
thumbsEl.style.display = 'flex';
|
||||
imageUrls.forEach((url, i) => {
|
||||
const t = document.createElement('img');
|
||||
t.src = url;
|
||||
t.className = 'thumb' + (i === 0 ? ' active' : '');
|
||||
t.onclick = () => showImg(i);
|
||||
thumbsEl.appendChild(t);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openFullscreen(i) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.9);z-index:999;display:flex;align-items:center;justify-content:center;';
|
||||
const img = document.createElement('img');
|
||||
img.src = _imgs[i];
|
||||
img.style.cssText = 'max-width:100%;max-height:100%;object-fit:contain;';
|
||||
overlay.appendChild(img);
|
||||
overlay.onclick = () => document.body.removeChild(overlay);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function parseQuickSpecs(spec, productionDate) {
|
||||
if (!spec) return [];
|
||||
const items = [];
|
||||
const degM = spec.match(/(\d+(?:\.\d+)?)\s*(?:度|°|%vol)/i);
|
||||
if (degM) items.push({ v: degM[1], u: '°', l: '酒精度' });
|
||||
const mlM = spec.match(/(\d+(?:\.\d+)?)\s*ml/i);
|
||||
if (mlM) items.push({ v: mlM[1], u: 'ml', l: '净含量' });
|
||||
const xiangM = spec.match(/([清浓酱兼馥凤]香)/);
|
||||
if (xiangM) items.push({ v: xiangM[1], u: '', l: '香型' });
|
||||
if (productionDate) {
|
||||
const yrM = productionDate.match(/^(\d{4})/);
|
||||
if (yrM) items.push({ v: yrM[1], u: '', l: '年份' });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function paramRow(label, value) {
|
||||
return `<div class="param-row"><div class="param-label">${label}</div><div class="param-val">${value}</div></div>`;
|
||||
}
|
||||
|
||||
function render(d) {
|
||||
const name = d.name || '';
|
||||
const series = d.series || '';
|
||||
const spec = d.spec || '';
|
||||
const brand = d.brand || '';
|
||||
const unit = d.unit || '';
|
||||
const description = d.description || '';
|
||||
const shop = d.shop;
|
||||
const batch = d.batch;
|
||||
const baseUrl = location.origin;
|
||||
|
||||
const images = (d.images || []).map(img => baseUrl + img.url);
|
||||
renderGallery(images);
|
||||
|
||||
document.getElementById('product-name').textContent = name;
|
||||
const subtitleParts = [series, spec].filter(Boolean);
|
||||
const subtitleEl = document.getElementById('product-subtitle');
|
||||
subtitleEl.textContent = subtitleParts.join(' · ');
|
||||
if (!subtitleParts.length) subtitleEl.style.display = 'none';
|
||||
|
||||
const tagsEl = document.getElementById('product-tags');
|
||||
if (brand) tagsEl.innerHTML += `<span class="tag tag-brand">${brand}</span>`;
|
||||
if (unit) tagsEl.innerHTML += `<span class="tag tag-unit">${unit}</span>`;
|
||||
|
||||
// Ribbon shop info
|
||||
if (shop && shop.name) {
|
||||
document.getElementById('ribbon-shop').textContent = `出售方:${shop.name}`;
|
||||
}
|
||||
|
||||
// Quick Specs — parse from spec string
|
||||
const quickSpecsEl = document.getElementById('quick-specs');
|
||||
const qs = parseQuickSpecs(spec, batch ? batch.production_date : null);
|
||||
if (qs.length > 0) {
|
||||
quickSpecsEl.style.display = 'grid';
|
||||
quickSpecsEl.innerHTML = qs.map(it => `
|
||||
<div class="quick-spec-item">
|
||||
<div class="quick-spec-val">${it.v}<span class="quick-spec-unit">${it.u||''}</span></div>
|
||||
<div class="quick-spec-label">${it.l}</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// Params
|
||||
let paramsHtml = '';
|
||||
if (spec) paramsHtml += paramRow('规格', spec);
|
||||
if (brand) paramsHtml += paramRow('品牌', brand);
|
||||
if (unit) paramsHtml += paramRow('单位', unit);
|
||||
if (description) paramsHtml += paramRow('描述', description);
|
||||
if (paramsHtml) {
|
||||
document.getElementById('params-rows').innerHTML = paramsHtml;
|
||||
} else {
|
||||
document.getElementById('params-card').style.display = 'none';
|
||||
}
|
||||
|
||||
// Authenticity (batch info)
|
||||
if (batch && (batch.batch_no || batch.production_date || batch.in_stock_date)) {
|
||||
const authCard = document.getElementById('auth-card');
|
||||
let authHtml = '';
|
||||
if (batch.batch_no) authHtml += `<div class="auth-lbl">批次号</div><div class="auth-val auth-mono">${batch.batch_no}</div>`;
|
||||
if (batch.production_date) authHtml += `<div class="auth-lbl">生产日期</div><div class="auth-val">${batch.production_date}</div>`;
|
||||
if (batch.in_stock_date) authHtml += `<div class="auth-lbl">入库日期</div><div class="auth-val">${batch.in_stock_date}</div>`;
|
||||
document.getElementById('auth-rows').innerHTML = authHtml;
|
||||
authCard.style.display = 'block';
|
||||
}
|
||||
|
||||
// Shop card
|
||||
if (shop) {
|
||||
const shopCard = document.getElementById('shop-card');
|
||||
const shopName = shop.name || '';
|
||||
document.getElementById('shop-display-name').textContent = shopName || '门店信息';
|
||||
let shopHtml = '';
|
||||
if (shop.code) shopHtml += paramRow('门店编号', shop.code);
|
||||
if (shop.address) shopHtml += paramRow('地址', shop.address);
|
||||
if (shop.phone) shopHtml += paramRow('电话', shop.phone);
|
||||
if (shopHtml) {
|
||||
document.getElementById('shop-rows').innerHTML = shopHtml;
|
||||
shopCard.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('state-loading').style.display = 'none';
|
||||
document.getElementById('content').style.display = 'block';
|
||||
}
|
||||
|
||||
function load() {
|
||||
const pid = getPublicId();
|
||||
if (!pid) {
|
||||
document.getElementById('state-loading').style.display = 'none';
|
||||
document.getElementById('state-error').style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('state-loading').style.display = 'flex';
|
||||
document.getElementById('state-error').style.display = 'none';
|
||||
document.getElementById('content').style.display = 'none';
|
||||
|
||||
fetch(`${API_BASE}/public/products/${pid}`)
|
||||
.then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
|
||||
.then(data => render(data.data))
|
||||
.catch(() => {
|
||||
document.getElementById('state-loading').style.display = 'none';
|
||||
document.getElementById('state-error').style.display = 'flex';
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user