feat: add Flutter mobile clients and staff delivery API

This commit is contained in:
david
2026-07-30 21:47:41 +08:00
parent 550efb3812
commit 36a5ced1c0
228 changed files with 17159 additions and 22 deletions

View File

@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../app/dependencies.dart';
class LoginPage extends StatefulWidget {
const LoginPage({required this.session, super.key});
final UserSession session;
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _phone = TextEditingController();
final _password = TextEditingController();
bool _submitting = false;
String? _error;
@override
void dispose() {
_phone.dispose();
_password.dispose();
super.dispose();
}
Future<void> _login() async {
setState(() {
_submitting = true;
_error = null;
});
try {
await widget.session.login(phone: _phone.text.trim(), password: _password.text);
} catch (error) {
if (mounted) setState(() => _error = error.toString());
} finally {
if (mounted) setState(() => _submitting = false);
}
}
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.health_and_safety_rounded,
size: 68,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 20),
Text(
'瓶安芯',
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.w900),
),
const SizedBox(height: 8),
const Text('安全服务与生活采购', textAlign: TextAlign.center),
const SizedBox(height: 36),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
autofillHints: const [AutofillHints.telephoneNumber],
decoration: const InputDecoration(
labelText: '手机号',
prefixIcon: Icon(Icons.phone_outlined),
),
),
const SizedBox(height: 14),
TextField(
controller: _password,
obscureText: true,
autofillHints: const [AutofillHints.password],
decoration: const InputDecoration(
labelText: '密码',
prefixIcon: Icon(Icons.lock_outline),
),
),
if (_error != null) ...[
const SizedBox(height: 12),
Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
],
const SizedBox(height: 20),
ElevatedButton(
onPressed: _submitting ? null : _login,
child: _submitting
? const SizedBox.square(
dimension: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('安全登录'),
),
TextButton(
onPressed: () => context.push('/register'),
child: const Text('首次使用?注册账号'),
),
const SizedBox(height: 12),
const Text(
'登录即表示同意用户协议和隐私政策',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
],
),
),
),
),
),
);
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import '../../../app/dependencies.dart';
import '../../../data/services/api_client.dart';
class RegisterPage extends StatefulWidget {
const RegisterPage({required this.session, super.key});
final UserSession session;
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _phone = TextEditingController();
final _name = TextEditingController();
final _address = TextEditingController();
final _password = TextEditingController();
final _code = TextEditingController();
String _requestIdentity = '';
bool _busy = false;
String? _message;
Future<void> _sendCode() async {
try {
final identity = await widget.session.sendCode(_phone.text.trim(), 'register');
setState(() {
_requestIdentity = identity;
_message = '验证码已发送';
});
} catch (error) {
setState(() => _message = error.toString());
}
}
Future<void> _register() async {
setState(() => _busy = true);
try {
final api = ApiClient(() => '');
await api.post(
'/heqi/client/v1/user/auth/register',
authenticated: false,
body: {
'phone': _phone.text.trim(),
'name': _name.text.trim(),
'address': _address.text.trim(),
'password': _password.text,
'code': _code.text.trim(),
'request_identity': _requestIdentity,
},
);
if (mounted) Navigator.of(context).pop();
} catch (error) {
if (mounted) setState(() => _message = error.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
void dispose() {
_phone.dispose();
_name.dispose();
_address.dispose();
_password.dispose();
_code.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('注册用户')),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(labelText: '手机号'),
),
const SizedBox(height: 12),
TextField(
controller: _name,
decoration: const InputDecoration(labelText: '姓名'),
),
const SizedBox(height: 12),
TextField(
controller: _address,
decoration: const InputDecoration(labelText: '服务地址'),
),
const SizedBox(height: 12),
TextField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: '登录密码'),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _code,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: '验证码'),
),
),
const SizedBox(width: 10),
OutlinedButton(onPressed: _sendCode, child: const Text('获取验证码')),
],
),
if (_message != null)
Padding(padding: const EdgeInsets.only(top: 12), child: Text(_message!)),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _busy || _requestIdentity.isEmpty ? null : _register,
child: const Text('创建账号'),
),
],
),
);
}