add
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:asset_assistant/pages/country_page.dart';
|
||||||
import 'package:asset_assistant/pages/exchange_add_page.dart';
|
import 'package:asset_assistant/pages/exchange_add_page.dart';
|
||||||
import 'package:asset_assistant/pages/exchange_page.dart';
|
import 'package:asset_assistant/pages/exchange_page.dart';
|
||||||
import 'package:asset_assistant/pages/home_page.dart';
|
import 'package:asset_assistant/pages/home_page.dart';
|
||||||
@@ -27,6 +28,7 @@ class MyApp extends StatelessWidget {
|
|||||||
routes: {
|
routes: {
|
||||||
'/login': (context) => const LoginPage(),
|
'/login': (context) => const LoginPage(),
|
||||||
'/home': (context) => HomePage(),
|
'/home': (context) => HomePage(),
|
||||||
|
'/country': (context) => CountryPage(),
|
||||||
'/exchange': (context) => ExchangePage(),
|
'/exchange': (context) => ExchangePage(),
|
||||||
'/exchange/add': (context) => AddExchangePage(),
|
'/exchange/add': (context) => AddExchangePage(),
|
||||||
},
|
},
|
||||||
|
|||||||
209
frontend/asset_assistant/lib/pages/country_add_page.dart
Normal file
209
frontend/asset_assistant/lib/pages/country_add_page.dart
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import 'package:asset_assistant/utils/host_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class AddCountryPage extends StatefulWidget {
|
||||||
|
const AddCountryPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddCountryPage> createState() => _AddCountryPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddCountryPageState extends State<AddCountryPage> {
|
||||||
|
// 输入控制器
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final TextEditingController _codeController = TextEditingController();
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
// 表单验证键
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
// 创建国家
|
||||||
|
Future<void> _createCountry() async {
|
||||||
|
if (!_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取用户ID
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final userId = prefs.getString('user_id');
|
||||||
|
if (userId == null) {
|
||||||
|
if (mounted) {
|
||||||
|
_showDialog('错误', '请先登录');
|
||||||
|
Navigator.pushReplacementNamed(context, '/login');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备请求数据
|
||||||
|
final baseUrl = HostUtils().currentHost;
|
||||||
|
const path = '/country/create';
|
||||||
|
final url = '$baseUrl$path';
|
||||||
|
|
||||||
|
final requestData = {
|
||||||
|
'name': _nameController.text.trim(),
|
||||||
|
'code': _codeController.text.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
final dio = Dio();
|
||||||
|
final response = await dio.post(
|
||||||
|
url,
|
||||||
|
data: requestData,
|
||||||
|
options: Options(headers: {'Content-Type': 'application/json'}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理响应
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final result = response.data;
|
||||||
|
if (result['success'] == true) {
|
||||||
|
if (mounted) {
|
||||||
|
_showDialog('成功', '国家创建成功', () {
|
||||||
|
Navigator.pop(context, true); // 返回并通知上一页刷新
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
_showDialog('失败', result['message'] ?? '创建失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
_showDialog('错误', '服务器响应异常: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
String errorMessage = '网络请求失败';
|
||||||
|
if (e.response != null) {
|
||||||
|
errorMessage = '请求失败: ${e.response?.statusCode}';
|
||||||
|
} else if (e.type == DioExceptionType.connectionTimeout) {
|
||||||
|
errorMessage = '连接超时,请检查网络';
|
||||||
|
} else if (e.type == DioExceptionType.connectionError) {
|
||||||
|
errorMessage = '网络连接错误';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
_showDialog('错误', errorMessage);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
_showDialog('错误', '发生未知错误: $e');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示对话框
|
||||||
|
void _showDialog(String title, String content, [VoidCallback? onConfirm]) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(content),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
onConfirm?.call();
|
||||||
|
},
|
||||||
|
child: const Text('确定'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('新增国家'),
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 4,
|
||||||
|
shadowColor: Colors.black12,
|
||||||
|
backgroundColor: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
onPressed: _isLoading ? null : _createCountry,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Container(
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 国家名称输入框
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '国家名称',
|
||||||
|
hintText: '请输入国家名称',
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.account_balance,
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return '请输入国家名称';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 国家代码输入框
|
||||||
|
TextFormField(
|
||||||
|
controller: _codeController,
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '国家代码',
|
||||||
|
hintText: '请输入国家代码',
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.code,
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return '请输入国家代码';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
361
frontend/asset_assistant/lib/pages/country_page.dart
Normal file
361
frontend/asset_assistant/lib/pages/country_page.dart
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import 'package:asset_assistant/pages/country_add_page.dart';
|
||||||
|
import 'package:asset_assistant/utils/host_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
// 国家数据模型
|
||||||
|
class Country {
|
||||||
|
final String countryId;
|
||||||
|
final String name;
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
Country({required this.countryId, required this.name, required this.code});
|
||||||
|
|
||||||
|
// 从JSON构建对象
|
||||||
|
factory Country.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Country(
|
||||||
|
countryId: json['country_id'],
|
||||||
|
name: json['name'],
|
||||||
|
code: json['code'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接口响应模型
|
||||||
|
class CountryResponse {
|
||||||
|
final bool success;
|
||||||
|
final String message;
|
||||||
|
final CountryData data;
|
||||||
|
|
||||||
|
CountryResponse({
|
||||||
|
required this.success,
|
||||||
|
required this.message,
|
||||||
|
required this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CountryResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CountryResponse(
|
||||||
|
success: json['success'],
|
||||||
|
message: json['message'],
|
||||||
|
data: CountryData.fromJson(json['data']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应数据模型
|
||||||
|
class CountryData {
|
||||||
|
final int total;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
final List<Country> items;
|
||||||
|
|
||||||
|
CountryData({
|
||||||
|
required this.total,
|
||||||
|
required this.page,
|
||||||
|
required this.pageSize,
|
||||||
|
required this.items,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CountryData.fromJson(Map<String, dynamic> json) {
|
||||||
|
var itemsList = json['items'] as List;
|
||||||
|
List<Country> items = itemsList.map((i) => Country.fromJson(i)).toList();
|
||||||
|
|
||||||
|
return CountryData(
|
||||||
|
total: json['total'],
|
||||||
|
page: json['page'],
|
||||||
|
pageSize: json['page_size'],
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CountryPage extends StatefulWidget {
|
||||||
|
const CountryPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CountryPage> createState() => _CountryPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CountryPageState extends State<CountryPage> {
|
||||||
|
List<Country> _countries = [];
|
||||||
|
bool _isLoading = true;
|
||||||
|
String? _errorMessage;
|
||||||
|
int _currentPage = 1;
|
||||||
|
final int _pageSize = 20;
|
||||||
|
bool _hasMoreData = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchCountries();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载国家列表数据
|
||||||
|
Future<void> _fetchCountries({bool isRefresh = false}) async {
|
||||||
|
if (isRefresh) {
|
||||||
|
setState(() {
|
||||||
|
_currentPage = 1;
|
||||||
|
_hasMoreData = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_hasMoreData && !isRefresh) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final baseUrl = HostUtils().currentHost;
|
||||||
|
final path = '/country/read';
|
||||||
|
final url = '$baseUrl$path';
|
||||||
|
|
||||||
|
final dio = Dio();
|
||||||
|
final response = await dio.get(
|
||||||
|
url,
|
||||||
|
queryParameters: {
|
||||||
|
'page': _currentPage,
|
||||||
|
'page_size': _pageSize,
|
||||||
|
// 为空时查询所有国家
|
||||||
|
'name': '',
|
||||||
|
'code': '',
|
||||||
|
'country_id': '',
|
||||||
|
},
|
||||||
|
options: Options(headers: {'Content-Type': 'application/json'}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final CountryResponse countryResponse = CountryResponse.fromJson(
|
||||||
|
response.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (countryResponse.success) {
|
||||||
|
setState(() {
|
||||||
|
if (isRefresh) {
|
||||||
|
_countries = countryResponse.data.items;
|
||||||
|
} else {
|
||||||
|
_countries.addAll(countryResponse.data.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否还有更多数据
|
||||||
|
_hasMoreData = _countries.length < countryResponse.data.total;
|
||||||
|
_currentPage++;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = countryResponse.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = '服务器响应异常: ${response.statusCode}';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
String errorMsg = '网络请求失败';
|
||||||
|
if (e.response != null) {
|
||||||
|
errorMsg = '请求失败: ${e.response?.statusCode}';
|
||||||
|
} else if (e.type == DioExceptionType.connectionTimeout) {
|
||||||
|
errorMsg = '连接超时,请检查网络';
|
||||||
|
} else if (e.type == DioExceptionType.connectionError) {
|
||||||
|
errorMsg = '网络连接错误';
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = errorMsg;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = '发生未知错误: $e';
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
Future<void> _refresh() async {
|
||||||
|
await _fetchCountries(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
void _loadMore() {
|
||||||
|
if (!_isLoading && _hasMoreData) {
|
||||||
|
_fetchCountries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('国家列表'),
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 4,
|
||||||
|
shadowColor: Colors.black12,
|
||||||
|
backgroundColor: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: () async {
|
||||||
|
// 跳转到新增页面,返回时刷新列表
|
||||||
|
final result = await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => const AddCountryPage()),
|
||||||
|
);
|
||||||
|
if (result == true) {
|
||||||
|
_fetchCountries(isRefresh: true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SafeArea(child: _buildBody(theme)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody(ThemeData theme) {
|
||||||
|
if (_isLoading && _countries.isEmpty) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_errorMessage != null && _countries.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: TextStyle(color: theme.colorScheme.error),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => _fetchCountries(isRefresh: true),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _refresh,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
itemCount: _countries.length + (_hasMoreData ? 1 : 0),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index < _countries.length) {
|
||||||
|
final country = _countries[index];
|
||||||
|
return _buildCountryItem(theme, country);
|
||||||
|
} else {
|
||||||
|
// 加载更多指示器
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Center(
|
||||||
|
child: _isLoading
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: const Text('没有更多数据了'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 监听滚动加载更多
|
||||||
|
controller: ScrollController()
|
||||||
|
..addListener(() {
|
||||||
|
if (_isLoading) return;
|
||||||
|
if (_hasMoreData &&
|
||||||
|
ScrollController().position.pixels >=
|
||||||
|
ScrollController().position.maxScrollExtent - 200) {
|
||||||
|
_loadMore();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建国家列表项
|
||||||
|
Widget _buildCountryItem(ThemeData theme, Country country) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// 可以添加点击事件
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
splashColor: theme.colorScheme.primary.withAlpha(26),
|
||||||
|
highlightColor: theme.colorScheme.primary.withAlpha(13),
|
||||||
|
child: Container(
|
||||||
|
height: 64,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.flag,
|
||||||
|
size: 24,
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
country.name,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'代码: ${country.code}',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios,
|
||||||
|
size: 18,
|
||||||
|
color: theme.hintColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 1,
|
||||||
|
thickness: 1,
|
||||||
|
indent: 72,
|
||||||
|
endIndent: 16,
|
||||||
|
color: theme.dividerColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ class HomePage extends StatelessWidget {
|
|||||||
final List<Map<String, dynamic>> features = [
|
final List<Map<String, dynamic>> features = [
|
||||||
{'icon': Icons.bar_chart, 'title': '数据分析', 'route': null},
|
{'icon': Icons.bar_chart, 'title': '数据分析', 'route': null},
|
||||||
{'icon': Icons.balance, 'title': '交易', 'route': null},
|
{'icon': Icons.balance, 'title': '交易', 'route': null},
|
||||||
|
{'icon': Icons.flag_circle, 'title': '国家', 'route': '/country'},
|
||||||
{'icon': Icons.account_balance, 'title': '交易所', 'route': '/exchange'},
|
{'icon': Icons.account_balance, 'title': '交易所', 'route': '/exchange'},
|
||||||
{'icon': Icons.branding_watermark, 'title': '品种', 'route': null},
|
{'icon': Icons.branding_watermark, 'title': '品种', 'route': null},
|
||||||
];
|
];
|
||||||
@@ -115,14 +116,12 @@ class HomePage extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
// 点击事件处理 - 如果有路由信息则导航
|
// 点击事件处理 - 如果有路由信息则导航
|
||||||
if (route != null && context.mounted) {
|
if (route != null && context.mounted) {
|
||||||
Navigator.push(
|
// 使用路由路径进行导航
|
||||||
context,
|
Navigator.pushNamed(context, route!);
|
||||||
MaterialPageRoute(builder: (context) => ExchangePage()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
splashColor: theme.colorScheme.primary.withAlpha(26), // 修改为withAlpha更兼容
|
splashColor: theme.colorScheme.primary.withAlpha(26),
|
||||||
highlightColor: theme.colorScheme.primary.withAlpha(13),
|
highlightColor: theme.colorScheme.primary.withAlpha(13),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 64,
|
height: 64,
|
||||||
|
|||||||
231
frontend/asset_assistant/lib/pages/read.go
Normal file
231
frontend/asset_assistant/lib/pages/read.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package logic4country
|
||||||
|
|
||||||
|
import (
|
||||||
|
"asset_assistant/db"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadRequest 读取请求参数结构
|
||||||
|
type ReadRequest struct {
|
||||||
|
CountryID string `form:"country_id"` // 国家ID,可选
|
||||||
|
Name string `form:"name"` // 国家名称,可选
|
||||||
|
Code string `form:"code"` // 国家代码,可选
|
||||||
|
Page string `form:"page"` // 页码,可选
|
||||||
|
PageSize string `form:"page_size"` // 每页条数,可选
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadData 读取响应数据结构
|
||||||
|
type ReadData struct {
|
||||||
|
Total int64 `json:"total"` // 总条数
|
||||||
|
Page int `json:"page"` // 当前页码
|
||||||
|
PageSize int `json:"page_size"` // 每页条数
|
||||||
|
Items []CountryInfoViewItem `json:"items"` // 数据列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountryInfoViewItem 视图数据项结构
|
||||||
|
type CountryInfoViewItem struct {
|
||||||
|
CountryID string `json:"country_id"` // 国家ID
|
||||||
|
Name string `json:"name"` // 国家名称
|
||||||
|
Code string `json:"code"` // 国家代码
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadResponse 读取响应结构
|
||||||
|
type ReadResponse struct {
|
||||||
|
Success bool `json:"success"` // 操作是否成功
|
||||||
|
Message string `json:"message"` // 提示信息
|
||||||
|
Data ReadData `json:"data"` // 响应数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadHandler 处理国家信息查询逻辑
|
||||||
|
func ReadHandler(c *gin.Context) {
|
||||||
|
startTime := time.Now()
|
||||||
|
// 获取或生成请求ID
|
||||||
|
reqID := c.Request.Header.Get("X-ReadRequest-ID")
|
||||||
|
if reqID == "" {
|
||||||
|
reqID = uuid.New().String()
|
||||||
|
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录请求接收日志
|
||||||
|
zap.L().Info("📥 收到国家查询请求",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 绑定请求参数
|
||||||
|
var req ReadRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
zap.L().Warn("⚠️ 请求参数解析失败",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusBadRequest, ReadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "请求参数格式错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证查询条件至少有一个不为空
|
||||||
|
if req.CountryID == "" && req.Name == "" && req.Code == "" {
|
||||||
|
zap.L().Warn("⚠️ 请求参数验证失败",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.String("reason", "country_id、name、code不能同时为空"),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusBadRequest, ReadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "请求参数错误:country_id、name、code不能同时为空",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理分页参数默认值
|
||||||
|
page, err := strconv.Atoi(req.Page)
|
||||||
|
if err != nil || page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, err := strconv.Atoi(req.PageSize)
|
||||||
|
if err != nil || pageSize < 1 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Debug("✅ 请求参数验证通过",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.String("country_id", req.CountryID),
|
||||||
|
zap.String("name", req.Name),
|
||||||
|
zap.String("code", req.Code),
|
||||||
|
zap.Int("page", page),
|
||||||
|
zap.Int("page_size", pageSize),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 构建查询条件和参数
|
||||||
|
whereClauses := []string{}
|
||||||
|
args := []interface{}{}
|
||||||
|
paramIndex := 1
|
||||||
|
|
||||||
|
if req.CountryID != "" {
|
||||||
|
whereClauses = append(whereClauses, "country_id = $"+strconv.Itoa(paramIndex))
|
||||||
|
args = append(args, req.CountryID)
|
||||||
|
paramIndex++
|
||||||
|
}
|
||||||
|
if req.Name != "" {
|
||||||
|
whereClauses = append(whereClauses, "name LIKE $"+strconv.Itoa(paramIndex))
|
||||||
|
args = append(args, "%"+req.Name+"%")
|
||||||
|
paramIndex++
|
||||||
|
}
|
||||||
|
if req.Code != "" {
|
||||||
|
whereClauses = append(whereClauses, "code LIKE $"+strconv.Itoa(paramIndex))
|
||||||
|
args = append(args, "%"+req.Code+"%")
|
||||||
|
paramIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建基础SQL
|
||||||
|
baseSQL := "SELECT country_id, name, code FROM country_info_view"
|
||||||
|
countSQL := "SELECT COUNT(*) FROM country_info_view"
|
||||||
|
if len(whereClauses) > 0 {
|
||||||
|
whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
|
||||||
|
baseSQL += whereStr
|
||||||
|
countSQL += whereStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算分页偏移量
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
// 拼接分页SQL(使用fmt.Sprintf更清晰)
|
||||||
|
querySQL := fmt.Sprintf("%s ORDER BY country_id LIMIT $%d OFFSET $%d", baseSQL, paramIndex, paramIndex+1)
|
||||||
|
args = append(args, pageSize, offset)
|
||||||
|
|
||||||
|
// 查询总条数(修正参数传递方式)
|
||||||
|
var total int64
|
||||||
|
countArgs := args[:len(args)-2] // 排除分页参数
|
||||||
|
err = db.DB.QueryRow(countSQL, countArgs...).Scan(&total)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("❌ 查询总条数失败",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusInternalServerError, ReadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "查询数据失败,请稍后重试",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行分页查询
|
||||||
|
rows, err := db.DB.Query(querySQL, args...)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("❌ 分页查询失败",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusInternalServerError, ReadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "查询数据失败,请稍后重试",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// 处理查询结果
|
||||||
|
var items []CountryInfoViewItem
|
||||||
|
for rows.Next() {
|
||||||
|
var item CountryInfoViewItem
|
||||||
|
if err := rows.Scan(&item.CountryID, &item.Name, &item.Code); err != nil {
|
||||||
|
zap.L().Error("❌ 解析查询结果失败",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusInternalServerError, ReadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "数据处理失败,请稍后重试",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查行迭代过程中是否发生错误
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
zap.L().Error("❌ 行迭代错误",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusInternalServerError, ReadResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "查询数据失败,请稍后重试",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录请求处理耗时
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
zap.L().Info("✅ 国家查询请求处理完成",
|
||||||
|
zap.String("req_id", reqID),
|
||||||
|
zap.Int64("total", total),
|
||||||
|
zap.Int("page", page),
|
||||||
|
zap.Int("page_size", pageSize),
|
||||||
|
zap.Duration("duration", duration),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
c.JSON(http.StatusOK, ReadResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "查询成功",
|
||||||
|
Data: ReadData{
|
||||||
|
Total: total,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
Items: items,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user