feat:增加朋友圈评论显示模块
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
print("忘记密码");
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"忘记密码?",
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildLoginButton(BuildContext context) {
|
|
||||||
return Align(
|
|
||||||
child: SizedBox(
|
|
||||||
height: 45,
|
height: 45,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: TDButton(
|
child: ElevatedButton(
|
||||||
text: '登录',
|
onPressed: _isLoading ? null : loginClick,
|
||||||
size: TDButtonSize.large,
|
style: ElevatedButton.styleFrom(
|
||||||
type: TDButtonType.fill,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
shape: TDButtonShape.rectangle,
|
foregroundColor: Colors.white,
|
||||||
theme: TDButtonTheme.primary,
|
elevation: 0,
|
||||||
onTap: () => loginClick(context),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'登录',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user