Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fc5776435 | |||
| bc252d1fcb | |||
| c0001f08c2 | |||
| 46ef7db480 | |||
| 0c96d5e325 | |||
| ced407ea87 | |||
| e550b73d6d | |||
| fa02fdbfd8 | |||
| 211fafc7e1 |
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: 本地编译与静态检查(后端 go build/vet + 前端 flutter analyze)
|
||||
argument-hint: 可选 backend | client,留空则全部
|
||||
model: claude-sonnet-4-6
|
||||
allowed-tools: Bash
|
||||
---
|
||||
|
||||
执行本地编译与静态检查,用于快速验证代码可编译、无警告。**不产出发布制品**(多平台打包由 CI 在发版时完成)。
|
||||
|
||||
参数 `$ARGUMENTS`:
|
||||
- 留空:检查后端 + 前端
|
||||
- `backend`:仅检查后端
|
||||
- `client`:仅检查前端
|
||||
|
||||
任意步骤失败立即停止并报告错误(贴出关键错误输出)。
|
||||
|
||||
## 后端检查(参数为空或 backend 时执行)
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
cd backend && go build ./... # 1. 编译通过
|
||||
cd backend && go vet ./... # 2. 无编译警告
|
||||
```
|
||||
|
||||
## 前端检查(参数为空或 client 时执行)
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
cd client && flutter analyze --no-fatal-infos --no-fatal-warnings # error 才算失败,warning/info 允许
|
||||
```
|
||||
|
||||
## 完成报告
|
||||
|
||||
全部通过后,用一行 `result:` 汇总(如 `result: 后端 build+vet 通过、前端 analyze 无 error`)。
|
||||
任一失败则停止,指出失败的命令和原因。
|
||||
@@ -1,4 +1,17 @@
|
||||
执行本地发版流程,版本号为 $ARGUMENTS。
|
||||
---
|
||||
description: 执行本地发版流程(build → test → CHANGELOG → commit → tag → push)
|
||||
argument-hint: <version> 如 1.0.22
|
||||
model: claude-sonnet-4-6
|
||||
allowed-tools: Bash, Read, Edit, Write
|
||||
---
|
||||
|
||||
执行本地发版流程。
|
||||
|
||||
**确定版本号:**
|
||||
- 如果 `$ARGUMENTS` 非空,使用它作为版本号(VERSION)。
|
||||
- 如果 `$ARGUMENTS` 为空,运行 `git describe --tags --abbrev=0` 获取最新 tag(如 `v1.0.20`),去掉 `v` 前缀后将 patch 号加 1(如 `1.0.21`),作为版本号(VERSION)。将自动确定的版本号告知用户。
|
||||
|
||||
后续所有步骤中,将确定好的版本号记为 VERSION。
|
||||
|
||||
按以下步骤顺序执行,任意步骤失败立即停止并报告错误。
|
||||
|
||||
@@ -23,13 +36,13 @@ cd client && flutter analyze --no-fatal-infos --no-fatal-warnings
|
||||
|
||||
## 4. 更新 CHANGELOG.md
|
||||
|
||||
检查 CHANGELOG.md 中是否已有 `## [$ARGUMENTS]` 版本节。
|
||||
检查 CHANGELOG.md 中是否已有 `## [VERSION]` 版本节。
|
||||
|
||||
- **已有**:直接使用,不修改。
|
||||
- **没有**:读取 `git log` 从上一个 tag 到 HEAD 的提交记录,自动生成一个版本节插入到文件顶部(位于已有版本节之前),格式如下:
|
||||
|
||||
```
|
||||
## [$ARGUMENTS] - <今天日期>
|
||||
## [VERSION] - <今天日期>
|
||||
|
||||
### 新功能
|
||||
- ...
|
||||
@@ -58,19 +71,19 @@ cd client && flutter analyze --no-fatal-infos --no-fatal-warnings
|
||||
将所有改动(包括 CHANGELOG.md)用以下格式提交到 main:
|
||||
|
||||
```
|
||||
chore: release v$ARGUMENTS
|
||||
chore: release vVERSION
|
||||
```
|
||||
|
||||
## 6. 打 tag
|
||||
|
||||
```bash
|
||||
git tag v$ARGUMENTS
|
||||
git tag vVERSION
|
||||
```
|
||||
|
||||
## 7. 推送
|
||||
|
||||
```bash
|
||||
git push ssh://git@git.51yanmei.com:2222/wangjia/jiu.git main v$ARGUMENTS
|
||||
git push ssh://git@git.51yanmei.com:2222/wangjia/jiu.git main vVERSION
|
||||
```
|
||||
|
||||
推送成功后提示用户:CI/CD 已触发,等待 Telegram 通知。
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
description: 运行本地测试(后端 go test + 前端 flutter test)
|
||||
argument-hint: 可选 backend | client,留空则全部
|
||||
model: claude-sonnet-4-6
|
||||
allowed-tools: Bash
|
||||
---
|
||||
|
||||
运行本地自动化测试。任意用例失败立即停止并报告(贴出失败用例与关键输出)。
|
||||
|
||||
参数 `$ARGUMENTS`:
|
||||
- 留空:后端 + 前端
|
||||
- `backend`:仅后端
|
||||
- `client`:仅前端
|
||||
|
||||
## 后端测试(参数为空或 backend 时执行)
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
cd backend && go test ./...
|
||||
```
|
||||
|
||||
不得有 FAIL。`[no test files]` 属正常(该包无测试)。
|
||||
|
||||
## 前端测试(参数为空或 client 时执行)
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
cd client && flutter test
|
||||
```
|
||||
|
||||
须 `All tests passed!`。
|
||||
|
||||
## 完成报告
|
||||
|
||||
全部通过后用一行 `result:` 汇总(如 `result: 后端 go test 全过、前端 121 测试全过`)。
|
||||
任一失败则停止,列出失败用例名与原因,不要自行修改业务代码(如需修复,告知用户或交给对应 coder)。
|
||||
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.22] - 2026-06-07
|
||||
|
||||
### 新功能
|
||||
- 官网移动端适配:手机浏览器下导航折叠、首页各区块单列排版、features 详情页和页脚自适应窄屏
|
||||
|
||||
### 改进
|
||||
- 客户端移动端布局优化:入库/出库详情页商品明细改为卡片式竖排,操作按钮收纳至溢出菜单,窄屏不再横向溢出
|
||||
- 信息架构整理:仓库管理移入基础数据,系统设置新增授权 Tab 和数据管理组,关于我们补充帮助文档/法律信息/系统信息入口
|
||||
|
||||
### 修复
|
||||
- 官网所有功能页死链修正(`/download.html` → `/download/`,`/docs.html` → `/docs/`)
|
||||
- 官网删除未实现功能的虚假宣传(红冲、调拨单、库存推送通知、过期预警等),文案与实际功能对齐
|
||||
- 官网客服邮箱更新为真实邮箱,移除假冒客服电话,页脚清除无目标死链
|
||||
- 导航新增「库存」「审核流」功能页入口,首页模块卡加「了解更多」链接
|
||||
- 下载页接口失败时按钮改为禁用态并提示,不再无响应
|
||||
- 首页 iOS 平台说明改为「TestFlight 内测中」,与下载页状态一致
|
||||
|
||||
## [1.0.21] - 2026-06-07
|
||||
|
||||
### 新功能
|
||||
- 商品详情页添加照片时,手机端(Android/iOS)支持直接拍照或从相册选择
|
||||
|
||||
## [1.0.20] - 2026-06-07
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"provider": "岩美技术有限公司",
|
||||
"website": "https://jiu.yanmei.com",
|
||||
"email": "yammy2023@163.com"
|
||||
"email": "yammy2023@163.com",
|
||||
"phone": "",
|
||||
"wechat": "",
|
||||
"terms_url": "https://jiu.yanmei.com/terms/",
|
||||
"privacy_url": "https://jiu.yanmei.com/privacy/",
|
||||
"docs_url": "https://jiu.yanmei.com/docs/"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>用于拍摄商品照片</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>用于从相册选择商品照片</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
|
||||
@@ -12,6 +12,11 @@ class AppInfo {
|
||||
static String provider = '';
|
||||
static String website = '';
|
||||
static String email = '';
|
||||
static String phone = '';
|
||||
static String wechat = '';
|
||||
static String termsUrl = '';
|
||||
static String privacyUrl = '';
|
||||
static String docsUrl = '';
|
||||
|
||||
/// 加载配置文件。失败时保留为空(不影响应用启动)。
|
||||
static Future<void> load() async {
|
||||
@@ -21,6 +26,11 @@ class AppInfo {
|
||||
provider = (map['provider'] as String?) ?? '';
|
||||
website = (map['website'] as String?) ?? '';
|
||||
email = (map['email'] as String?) ?? '';
|
||||
phone = (map['phone'] as String?) ?? '';
|
||||
wechat = (map['wechat'] as String?) ?? '';
|
||||
termsUrl = (map['terms_url'] as String?) ?? '';
|
||||
privacyUrl = (map['privacy_url'] as String?) ?? '';
|
||||
docsUrl = (map['docs_url'] as String?) ?? '';
|
||||
} catch (e) {
|
||||
debugPrint('AppInfo.load failed: $e');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:io';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../core/config/app_info.dart';
|
||||
@@ -11,10 +12,9 @@ import '../../core/theme/app_theme.dart';
|
||||
import '../../core/update/app_updater.dart';
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import '../../providers/feedback_provider.dart';
|
||||
import '../../providers/license_provider.dart';
|
||||
import '../../providers/update_provider.dart';
|
||||
|
||||
/// 「关于我们」独立页面(左侧菜单项)。原为系统设置里的「关于」Tab。
|
||||
/// 「关于我们」独立页面(左侧菜单项)。
|
||||
class AboutScreen extends ConsumerStatefulWidget {
|
||||
const AboutScreen({super.key});
|
||||
|
||||
@@ -27,7 +27,6 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final appVersion = ref.watch(appVersionProvider).valueOrNull ?? 'v1.0.0';
|
||||
final updateInfo = ref.watch(updateProvider).valueOrNull;
|
||||
final licenseAsync = ref.watch(licenseProvider);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
@@ -80,76 +79,33 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── 授权信息 ──
|
||||
// ── 帮助与文档 ──
|
||||
_AboutSection(
|
||||
title: '授权信息',
|
||||
title: '帮助与文档',
|
||||
children: [
|
||||
licenseAsync.when(
|
||||
loading: () => const _AboutRow(label: '授权状态', value: '加载中…'),
|
||||
error: (_, __) =>
|
||||
const _AboutRow(label: '授权状态', value: '暂无授权信息'),
|
||||
data: (lic) {
|
||||
if (lic == null) {
|
||||
return const _AboutRow(label: '授权状态', value: '未激活');
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
_AboutRow(label: '授权类型', value: lic.typeLabel),
|
||||
_AboutRow(
|
||||
label: '授权状态',
|
||||
value: lic.isExpired
|
||||
? '已过期'
|
||||
: lic.isActive
|
||||
? '正常'
|
||||
: '已停用',
|
||||
valueColor: lic.isExpired
|
||||
? AppTheme.danger
|
||||
: lic.isActive
|
||||
? AppTheme.success
|
||||
: AppTheme.textSecondary,
|
||||
),
|
||||
if (lic.expiresAt != null)
|
||||
_AboutRow(
|
||||
label: '到期时间',
|
||||
value: DateFormat('yyyy-MM-dd').format(lic.expiresAt!),
|
||||
trailing: lic.daysRemaining != null &&
|
||||
lic.daysRemaining! <= 30
|
||||
? Chip(
|
||||
label: Text(
|
||||
lic.isExpired
|
||||
? '已过期'
|
||||
: '剩余 ${lic.daysRemaining} 天',
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.white),
|
||||
),
|
||||
backgroundColor: lic.isExpired
|
||||
? AppTheme.danger
|
||||
: Colors.orange,
|
||||
padding: EdgeInsets.zero,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
else
|
||||
const _AboutRow(label: '到期时间', value: '永久有效'),
|
||||
if (lic.activatedAt != null)
|
||||
_AboutRow(
|
||||
label: '激活时间',
|
||||
value: DateFormat('yyyy-MM-dd')
|
||||
.format(lic.activatedAt!),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const _AboutRow(label: '使用手册', value: '查看产品使用文档与操作指南'),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _showRenewDialog(),
|
||||
icon: const Icon(Icons.card_membership, size: 16),
|
||||
label: const Text('续费 / 升级授权'),
|
||||
onPressed: () async {
|
||||
final url = Uri.parse(
|
||||
AppInfo.docsUrl.isNotEmpty
|
||||
? AppInfo.docsUrl
|
||||
: '${AppInfo.website}/docs/');
|
||||
if (await canLaunchUrl(url)) launchUrl(url);
|
||||
},
|
||||
icon: const Icon(Icons.menu_book_outlined, size: 16),
|
||||
label: const Text('打开文档'),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
final url = Uri.parse('${AppInfo.website}/downloads/');
|
||||
if (await canLaunchUrl(url)) launchUrl(url);
|
||||
},
|
||||
icon: const Icon(Icons.history_outlined, size: 16),
|
||||
label: const Text('更新日志'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -162,9 +118,25 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
title: '关于我们',
|
||||
children: [
|
||||
_AboutRow(label: '服务商', value: AppInfo.provider),
|
||||
_AboutRow(label: '官方网站', value: AppInfo.website),
|
||||
_AboutRow(
|
||||
label: '官方网站',
|
||||
value: AppInfo.website,
|
||||
trailing: AppInfo.website.isNotEmpty
|
||||
? TextButton(
|
||||
onPressed: () async {
|
||||
final uri = Uri.parse(AppInfo.website);
|
||||
if (await canLaunchUrl(uri)) launchUrl(uri);
|
||||
},
|
||||
child: const Text('访问', style: TextStyle(fontSize: 12)),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
_AboutRow(label: '联系邮箱', value: AppInfo.email),
|
||||
const _AboutRow(label: '技术支持', value: '周一至周五 9:00 - 18:00'),
|
||||
if (AppInfo.phone.isNotEmpty)
|
||||
_AboutRow(label: '联系电话', value: AppInfo.phone),
|
||||
if (AppInfo.wechat.isNotEmpty)
|
||||
_AboutRow(label: '微信', value: AppInfo.wechat),
|
||||
const _AboutRow(label: '服务时间', value: '周一至周五 9:00–18:00'),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@@ -184,6 +156,81 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── 扫码防伪 ──
|
||||
const _AboutSection(
|
||||
title: '扫码防伪',
|
||||
children: [
|
||||
_AboutRow(
|
||||
label: '功能说明',
|
||||
value: '每件商品可生成专属二维码,顾客扫码即可查看商品名称、系列、规格及批次信息,帮助验证商品真实性。',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── 法律信息 ──
|
||||
_AboutSection(
|
||||
title: '法律信息',
|
||||
children: [
|
||||
if (AppInfo.termsUrl.isNotEmpty)
|
||||
_AboutRow(
|
||||
label: '服务条款',
|
||||
value: '查看服务条款',
|
||||
trailing: TextButton(
|
||||
onPressed: () async {
|
||||
final uri = Uri.parse(AppInfo.termsUrl);
|
||||
if (await canLaunchUrl(uri)) launchUrl(uri);
|
||||
},
|
||||
child: const Text('查看', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
if (AppInfo.privacyUrl.isNotEmpty)
|
||||
_AboutRow(
|
||||
label: '隐私政策',
|
||||
value: '查看隐私政策',
|
||||
trailing: TextButton(
|
||||
onPressed: () async {
|
||||
final uri = Uri.parse(AppInfo.privacyUrl);
|
||||
if (await canLaunchUrl(uri)) launchUrl(uri);
|
||||
},
|
||||
child: const Text('查看', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
_AboutRow(label: '版权', value: '© ${DateTime.now().year} ${AppInfo.provider}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── 系统信息 ──
|
||||
_AboutSection(
|
||||
title: '系统信息',
|
||||
children: [
|
||||
_AboutRow(
|
||||
label: '运行平台',
|
||||
value: kIsWeb
|
||||
? 'Web'
|
||||
: Platform.isAndroid
|
||||
? 'Android'
|
||||
: Platform.isIOS
|
||||
? 'iOS'
|
||||
: Platform.isMacOS
|
||||
? 'macOS'
|
||||
: Platform.isWindows
|
||||
? 'Windows'
|
||||
: Platform.operatingSystem,
|
||||
),
|
||||
_AboutRow(label: '应用版本', value: appVersion),
|
||||
FutureBuilder<PackageInfo>(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (ctx, snap) => _AboutRow(
|
||||
label: '构建号',
|
||||
value: snap.data?.buildNumber ?? '-',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── 意见反馈 ──
|
||||
_AboutSection(
|
||||
title: '意见反馈',
|
||||
@@ -215,43 +262,6 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showRenewDialog() {
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('续费 / 升级授权'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('请联系我们获取续费报价:'),
|
||||
const SizedBox(height: 12),
|
||||
SelectableText('📧 ${AppInfo.email}',
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: AppInfo.email));
|
||||
if (ctx.mounted) {
|
||||
Navigator.pop(ctx);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('邮箱已复制到剪贴板')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('复制邮箱'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFeedbackDialog({required bool isBug}) {
|
||||
showAppDialog(
|
||||
context: context,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -91,7 +92,56 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端(Android/iOS)支持拍照,用 image_picker;桌面/Web 用 file_picker 选文件。
|
||||
// 用 defaultTargetPlatform 而非 dart:io 的 Platform,避免在参与 Web 编译的文件里引入 dart:io。
|
||||
bool get _isMobile =>
|
||||
!kIsWeb &&
|
||||
(defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS);
|
||||
|
||||
Future<void> _pickAndUpload() async {
|
||||
if (_isMobile) {
|
||||
await _pickFromCameraOrGallery();
|
||||
} else {
|
||||
await _pickFromFiles();
|
||||
}
|
||||
}
|
||||
|
||||
/// 移动端:底部弹出「拍照 / 从相册选择」
|
||||
Future<void> _pickFromCameraOrGallery() async {
|
||||
final source = await showModalBottomSheet<ImageSource>(
|
||||
context: context,
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt_outlined),
|
||||
title: const Text('拍照'),
|
||||
onTap: () => Navigator.pop(ctx, ImageSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library_outlined),
|
||||
title: const Text('从相册选择'),
|
||||
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (source == null) return;
|
||||
|
||||
final XFile? xfile = await ImagePicker().pickImage(
|
||||
source: source,
|
||||
maxWidth: 2000,
|
||||
imageQuality: 85,
|
||||
);
|
||||
if (xfile == null) return;
|
||||
await _uploadImage(filePath: xfile.path, fileName: xfile.name);
|
||||
}
|
||||
|
||||
/// 桌面 / Web:文件选择器
|
||||
Future<void> _pickFromFiles() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
allowMultiple: false,
|
||||
@@ -102,13 +152,17 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
final String? filePath = kIsWeb ? null : file.path;
|
||||
final fileBytes = file.bytes;
|
||||
if (filePath == null && fileBytes == null) return;
|
||||
await _uploadImage(
|
||||
filePath: filePath, bytes: fileBytes, fileName: file.name);
|
||||
}
|
||||
|
||||
Future<void> _uploadImage(
|
||||
{String? filePath, Uint8List? bytes, required String fileName}) async {
|
||||
setState(() => _uploading = true);
|
||||
try {
|
||||
final img = await ref
|
||||
.read(productRepositoryProvider)
|
||||
.uploadImage(_product!.id, filePath,
|
||||
bytes: fileBytes, fileName: file.name);
|
||||
final img = await ref.read(productRepositoryProvider).uploadImage(
|
||||
_product!.id, filePath,
|
||||
bytes: bytes, fileName: fileName);
|
||||
_updateImages([..._product!.images, img]);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/warehouse.dart';
|
||||
import '../../providers/product_option_provider.dart';
|
||||
import '../../providers/warehouse_provider.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
@@ -41,11 +43,13 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
Tab(text: '商品名称'),
|
||||
Tab(text: '系列'),
|
||||
Tab(text: '规格'),
|
||||
Tab(text: '仓库'),
|
||||
],
|
||||
tabViews: [
|
||||
_buildNameTab(),
|
||||
_buildSeriesTab(),
|
||||
_buildSpecTab(),
|
||||
_buildWarehousesTab(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -451,6 +455,123 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 仓库 tab ──────────────────────────────────────────────
|
||||
|
||||
Widget _buildWarehousesTab() {
|
||||
final asyncWarehouses = ref.watch(warehouseListProvider);
|
||||
return asyncWarehouses.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => _buildError(() => ref.read(warehouseListProvider.notifier).reload()),
|
||||
data: (warehouses) => DataTableCard(
|
||||
totalCount: warehouses.length,
|
||||
page: 1,
|
||||
pageSize: warehouses.length + 1,
|
||||
onPageChanged: (_) {},
|
||||
onPageSizeChanged: (_) {},
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showWarehouseDialog(),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('新建'),
|
||||
),
|
||||
],
|
||||
),
|
||||
mobileCards: warehouses
|
||||
.map((w) => MobileListCard(
|
||||
title: Text(w.name),
|
||||
subtitle: w.location?.isNotEmpty == true ? Text(w.location!) : null,
|
||||
fields: [
|
||||
if (w.isDefault) const MobileCardField('默认仓库', '是'),
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => _showWarehouseDialog(warehouse: w),
|
||||
child: const Text('编辑', style: TextStyle(fontSize: 13)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _confirmDeleteWarehouse(w),
|
||||
child: const Text('删除',
|
||||
style: TextStyle(fontSize: 13, color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
))
|
||||
.toList(),
|
||||
columns: const [
|
||||
DataColumn(label: Text('仓库名称')),
|
||||
DataColumn(label: Text('位置')),
|
||||
DataColumn(label: Text('默认仓库')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: warehouses.isEmpty
|
||||
? [const DataRow(cells: [
|
||||
DataCell(SizedBox()),
|
||||
DataCell(Text('暂无仓库', style: TextStyle(color: AppTheme.textSecondary))),
|
||||
DataCell(SizedBox()),
|
||||
DataCell(SizedBox()),
|
||||
])]
|
||||
: warehouses
|
||||
.map((w) => DataRow(cells: [
|
||||
DataCell(Text(w.name, style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||
DataCell(Text(w.location ?? '-')),
|
||||
DataCell(w.isDefault
|
||||
? const Icon(Icons.check_circle, color: AppTheme.success, size: 18)
|
||||
: const SizedBox()),
|
||||
DataCell(_actionButtons(
|
||||
onEdit: () => _showWarehouseDialog(warehouse: w),
|
||||
onDelete: () => _confirmDeleteWarehouse(w),
|
||||
)),
|
||||
]))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showWarehouseDialog({Warehouse? warehouse}) {
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _WarehouseFormDialog(
|
||||
warehouse: warehouse,
|
||||
onSaved: () => ref.read(warehouseListProvider.notifier).reload(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDeleteWarehouse(Warehouse w) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确认删除仓库「${w.name}」?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('取消')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true && mounted) {
|
||||
try {
|
||||
await ref.read(warehouseListProvider.notifier).deleteWarehouse(w.id);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('删除成功'), backgroundColor: AppTheme.success));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('删除失败:$e'), backgroundColor: AppTheme.danger));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showOptionDialog({
|
||||
required String title,
|
||||
required bool hasQuantity,
|
||||
@@ -535,3 +656,119 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WarehouseFormDialog extends ConsumerStatefulWidget {
|
||||
final Warehouse? warehouse;
|
||||
final VoidCallback onSaved;
|
||||
|
||||
const _WarehouseFormDialog({this.warehouse, required this.onSaved});
|
||||
|
||||
@override
|
||||
ConsumerState<_WarehouseFormDialog> createState() => _WarehouseFormDialogState();
|
||||
}
|
||||
|
||||
class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameCtrl;
|
||||
late final TextEditingController _locationCtrl;
|
||||
late bool _isDefault;
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameCtrl = TextEditingController(text: widget.warehouse?.name ?? '');
|
||||
_locationCtrl = TextEditingController(text: widget.warehouse?.location ?? '');
|
||||
_isDefault = widget.warehouse?.isDefault ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_locationCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _saving = true);
|
||||
final data = {
|
||||
'name': _nameCtrl.text.trim(),
|
||||
if (_locationCtrl.text.trim().isNotEmpty) 'location': _locationCtrl.text.trim(),
|
||||
'is_default': _isDefault,
|
||||
};
|
||||
try {
|
||||
final notifier = ref.read(warehouseListProvider.notifier);
|
||||
if (widget.warehouse != null) {
|
||||
await notifier.updateWarehouse(widget.warehouse!.id, data);
|
||||
} else {
|
||||
await notifier.createWarehouse(data);
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
widget.onSaved();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(widget.warehouse != null ? '仓库更新成功' : '仓库创建成功'),
|
||||
backgroundColor: AppTheme.success,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('保存失败:$e'), backgroundColor: AppTheme.danger));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.warehouse != null ? '编辑仓库' : '新建仓库'),
|
||||
content: SizedBox(
|
||||
width: context.dialogWidth(400),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(labelText: '仓库名称'),
|
||||
validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _locationCtrl,
|
||||
decoration: const InputDecoration(labelText: '位置'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
title: const Text('设为默认仓库'),
|
||||
value: _isDefault,
|
||||
onChanged: (v) => setState(() => _isDefault = v ?? false),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('保存'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,20 @@ import 'package:dio/dio.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/auth/auth_state.dart';
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../core/config/app_info.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/number_rule.dart';
|
||||
import '../../models/user.dart';
|
||||
import '../../models/warehouse.dart';
|
||||
import '../../providers/license_provider.dart';
|
||||
import '../../providers/number_rule_provider.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../providers/warehouse_provider.dart';
|
||||
import '../../providers/shop_provider.dart';
|
||||
import '../../models/shop.dart';
|
||||
|
||||
@@ -56,10 +57,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
tabs: [
|
||||
Tab(text: '酒行信息'),
|
||||
Tab(text: '用户管理'),
|
||||
Tab(text: '仓库管理'),
|
||||
Tab(text: '编号规则'),
|
||||
Tab(text: '系统参数'),
|
||||
Tab(text: '数据导入'),
|
||||
Tab(text: '授权'),
|
||||
Tab(text: '数据管理'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -69,9 +70,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
children: [
|
||||
_buildShopInfoTab(),
|
||||
_buildUsersTab(),
|
||||
_buildWarehousesTab(),
|
||||
_buildNumberRulesTab(),
|
||||
_buildSystemParamsTab(),
|
||||
_buildLicenseTab(),
|
||||
_buildImportTab(),
|
||||
],
|
||||
),
|
||||
@@ -289,153 +290,151 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWarehousesTab() {
|
||||
final asyncWarehouses = ref.watch(warehouseListProvider);
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 52,
|
||||
color: AppTheme.surface,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showWarehouseDialog(context),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('新建'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: asyncWarehouses.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.cloud_off, size: 40, color: AppTheme.textSecondary),
|
||||
const SizedBox(height: 12),
|
||||
const Text('暂无数据,网络不可用',
|
||||
style: const TextStyle(color: AppTheme.textSecondary)),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
ref.read(warehouseListProvider.notifier).reload(),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
// ── 授权 Tab ──────────────────────────────────────────────
|
||||
Widget _buildLicenseTab() {
|
||||
final licenseAsync = ref.watch(licenseProvider);
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('授权信息',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
const Text('当前门店的授权状态与到期信息',
|
||||
style: TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: licenseAsync.when(
|
||||
loading: () => const _ParamRow(label: '授权状态', value: '加载中…'),
|
||||
error: (_, __) => const _ParamRow(label: '授权状态', value: '暂无授权信息'),
|
||||
data: (lic) {
|
||||
if (lic == null) {
|
||||
return Column(
|
||||
children: [
|
||||
const _ParamRow(label: '授权状态', value: '未激活'),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _showRenewLicenseDialog,
|
||||
icon: const Icon(Icons.card_membership, size: 16),
|
||||
label: const Text('续费 / 升级授权'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final statusColor = lic.isExpired
|
||||
? AppTheme.danger
|
||||
: lic.isActive
|
||||
? AppTheme.success
|
||||
: AppTheme.textSecondary;
|
||||
final statusText = lic.isExpired
|
||||
? '已过期'
|
||||
: lic.isActive
|
||||
? '正常'
|
||||
: '已停用';
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_ParamRow(label: '授权类型', value: lic.typeLabel),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 180,
|
||||
child: Text('授权状态',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary)),
|
||||
),
|
||||
Text(statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: statusColor)),
|
||||
if (lic.daysRemaining != null &&
|
||||
lic.daysRemaining! <= 30) ...[
|
||||
const SizedBox(width: 8),
|
||||
Chip(
|
||||
label: Text(
|
||||
lic.isExpired
|
||||
? '已过期'
|
||||
: '剩余 ${lic.daysRemaining} 天',
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.white),
|
||||
),
|
||||
backgroundColor:
|
||||
lic.isExpired ? AppTheme.danger : Colors.orange,
|
||||
padding: EdgeInsets.zero,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (lic.expiresAt != null)
|
||||
_ParamRow(
|
||||
label: '到期时间',
|
||||
value: DateFormat('yyyy-MM-dd').format(lic.expiresAt!))
|
||||
else
|
||||
const _ParamRow(label: '到期时间', value: '永久有效'),
|
||||
if (lic.activatedAt != null)
|
||||
_ParamRow(
|
||||
label: '激活时间',
|
||||
value: DateFormat('yyyy-MM-dd').format(lic.activatedAt!)),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _showRenewLicenseDialog,
|
||||
icon: const Icon(Icons.card_membership, size: 16),
|
||||
label: const Text('续费 / 升级授权'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
data: (warehouses) {
|
||||
if (warehouses.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('暂无仓库',
|
||||
style:
|
||||
TextStyle(color: AppTheme.textSecondary)));
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: DataTable(
|
||||
headingRowColor:
|
||||
WidgetStateProperty.all(const Color(0xFFF0F4FF)),
|
||||
columns: const [
|
||||
DataColumn(label: Text('仓库名称')),
|
||||
DataColumn(label: Text('位置')),
|
||||
DataColumn(label: Text('默认仓库')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: warehouses
|
||||
.map((w) => DataRow(cells: [
|
||||
DataCell(Text(w.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500))),
|
||||
DataCell(Text(w.location ?? '-')),
|
||||
DataCell(w.isDefault
|
||||
? const Icon(Icons.check_circle,
|
||||
color: AppTheme.success, size: 18)
|
||||
: const SizedBox()),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
key: Key('btn_edit_${w.id}'),
|
||||
onPressed: () =>
|
||||
_showWarehouseDialog(context,
|
||||
warehouse: w),
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
TextButton(
|
||||
key: Key('btn_delete_${w.id}'),
|
||||
onPressed: () =>
|
||||
_confirmDeleteWarehouse(context, w),
|
||||
child: const Text('删除',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
)),
|
||||
]))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDeleteWarehouse(
|
||||
BuildContext context, Warehouse w) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确认删除仓库「${w.name}」?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('取消')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.danger,
|
||||
foregroundColor: Colors.white),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true && mounted) {
|
||||
try {
|
||||
await ref
|
||||
.read(warehouseListProvider.notifier)
|
||||
.deleteWarehouse(w.id);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('删除成功'),
|
||||
backgroundColor: AppTheme.success));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('删除失败:$e'),
|
||||
backgroundColor: AppTheme.danger));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showWarehouseDialog(BuildContext context, {Warehouse? warehouse}) {
|
||||
void _showRenewLicenseDialog() {
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _WarehouseFormDialog(
|
||||
warehouse: warehouse,
|
||||
onSaved: () =>
|
||||
ref.read(warehouseListProvider.notifier).reload(),
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('续费 / 升级授权'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('请联系我们获取续费报价:'),
|
||||
const SizedBox(height: 12),
|
||||
SelectableText('📧 ${AppInfo.email}',
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: AppInfo.email));
|
||||
if (ctx.mounted) {
|
||||
Navigator.pop(ctx);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('邮箱已复制到剪贴板')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('复制邮箱'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -760,133 +759,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _WarehouseFormDialog extends ConsumerStatefulWidget {
|
||||
final Warehouse? warehouse;
|
||||
final VoidCallback onSaved;
|
||||
|
||||
const _WarehouseFormDialog({this.warehouse, required this.onSaved});
|
||||
|
||||
@override
|
||||
ConsumerState<_WarehouseFormDialog> createState() =>
|
||||
_WarehouseFormDialogState();
|
||||
}
|
||||
|
||||
class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameCtrl;
|
||||
late final TextEditingController _locationCtrl;
|
||||
late bool _isDefault;
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameCtrl = TextEditingController(text: widget.warehouse?.name ?? '');
|
||||
_locationCtrl =
|
||||
TextEditingController(text: widget.warehouse?.location ?? '');
|
||||
_isDefault = widget.warehouse?.isDefault ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_locationCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _saving = true);
|
||||
final data = {
|
||||
'name': _nameCtrl.text.trim(),
|
||||
if (_locationCtrl.text.trim().isNotEmpty)
|
||||
'location': _locationCtrl.text.trim(),
|
||||
'is_default': _isDefault,
|
||||
};
|
||||
try {
|
||||
final notifier = ref.read(warehouseListProvider.notifier);
|
||||
if (widget.warehouse != null) {
|
||||
await notifier.updateWarehouse(widget.warehouse!.id, data);
|
||||
} else {
|
||||
await notifier.createWarehouse(data);
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
widget.onSaved();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
widget.warehouse != null ? '仓库更新成功' : '仓库创建成功'),
|
||||
backgroundColor: AppTheme.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('保存失败:$e'),
|
||||
backgroundColor: AppTheme.danger),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.warehouse != null ? '编辑仓库' : '新建仓库'),
|
||||
content: SizedBox(
|
||||
width: context.dialogWidth(400),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(labelText: '仓库名称'),
|
||||
validator: (v) =>
|
||||
(v == null || v.isEmpty) ? '不能为空' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _locationCtrl,
|
||||
decoration: const InputDecoration(labelText: '位置'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
title: const Text('设为默认仓库'),
|
||||
value: _isDefault,
|
||||
onChanged: (v) => setState(() => _isDefault = v ?? false),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('保存'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 新增/编辑用户弹窗 ─────────────────────────────────────
|
||||
class _UserFormDialog extends ConsumerStatefulWidget {
|
||||
final AppUser? user;
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../providers/inventory_provider.dart';
|
||||
import '../../providers/stock_in_provider.dart';
|
||||
import '../../providers/warehouse_provider.dart';
|
||||
import '../../widgets/searchable_option_field.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
|
||||
class StockInFormScreen extends ConsumerStatefulWidget {
|
||||
final int? editOrderId;
|
||||
@@ -296,6 +297,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
final asyncWarehouses = ref.watch(warehouseListProvider);
|
||||
final asyncSuppliers = ref.watch(supplierListProvider);
|
||||
final currentUser = ref.watch(authStateProvider).user;
|
||||
final isMobile = context.isMobile;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
@@ -316,35 +318,71 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
Text(_isEdit ? '修改入库单' : '新建入库单',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
if (_isEdit && _loadedOrder != null) ...[
|
||||
OutlinedButton.icon(
|
||||
onPressed: _printOrder,
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: const Text('打印'),
|
||||
// 窄屏:仅保留主操作「提交」,其余收进溢出菜单,避免按钮挤爆顶栏
|
||||
if (isMobile) ...[
|
||||
ElevatedButton(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
child: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('提交'),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (v) {
|
||||
switch (v) {
|
||||
case 'draft':
|
||||
_submit(true);
|
||||
break;
|
||||
case 'print':
|
||||
_printOrder();
|
||||
break;
|
||||
case 'cancel':
|
||||
context.go('/stock-in');
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(value: 'draft', child: Text('保存草稿')),
|
||||
if (_isEdit && _loadedOrder != null)
|
||||
const PopupMenuItem(value: 'print', child: Text('打印')),
|
||||
const PopupMenuItem(value: 'cancel', child: Text('取消')),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
if (_isEdit && _loadedOrder != null) ...[
|
||||
OutlinedButton.icon(
|
||||
onPressed: _printOrder,
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: const Text('打印'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : () => _submit(true),
|
||||
child: const Text('保存草稿'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.send, size: 16),
|
||||
label: const Text('提交审核'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/stock-in'),
|
||||
icon: const Icon(Icons.cancel_outlined, size: 16),
|
||||
label: const Text('取消'),
|
||||
),
|
||||
],
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : () => _submit(true),
|
||||
child: const Text('保存草稿'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.send, size: 16),
|
||||
label: const Text('提交审核'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/stock-in'),
|
||||
icon: const Icon(Icons.cancel_outlined, size: 16),
|
||||
label: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -498,41 +536,53 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36), // 序号
|
||||
1: FlexColumnWidth(1.2), // 商品编码
|
||||
2: FlexColumnWidth(2.0), // 名称
|
||||
3: FlexColumnWidth(1.3), // 系列
|
||||
4: FlexColumnWidth(1.3), // 规格
|
||||
5: FlexColumnWidth(0.9), // 单品数量
|
||||
6: FlexColumnWidth(1.0), // 数量
|
||||
7: FlexColumnWidth(1.0), // 单价
|
||||
8: FlexColumnWidth(1.0), // 金额
|
||||
9: FlexColumnWidth(1.2), // 批次号
|
||||
10: FlexColumnWidth(1.2), // 生产日期
|
||||
11: FixedColumnWidth(60), // 操作
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||
children: [
|
||||
'序号', '商品编码', '名称', '系列', '规格', '单品数量', '数量', '单价', '金额', '批次号', '生产日期', '操作',
|
||||
]
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 10),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
...List.generate(_items.length, (i) => _buildItemRow(i)),
|
||||
],
|
||||
),
|
||||
// 窄屏:逐项卡片竖排,避免 12 列表格横向溢出;宽屏保持表格
|
||||
if (isMobile)
|
||||
Column(
|
||||
children: List.generate(
|
||||
_items.length,
|
||||
(i) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 10),
|
||||
child: _buildItemCard(i),
|
||||
)),
|
||||
)
|
||||
else
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36), // 序号
|
||||
1: FlexColumnWidth(1.2), // 商品编码
|
||||
2: FlexColumnWidth(2.0), // 名称
|
||||
3: FlexColumnWidth(1.3), // 系列
|
||||
4: FlexColumnWidth(1.3), // 规格
|
||||
5: FlexColumnWidth(0.9), // 单品数量
|
||||
6: FlexColumnWidth(1.0), // 数量
|
||||
7: FlexColumnWidth(1.0), // 单价
|
||||
8: FlexColumnWidth(1.0), // 金额
|
||||
9: FlexColumnWidth(1.2), // 批次号
|
||||
10: FlexColumnWidth(1.2), // 生产日期
|
||||
11: FixedColumnWidth(60), // 操作
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||
children: [
|
||||
'序号', '商品编码', '名称', '系列', '规格', '单品数量', '数量', '单价', '金额', '批次号', '生产日期', '操作',
|
||||
]
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 10),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
...List.generate(_items.length, (i) => _buildItemRow(i)),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
@@ -566,104 +616,191 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 明细字段构件(表格行与移动卡片共用,保持两种布局一致)──────────
|
||||
|
||||
Widget _nameField(_ItemRow item) {
|
||||
final asyncNames = ref.watch(productNameListProvider);
|
||||
return asyncNames.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('加载失败'),
|
||||
data: (names) => SearchableOptionField(
|
||||
options: names
|
||||
.map((o) => OptionItem(id: o.id, name: o.name, code: o.code))
|
||||
.toList(),
|
||||
selectedId: item.selectedNameId,
|
||||
hint: '选择名称',
|
||||
dialogTitle: '选择商品名称',
|
||||
isRequired: true,
|
||||
onChanged: (v) => setState(() {
|
||||
item.selectedNameId = v;
|
||||
item.productId = null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seriesField(_ItemRow item) {
|
||||
final asyncSeries = ref.watch(productSeriesListProvider);
|
||||
return asyncSeries.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('加载失败'),
|
||||
data: (series) => SearchableOptionField(
|
||||
options: series
|
||||
.map((o) => OptionItem(id: o.id, name: o.name, code: o.code))
|
||||
.toList(),
|
||||
selectedId: item.selectedSeriesId,
|
||||
hint: '选择系列',
|
||||
dialogTitle: '选择系列',
|
||||
isRequired: true,
|
||||
onChanged: (v) => setState(() {
|
||||
item.selectedSeriesId = v;
|
||||
item.productId = null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _specField(_ItemRow item) {
|
||||
final asyncSpecs = ref.watch(productSpecListProvider);
|
||||
return asyncSpecs.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('加载失败'),
|
||||
data: (specs) => SearchableOptionField(
|
||||
options: specs
|
||||
.map((o) => OptionItem(id: o.id, name: o.name, code: o.code))
|
||||
.toList(),
|
||||
selectedId: item.selectedSpecId,
|
||||
hint: '选择规格',
|
||||
dialogTitle: '选择规格',
|
||||
isRequired: true,
|
||||
onChanged: (v) => setState(() {
|
||||
item.selectedSpecId = v;
|
||||
item.productId = null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _qtyField(_ItemRow item) {
|
||||
return TextFormField(
|
||||
controller: item.qtyCtrl,
|
||||
decoration: const InputDecoration(hintText: '0', isDense: true),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))
|
||||
],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '不能为空';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _priceField(_ItemRow item) {
|
||||
return TextFormField(
|
||||
controller: item.priceCtrl,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '0.00', prefixText: '¥', isDense: true),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))
|
||||
],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '不能为空';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _batchField(_ItemRow item) {
|
||||
return TextFormField(
|
||||
controller: item.batchNoCtrl,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '选填',
|
||||
labelText: '批次号',
|
||||
isDense: true,
|
||||
),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
onChanged: (_) => setState(() {}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dateField(_ItemRow item) {
|
||||
return TextFormField(
|
||||
controller: item.productionDateCtrl,
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请选择',
|
||||
labelText: '* 生产日期',
|
||||
isDense: true,
|
||||
suffixIcon: Icon(Icons.calendar_today, size: 14),
|
||||
),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: item.productionDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
locale: const Locale('zh', 'CN'),
|
||||
);
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
item.productionDate = date;
|
||||
item.productionDateCtrl.text =
|
||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
int _specQtyOf(_ItemRow item) =>
|
||||
ref.read(productSpecListProvider).valueOrNull
|
||||
?.where((o) => o.id == item.selectedSpecId)
|
||||
.firstOrNull
|
||||
?.quantity ??
|
||||
0;
|
||||
|
||||
String _productCodeOf(_ItemRow item) =>
|
||||
ref.read(productNameListProvider).valueOrNull
|
||||
?.where((o) => o.id == item.selectedNameId)
|
||||
.firstOrNull
|
||||
?.code ??
|
||||
'';
|
||||
|
||||
TableRow _buildItemRow(int index) {
|
||||
final item = _items[index];
|
||||
final asyncNames = ref.watch(productNameListProvider);
|
||||
final asyncSeries = ref.watch(productSeriesListProvider);
|
||||
final asyncSpecs = ref.watch(productSpecListProvider);
|
||||
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
|
||||
final price = double.tryParse(item.priceCtrl.text) ?? 0;
|
||||
final amount = qty * price;
|
||||
final specQty = asyncSpecs.valueOrNull
|
||||
?.where((o) => o.id == item.selectedSpecId)
|
||||
.firstOrNull
|
||||
?.quantity ?? 0;
|
||||
final productCode = asyncNames.valueOrNull
|
||||
?.where((o) => o.id == item.selectedNameId)
|
||||
.firstOrNull
|
||||
?.code ?? '';
|
||||
final specQty = _specQtyOf(item);
|
||||
final productCode = _productCodeOf(item);
|
||||
|
||||
return TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven ? Colors.white : const Color(0xFFFAFAFA),
|
||||
),
|
||||
children: [
|
||||
// 序号
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Text('${index + 1}',
|
||||
style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
|
||||
),
|
||||
// 商品编码
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Text(productCode,
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary)),
|
||||
),
|
||||
// 名称
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: asyncNames.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('加载失败'),
|
||||
data: (names) => SearchableOptionField(
|
||||
options: names
|
||||
.map((o) => OptionItem(id: o.id, name: o.name, code: o.code))
|
||||
.toList(),
|
||||
selectedId: item.selectedNameId,
|
||||
hint: '选择名称',
|
||||
dialogTitle: '选择商品名称',
|
||||
isRequired: true,
|
||||
onChanged: (v) => setState(() {
|
||||
item.selectedNameId = v;
|
||||
item.productId = null;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 系列
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: asyncSeries.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('加载失败'),
|
||||
data: (series) => SearchableOptionField(
|
||||
options: series
|
||||
.map((o) => OptionItem(id: o.id, name: o.name, code: o.code))
|
||||
.toList(),
|
||||
selectedId: item.selectedSeriesId,
|
||||
hint: '选择系列',
|
||||
dialogTitle: '选择系列',
|
||||
isRequired: true,
|
||||
onChanged: (v) => setState(() {
|
||||
item.selectedSeriesId = v;
|
||||
item.productId = null;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 规格
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: asyncSpecs.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (_, __) => const Text('加载失败'),
|
||||
data: (specs) => SearchableOptionField(
|
||||
options: specs
|
||||
.map((o) => OptionItem(id: o.id, name: o.name, code: o.code))
|
||||
.toList(),
|
||||
selectedId: item.selectedSpecId,
|
||||
hint: '选择规格',
|
||||
dialogTitle: '选择规格',
|
||||
isRequired: true,
|
||||
onChanged: (v) => setState(() {
|
||||
item.selectedSpecId = v;
|
||||
item.productId = null;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 单品数量
|
||||
Padding(padding: const EdgeInsets.all(4), child: _nameField(item)),
|
||||
Padding(padding: const EdgeInsets.all(4), child: _seriesField(item)),
|
||||
Padding(padding: const EdgeInsets.all(4), child: _specField(item)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Text(
|
||||
@@ -674,46 +811,8 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// 数量
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: item.qtyCtrl,
|
||||
decoration: const InputDecoration(hintText: '0', isDense: true),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))
|
||||
],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '不能为空';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
// 单价
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: item.priceCtrl,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '0.00', prefixText: '¥', isDense: true),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))
|
||||
],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '不能为空';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
// 金额
|
||||
Padding(padding: const EdgeInsets.all(4), child: _qtyField(item)),
|
||||
Padding(padding: const EdgeInsets.all(4), child: _priceField(item)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Text(
|
||||
@@ -721,52 +820,8 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
// 批次号
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: item.batchNoCtrl,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '选填',
|
||||
labelText: '批次号',
|
||||
isDense: true,
|
||||
),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
// 生产日期
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: item.productionDateCtrl,
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请选择',
|
||||
labelText: '* 生产日期',
|
||||
isDense: true,
|
||||
suffixIcon: Icon(Icons.calendar_today, size: 14),
|
||||
),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: item.productionDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
locale: const Locale('zh', 'CN'),
|
||||
);
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
item.productionDate = date;
|
||||
item.productionDateCtrl.text =
|
||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
// 操作
|
||||
Padding(padding: const EdgeInsets.all(4), child: _batchField(item)),
|
||||
Padding(padding: const EdgeInsets.all(4), child: _dateField(item)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
child: IconButton(
|
||||
@@ -781,6 +836,38 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 窄屏(手机):每个明细行渲染为一张卡片,字段竖排,避免表格横向溢出。
|
||||
Widget _buildItemCard(int index) {
|
||||
final item = _items[index];
|
||||
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
|
||||
final price = double.tryParse(item.priceCtrl.text) ?? 0;
|
||||
final amount = qty * price;
|
||||
final specQty = _specQtyOf(item);
|
||||
final productCode = _productCodeOf(item);
|
||||
|
||||
return MobileListCard(
|
||||
title: Text('商品 ${index + 1}'),
|
||||
subtitle: productCode.isNotEmpty ? Text('编码 $productCode') : null,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, size: 20, color: AppTheme.danger),
|
||||
onPressed: _items.length > 1 ? () => _removeItem(index) : null,
|
||||
tooltip: '删除',
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
fields: [
|
||||
MobileCardField('名称', null, valueWidget: _nameField(item)),
|
||||
MobileCardField('系列', null, valueWidget: _seriesField(item)),
|
||||
MobileCardField('规格', null, valueWidget: _specField(item)),
|
||||
MobileCardField('单品数量', specQty > 0 ? '$specQty' : '-'),
|
||||
MobileCardField('数量', null, valueWidget: _qtyField(item)),
|
||||
MobileCardField('单价', null, valueWidget: _priceField(item)),
|
||||
MobileCardField('金额', '¥${amount.toStringAsFixed(2)}'),
|
||||
MobileCardField('批次号', null, valueWidget: _batchField(item)),
|
||||
MobileCardField('生产日期', null, valueWidget: _dateField(item)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInventoryCell(int? productId) {
|
||||
if (productId == null) {
|
||||
return const Padding(
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../providers/inventory_provider.dart';
|
||||
import '../../providers/partner_provider.dart';
|
||||
import '../../providers/stock_out_provider.dart';
|
||||
import '../../providers/warehouse_provider.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
|
||||
// Aggregated per-product inventory item for the picker dialog
|
||||
class _PickerItem {
|
||||
@@ -307,6 +308,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
final asyncWarehouses = ref.watch(warehouseListProvider);
|
||||
final asyncCustomers = ref.watch(customerListProvider);
|
||||
final currentUser = ref.watch(authStateProvider).user;
|
||||
final isMobile = context.isMobile;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
@@ -327,35 +329,71 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
Text(_isEdit ? '修改出库单' : '新建出库单',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
if (_isEdit && _loadedOrder != null) ...[
|
||||
OutlinedButton.icon(
|
||||
onPressed: _printOrder,
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: const Text('打印'),
|
||||
// 窄屏:仅保留主操作「提交」,其余收进溢出菜单,避免按钮挤爆顶栏
|
||||
if (isMobile) ...[
|
||||
ElevatedButton(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
child: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('提交'),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (v) {
|
||||
switch (v) {
|
||||
case 'draft':
|
||||
_submit(true);
|
||||
break;
|
||||
case 'print':
|
||||
_printOrder();
|
||||
break;
|
||||
case 'cancel':
|
||||
context.go('/stock-out');
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(value: 'draft', child: Text('保存草稿')),
|
||||
if (_isEdit && _loadedOrder != null)
|
||||
const PopupMenuItem(value: 'print', child: Text('打印')),
|
||||
const PopupMenuItem(value: 'cancel', child: Text('取消')),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
if (_isEdit && _loadedOrder != null) ...[
|
||||
OutlinedButton.icon(
|
||||
onPressed: _printOrder,
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: const Text('打印'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : () => _submit(true),
|
||||
child: const Text('保存草稿'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.send, size: 16),
|
||||
label: const Text('提交审核'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/stock-out'),
|
||||
icon: const Icon(Icons.cancel_outlined, size: 16),
|
||||
label: const Text('取消'),
|
||||
),
|
||||
],
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : () => _submit(true),
|
||||
child: const Text('保存草稿'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.send, size: 16),
|
||||
label: const Text('提交审核'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/stock-out'),
|
||||
icon: const Icon(Icons.cancel_outlined, size: 16),
|
||||
label: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -507,38 +545,50 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36), // 序号
|
||||
1: FlexColumnWidth(1.2), // 商品编码
|
||||
2: FlexColumnWidth(2.0), // 商品名称
|
||||
3: FlexColumnWidth(1.2), // 系列
|
||||
4: FlexColumnWidth(1.2), // 规格
|
||||
5: FlexColumnWidth(1.0), // 单价
|
||||
6: FlexColumnWidth(0.8), // 数量
|
||||
7: FlexColumnWidth(1.0), // 金额
|
||||
8: FixedColumnWidth(48), // 操作
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||
children: [
|
||||
'序号', '商品编码', '商品名称', '系列', '规格', '单价', '数量', '金额', '操作',
|
||||
]
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 10),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
...List.generate(_items.length, (i) => _buildItemRow(i)),
|
||||
],
|
||||
),
|
||||
// 窄屏:逐项卡片竖排,避免 9 列表格横向溢出;宽屏保持表格
|
||||
if (isMobile)
|
||||
Column(
|
||||
children: List.generate(
|
||||
_items.length,
|
||||
(i) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 10),
|
||||
child: _buildItemCard(i),
|
||||
)),
|
||||
)
|
||||
else
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36), // 序号
|
||||
1: FlexColumnWidth(1.2), // 商品编码
|
||||
2: FlexColumnWidth(2.0), // 商品名称
|
||||
3: FlexColumnWidth(1.2), // 系列
|
||||
4: FlexColumnWidth(1.2), // 规格
|
||||
5: FlexColumnWidth(1.0), // 单价
|
||||
6: FlexColumnWidth(0.8), // 数量
|
||||
7: FlexColumnWidth(1.0), // 金额
|
||||
8: FixedColumnWidth(48), // 操作
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||
children: [
|
||||
'序号', '商品编码', '商品名称', '系列', '规格', '单价', '数量', '金额', '操作',
|
||||
]
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 10),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
...List.generate(_items.length, (i) => _buildItemRow(i)),
|
||||
],
|
||||
),
|
||||
if (_items.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
@@ -583,7 +633,6 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
|
||||
final price = item.unitPrice ?? 0;
|
||||
final amount = qty * price;
|
||||
final available = _inventoryMap[item.productId];
|
||||
|
||||
return TableRow(
|
||||
decoration: BoxDecoration(
|
||||
@@ -603,22 +652,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
// 单价
|
||||
_cell(Text(price > 0 ? '¥${price.toStringAsFixed(2)}' : '-', style: const TextStyle(fontSize: 13))),
|
||||
// 数量
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: item.qtyCtrl,
|
||||
decoration: const InputDecoration(hintText: '0', isDense: true),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '不能为空';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.all(4), child: _qtyField(item)),
|
||||
// 金额
|
||||
_cell(Text('¥${amount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))),
|
||||
// 操作
|
||||
@@ -636,6 +670,48 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _qtyField(_ItemRow item) {
|
||||
return TextFormField(
|
||||
controller: item.qtyCtrl,
|
||||
decoration: const InputDecoration(hintText: '0', isDense: true),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '不能为空';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 窄屏(手机):每个明细行渲染为一张卡片,字段竖排,避免表格横向溢出。
|
||||
Widget _buildItemCard(int index) {
|
||||
final item = _items[index];
|
||||
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
|
||||
final price = item.unitPrice ?? 0;
|
||||
final amount = qty * price;
|
||||
|
||||
return MobileListCard(
|
||||
title: Text(item.productName),
|
||||
subtitle: item.productCode.isNotEmpty ? Text('编码 ${item.productCode}') : null,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, size: 20, color: AppTheme.danger),
|
||||
onPressed: () => _removeItem(index),
|
||||
tooltip: '删除',
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
fields: [
|
||||
if (item.series.isNotEmpty) MobileCardField('系列', item.series),
|
||||
if (item.spec.isNotEmpty) MobileCardField('规格', item.spec),
|
||||
MobileCardField('单价', price > 0 ? '¥${price.toStringAsFixed(2)}' : '-'),
|
||||
MobileCardField('数量', null, valueWidget: _qtyField(item)),
|
||||
MobileCardField('金额', '¥${amount.toStringAsFixed(2)}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _cell(Widget child) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: child,
|
||||
|
||||
@@ -6,6 +6,7 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_picker
|
||||
import file_selector_macos
|
||||
import package_info_plus
|
||||
import printing
|
||||
import shared_preferences_foundation
|
||||
@@ -13,6 +14,7 @@ import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
|
||||
@@ -161,6 +161,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.7"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.5"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+5"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -253,6 +285,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+19"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+6"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2+1"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.1"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -25,6 +25,7 @@ dependencies:
|
||||
printing: ^5.13.0
|
||||
pdf: ^3.10.8
|
||||
web: ^1.1.0
|
||||
image_picker: ^1.2.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
PrintingPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
printing
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Web 站点问题清单 / TODO · 岩美</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 0 0 80px;
|
||||
font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #1f2933; background: #f7f9fb; line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
header {
|
||||
background: linear-gradient(135deg, #0A1F3B 0%, #15407D 100%);
|
||||
color: #fff; padding: 40px 32px 32px;
|
||||
}
|
||||
header h1 { margin: 0 0 8px; font-size: 26px; font-weight: 700; }
|
||||
header p { margin: 0; color: #ADC9EA; font-size: 14px; }
|
||||
.wrap { max-width: 1040px; margin: 0 auto; padding: 0 24px; }
|
||||
.meta { margin-top: 16px; font-size: 13px; color: #8aa3c4; }
|
||||
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 10px; margin: 24px auto 8px; max-width: 1040px; padding: 0 24px; }
|
||||
.tag { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
||||
.t-block { background: #fde8e8; color: #b91c1c; } /* 阻断/必须改 */
|
||||
.t-high { background: #fef0d8; color: #9a6700; } /* 重要 */
|
||||
.t-low { background: #e6f0fb; color: #1d4ed8; } /* 一般/优化 */
|
||||
.t-known { background: #ececec; color: #555; } /* 已知占位 */
|
||||
.t-ver { background: #e3f5ea; color: #1f7a44; } /* 已核实 */
|
||||
.t-done { background: #d1f0dc; color: #1f7a44; } /* 已完成 */
|
||||
|
||||
section { max-width: 1040px; margin: 28px auto 0; padding: 0 24px; }
|
||||
h2 {
|
||||
font-size: 18px; font-weight: 700; color: #0A1F3B;
|
||||
border-left: 4px solid #2563AC; padding-left: 12px; margin: 0 0 14px;
|
||||
}
|
||||
ul.todo { list-style: none; margin: 0; padding: 0; }
|
||||
ul.todo li {
|
||||
background: #fff; border: 1px solid #e4e9ef; border-radius: 8px;
|
||||
padding: 14px 16px; margin-bottom: 10px;
|
||||
display: grid; grid-template-columns: 22px 1fr; gap: 12px; align-items: start;
|
||||
}
|
||||
ul.todo li:has(input:checked) { background: #f3faf5; border-color: #b7e4c7; opacity: 0.7; }
|
||||
ul.todo li:has(input:checked) .item-title { color: #52606d; text-decoration: line-through; }
|
||||
ul.todo li input[type=checkbox] { margin-top: 4px; width: 16px; height: 16px; }
|
||||
.item-title { font-weight: 600; font-size: 15px; }
|
||||
.item-title .tag { margin-left: 8px; vertical-align: middle; }
|
||||
.item-desc { font-size: 13.5px; color: #52606d; margin-top: 4px; }
|
||||
.item-desc code {
|
||||
background: #f0f2f5; padding: 1px 5px; border-radius: 3px;
|
||||
font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: #b91c1c;
|
||||
}
|
||||
.file { font-family: "JetBrains Mono", monospace; font-size: 12px; color: #7b8794; }
|
||||
.note { background: #fffbe9; border:1px solid #f0e3b0; border-radius:8px; padding:12px 16px; font-size:13px; color:#6b5b1d; margin-top:14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="wrap">
|
||||
<h1>Web 营销站点 — 问题清单 / TODO</h1>
|
||||
<p>检查范围:产品介绍(首页)· 功能详情页 · 价格 · 下载 · 文档 · 注册 · 页眉页脚</p>
|
||||
<div class="meta">生成日期 2026-06-07 · 对照后端实现核对 · 代码版本 v1.0.20</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="legend">
|
||||
<span class="tag t-block">阻断 必须改</span>
|
||||
<span class="tag t-high">重要</span>
|
||||
<span class="tag t-low">一般/优化</span>
|
||||
<span class="tag t-known">已知占位(暂留)</span>
|
||||
<span class="tag t-ver">已核实代码</span>
|
||||
<span class="tag t-done">已完成</span>
|
||||
</div>
|
||||
|
||||
<!-- ============ 虚假/夸大宣传(最优先,已核实代码) ============ -->
|
||||
<section>
|
||||
<h2>一、功能宣传与实际实现不符(已对照后端代码核实)</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「红冲入库 / 红冲出库」未实现 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
|
||||
<div class="item-desc">后端 <code>internal/</code> 全局无任何红冲 / reversal / 冲销逻辑,但首页 FAQ「审批通过后发现错误」与 <span class="file">features/approval.html</span> 第 5 块(红冲 / 净额演示)大篇幅宣传。属虚假宣传,需删除或落地功能。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「调拨单」未实现 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
|
||||
<div class="item-desc">后端无 transfer / 调拨 任何实现。<span class="file">features/approval.html</span> 状态机把「调拨单」与入库/出库/盘点并列为四大单据。需移除调拨相关展示。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「库存预警 / 阈值推送」严重夸大 <span class="tag t-block">阻断</span><span class="tag t-ver">已核实</span></div>
|
||||
<div class="item-desc">后端仅有 <code>min_stock</code> 单一字段用于显示「偏低/告急」标记,<b>没有</b>任何告警、推送、按 SKU 多级阈值配置、按操作员分发的能力。但首页「库存预警实时提醒」与 <span class="file">features/inventory.html</span> 整块「预警阈值配置 / 推送给张磊 / 短信通知李建国」均为虚构界面。需大幅收敛文案。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「短信通知 / 邮件提醒」未实现 <span class="tag t-high">重要</span><span class="tag t-ver">已核实</span></div>
|
||||
<div class="item-desc">后端无 SMS / 邮件发送能力。inventory/approval 页多处「短信通知」「邮件提醒」需删除。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「过期预警 / 保质期推算」未实现 <span class="tag t-high">重要</span><span class="tag t-ver">已核实</span></div>
|
||||
<div class="item-desc">后端 expire 相关仅为「license 授权到期」,无商品保质期/过期预警。<span class="file">features/inventory.html</span> 批次块的「过期预警」与阈值卡「临近过期 30 天邮件提醒」为虚构。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「智能补货建议 v1.7 Beta」「SSO 单点登录」未实现 <span class="tag t-low">一般</span><span class="tag t-ver">已核实</span></div>
|
||||
<div class="item-desc">补货建议(inventory 页标 v1.7 Beta)、价格连锁版的「SSO 单点登录」后端均无。若作为路线图保留需明确标注「规划中」,否则移除。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「允许超量出库」可配置开关 待核实 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">approval 页声称系统设置可开启「允许超量出库」,后端未检索到 oversell/超量 配置项,需确认是否真有该设置。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">营销数据疑似杜撰 <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">首页 Trust 区「1,280+ 门店 / 8.4% 损耗下降 / 99.9% 可用性」无数据支撑;功能页「14 个审计字段」「审批延迟 ≤1s」等均需核实,否则改为中性表述。</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="note">✅ 已核实<b>确实存在</b>的功能(文案无需改):FIFO 批次扣减出库(<code>stock.go ApproveStockOut</code>)、多仓库(<code>warehouse</code>)、库存盘点、入库/出库审批联动库存+应付应收、商品 <code>min_stock</code> 状态标记、拼音搜索、扫码验真(scan.html)。</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ 版本/品牌一致性 ============ -->
|
||||
<section>
|
||||
<h2>二、版本号与品牌/域名一致性</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">首页 Hero 版本号造假 <span class="tag t-block">阻断</span></div>
|
||||
<div class="item-desc">首页徽标硬编码「v1.6 · 新增多仓库联动盘点」,inventory 页又写「v1.6.2」,而真实版本为 <b>v1.0.20</b>(CHANGELOG)。需统一为真实版本,最好像下载页一样从 <code>changelog</code> 数据动态取。<span class="file">index.njk:180-183</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">品牌域名三处不统一 <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">首页预览写 <code>yanmei.app/inventory</code>、客服邮箱 <code>support@yanmei.app</code>,而真实应用域名是 <code>jiu.51yanmei.com</code>。需统一域名口径。<span class="file">index.njk:201 · _data/site.json</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">下载页 API 域名硬编码 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc"><code>https://jiu.51yanmei.com</code> 直接写死在 <span class="file">download.njk</span> 的 JS 里(释放接口、各端下载、Web 入口),应抽到 <code>_data/site.json</code> 统一管理,便于换域名/环境。</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============ 页眉 ============ -->
|
||||
<section>
|
||||
<h2>三、页眉(导航)</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">功能详情页死链:<code>/download.html</code> <span class="tag t-block">阻断</span></div>
|
||||
<div class="item-desc">两个功能页(手写 HTML)导航和页脚都链向 <code>/download.html</code>、<code>/docs.html</code>,但 Eleventy 实际产出 pretty URL <code>/download/</code>、<code>/docs/</code>。dist 中无 <code>download.html</code> → 死链。统一改为 <code>/download/</code>、<code>/docs/</code>。<span class="file">features/inventory.html:660-661 · approval.html 同</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">功能详情页未进主导航(孤岛页面) <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc"><span class="file">features/inventory.html</span>、<span class="file">approval.html</span> 制作精良,但主站导航「产品」只跳首页锚点 <code>/#modules</code>,这两页仅能从页脚或彼此跳转进入。建议在导航或首页模块卡加「了解更多」入口。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" />
|
||||
<div>
|
||||
<div class="item-title">两套 CSS / 设计令牌并存 <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">主站 njk 用 <code>color.css + style.css</code>,功能页用 <code>colors_and_type.css</code>,导航/页脚样式各写一份。维护分裂、易不一致,建议合并到一套并让功能页也走 base.njk 模板。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">注册入口不一致 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">功能页 JS 未登录态指向 <code>/app/login</code>、<code>/app/register</code>(Flutter 内路由),而 njk 站点有独立 web 注册页 <code>/register/</code>。两套注册入口口径不一,需统一。</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============ 页脚 ============ -->
|
||||
<section>
|
||||
<h2>四、页脚</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">大量占位死链 <code>href="#"</code> <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">页脚以下链接全部为空锚点,需逐一落地或移除:API 文档、视频教程、数据导入模板、提交工单、技术支持、服务等级、关于岩美、客户案例、招贤纳士、联系我们、版本更新。<span class="file">footer.njk · features/*.html</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" />
|
||||
<div>
|
||||
<div class="item-title">备案号为占位假值 <span class="tag t-known">已知占位</span></div>
|
||||
<div class="item-desc">现为 <code>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</code>(全 0)。按要求暂填,<b>上线前必须替换为真实备案号</b>(且 ICP 主体地需与实际服务器/主体一致,岩美若在沪外需核对)。<span class="file">_data/site.json:5</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">客服邮箱/电话为占位 <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">邮箱 <code>support@yanmei.app</code>(与真实域名不符)、电话 <code>400-880-8888</code>(明显假号)。需换成真实联系方式。<span class="file">_data/site.json:7-8</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" />
|
||||
<div>
|
||||
<div class="item-title">页脚双份维护 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">njk 站点用 <code>footer.njk</code>(读 site.json 变量),功能页则把页脚、备案、联系方式硬编码各写一份,改一处不同步。根因同「两套模板」,建议功能页接入 base.njk。</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============ 价格 ============ -->
|
||||
<section>
|
||||
<h2>五、价格</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">价格数字需确认是否真实对外 <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">单店版 <code>¥299/月</code>、年付 <code>¥2,988/年</code>、连锁版「定制」疑为占位。上线前需确认真实报价。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「购买 / 联系销售 / 预约演示」按钮无对应流程 <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">价格卡「购买」「联系销售」、Hero「观看演示」、CTA「预约 1 对 1 演示」全部指向 <code>/app/</code>,但并无支付、销售联系、演示预约流程。按钮文案与去向不符。<span class="file">index.njk:554,568,188,622</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">「试用版」与「单店版」文案口径混乱 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">价格区把「试用版(¥0/30天)」与「单店版(¥299)」分列两卡,而 FAQ 又称「标准版提供 30 天免费试用」。试用 vs 标准 的口径需统一。</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============ 下载 ============ -->
|
||||
<section>
|
||||
<h2>六、下载页</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">下载按钮失败无降级 <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">Windows/macOS/Android 按钮初始 <code>href="#"</code>,依赖运行时 fetch <code>/api/v1/public/release</code> 回填真实下载地址;接口失败 / 无数据时点击无任何反应、也无提示。需加失败兜底(如提示「暂未就绪」或禁用态)。<span class="file">download.njk:500-543</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">iOS 状态依赖接口字段 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">iOS 卡片仅当接口返回 <code>download_urls.ios</code> 才从「敬请期待」切换为「TestFlight 安装」。需确认 TestFlight 公开链接是否已配到 release 接口,否则 iOS 永远显示敬请期待(与首页「五端就绪」矛盾)。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">首页「五端就绪」与下载页 iOS 状态矛盾 <span class="tag t-high">重要</span></div>
|
||||
<div class="item-desc">首页平台区把 Web/Win/macOS/iOS/Android 全列为可用,下载页 iOS 仍是「敬请期待」。需对齐口径。</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============ 文档 ============ -->
|
||||
<section>
|
||||
<h2>七、文档页</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title"><code>⌘K</code> 搜索快捷键是装饰 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">文档顶栏显示 <code>⌘K</code> 徽标但未绑定键盘事件聚焦搜索框;搜索本身仅按整章显隐,无关键词高亮/定位。要么实现快捷键,要么去掉徽标。<span class="file">docs.njk</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">反馈接口路径不一致 / 待确认 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">文档反馈用相对路径 <code>/api/v1/public/feedback</code>,下载页却用绝对域名。需确认该端点真实存在且同源可达。<span class="file">docs.njk:182</span></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============ 注册 ============ -->
|
||||
<section>
|
||||
<h2>八、注册页</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" />
|
||||
<div>
|
||||
<div class="item-title">注册接口同源依赖 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">用 <code>window.location.origin + '/api/v1/public/register'</code>,要求静态站与后端同源部署;本地预览或分离部署时会指错。需确认部署形态(与下载页写死域名的做法也不统一)。<span class="file">register.njk:175</span></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" />
|
||||
<div>
|
||||
<div class="item-title">成功提示路径需核对 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">提示「忘记门店编码可在『系统设置 → 关于』查看」,需与实际客户端菜单路径一致。</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ============ 构建/工程 ============ -->
|
||||
<section>
|
||||
<h2>九、构建 / 工程结构</h2>
|
||||
<ul class="todo">
|
||||
<li><input type="checkbox" checked />
|
||||
<div>
|
||||
<div class="item-title">dist 残留旧产物 <code>docs.html</code> <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc">dist 同时存在 <code>docs.html</code>(疑似旧构建残留)与 <code>docs/index.html</code>,造成同内容双 URL。建议清理 dist 后全量重建,避免历史残留。</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><input type="checkbox" />
|
||||
<div>
|
||||
<div class="item-title">scan.html / features 走 passthrough,不享模板 <span class="tag t-low">一般</span></div>
|
||||
<div class="item-desc"><code>.eleventy.js</code> 把 features、scan.html 直接复制,不经 njk 模板,导致页脚、备案、域名、CSS 都得手动同步——是上面多个「双份维护」问题的工程根因。建议长期改造为模板化。</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+2
-2
@@ -4,9 +4,9 @@
|
||||
"copyright": "© 2026 岩美科技 · 保留所有权利",
|
||||
"icp": "沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号",
|
||||
"support": {
|
||||
"email": "support@yanmei.app",
|
||||
"phone": "400-880-8888"
|
||||
"email": "yammy2023@163.com"
|
||||
},
|
||||
"appUrl": "/app/",
|
||||
"appBaseUrl": "https://jiu.51yanmei.com",
|
||||
"lucideVersion": "0.469.0"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<img src="/assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
<div>{{ site.support.email }}</div>
|
||||
<div>{{ site.support.phone }}</div>
|
||||
<div><a href="mailto:{{ site.support.email }}">{{ site.support.email }}</a></div>
|
||||
{% if site.support.phone %}<div>{{ site.support.phone }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
@@ -16,34 +16,27 @@
|
||||
<li><a href="/#platforms">多端覆盖</a></li>
|
||||
<li><a href="/#reports">数据洞察</a></li>
|
||||
<li><a href="/#pricing">价格方案</a></li>
|
||||
<li><a href="#">版本更新</a></li>
|
||||
<li><a href="/download/">版本更新</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>资源</h5>
|
||||
<ul>
|
||||
<li><a href="/docs/">使用手册</a></li>
|
||||
<li><a href="#">API 文档</a></li>
|
||||
<li><a href="#">视频教程</a></li>
|
||||
<li><a href="#">数据导入模板</a></li>
|
||||
<li><a href="/download/">下载客户端</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>客户支持</h5>
|
||||
<ul>
|
||||
<li><a href="/#faq">常见问题</a></li>
|
||||
<li><a href="#">提交工单</a></li>
|
||||
<li><a href="#">技术支持</a></li>
|
||||
<li><a href="#">服务等级</a></li>
|
||||
<li><a href="mailto:{{ site.support.email }}">联系我们</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="#">关于岩美</a></li>
|
||||
<li><a href="#">客户案例</a></li>
|
||||
<li><a href="#">招贤纳士</a></li>
|
||||
<li><a href="#">联系我们</a></li>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
</a>
|
||||
<div class="topnav-links">
|
||||
<a href="/#modules">产品</a>
|
||||
<a href="/features/inventory.html">库存</a>
|
||||
<a href="/features/approval.html">审核流</a>
|
||||
<a href="/#pricing">价格</a>
|
||||
<a href="/download/">下载</a>
|
||||
<a href="/docs/">文档</a>
|
||||
@@ -13,5 +15,20 @@
|
||||
<div class="topnav-cta" id="nav-cta">
|
||||
<a href="/register/" class="btn btn-primary btn-sm">注册门店</a>
|
||||
</div>
|
||||
<button class="nav-hamburger" id="nav-hamburger" aria-label="菜单" aria-expanded="false">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="nav-mobile-menu" id="nav-mobile-menu">
|
||||
<a href="/#modules">产品</a>
|
||||
<a href="/features/inventory.html">库存管理</a>
|
||||
<a href="/features/approval.html">审核流</a>
|
||||
<a href="/#pricing">价格</a>
|
||||
<a href="/download/">下载</a>
|
||||
<a href="/docs/">文档</a>
|
||||
<a href="/#faq">支持</a>
|
||||
<div class="nav-mobile-cta" id="nav-mobile-cta">
|
||||
<a href="/register/" class="btn btn-primary">注册门店</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -345,3 +345,8 @@
|
||||
.docs-subnav-inner { padding: 10px 16px; }
|
||||
.docs-search { max-width: none; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.docs-layout { padding: 12px; }
|
||||
.docs-main table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
.docs-toc { font-size: 13px; }
|
||||
}
|
||||
|
||||
+50
-6
@@ -23,16 +23,35 @@
|
||||
if (el) el.classList.toggle('open');
|
||||
}
|
||||
|
||||
// 点击外部关闭 dropdown
|
||||
document.addEventListener('click', function(e) {
|
||||
var menu = document.getElementById('userDropdown');
|
||||
function toggleMobileMenu() {
|
||||
var menu = document.getElementById('nav-mobile-menu');
|
||||
var btn = document.getElementById('nav-hamburger');
|
||||
if (!menu) return;
|
||||
var navUser = menu.closest('.nav-user');
|
||||
if (navUser && !navUser.contains(e.target)) menu.classList.remove('open');
|
||||
var isOpen = menu.classList.toggle('open');
|
||||
if (btn) btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
}
|
||||
|
||||
// 点击外部关闭 dropdown 和 mobile menu
|
||||
document.addEventListener('click', function(e) {
|
||||
var userMenu = document.getElementById('userDropdown');
|
||||
if (userMenu) {
|
||||
var navUser = userMenu.closest('.nav-user');
|
||||
if (navUser && !navUser.contains(e.target)) userMenu.classList.remove('open');
|
||||
}
|
||||
var mobileMenu = document.getElementById('nav-mobile-menu');
|
||||
var hamburger = document.getElementById('nav-hamburger');
|
||||
if (mobileMenu && hamburger) {
|
||||
var topnav = mobileMenu.closest('nav');
|
||||
if (topnav && !topnav.contains(e.target)) {
|
||||
mobileMenu.classList.remove('open');
|
||||
hamburger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function renderNav() {
|
||||
var cta = document.getElementById('nav-cta');
|
||||
var mobileCta = document.getElementById('nav-mobile-cta');
|
||||
if (!cta) return;
|
||||
var user = getAuthUser();
|
||||
if (user) {
|
||||
@@ -58,22 +77,47 @@
|
||||
+ '<div class="nav-dropdown-divider"></div>'
|
||||
+ '<button class="logout" onclick="doLogout()"><i data-lucide="log-out" class="icon"></i>退出登录</button>'
|
||||
+ '</div></div>';
|
||||
if (mobileCta) {
|
||||
mobileCta.innerHTML =
|
||||
'<a href="/app/" class="btn btn-ghost">进入管理端</a>'
|
||||
+ '<button class="btn btn-ghost" onclick="doLogout()">退出登录</button>';
|
||||
}
|
||||
} else {
|
||||
cta.innerHTML =
|
||||
'<a href="/app/" class="btn btn-ghost">登录</a>'
|
||||
+ '<a href="/app/" class="btn btn-primary">免费试用</a>';
|
||||
if (mobileCta) {
|
||||
mobileCta.innerHTML =
|
||||
'<a href="/app/" class="btn btn-ghost">登录</a>'
|
||||
+ '<a href="/register/" class="btn btn-primary">注册门店</a>';
|
||||
}
|
||||
}
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// 暴露给 onclick 调用
|
||||
window.toggleUserMenu = toggleUserMenu;
|
||||
window.toggleUserMenu = toggleUserMenu;
|
||||
window.toggleMobileMenu = toggleMobileMenu;
|
||||
window.doLogout = doLogout;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
renderNav();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
var hamburger = document.getElementById('nav-hamburger');
|
||||
if (hamburger) hamburger.addEventListener('click', toggleMobileMenu);
|
||||
|
||||
// 点击 mobile menu 链接后自动收起
|
||||
var mobileMenu = document.getElementById('nav-mobile-menu');
|
||||
if (mobileMenu) {
|
||||
mobileMenu.querySelectorAll('a').forEach(function(a) {
|
||||
a.addEventListener('click', function() {
|
||||
mobileMenu.classList.remove('open');
|
||||
if (hamburger) hamburger.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 页内锚点平滑滚动
|
||||
document.querySelectorAll('a[href^="#"]').forEach(function(a) {
|
||||
a.addEventListener('click', function(e) {
|
||||
|
||||
@@ -128,6 +128,41 @@ button { font-family: inherit; cursor: pointer; }
|
||||
.nav-dropdown-divider { height: 1px; background: var(--border-subtle); margin: 4px 0; }
|
||||
.nav-dropdown .logout { color: var(--danger-600); }
|
||||
|
||||
/* Hamburger button (hidden on desktop) */
|
||||
.nav-hamburger {
|
||||
display: none; flex-direction: column; justify-content: center; gap: 5px;
|
||||
width: 40px; height: 40px; padding: 8px;
|
||||
background: none; border: none; cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.nav-hamburger span {
|
||||
display: block; height: 2px; width: 22px;
|
||||
background: var(--gray-700); border-radius: 2px;
|
||||
transition: transform 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
/* Mobile menu (hidden by default) */
|
||||
.nav-mobile-menu {
|
||||
display: none; flex-direction: column;
|
||||
background: var(--gray-0);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
padding: 8px 0 16px;
|
||||
}
|
||||
.nav-mobile-menu.open { display: flex; }
|
||||
.nav-mobile-menu a {
|
||||
padding: 12px 24px; font-size: var(--text-md);
|
||||
color: var(--gray-700); font-weight: 500;
|
||||
transition: background var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
.nav-mobile-menu a:hover { background: var(--gray-50); color: var(--brand-900); }
|
||||
.nav-mobile-cta {
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
padding: 12px 24px 0;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
margin-top: 8px;
|
||||
}
|
||||
.nav-mobile-cta .btn { justify-content: center; }
|
||||
|
||||
/* -- Footer -- */
|
||||
footer {
|
||||
background: var(--gray-25);
|
||||
@@ -456,4 +491,17 @@ footer {
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.grid-3, .grid-4, .grid-5 { grid-template-columns: repeat(2, 1fr); }
|
||||
/* Show hamburger, hide desktop nav links */
|
||||
.nav-hamburger { display: flex; }
|
||||
.topnav-links, .topnav-cta { display: none; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.topnav-inner { padding: 0 16px; }
|
||||
.footer-grid { grid-template-columns: 1fr; gap: 32px; }
|
||||
.footer-bottom { flex-direction: column; gap: 8px; text-align: center; }
|
||||
.grid-3, .grid-4, .grid-5 { grid-template-columns: 1fr; }
|
||||
.container { padding: 0 16px; }
|
||||
.section { padding-top: 48px; padding-bottom: 48px; }
|
||||
h1, .h1 { font-size: clamp(1.75rem, 7vw, 2.5rem); }
|
||||
h2, .h2 { font-size: clamp(1.375rem, 5vw, 2rem); }
|
||||
}
|
||||
|
||||
+8
-2
@@ -147,6 +147,14 @@ pageExtraCss: /assets/docs.css
|
||||
chapters.forEach(function(ch) { observer.observe(ch); });
|
||||
}
|
||||
|
||||
// ⌘K / Ctrl+K 聚焦搜索框
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
var inp = document.getElementById('docs-search-input');
|
||||
if (inp) { e.preventDefault(); inp.focus(); inp.select(); }
|
||||
}
|
||||
});
|
||||
|
||||
// Simple search: highlight matching text in main content
|
||||
var searchInput = document.getElementById('docs-search-input');
|
||||
if (searchInput) {
|
||||
@@ -178,8 +186,6 @@ pageExtraCss: /assets/docs.css
|
||||
}
|
||||
|
||||
function sendFeedback(val) {
|
||||
// Fire-and-forget; no user-visible side effect needed
|
||||
try { fetch('/api/v1/public/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ helpful: val === 'yes' }) }); } catch(e) {}
|
||||
var fb = document.querySelector('.docs-feedback');
|
||||
if (fb) fb.innerHTML = '<span style="color:var(--success-700);font-size:var(--text-md);">感谢您的反馈!</span>';
|
||||
}
|
||||
|
||||
+26
-5
@@ -246,6 +246,12 @@ pageStyle: |
|
||||
.dl-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.page-header { padding: 48px 0 40px; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.ph-meta { flex-wrap: wrap; gap: 12px; }
|
||||
.dl-grid { grid-template-columns: 1fr; }
|
||||
.changelog { grid-template-columns: 1fr; }
|
||||
.changelog-version { font-size: 13px; }
|
||||
}
|
||||
---
|
||||
|
||||
<!-- ============================== PAGE HEADER ============================== -->
|
||||
@@ -295,7 +301,7 @@ pageStyle: |
|
||||
</div>
|
||||
</div>
|
||||
<div class="detect-card-actions">
|
||||
<a href="https://jiu.51yanmei.com/app/" class="btn btn-primary btn-lg" id="detect-btn">
|
||||
<a href="{{ site.appBaseUrl }}/app/" class="btn btn-primary btn-lg" id="detect-btn">
|
||||
<i data-lucide="arrow-right" class="icon"></i>
|
||||
立即使用 Web 版
|
||||
</a>
|
||||
@@ -317,7 +323,7 @@ pageStyle: |
|
||||
<h3 class="dl-card-name">Web 版</h3>
|
||||
<p class="dl-card-desc">浏览器直接访问,功能完整,无需安装,支持 PC 与平板。</p>
|
||||
<span class="dl-status available">✓ 已上线</span>
|
||||
<a href="https://jiu.51yanmei.com/app/" class="dl-card-btn primary">
|
||||
<a href="{{ site.appBaseUrl }}/app/" class="dl-card-btn primary">
|
||||
<i data-lucide="arrow-right" class="icon"></i>立即使用
|
||||
</a>
|
||||
</div>
|
||||
@@ -427,8 +433,9 @@ pageStyle: |
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var APP_BASE = '{{ site.appBaseUrl }}';
|
||||
var platforms = {
|
||||
web: { name: 'Web 版', desc: '浏览器直接访问,功能完整,无需下载安装。', icon: 'globe', btnText: '立即使用 Web 版', btnHref: 'https://jiu.51yanmei.com/app/', available: true },
|
||||
web: { name: 'Web 版', desc: '浏览器直接访问,功能完整,无需下载安装。', icon: 'globe', btnText: '立即使用 Web 版', btnHref: APP_BASE + '/app/', available: true },
|
||||
ios: { name: 'iOS', desc: 'iPhone 与 iPad 通用,通过 TestFlight 安装。', icon: 'smartphone', btnText: '敬请期待', available: false },
|
||||
android: { name: 'Android', desc: '手机与平板通用,APK 直接安装,支持 Android 6.0 及以上。', icon: 'tablet-smartphone', btnText: '下载 Android 版', btnHref: '#', available: true },
|
||||
windows: { name: 'Windows 桌面', desc: '原生桌面客户端,支持 Win 10/11(64 位),离线模式与本地打印。', icon: 'monitor', btnText: '下载 Windows 版', btnHref: '#', available: true },
|
||||
@@ -497,7 +504,7 @@ pageStyle: |
|
||||
updateDetectCard(detected);
|
||||
highlightPlatformCard(detected);
|
||||
|
||||
fetch('https://jiu.51yanmei.com/api/v1/public/release')
|
||||
fetch('{{ site.appBaseUrl }}/api/v1/public/release')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data && data.version) updateVersionBadges(data.version);
|
||||
@@ -540,7 +547,21 @@ pageStyle: |
|
||||
if (detected === 'ios') updateDetectCard(detected);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
.catch(function() {
|
||||
// API 不可达时,将仍为 href="#" 的下载按钮改为禁用态
|
||||
['windows-dl-btn', 'macos-dl-btn', 'android-dl-btn'].forEach(function(id) {
|
||||
var btn = document.getElementById(id);
|
||||
if (btn && btn.getAttribute('href') === '#') {
|
||||
btn.className = 'dl-card-btn disabled';
|
||||
btn.innerHTML = '暂时无法获取下载地址';
|
||||
}
|
||||
});
|
||||
var detectBtn = document.getElementById('detect-btn');
|
||||
if (detectBtn && detectBtn.getAttribute('href') === '#') {
|
||||
detectBtn.className = 'btn btn-secondary btn-lg';
|
||||
detectBtn.innerHTML = '暂时无法连接服务器';
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
+76
-123
@@ -682,7 +682,7 @@ button { font-family: inherit; cursor: pointer; }
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
BLOCK 5: REVERSAL / RED-MARK
|
||||
REVERSAL STYLES (unused, kept for possible future use)
|
||||
==================================================================== */
|
||||
.reversal-pair {
|
||||
display: grid;
|
||||
@@ -902,6 +902,28 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
.perm-row { grid-template-columns: 130px repeat(4, 1fr); }
|
||||
.reversal-arrow { transform: rotate(90deg); padding: 16px 0; }
|
||||
}
|
||||
/* Hamburger */
|
||||
.nav-hamburger { display: none; flex-direction: column; justify-content: center; gap: 5px; width: 40px; height: 40px; padding: 8px; background: none; border: none; cursor: pointer; border-radius: 8px; }
|
||||
.nav-hamburger span { display: block; height: 2px; width: 22px; background: var(--gray-700); border-radius: 2px; }
|
||||
.nav-mobile-menu { display: none; flex-direction: column; background: var(--gray-0); border-top: 1px solid var(--border-subtle); padding: 8px 0 16px; }
|
||||
.nav-mobile-menu.open { display: flex; }
|
||||
.nav-mobile-menu a { padding: 12px 24px; font-size: var(--text-md); color: var(--gray-700); font-weight: 500; }
|
||||
.nav-mobile-menu a:hover { background: var(--gray-50); color: var(--brand-900); }
|
||||
.nav-mobile-cta { display: flex; flex-direction: column; gap: 8px; padding: 12px 24px 0; border-top: 1px solid var(--border-subtle); margin-top: 8px; }
|
||||
@media (max-width: 768px) {
|
||||
.nav-hamburger { display: flex; }
|
||||
.topnav-links, .topnav-cta { display: none; }
|
||||
.feat-hero-inner { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.topnav-inner { padding: 0 16px; }
|
||||
.feat-hero h1 { font-size: 28px; }
|
||||
.footer-grid { grid-template-columns: 1fr; gap: 32px; }
|
||||
.footer-bottom { flex-direction: column; gap: 8px; text-align: center; }
|
||||
.usecases-grid { grid-template-columns: 1fr; }
|
||||
.perm-row { overflow-x: auto; }
|
||||
.container { padding: 0 16px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -914,11 +936,25 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<div class="topnav-links">
|
||||
<a href="/#modules" class="active">产品</a>
|
||||
<a href="/#pricing">价格</a>
|
||||
<a href="/download.html">下载</a>
|
||||
<a href="/docs.html">文档</a>
|
||||
<a href="/download/">下载</a>
|
||||
<a href="/docs/">文档</a>
|
||||
<a href="/#faq">支持</a>
|
||||
</div>
|
||||
<div class="topnav-cta" id="nav-cta"></div>
|
||||
<button class="nav-hamburger" id="nav-hamburger" aria-label="菜单" aria-expanded="false">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="nav-mobile-menu" id="nav-mobile-menu">
|
||||
<a href="/#modules">产品</a>
|
||||
<a href="/#pricing">价格</a>
|
||||
<a href="/download/">下载</a>
|
||||
<a href="/docs/">文档</a>
|
||||
<a href="/#faq">支持</a>
|
||||
<div class="nav-mobile-cta">
|
||||
<a href="/app/" class="btn btn-ghost">登录</a>
|
||||
<a href="/register/" class="btn btn-primary">注册门店</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -936,26 +972,26 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<p class="eyebrow">功能详情 · 审核驱动业务流</p>
|
||||
<h1>每一笔库存变动,<br/>都经过一次<em>签字</em>。</h1>
|
||||
<p class="lead">
|
||||
入库、出库、盘点、调拨 —— 统一状态机,统一审批界面。审批不仅是一个按钮,是一笔事务:库存动了,账款也跟着动,全程留痕。
|
||||
入库、出库、盘点 —— 统一状态机,统一审批界面。审批不仅是一个按钮,是一笔事务:库存动了,账款也跟着动,全程留痕。
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a href="#" class="btn btn-primary btn-lg">免费试用 30 天</a>
|
||||
<a href="/docs.html" class="btn btn-secondary btn-lg">
|
||||
<a href="/docs/" class="btn btn-secondary btn-lg">
|
||||
<i data-lucide="book-open" class="icon"></i>查看文档
|
||||
</a>
|
||||
</div>
|
||||
<div class="quick-stats">
|
||||
<div>
|
||||
<div class="quick-stat-label">审批延迟</div>
|
||||
<div class="quick-stat-val">≤ 1s</div>
|
||||
<div class="quick-stat-label">单据类型</div>
|
||||
<div class="quick-stat-val">3 种</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="quick-stat-label">审计字段</div>
|
||||
<div class="quick-stat-val">14</div>
|
||||
<div class="quick-stat-label">操作留痕</div>
|
||||
<div class="quick-stat-val">全程</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="quick-stat-label">回滚机制</div>
|
||||
<div class="quick-stat-val">红冲</div>
|
||||
<div class="quick-stat-label">审批联动</div>
|
||||
<div class="quick-stat-val">库存 + 账款</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1024,7 +1060,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<div class="feat-tag"><i data-lucide="git-branch" class="icon"></i>统一状态机</div>
|
||||
<h2>四种状态,<br/>覆盖所有业务单据</h2>
|
||||
<p class="lead">
|
||||
入库、出库、盘点、调拨 —— 全部走同一套状态:草稿 → 待审核 → 已审批 / 已拒绝。员工学一次,全员通用。
|
||||
入库、出库、盘点 —— 全部走同一套状态:草稿 → 待审核 → 已审批 / 已拒绝。员工学一次,全员通用。
|
||||
</p>
|
||||
<ul class="feat-bullets">
|
||||
<li>
|
||||
@@ -1044,7 +1080,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<li>
|
||||
<i data-lucide="layers" class="icon"></i>
|
||||
<div>
|
||||
<strong>一套界面,四种单据复用</strong>
|
||||
<strong>一套界面,三种单据复用</strong>
|
||||
前端审批界面统一,节省培训成本,避免分模块状态差异。
|
||||
</div>
|
||||
</li>
|
||||
@@ -1057,7 +1093,6 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<div class="state-tab active">入库单</div>
|
||||
<div class="state-tab">出库单</div>
|
||||
<div class="state-tab">盘点单</div>
|
||||
<div class="state-tab">调拨单</div>
|
||||
</div>
|
||||
<div class="state-machine-meta">stock_in_orders.status</div>
|
||||
|
||||
@@ -1107,7 +1142,6 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<span class="sm-applies-pill"><i data-lucide="package-plus" class="icon"></i>入库单</span>
|
||||
<span class="sm-applies-pill"><i data-lucide="package-minus" class="icon"></i>出库单</span>
|
||||
<span class="sm-applies-pill"><i data-lucide="clipboard-check" class="icon"></i>盘点单</span>
|
||||
<span class="sm-applies-pill"><i data-lucide="arrow-right-left" class="icon"></i>调拨单</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1227,13 +1261,6 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
校验失败时单据状态回到「待审核」,无需重新录入。
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<i data-lucide="settings-2" class="icon"></i>
|
||||
<div>
|
||||
<strong>可配置宽松模式</strong>
|
||||
系统设置中可开启「允许超量出库」(备货场景使用),缺省关闭。
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1399,88 +1426,6 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============== BLOCK 5: Reversal ============== -->
|
||||
<section class="feat-block">
|
||||
<div class="container feat-block-inner">
|
||||
<div class="feat-text">
|
||||
<div class="feat-tag" style="background: var(--accent-50); color: var(--accent-700);"><i data-lucide="undo" class="icon"></i>红冲机制</div>
|
||||
<h2>审批不可撤销,<br/>但错误可以修正</h2>
|
||||
<p class="lead">
|
||||
审批一旦通过,原单据永远是历史的真实记录。如有错误,只能通过红冲(反向调整单)来修正 —— 既保留审计链,又能纠正库存与账款。
|
||||
</p>
|
||||
<ul class="feat-bullets">
|
||||
<li>
|
||||
<i data-lucide="archive" class="icon"></i>
|
||||
<div>
|
||||
<strong>原单不动</strong>
|
||||
原始单据完整保留,不被覆盖、不被删除,永久可查。
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<i data-lucide="rotate-ccw" class="icon"></i>
|
||||
<div>
|
||||
<strong>反向调整自动生成</strong>
|
||||
点击「红冲」一键生成反向单据,数量取反、关联原单据 ID、状态自动进入「待审核」。
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<i data-lucide="calculator" class="icon"></i>
|
||||
<div>
|
||||
<strong>净库存为零</strong>
|
||||
红冲单审批通过后,库存与账款回到红冲前的状态,但审计链上多了两笔记录而非清除。
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feat-visual">
|
||||
<div class="reversal-pair">
|
||||
<div class="reversal-doc original">
|
||||
<div class="reversal-stamp-mini" style="color: var(--success-500); border-color: var(--success-500);">已审批</div>
|
||||
<div class="reversal-doc-id">RK20260523000007</div>
|
||||
<div class="reversal-doc-meta">2026-05-23 · 入库 · 茅台经销华东</div>
|
||||
<div class="reversal-doc-row"><div>茅台飞天 53°</div><div>+4</div></div>
|
||||
<div class="reversal-doc-row"><div>五粮液第八代</div><div>+20</div></div>
|
||||
<div class="reversal-doc-row"><div>剑南春水晶剑</div><div>+2</div></div>
|
||||
<div class="reversal-doc-row"><div>拉菲传奇</div><div>+10</div></div>
|
||||
<div class="reversal-doc-foot"><div>合计</div><div>+36 · ¥48,400</div></div>
|
||||
</div>
|
||||
|
||||
<div class="reversal-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
|
||||
<span class="reversal-arrow-label">红冲</span>
|
||||
</div>
|
||||
|
||||
<div class="reversal-doc reversal">
|
||||
<div class="reversal-stamp-mini">红 冲</div>
|
||||
<div class="reversal-doc-id">RK20260524000001</div>
|
||||
<div class="reversal-doc-meta">2026-05-24 · 反向调整 · 关联 ...000007</div>
|
||||
<div class="reversal-doc-row"><div>茅台飞天 53°</div><div>−4</div></div>
|
||||
<div class="reversal-doc-row"><div>五粮液第八代</div><div>−20</div></div>
|
||||
<div class="reversal-doc-row"><div>剑南春水晶剑</div><div>−2</div></div>
|
||||
<div class="reversal-doc-row"><div>拉菲传奇</div><div>−10</div></div>
|
||||
<div class="reversal-doc-foot"><div>合计</div><div>−36 · −¥48,400</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reversal-net">
|
||||
<div class="reversal-net-item">
|
||||
<div class="reversal-net-label">原单库存影响</div>
|
||||
<div class="reversal-net-val">+36 瓶</div>
|
||||
</div>
|
||||
<div class="reversal-net-item">
|
||||
<div class="reversal-net-label">红冲库存影响</div>
|
||||
<div class="reversal-net-val" style="color: #C97B86;">−36 瓶</div>
|
||||
</div>
|
||||
<div class="reversal-net-item">
|
||||
<div class="reversal-net-label">净影响</div>
|
||||
<div class="reversal-net-val" style="color: var(--success-500);">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============== BLOCK 6: Permission matrix ============== -->
|
||||
<section class="feat-block reverse">
|
||||
<div class="container feat-block-inner">
|
||||
@@ -1593,8 +1538,8 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<div class="usecase-card">
|
||||
<i data-lucide="rotate-ccw" class="icon"></i>
|
||||
<h4>录错了不慌</h4>
|
||||
<p>新员工录错入库单一次性多写了 10 瓶,审批通过后才发现。点击「红冲」,反向调整单一键生成,1 分钟修正库存与账款。</p>
|
||||
<div class="quote">"原始数据保留,红冲也留痕,内审完全不会有问题。"</div>
|
||||
<p>审批通过的单据不可撤销,但可补录一张方向相反的修正单提交审核,两笔记录都完整保留在审计链中,库存与账款同步修正。</p>
|
||||
<div class="quote">"原始数据保留,修正也留痕,内审完全不会有问题。"</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1609,7 +1554,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<p>30 天免费试用,无需绑定支付方式。所有审批与审计数据可随时导出。</p>
|
||||
</div>
|
||||
<div class="cta-strip-actions">
|
||||
<a href="#" class="btn btn-primary btn-lg">
|
||||
<a href="/register/" class="btn btn-primary btn-lg">
|
||||
免费开通试用<i data-lucide="arrow-right" class="icon"></i>
|
||||
</a>
|
||||
<a href="/features/inventory.html" class="btn btn-secondary btn-lg">
|
||||
@@ -1628,8 +1573,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<img src="../assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
<div>support@yanmei.app</div>
|
||||
<div>400-880-8888</div>
|
||||
<div>yammy2023@163.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
@@ -1639,34 +1583,27 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<li><a href="/features/inventory.html">库存管理</a></li>
|
||||
<li><a href="/features/approval.html">审核流</a></li>
|
||||
<li><a href="/#pricing">价格方案</a></li>
|
||||
<li><a href="/download.html">下载客户端</a></li>
|
||||
<li><a href="/download/">下载客户端</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>资源</h5>
|
||||
<ul>
|
||||
<li><a href="/docs.html">使用手册</a></li>
|
||||
<li><a href="#">API 文档</a></li>
|
||||
<li><a href="#">视频教程</a></li>
|
||||
<li><a href="#">数据导入模板</a></li>
|
||||
<li><a href="/docs/">使用手册</a></li>
|
||||
<li><a href="/download/">下载客户端</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>客户支持</h5>
|
||||
<ul>
|
||||
<li><a href="/#faq">常见问题</a></li>
|
||||
<li><a href="#">提交工单</a></li>
|
||||
<li><a href="#">技术支持</a></li>
|
||||
<li><a href="#">服务等级</a></li>
|
||||
<li><a href="mailto:yammy2023@163.com">联系我们</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="#">关于岩美</a></li>
|
||||
<li><a href="#">客户案例</a></li>
|
||||
<li><a href="#">招贤纳士</a></li>
|
||||
<li><a href="#">联系我们</a></li>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1696,6 +1633,13 @@ function toggleUserMenu() {
|
||||
var el = document.getElementById('userDropdown');
|
||||
if (el) el.classList.toggle('open');
|
||||
}
|
||||
function toggleMobileMenu() {
|
||||
var menu = document.getElementById('nav-mobile-menu');
|
||||
var btn = document.getElementById('nav-hamburger');
|
||||
if (!menu) return;
|
||||
var isOpen = menu.classList.toggle('open');
|
||||
if (btn) btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
}
|
||||
function renderNav() {
|
||||
var cta = document.getElementById('nav-cta');
|
||||
if (!cta) return;
|
||||
@@ -1722,8 +1666,8 @@ function renderNav() {
|
||||
'</div>';
|
||||
} else {
|
||||
cta.innerHTML =
|
||||
'<a href="/app/login" class="btn btn-ghost">登录</a>' +
|
||||
'<a href="/app/register" class="btn btn-primary">免费试用</a>';
|
||||
'<a href="/app/" class="btn btn-ghost">登录</a>' +
|
||||
'<a href="/register/" class="btn btn-primary">免费试用</a>';
|
||||
}
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
@@ -1737,6 +1681,15 @@ document.addEventListener('click', function(e) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
renderNav();
|
||||
var hamburger = document.getElementById('nav-hamburger');
|
||||
if (hamburger) hamburger.addEventListener('click', toggleMobileMenu);
|
||||
var mobileMenu = document.getElementById('nav-mobile-menu');
|
||||
if (mobileMenu) mobileMenu.querySelectorAll('a').forEach(function(a) {
|
||||
a.addEventListener('click', function() {
|
||||
mobileMenu.classList.remove('open');
|
||||
if (hamburger) hamburger.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+90
-72
@@ -645,6 +645,27 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
.feat-hero h1 { font-size: 36px; }
|
||||
.composite { height: 480px; }
|
||||
}
|
||||
/* Hamburger */
|
||||
.nav-hamburger { display: none; flex-direction: column; justify-content: center; gap: 5px; width: 40px; height: 40px; padding: 8px; background: none; border: none; cursor: pointer; border-radius: 8px; }
|
||||
.nav-hamburger span { display: block; height: 2px; width: 22px; background: var(--gray-700); border-radius: 2px; }
|
||||
.nav-mobile-menu { display: none; flex-direction: column; background: var(--gray-0); border-top: 1px solid var(--border-subtle); padding: 8px 0 16px; }
|
||||
.nav-mobile-menu.open { display: flex; }
|
||||
.nav-mobile-menu a { padding: 12px 24px; font-size: var(--text-md); color: var(--gray-700); font-weight: 500; }
|
||||
.nav-mobile-menu a:hover { background: var(--gray-50); color: var(--brand-900); }
|
||||
.nav-mobile-cta { display: flex; flex-direction: column; gap: 8px; padding: 12px 24px 0; border-top: 1px solid var(--border-subtle); margin-top: 8px; }
|
||||
@media (max-width: 768px) {
|
||||
.nav-hamburger { display: flex; }
|
||||
.topnav-links, .topnav-cta { display: none; }
|
||||
.composite { display: none; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.topnav-inner { padding: 0 16px; }
|
||||
.feat-hero h1 { font-size: 28px; }
|
||||
.footer-grid { grid-template-columns: 1fr; gap: 32px; }
|
||||
.footer-bottom { flex-direction: column; gap: 8px; text-align: center; }
|
||||
.usecases-grid { grid-template-columns: 1fr; }
|
||||
.container { padding: 0 16px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -657,11 +678,25 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<div class="topnav-links">
|
||||
<a href="/#modules" class="active">产品</a>
|
||||
<a href="/#pricing">价格</a>
|
||||
<a href="/download.html">下载</a>
|
||||
<a href="/docs.html">文档</a>
|
||||
<a href="/download/">下载</a>
|
||||
<a href="/docs/">文档</a>
|
||||
<a href="/#faq">支持</a>
|
||||
</div>
|
||||
<div class="topnav-cta" id="nav-cta"></div>
|
||||
<button class="nav-hamburger" id="nav-hamburger" aria-label="菜单" aria-expanded="false">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="nav-mobile-menu" id="nav-mobile-menu">
|
||||
<a href="/#modules">产品</a>
|
||||
<a href="/#pricing">价格</a>
|
||||
<a href="/download/">下载</a>
|
||||
<a href="/docs/">文档</a>
|
||||
<a href="/#faq">支持</a>
|
||||
<div class="nav-mobile-cta">
|
||||
<a href="/app/" class="btn btn-ghost">登录</a>
|
||||
<a href="/register/" class="btn btn-primary">注册门店</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -683,7 +718,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a href="#" class="btn btn-primary btn-lg">免费试用 30 天</a>
|
||||
<a href="/docs.html" class="btn btn-secondary btn-lg">
|
||||
<a href="/docs/" class="btn btn-secondary btn-lg">
|
||||
<i data-lucide="book-open" class="icon"></i>查看文档
|
||||
</a>
|
||||
</div>
|
||||
@@ -778,8 +813,8 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<!-- Floating alert card -->
|
||||
<div class="composite-card composite-secondary">
|
||||
<div class="alert-card-title" style="display: flex; align-items: center; gap: 6px;">
|
||||
<i data-lucide="bell" class="icon" style="width: 12px; height: 12px; color: var(--warning-500);"></i>
|
||||
库存预警
|
||||
<i data-lucide="alert-triangle" class="icon" style="width: 12px; height: 12px; color: var(--warning-500);"></i>
|
||||
低库存商品
|
||||
</div>
|
||||
<div class="alert-row danger">
|
||||
<div class="name">轩尼诗 VSOP</div>
|
||||
@@ -948,13 +983,6 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
出库时按"先进先出"自动从最早批次扣减,留下完整的批次→客户对应关系。
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<i data-lucide="alert-circle" class="icon"></i>
|
||||
<div>
|
||||
<strong>过期预警</strong>
|
||||
按生产日期 + 保质期推算,临近到期前自动标记。
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<i data-lucide="qr-code" class="icon"></i>
|
||||
<div>
|
||||
@@ -1035,7 +1063,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<i data-lucide="warehouse" class="icon"></i>
|
||||
<div>
|
||||
<strong>跨多仓库联动</strong>
|
||||
v1.6.2 新增 — 一次盘点可同步多仓库,避免分仓时数据不一致。
|
||||
一次盘点可同步多仓库,避免分仓时数据不一致。
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
@@ -1132,7 +1160,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<div class="feat-tag"><i data-lucide="scroll-text" class="icon"></i>库存流水审计</div>
|
||||
<h2>谁动了我的库存?<br/>每一次都查得清楚</h2>
|
||||
<p class="lead">
|
||||
所有库存变动都会自动写入流水表,包括入库、出库、盘点调整、单据红冲,附带时间戳与经办人。
|
||||
所有库存变动都会自动写入流水表,包括入库、出库、盘点调整,附带时间戳与经办人。
|
||||
</p>
|
||||
<ul class="feat-bullets">
|
||||
<li>
|
||||
@@ -1212,31 +1240,31 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<section class="feat-block">
|
||||
<div class="container feat-block-inner">
|
||||
<div class="feat-text">
|
||||
<div class="feat-tag"><i data-lucide="bell" class="icon"></i>库存预警</div>
|
||||
<h2>该补货之前,<br/>系统已经替你看见了</h2>
|
||||
<div class="feat-tag"><i data-lucide="alert-triangle" class="icon"></i>库存状态</div>
|
||||
<h2>一眼看出哪些商品<br/>需要补货</h2>
|
||||
<p class="lead">
|
||||
每个 SKU 都可单独设置安全库存阈值,触发即在 Web 与移动端同步推送,永远不会错过补货时机。
|
||||
为每件商品设置最低库存量,系统根据当前库存自动标记「充足 / 偏低 / 告急」,无需手工比对。
|
||||
</p>
|
||||
<ul class="feat-bullets">
|
||||
<li>
|
||||
<i data-lucide="sliders-horizontal" class="icon"></i>
|
||||
<div>
|
||||
<strong>按 SKU 灵活配置</strong>
|
||||
热销商品阈值高,冷门商品阈值低 — 不需要一刀切。
|
||||
<strong>按 SKU 设置最低库存</strong>
|
||||
热销商品阈值高,冷门商品阈值低 — 每件单独配置。
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<i data-lucide="bell-ring" class="icon"></i>
|
||||
<i data-lucide="list-filter" class="icon"></i>
|
||||
<div>
|
||||
<strong>多渠道通知</strong>
|
||||
Web 顶栏、移动 App 推送、可选邮件 / 短信通知。
|
||||
<strong>库存列表一目了然</strong>
|
||||
库存状态标签跟随列表显示,随时筛选「偏低」「告急」的 SKU。
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<i data-lucide="trending-up" class="icon"></i>
|
||||
<i data-lucide="qr-code" class="icon"></i>
|
||||
<div>
|
||||
<strong>智能补货建议</strong>
|
||||
基于近 30 天销售速率 × 补货周期,给出建议补货量。<small style="color: var(--brand-500);">v1.7 Beta</small>
|
||||
<strong>商品二维码防伪</strong>
|
||||
每件商品生成专属二维码,顾客扫码即可查看批次与防伪信息。
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1244,38 +1272,20 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
|
||||
<div class="feat-visual">
|
||||
<div class="alert-config-card">
|
||||
<h4>预警阈值 · 茅台飞天 53°</h4>
|
||||
<p class="sub">当库存低于阈值时,自动推送给指定操作员。</p>
|
||||
|
||||
<div class="threshold-bar">
|
||||
<div class="threshold-track"></div>
|
||||
<div class="threshold-marker" style="left: 30%;"></div>
|
||||
<div class="threshold-tick" style="left: 0%;">0</div>
|
||||
<div class="threshold-tick" style="left: 18%;">20</div>
|
||||
<div class="threshold-tick" style="left: 42%;">50</div>
|
||||
<div class="threshold-tick" style="left: 100%;">200</div>
|
||||
<div class="threshold-label" style="left: 9%; color: var(--danger-700);">告急</div>
|
||||
<div class="threshold-label" style="left: 30%; color: var(--warning-700);">偏低</div>
|
||||
<div class="threshold-label" style="left: 70%; color: var(--success-700);">充足</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin: 16px 0 8px;">
|
||||
<span style="font-size: 13px; color: var(--gray-700);">当前阈值</span>
|
||||
<span style="font-family: var(--font-mono); font-size: 18px; font-weight: 600; color: var(--brand-700);">35 瓶</span>
|
||||
</div>
|
||||
|
||||
<div class="alert-rules">
|
||||
<div class="alert-rule">
|
||||
<div>低于 <strong>50 瓶</strong> · 推送给 <strong>张磊(仓储)</strong></div>
|
||||
<div class="switch"></div>
|
||||
<h4>库存状态一览</h4>
|
||||
<p class="sub">基于最低库存量 (min_stock) 自动标记状态。</p>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;margin-top:12px;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--gray-25);border-radius:8px;">
|
||||
<span style="font-size:13px;font-weight:500;">茅台飞天 53°</span>
|
||||
<span style="font-size:12px;color:var(--success-700);background:var(--success-50);padding:2px 10px;border-radius:20px;font-weight:600;">充足</span>
|
||||
</div>
|
||||
<div class="alert-rule">
|
||||
<div>低于 <strong>20 瓶</strong> · 短信通知 <strong>李建国(采购)</strong></div>
|
||||
<div class="switch"></div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--gray-25);border-radius:8px;">
|
||||
<span style="font-size:13px;font-weight:500;">剑南春水晶剑</span>
|
||||
<span style="font-size:12px;color:var(--warning-700);background:var(--warning-50);padding:2px 10px;border-radius:20px;font-weight:600;">偏低</span>
|
||||
</div>
|
||||
<div class="alert-rule">
|
||||
<div>临近过期 <strong>30 天</strong> · 邮件提醒</div>
|
||||
<div class="switch off"></div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--gray-25);border-radius:8px;">
|
||||
<span style="font-size:13px;font-weight:500;">轩尼诗 VSOP</span>
|
||||
<span style="font-size:12px;color:var(--danger-700);background:var(--danger-50);padding:2px 10px;border-radius:20px;font-weight:600;">告急</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1323,7 +1333,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<p>30 天免费试用,无需绑定支付。所有数据云端实时备份,可随时导出。</p>
|
||||
</div>
|
||||
<div class="cta-strip-actions">
|
||||
<a href="#" class="btn btn-primary btn-lg">
|
||||
<a href="/register/" class="btn btn-primary btn-lg">
|
||||
免费开通试用<i data-lucide="arrow-right" class="icon"></i>
|
||||
</a>
|
||||
<a href="/features/approval.html" class="btn btn-secondary btn-lg">
|
||||
@@ -1342,8 +1352,7 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<img src="../assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
<div>support@yanmei.app</div>
|
||||
<div>400-880-8888</div>
|
||||
<div>yammy2023@163.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
@@ -1353,34 +1362,27 @@ footer { background: var(--gray-25); border-top: 1px solid var(--border-subtle);
|
||||
<li><a href="/features/inventory.html">库存管理</a></li>
|
||||
<li><a href="/features/approval.html">审核流</a></li>
|
||||
<li><a href="/#pricing">价格方案</a></li>
|
||||
<li><a href="/download.html">下载客户端</a></li>
|
||||
<li><a href="/download/">下载客户端</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>资源</h5>
|
||||
<ul>
|
||||
<li><a href="/docs.html">使用手册</a></li>
|
||||
<li><a href="#">API 文档</a></li>
|
||||
<li><a href="#">视频教程</a></li>
|
||||
<li><a href="#">数据导入模板</a></li>
|
||||
<li><a href="/docs/">使用手册</a></li>
|
||||
<li><a href="/download/">下载客户端</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>客户支持</h5>
|
||||
<ul>
|
||||
<li><a href="/#faq">常见问题</a></li>
|
||||
<li><a href="#">提交工单</a></li>
|
||||
<li><a href="#">技术支持</a></li>
|
||||
<li><a href="#">服务等级</a></li>
|
||||
<li><a href="mailto:yammy2023@163.com">联系我们</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="#">关于岩美</a></li>
|
||||
<li><a href="#">客户案例</a></li>
|
||||
<li><a href="#">招贤纳士</a></li>
|
||||
<li><a href="#">联系我们</a></li>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1410,6 +1412,13 @@ function toggleUserMenu() {
|
||||
var el = document.getElementById('userDropdown');
|
||||
if (el) el.classList.toggle('open');
|
||||
}
|
||||
function toggleMobileMenu() {
|
||||
var menu = document.getElementById('nav-mobile-menu');
|
||||
var btn = document.getElementById('nav-hamburger');
|
||||
if (!menu) return;
|
||||
var isOpen = menu.classList.toggle('open');
|
||||
if (btn) btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
}
|
||||
function renderNav() {
|
||||
var cta = document.getElementById('nav-cta');
|
||||
if (!cta) return;
|
||||
@@ -1436,8 +1445,8 @@ function renderNav() {
|
||||
'</div>';
|
||||
} else {
|
||||
cta.innerHTML =
|
||||
'<a href="/app/login" class="btn btn-ghost">登录</a>' +
|
||||
'<a href="/app/register" class="btn btn-primary">免费试用</a>';
|
||||
'<a href="/app/" class="btn btn-ghost">登录</a>' +
|
||||
'<a href="/register/" class="btn btn-primary">免费试用</a>';
|
||||
}
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
@@ -1451,6 +1460,15 @@ document.addEventListener('click', function(e) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
renderNav();
|
||||
var hamburger = document.getElementById('nav-hamburger');
|
||||
if (hamburger) hamburger.addEventListener('click', toggleMobileMenu);
|
||||
var mobileMenu = document.getElementById('nav-mobile-menu');
|
||||
if (mobileMenu) mobileMenu.querySelectorAll('a').forEach(function(a) {
|
||||
a.addEventListener('click', function() {
|
||||
mobileMenu.classList.remove('open');
|
||||
if (hamburger) hamburger.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+40
-21
@@ -170,6 +170,19 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
.hero h1 { font-size: 40px; }
|
||||
.roles-row { grid-template-columns: 140px 1fr repeat(4, 60px); padding: 12px 16px; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
/* Hide decorative app preview on mobile to prevent overflow */
|
||||
.preview-shell { display: none; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.hero h1 { font-size: 28px; }
|
||||
.hero p.lead { font-size: var(--text-md); }
|
||||
.hero-cta { flex-direction: column; }
|
||||
.hero-cta .btn { width: 100%; justify-content: center; }
|
||||
.module-card.feature-card { grid-column: span 1; }
|
||||
.roles-row { grid-template-columns: 1fr; }
|
||||
.roles-table .roles-row > *:nth-child(n+3) { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- ====================== HERO ====================== -->
|
||||
@@ -177,15 +190,15 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<div class="container hero-inner">
|
||||
<div>
|
||||
<div class="d-inline-flex items-center gap-8 bg-0 border rounded-pill fs-sm text-gray-7 mb-24" style="padding: 6px 14px 6px 8px;">
|
||||
<span class="badge badge-brand">v1.6</span>
|
||||
<span>新增多仓库联动盘点 ·</span>
|
||||
<span class="badge badge-brand">v{{ changelog[0].version }}</span>
|
||||
<span>{{ changelog[0].date }} 更新 ·</span>
|
||||
<span class="text-brand fw-5">查看更新</span>
|
||||
</div>
|
||||
<h1>从入库到出库,<br/><em>一套系统</em>管到底。</h1>
|
||||
<p class="lead">岩美酒库管理系统,为酒行与酒店设计的库存、审核、财务一体化平台。审核驱动业务流,库存与账款自动同步,五端无缝接入。</p>
|
||||
<div class="d-flex gap-12 mb-28">
|
||||
<a href="/register/" class="btn btn-primary btn-lg">免费试用 30 天<i data-lucide="arrow-right" class="icon"></i></a>
|
||||
<a href="/app/" class="btn btn-secondary btn-lg"><i data-lucide="play-circle" class="icon"></i>观看演示</a>
|
||||
<a href="/docs/" class="btn btn-secondary btn-lg"><i data-lucide="book-open" class="icon"></i>查看使用手册</a>
|
||||
</div>
|
||||
<div class="d-flex items-center gap-18 text-muted fs-sm">
|
||||
<div class="hero-mini-item d-flex items-center gap-6"><i data-lucide="check" class="icon"></i>无需安装</div>
|
||||
@@ -273,11 +286,11 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<section class="trust">
|
||||
<div class="container">
|
||||
<div class="trust-grid">
|
||||
<div class="fs-sm text-muted">服务各类酒水经营场所</div>
|
||||
<div><div class="trust-stat-num">1,280<sup>+</sup></div><div class="fs-xs text-muted mt-4">酒行 / 酒店门店</div></div>
|
||||
<div><div class="trust-stat-num">8.4<sup>%</sup></div><div class="fs-xs text-muted mt-4">平均损耗下降</div></div>
|
||||
<div><div class="trust-stat-num">99.9<sup>%</sup></div><div class="fs-xs text-muted mt-4">系统可用性</div></div>
|
||||
<div class="fs-sm text-muted">专为酒水经营场所设计</div>
|
||||
<div><div class="trust-stat-num">5<sup>端</sup></div><div class="fs-xs text-muted mt-4">Web / iOS / Android / Win / Mac</div></div>
|
||||
<div><div class="trust-stat-num">多<sup>仓</sup></div><div class="fs-xs text-muted mt-4">支持多仓库独立管理</div></div>
|
||||
<div><div class="trust-stat-num">FIFO</div><div class="fs-xs text-muted mt-4">批次先进先出追溯</div></div>
|
||||
<div><div class="trust-stat-num">0</div><div class="fs-xs text-muted mt-4">需安装依赖,Web 即用</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -286,8 +299,8 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<section class="section" id="modules">
|
||||
<div class="container">
|
||||
<p class="eyebrow">核心模块</p>
|
||||
<h2 class="section-title">七大模块,覆盖酒水经营全流程</h2>
|
||||
<p class="section-sub">从商品建档到入库出库、从财务结算到数据导入,岩美一站式承载日常运营。</p>
|
||||
<h2 class="section-title">全流程模块,覆盖酒水经营每一个环节</h2>
|
||||
<p class="section-sub">从商品建档到入库出库、从财务结算到扫码防伪,岩美一站式承载日常运营。</p>
|
||||
<div class="grid-3 gap-16 mt-48">
|
||||
<div class="module-card feature-card card p-24 transition">
|
||||
<div class="icon-box-lg bg-brand-50 text-brand mb-16"><i data-lucide="package-plus" class="icon-md"></i></div>
|
||||
@@ -299,6 +312,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<li><i data-lucide="check" class="icon"></i>支持单据打印、商品标签打印</li>
|
||||
<li><i data-lucide="check" class="icon"></i>批次号追踪,生产日期可追溯</li>
|
||||
</ul>
|
||||
<a href="/features/approval.html" class="fs-xs text-brand mt-12 d-inline-block">了解审核流详情 →</a>
|
||||
</div>
|
||||
<div class="module-card card p-24 transition">
|
||||
<div class="icon-box-lg bg-brand-50 text-brand mb-16"><i data-lucide="package-minus" class="icon-md"></i></div>
|
||||
@@ -309,6 +323,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<div class="icon-box-lg bg-brand-50 text-brand mb-16"><i data-lucide="package" class="icon-md"></i></div>
|
||||
<h3 class="fs-xl fw-6 text-brand-900 m-0 mb-6 ls-1">库存管理</h3>
|
||||
<p class="fs-sm lh-16 text-muted m-0">实时查询每个 SKU 的库存数量、所在仓库、批次。支持全仓盘点与库存流水。</p>
|
||||
<a href="/features/inventory.html" class="fs-xs text-brand mt-12 d-inline-block">了解更多 →</a>
|
||||
</div>
|
||||
<div class="module-card card p-24 transition">
|
||||
<div class="icon-box-lg bg-brand-50 text-brand mb-16"><i data-lucide="wallet" class="icon-md"></i></div>
|
||||
@@ -330,6 +345,11 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<h3 class="fs-xl fw-6 text-brand-900 m-0 mb-6 ls-1">系统设置</h3>
|
||||
<p class="fs-sm lh-16 text-muted m-0">用户与权限、多仓库、编号规则、参数与数据导入 — 自助配置,无需开发。</p>
|
||||
</div>
|
||||
<div class="module-card card p-24 transition">
|
||||
<div class="icon-box-lg bg-brand-50 text-brand mb-16"><i data-lucide="qr-code" class="icon-md"></i></div>
|
||||
<h3 class="fs-xl fw-6 text-brand-900 m-0 mb-6 ls-1">扫码防伪</h3>
|
||||
<p class="fs-sm lh-16 text-muted m-0">每件商品生成专属二维码,顾客扫码可查看商品名称、批次、出售门店与「已通过岩美防伪验证」标识。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -383,7 +403,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<div class="platform-card card py-24 px-16 text-center transition"><i data-lucide="globe" class="icon-xl text-brand mx-auto mb-12"></i><h4 class="m-0 mb-4 fs-md fw-6 text-brand-900">Web</h4><p class="m-0 fs-xs text-muted">主流浏览器,无需安装</p></div>
|
||||
<div class="platform-card card py-24 px-16 text-center transition"><i data-lucide="monitor" class="icon-xl text-brand mx-auto mb-12"></i><h4 class="m-0 mb-4 fs-md fw-6 text-brand-900">Windows</h4><p class="m-0 fs-xs text-muted">桌面客户端</p></div>
|
||||
<div class="platform-card card py-24 px-16 text-center transition"><i data-lucide="laptop" class="icon-xl text-brand mx-auto mb-12"></i><h4 class="m-0 mb-4 fs-md fw-6 text-brand-900">macOS</h4><p class="m-0 fs-xs text-muted">原生应用</p></div>
|
||||
<div class="platform-card card py-24 px-16 text-center transition"><i data-lucide="smartphone" class="icon-xl text-brand mx-auto mb-12"></i><h4 class="m-0 mb-4 fs-md fw-6 text-brand-900">iOS</h4><p class="m-0 fs-xs text-muted">iPhone / iPad</p></div>
|
||||
<div class="platform-card card py-24 px-16 text-center transition"><i data-lucide="smartphone" class="icon-xl text-brand mx-auto mb-12"></i><h4 class="m-0 mb-4 fs-md fw-6 text-brand-900">iOS</h4><p class="m-0 fs-xs text-muted">TestFlight 内测中</p></div>
|
||||
<div class="platform-card card py-24 px-16 text-center transition"><i data-lucide="tablet-smartphone" class="icon-xl text-brand mx-auto mb-12"></i><h4 class="m-0 mb-4 fs-md fw-6 text-brand-900">Android</h4><p class="m-0 fs-xs text-muted">手机 / 平板</p></div>
|
||||
</div>
|
||||
<div class="platforms-visual">
|
||||
@@ -391,10 +411,10 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<h3>仓库里也能用得顺手</h3>
|
||||
<p>移动端扫码即查库存、入库、出库,断网也能继续工作,恢复网络后自动同步。</p>
|
||||
<div class="d-flex flex-col gap-10">
|
||||
<div class="pv-feature"><i data-lucide="scan-line" class="icon"></i>扫码即查商品 · 二维码标签</div>
|
||||
<div class="pv-feature"><i data-lucide="scan-line" class="icon"></i>扫码即查商品 · 商品二维码防伪</div>
|
||||
<div class="pv-feature"><i data-lucide="wifi-off" class="icon"></i>离线缓存,断网继续作业</div>
|
||||
<div class="pv-feature"><i data-lucide="bell" class="icon"></i>实时推送审批与库存预警</div>
|
||||
<div class="pv-feature"><i data-lucide="printer" class="icon"></i>蓝牙连接打印商品标签</div>
|
||||
<div class="pv-feature"><i data-lucide="layout-grid" class="icon"></i>窄屏自动切换卡片流布局</div>
|
||||
<div class="pv-feature"><i data-lucide="camera" class="icon"></i>手机拍照直接添加商品图片</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-stack">
|
||||
@@ -486,7 +506,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
</div>
|
||||
<div class="d-grid gap-16" style="grid-template-columns:36px 1fr">
|
||||
<div class="icon-box-md bg-brand-50 text-brand"><i data-lucide="alert-triangle" class="icon-md"></i></div>
|
||||
<div><h4 class="m-0 mb-4 fs-lg fw-6 text-brand-900">库存预警实时提醒</h4><p class="m-0 fs-sm text-muted lh-16">SKU 库存低于阈值,系统在 Web 与移动端同步推送。永远不会错过补货时机。</p></div>
|
||||
<div><h4 class="m-0 mb-4 fs-lg fw-6 text-brand-900">库存状态实时标识</h4><p class="m-0 fs-sm text-muted lh-16">设置商品最低库存量,系统自动标记「充足 / 偏低 / 告急」,一眼识别需补货的 SKU。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -551,7 +571,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>云端每日备份</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>工作日 9-18 点支持</li>
|
||||
</ul>
|
||||
<a href="/app/" class="btn btn-primary w-full justify-center mt-auto">购买</a>
|
||||
<a href="/register/" class="btn btn-primary w-full justify-center mt-auto">免费试用 30 天</a>
|
||||
</div>
|
||||
<div class="price-card custom card d-flex flex-col" style="padding:32px 28px">
|
||||
<div class="fs-sm text-muted fw-5 mb-4">企业方案</div>
|
||||
@@ -562,10 +582,10 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>多门店 · 总部数据汇总</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>不限用户数</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>支持私有部署 / 内网</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>SSO 单点登录</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>专属实施与培训服务</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>7×24 专属客户经理</li>
|
||||
</ul>
|
||||
<a href="/app/" class="btn btn-secondary w-full justify-center mt-auto">联系销售</a>
|
||||
<a href="mailto:{{ site.support.email }}" class="btn btn-secondary w-full justify-center mt-auto">联系销售</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -591,7 +611,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
</details>
|
||||
<details class="card mb-12 overflow-hidden">
|
||||
<summary class="faq-q">审批通过后发现错误,怎么办?<i data-lucide="plus" class="icon"></i></summary>
|
||||
<div class="fs-md text-muted lh-17" style="padding:0 24px 22px">审批通过后操作不可撤销。如发现错误,可由具备权限的用户新建反向调整单据(红冲入库 / 红冲出库)来修正库存与账款。这样可以保留完整的审计链。</div>
|
||||
<div class="fs-md text-muted lh-17" style="padding:0 24px 22px">审批通过后操作不可撤销。如发现录入错误,可由具备权限的用户新建一张反向单据(如:误入 10 箱则再出库 10 箱)来修正库存与应收应付,审批链完整保留。</div>
|
||||
</details>
|
||||
<details class="card mb-12 overflow-hidden">
|
||||
<summary class="faq-q">数据安全如何保障?<i data-lucide="plus" class="icon"></i></summary>
|
||||
@@ -599,7 +619,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
</details>
|
||||
<details class="card mb-12 overflow-hidden">
|
||||
<summary class="faq-q">可以试用多长时间?<i data-lucide="plus" class="icon"></i></summary>
|
||||
<div class="fs-md text-muted lh-17" style="padding:0 24px 22px">标准版提供 30 天免费试用,无需绑定支付方式。试用期可使用全部功能、五端访问。30 天后可继续按月或按年付费,已录入的数据完整保留。</div>
|
||||
<div class="fs-md text-muted lh-17" style="padding:0 24px 22px">注册后即可免费试用 30 天,全部功能开放、五端可用,无需绑定支付方式。30 天到期后,升级到单店版(¥299/月)即可继续使用,已录入的数据完整保留。</div>
|
||||
</details>
|
||||
<details class="card mb-12 overflow-hidden">
|
||||
<summary class="faq-q">只读账号能做什么?<i data-lucide="plus" class="icon"></i></summary>
|
||||
@@ -619,8 +639,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
</div>
|
||||
<div class="d-flex flex-col gap-12 items-start">
|
||||
<a href="/register/" class="btn btn-primary btn-lg">立即开通试用<i data-lucide="arrow-right" class="icon"></i></a>
|
||||
<a href="/app/" class="btn btn-secondary btn-lg"><i data-lucide="phone" class="icon"></i>预约 1 对 1 演示</a>
|
||||
<span class="fs-sm" style="color:#99A3B3">销售热线 {{ site.support.phone }} · 工作日 9:00-18:00</span>
|
||||
<a href="mailto:{{ site.support.email }}?subject=咨询岩美连锁版" class="btn btn-secondary btn-lg"><i data-lucide="mail" class="icon"></i>邮件咨询</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user