feat(client): 初版 Flutter UI 框架,仿照参考截图实现

- 蓝色顶栏(#1565C0)+ 深蓝侧边栏(#0D47A1),含折叠/展开
- 底部状态栏:门店编号、登录用户、登录时间、实时时钟、版本号
- 登录页:居中卡片,支持密码显示/隐藏
- 入库单列表:筛选/搜索/日期选择/分页,三个 Tab(入库单/查询/审核)
- 新建入库单:基本信息 + 商品明细动态增删,实时计算合计金额
- 出库单列表:同入库单风格
- 库存查询:4 张统计卡片 + 颜色高亮缺货/预警行,库存预警 Tab
- 往来单位、财务、商品、系统设置(用户/仓库/编号规则/系统参数)
- 使用 Riverpod 状态管理 + go_router 路由 + Dio HTTP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-04 12:51:41 +08:00
parent d6397083c0
commit 76ab19ea47
22 changed files with 6386 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../auth/auth_state.dart';
final apiClientProvider = Provider<ApiClient>((ref) {
final authState = ref.watch(authStateProvider);
return ApiClient(token: authState.user?.token);
});
class ApiClient {
late final Dio _dio;
ApiClient({String? token}) {
_dio = Dio(BaseOptions(
baseUrl: 'http://localhost:8080/api/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
},
));
}
Future<Response> get(String path, {Map<String, dynamic>? params}) =>
_dio.get(path, queryParameters: params);
Future<Response> post(String path, {dynamic data}) =>
_dio.post(path, data: data);
Future<Response> put(String path, {dynamic data}) =>
_dio.put(path, data: data);
Future<Response> delete(String path) => _dio.delete(path);
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AuthUser {
final String token;
final String username;
final String hotelName;
final String hotelNo;
final int hotelId;
const AuthUser({
required this.token,
required this.username,
required this.hotelName,
required this.hotelNo,
required this.hotelId,
});
}
class AuthState {
final AuthUser? user;
const AuthState({this.user});
bool get isLoggedIn => user != null;
}
class AuthNotifier extends StateNotifier<AuthState> {
AuthNotifier() : super(const AuthState());
void login(AuthUser user) => state = AuthState(user: user);
void logout() => state = const AuthState();
}
final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>(
(ref) => AuthNotifier(),
);
+63
View File
@@ -0,0 +1,63 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../screens/auth/login_screen.dart';
import '../../screens/shell/app_shell.dart';
import '../../screens/stock_in/stock_in_list_screen.dart';
import '../../screens/stock_in/stock_in_form_screen.dart';
import '../../screens/stock_out/stock_out_list_screen.dart';
import '../../screens/inventory/inventory_list_screen.dart';
import '../../screens/inventory/inventory_check_screen.dart';
import '../../screens/partners/partners_screen.dart';
import '../../screens/finance/finance_screen.dart';
import '../../screens/products/products_screen.dart';
import '../../screens/settings/settings_screen.dart';
import '../auth/auth_state.dart';
final appRouterProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateProvider);
return GoRouter(
initialLocation: '/login',
redirect: (context, state) {
final isLoggedIn = authState.isLoggedIn;
final isLoginRoute = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginRoute) return '/login';
if (isLoggedIn && isLoginRoute) return '/stock-in';
return null;
},
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
ShellRoute(
builder: (context, state, child) => AppShell(child: child),
routes: [
GoRoute(
path: '/stock-in',
builder: (_, __) => const StockInListScreen()),
GoRoute(
path: '/stock-in/new',
builder: (_, __) => const StockInFormScreen()),
GoRoute(
path: '/stock-out',
builder: (_, __) => const StockOutListScreen()),
GoRoute(
path: '/inventory',
builder: (_, __) => const InventoryListScreen()),
GoRoute(
path: '/inventory/check',
builder: (_, __) => const InventoryCheckScreen()),
GoRoute(
path: '/partners', builder: (_, __) => const PartnersScreen()),
GoRoute(
path: '/finance', builder: (_, __) => const FinanceScreen()),
GoRoute(
path: '/products', builder: (_, __) => const ProductsScreen()),
GoRoute(
path: '/settings', builder: (_, __) => const SettingsScreen()),
],
),
],
);
});
+99
View File
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
class AppTheme {
static const Color primary = Color(0xFF1565C0);
static const Color primaryDark = Color(0xFF0D47A1);
static const Color primaryLight = Color(0xFF1976D2);
static const Color accent = Color(0xFFFF6F00);
static const Color success = Color(0xFF2E7D32);
static const Color danger = Color(0xFFC62828);
static const Color background = Color(0xFFF5F5F5);
static const Color surface = Color(0xFFFFFFFF);
static const Color border = Color(0xFFE0E0E0);
static const Color textPrimary = Color(0xFF212121);
static const Color textSecondary = Color(0xFF757575);
static ThemeData light() {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primary,
brightness: Brightness.light,
).copyWith(
primary: primary,
surface: surface,
onPrimary: Colors.white,
),
scaffoldBackgroundColor: background,
appBarTheme: const AppBarTheme(
backgroundColor: primary,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: false,
toolbarHeight: 56,
),
cardTheme: CardTheme(
color: surface,
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: const BorderSide(color: border, width: 0.5),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
minimumSize: const Size(0, 36),
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: primary,
minimumSize: const Size(0, 36),
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
side: const BorderSide(color: primary),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primary,
minimumSize: const Size(0, 36),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: primary, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
isDense: true,
filled: true,
fillColor: surface,
),
dividerTheme: const DividerThemeData(color: border, thickness: 0.5),
textTheme: const TextTheme(
bodyLarge: TextStyle(fontSize: 14, color: textPrimary),
bodyMedium: TextStyle(fontSize: 14, color: textPrimary),
bodySmall: TextStyle(fontSize: 12, color: textSecondary),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: textPrimary),
labelMedium: TextStyle(fontSize: 12, color: textSecondary),
),
);
}
}