This commit is contained in:
vipg
2025-11-19 16:17:01 +08:00
parent 8b38fb2bb7
commit d05a4cb7e2
5 changed files with 807 additions and 5 deletions

View File

@@ -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_page.dart';
import 'package:asset_assistant/pages/home_page.dart';
@@ -27,6 +28,7 @@ class MyApp extends StatelessWidget {
routes: {
'/login': (context) => const LoginPage(),
'/home': (context) => HomePage(),
'/country': (context) => CountryPage(),
'/exchange': (context) => ExchangePage(),
'/exchange/add': (context) => AddExchangePage(),
},

View 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;
},
),
],
),
),
),
),
);
}
}

View 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,
),
],
);
}
}

View File

@@ -6,6 +6,7 @@ class HomePage extends StatelessWidget {
final List<Map<String, dynamic>> features = [
{'icon': Icons.bar_chart, '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.branding_watermark, 'title': '品种', 'route': null},
];
@@ -115,14 +116,12 @@ class HomePage extends StatelessWidget {
onTap: () {
// 点击事件处理 - 如果有路由信息则导航
if (route != null && context.mounted) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ExchangePage()),
);
// 使用路由路径进行导航
Navigator.pushNamed(context, route!);
}
},
borderRadius: BorderRadius.circular(8),
splashColor: theme.colorScheme.primary.withAlpha(26), // 修改为withAlpha更兼容
splashColor: theme.colorScheme.primary.withAlpha(26),
highlightColor: theme.colorScheme.primary.withAlpha(13),
child: Container(
height: 64,

View 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,
},
})
}