feat:增加朋友圈评论显示模块

This commit is contained in:
2025-11-20 19:55:44 +08:00
parent 4f033a4d0a
commit 515021781f
4 changed files with 176 additions and 62 deletions

View File

@@ -9,22 +9,85 @@ import 'package:share_plus/share_plus.dart';
/// 截图工具类 /// 截图工具类
class ScreenshotUtil { class ScreenshotUtil {
static double pixelRatio = 3.0;
static Duration delay = const Duration(milliseconds: 50);
static String watermarkText = '来自 食光集';
// 截图功能 // 截图功能
static Future<Uint8List?> captureRecipeImage({ static Future<Uint8List?> captureImage({required GlobalKey globalKey}) async {
required GlobalKey globalKey,
double pixelRatio = 3.0,
Duration delay = const Duration(milliseconds: 50),
}) async {
// 等待一帧确保UI已渲染 // 等待一帧确保UI已渲染
await Future.delayed(delay); await Future.delayed(delay);
final RenderObject? render = globalKey.currentContext!.findRenderObject(); final RenderObject? render = globalKey.currentContext!.findRenderObject();
final RenderRepaintBoundary boundary = render as RenderRepaintBoundary; final RenderRepaintBoundary boundary = render as RenderRepaintBoundary;
final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio); final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
final ByteData? byteData = await image.toByteData(
// 添加水印
return await _addWatermarkToImage(image);
}
// 添加水印到图片
static Future<Uint8List> _addWatermarkToImage(ui.Image image) async {
// 创建画布
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
// 绘制原始图片
canvas.drawImage(image, Offset.zero, Paint());
// 添加文字水印
_drawTextWatermark(canvas, image);
// 生成最终图片
final picture = recorder.endRecording();
final watermarkedImage = await picture.toImage(image.width, image.height);
final ByteData? byteData = await watermarkedImage.toByteData(
format: ui.ImageByteFormat.png, format: ui.ImageByteFormat.png,
); );
return byteData?.buffer.asUint8List(); return byteData!.buffer.asUint8List();
}
// 绘制文字水印
static void _drawTextWatermark(Canvas canvas, ui.Image image) {
final textPainter = TextPainter(
text: TextSpan(
text: watermarkText,
style: TextStyle(
color: Color(0x99FFFFFF),
fontSize: 28,
fontFamily: 'CustomFont',
fontWeight: FontWeight.w600,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: image.width.toDouble());
// 计算水印位置(右下角,带边距)
final double x = image.width - textPainter.width - 40;
final double y = image.height - textPainter.height - 40;
// 绘制文字背景
final backgroundPaint =
Paint()
..color = const ui.Color(0x4D000000)
..style = PaintingStyle.fill;
// 绘制圆角矩形背景
final backgroundRect = RRect.fromRectAndRadius(
Rect.fromLTWH(
x - 12,
y - 6,
textPainter.width + 24,
textPainter.height + 12,
),
Radius.circular(6),
);
canvas.drawRRect(backgroundRect, backgroundPaint);
// 绘制文字
textPainter.paint(canvas, Offset(x, y));
} }
// 保存图片到临时文件 // 保存图片到临时文件
@@ -41,7 +104,7 @@ class ScreenshotUtil {
required String shareText, required String shareText,
}) async { }) async {
try { try {
final imageBytes = await captureRecipeImage(globalKey: globalKey); final imageBytes = await captureImage(globalKey: globalKey);
if (imageBytes != null) { if (imageBytes != null) {
final imageFile = await saveImageToFile(imageBytes, fileName); final imageFile = await saveImageToFile(imageBytes, fileName);

View File

@@ -5,7 +5,6 @@ import 'package:food_hub_app/models/session.dart';
import 'package:food_hub_app/utils/sp_util.dart'; import 'package:food_hub_app/utils/sp_util.dart';
import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/common/index.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
const LoginPage({super.key}); const LoginPage({super.key});
@@ -20,6 +19,7 @@ class _LoginPage extends State<LoginPage> {
String _username = "Cxx0822", _password = "19940822Cxx"; String _username = "Cxx0822", _password = "19940822Cxx";
bool _isRemember = false; bool _isRemember = false;
bool _isObscure = true; bool _isObscure = true;
bool _isLoading = false;
Color _eyeColor = Colors.grey; Color _eyeColor = Colors.grey;
final List _loginMethod = [ final List _loginMethod = [
{"title": "phone", "icon": Icons.phone_android}, {"title": "phone", "icon": Icons.phone_android},
@@ -34,7 +34,7 @@ class _LoginPage extends State<LoginPage> {
} }
Future<void> _loadRememberState() async { Future<void> _loadRememberState() async {
bool? isRemember = await SPUtil.getBool('isRemember'); bool? isRemember = SPUtil.getBool('isRemember');
if (isRemember) { if (isRemember) {
setState(() { setState(() {
_isRemember = true; _isRemember = true;
@@ -60,7 +60,11 @@ class _LoginPage extends State<LoginPage> {
SPUtil.set('token', token); SPUtil.set('token', token);
} }
void loginClick(BuildContext context) async { void loginClick() async {
setState(() {
_isLoading = true;
});
// 表单校验通过才会继续执行 // 表单校验通过才会继续执行
if ((_formKey.currentState as FormState).validate()) { if ((_formKey.currentState as FormState).validate()) {
(_formKey.currentState as FormState).save(); (_formKey.currentState as FormState).save();
@@ -70,9 +74,11 @@ class _LoginPage extends State<LoginPage> {
handleRememberState(); handleRememberState();
handleTokenState(session.saToken.tokenValue); handleTokenState(session.saToken.tokenValue);
Navigator.pushNamed(context, '/home'); Navigator.pushNamed(context, '/home');
} else {
showErrorToast('请先输入信息');
} }
setState(() {
_isLoading = false;
});
} }
@override @override
@@ -81,13 +87,9 @@ class _LoginPage extends State<LoginPage> {
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
body: Form( body: Form(
key: _formKey, key: _formKey,
// autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column( child: Column(
children: [ children: [
// 可滚动的主要内容区域
Expanded(child: buildLoginForm()), Expanded(child: buildLoginForm()),
// 固定在底部的其他登录选项区域
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50),
child: Column( child: Column(
@@ -123,18 +125,11 @@ class _LoginPage extends State<LoginPage> {
const SizedBox(height: 60), const SizedBox(height: 60),
buildUsernameTextField(), buildUsernameTextField(),
const SizedBox(height: 20), const SizedBox(height: 20),
buildPasswordTextField(context), buildPasswordTextField(),
Row( const SizedBox(height: 10),
mainAxisAlignment: MainAxisAlignment.spaceBetween, buildRememberPasswordCheckbox(),
crossAxisAlignment: CrossAxisAlignment.center, const SizedBox(height: 20),
children: [ buildLoginButton(),
buildRememberPasswordCheckbox(context), // 记住密码(左对齐)
buildForgetPasswordText(context),
],
),
const SizedBox(height: 20), // 调整与登录按钮的间距
buildLoginButton(context),
], ],
); );
} }
@@ -160,7 +155,7 @@ class _LoginPage extends State<LoginPage> {
); );
} }
Widget buildPasswordTextField(BuildContext context) { Widget buildPasswordTextField() {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -198,10 +193,12 @@ class _LoginPage extends State<LoginPage> {
); );
} }
Widget buildRememberPasswordCheckbox(BuildContext context) { Widget buildRememberPasswordCheckbox() {
return Row( return Row(
children: [ children: [
Checkbox( Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
value: _isRemember, value: _isRemember,
onChanged: (bool? value) { onChanged: (bool? value) {
setState(() { setState(() {
@@ -214,40 +211,40 @@ class _LoginPage extends State<LoginPage> {
); );
} }
Widget buildForgetPasswordText(BuildContext context) { Widget buildLoginButton() {
return Align( return SizedBox(
alignment: Alignment.centerRight, height: 45,
child: TextButton( width: double.infinity,
onPressed: () { child: ElevatedButton(
print("忘记密码"); onPressed: _isLoading ? null : loginClick,
}, style: ElevatedButton.styleFrom(
child: const Text( backgroundColor: Theme.of(context).colorScheme.primary,
"忘记密码?", foregroundColor: Colors.white,
style: TextStyle(fontSize: 14, color: Colors.grey), elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
), child: const Text(
); '登录',
} style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
Widget buildLoginButton(BuildContext context) {
return Align(
child: SizedBox(
height: 45,
width: double.infinity,
child: TDButton(
text: '登录',
size: TDButtonSize.large,
type: TDButtonType.fill,
shape: TDButtonShape.rectangle,
theme: TDButtonTheme.primary,
onTap: () => loginClick(context),
), ),
), ),
); );
} }
Widget buildOtherLoginText() { Widget buildOtherLoginText() {
return TDDivider(text: '其他方式登录', alignment: TextAlignment.center); return Row(
children: [
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'其他方式登录',
style: TextStyle(color: Colors.grey, fontSize: 14),
),
),
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
],
);
} }
Widget buildOtherMethod(context) { Widget buildOtherMethod(context) {

View File

@@ -33,6 +33,10 @@ class MomentCard extends StatelessWidget {
_buildTime(), _buildTime(),
const SizedBox(height: 5), const SizedBox(height: 5),
_buildActionButtons(), _buildActionButtons(),
if (moment.commentList!.isNotEmpty) ...[
const SizedBox(height: 8),
_buildCommentList(),
],
], ],
), ),
), ),
@@ -189,4 +193,55 @@ class MomentCard extends StatelessWidget {
], ],
); );
} }
Widget _buildCommentItem(Comment comment) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
children: [
TextSpan(
text: comment.username,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
fontFamily: 'CustomFont',
color: Colors.black,
),
),
const TextSpan(text: ': ', style: TextStyle(color: Colors.black)),
TextSpan(
text: comment.content,
style: const TextStyle(
fontSize: 14,
fontFamily: 'CustomFont',
color: Colors.black87,
),
),
],
),
),
],
);
}
Widget _buildCommentList() {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: moment.commentList!.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) {
return _buildCommentItem(moment.commentList![index]);
},
),
);
}
} }

View File

@@ -27,19 +27,18 @@ class _RecipeListState extends State<RecipeList> {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return Container( return Container(
width: 150, width: 140,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!), border: Border.all(color: Colors.grey[200]!),
), ),
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),
child: DropdownButton<String>( child: DropdownButton<String>(
value: provider.queryCategory, value: provider.queryCategory,
hint: Text('请选择菜谱类别', style: TextStyle(color: Colors.grey[500])),
isExpanded: true, isExpanded: true,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
dropdownColor: Colors.white, dropdownColor: colors.surfaceContainer,
elevation: 6, elevation: 6,
underline: Container(), underline: Container(),
items: items: