初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -0,0 +1,428 @@
import 'dart:io';
import 'package:easy_rich_text/easy_rich_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/tools_base/widget/add_media_source_button.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../../../home/widget/support_up_alert.dart';
import '../chose_tag_page.dart';
import '../publish_logic.dart';
import 'video_cover_widget.dart';
/// 发布页各输入区统一的卡片底色
const publishDecoration = BoxDecoration(
color: Color(0x0DFFFFFF),
borderRadius: BorderRadius.all(Radius.circular(10)),
);
enum LinkType { topic, video, link }
/// 添加视频
class ChoseVideoWidget extends StatelessWidget {
ChoseVideoWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('添加视频',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500)),
10.sizeBoxW,
Text('最大300M以内',
style: TextStyle(color: Color(0xff666666), fontSize: 12.sp)),
],
),
14.h.sizeBoxH,
Obx(() {
final cellW = (Get.width - 52) / 3;
return Row(children: [
6.h.sizeBoxW,
if (controller.video.value.localPath == null)
AddMediaSourceButton(
width: cellW,
height: cellW,
onTap: controller.choseVideo,
isVideo: true)
else
_cover(cellW),
]);
})
],
),
);
}
/// 已选视频:封面 + 右上角删除
Widget _cover(double size) {
return Container(
constraints: const BoxConstraints(maxWidth: 230, maxHeight: 219),
child: Stack(
children: [
VideoCoverWidget(controller.video.value.localCover ?? '',
width: size, height: size, isLocal: true),
Positioned(
right: 6,
top: 6,
child: GestureDetector(
onTap: () => controller.deleteSelectVideo(),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .8),
borderRadius: BorderRadius.circular(10)),
child:
const Icon(Icons.close, size: 12, color: Color(0xff333333)),
),
),
)
],
),
);
}
}
/// 选择标签
class TopicWidget extends StatelessWidget {
TopicWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return Obx(
() => GestureDetector(
onTap: () async {
final model = await Get.to(ChoseTopicPage(
entry: controller.type.tag, selectTags: controller.topics));
if (model is TagsBean) {
controller.topics
..clear()
..add(model);
}
},
child: Container(
margin: EdgeInsets.only(top: 12.h),
padding: EdgeInsets.symmetric(horizontal: 14.w),
alignment: Alignment.centerLeft,
height: 60.h,
decoration: publishDecoration,
child: controller.topics.isEmpty ? _emptyTip() : _topicInfo(),
),
),
);
}
/// 未选标签时的占位提示
Widget _emptyTip() {
return Row(
children: [
Text("#",
style: TextStyle(
color: Color(0xff989898),
fontSize: 24.sp,
fontWeight: FontWeight.bold)),
10.sizeBoxW,
Text('选择标签',
style: TextStyle(
color: Color(0xff989898),
fontSize: 12.sp,
fontWeight: FontWeight.w500)),
const Spacer(),
Icon(Icons.arrow_forward_ios, size: 16.sp, color: Color(0xffDCDCDC)),
],
);
}
/// 已选标签:封面 + 名称(点击交给外层跳转,别再套手势,否则会截走外层的 onTap)
Widget _topicInfo() {
return Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: NetworkImageLoader(
imageUrl: controller.topics.first.coverImg ?? '',
width: 38,
height: 38),
),
5.sizeBoxW,
Text(
controller.topics.firstOrNull?.name ?? '',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500),
),
Spacer(),
Icon(Icons.arrow_forward_ios, size: 16.sp, color: Color(0xffDCDCDC)),
],
);
}
}
/// 输入标题
class InputTitleWidget extends StatelessWidget {
InputTitleWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return Container(
height: 60.h,
margin: EdgeInsets.only(top: 14.h),
padding: EdgeInsets.symmetric(horizontal: 14.w),
alignment: Alignment.centerLeft,
decoration: publishDecoration,
child: TextField(
maxLines: 1,
textAlign: TextAlign.left,
textAlignVertical: TextAlignVertical.center,
style: TextStyle(color: Colors.white, fontSize: 16.sp, height: 22 / 15),
focusNode: controller.titleFouceN,
controller: controller.titleController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: '请填写标题',
hintStyle: TextStyle(
color: Color(0xff525252), fontSize: 12.sp, height: 22 / 15),
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
);
}
}
/// 输入正文(右下角实时字数)
class InputDescribeWidget extends StatelessWidget {
InputDescribeWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return InkWell(
enableFeedback: false,
onTap: () => controller.contentFouceN.requestFocus(),
child: Container(
constraints: BoxConstraints(minHeight: 171.h),
margin: const EdgeInsets.only(top: 14),
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 11.h),
decoration: publishDecoration,
child: Stack(
children: [
TextField(
style: TextStyle(color: Colors.white, fontSize: 16.sp),
maxLength: 300,
maxLines: null,
focusNode: controller.contentFouceN,
controller: controller.describeController,
decoration: InputDecoration(
border: InputBorder.none,
hintStyle: TextStyle(color: Color(0xff525252), fontSize: 12.sp),
hintMaxLines: 10,
hintText: '有趣的介绍能让你的逼格提高N个档次!...',
counter: SizedBox(),
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
Positioned(
bottom: 0,
right: 0,
// 字数计数:只监听不接管所有权(释放归 PublishLogic.onClose)
child: ValueListenableBuilder(
valueListenable: controller.describeController,
builder: (_, value, __) => Text(
'${value.text.length}/300',
style: TextStyle(color: Color(0xff757575), fontSize: 12),
),
),
)
],
),
),
);
}
}
/// 添加图片(最多 9 张,列表末位固定是「添加」占位)
class ChoseImageWidget extends StatelessWidget {
final String? hint;
final String? highlightText;
final Color? highlightTextColor;
final double? highlightTextSize;
ChoseImageWidget(
{super.key,
this.hint,
this.highlightText,
this.highlightTextColor,
this.highlightTextSize});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 14),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(
children: [
Text('添加图片',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500)),
EasyRichText(
hint ?? '添加图集 图片数量最多上传9张哦~',
defaultStyle:
TextStyle(color: Color(0xff666666), fontSize: 12.sp),
patternList: [
EasyRichTextPattern(
targetString: highlightText ?? '',
style: TextStyle(
color: highlightTextColor ?? Colors.white,
fontSize: highlightTextSize ?? 16.sp,
fontWeight: FontWeight.w500),
)
],
),
],
),
14.h.sizeBoxH,
LayoutBuilder(builder: (__, constrains) {
final cellW = (constrains.maxWidth - 16) / 3;
final images = controller.imgs;
return Obx(
() => Wrap(
runSpacing: 8,
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.start,
children: List.generate(images.length, (i) {
// 末位是占位项:满 10 项(9 张图)就不再给「添加」按钮
if (i < images.length - 1) return _imgCell(images[i], cellW);
if (i == 9) return const SizedBox.shrink();
return AddMediaSourceButton(
isVideo: false,
onTap: controller.choseImg,
height: cellW,
width: cellW,
title: "添加图片",
);
}),
),
);
}),
]),
);
}
/// 单张已选图片 + 右上角删除
Widget _imgCell(String path, double size) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: size,
height: size,
child: Stack(
children: [
Image.file(File(path),
width: size, height: size, fit: BoxFit.cover),
Positioned(
right: 6,
top: 6,
child: GestureDetector(
onTap: () => controller.imgs.remove(path),
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .8),
borderRadius: BorderRadius.circular(10)),
alignment: Alignment.center,
child: const Icon(Icons.close, size: 14),
),
),
)
],
),
),
);
}
}
/// 设置价格(金币)
class SettingGoldWidget extends StatelessWidget {
SettingGoldWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
final res = await Get.bottomSheet(
SupportUPAlert(
isPublish: true,
selectCoin: int.tryParse(controller.gold.value ?? '')),
);
controller.gold.value = res?.toString();
},
child: Container(
margin: const EdgeInsets.only(top: 18),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: publishDecoration,
child: Row(
children: [
Image.asset('community_coin.webp'.communityPath,
width: 20, height: 20),
10.sizeBoxW,
const Text('设置价格',
style: TextStyle(
color: Color(0xFF989898),
fontSize: 12,
fontWeight: FontWeight.w600)),
const Spacer(),
Obx(
() => Text(
controller.gold.value == null
? '免费'
: '${controller.gold.value} 金币',
style: TextStyle(
color: controller.gold.value == null
? Color(0xFF989898)
: AppColors.actionRed,
// color: Color(0xffF68216),
fontSize: 12,
fontWeight: FontWeight.w400),
),
),
5.sizeBoxW,
const Icon(Icons.arrow_forward_ios,
color: Color(0xffDCDCDC), size: 12)
],
),
),
);
}
}
@@ -0,0 +1,57 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
/// 视频封面:本地/网络图 + 可选暗色遮罩 + 居中播放按钮
/// [ratio] 传了就按比例撑开,不传则由外部约束决定尺寸
class VideoCoverWidget extends StatelessWidget {
final String cover;
final double? ratio;
final double? width;
final double? height;
final bool isLocal;
final bool showMask;
final double borderRadius;
const VideoCoverWidget(
this.cover, {
super.key,
this.ratio,
this.width,
this.height,
this.isLocal = false,
this.showMask = false,
this.borderRadius = 8,
});
@override
Widget build(BuildContext context) {
final maskW = width ?? double.infinity;
final maskH = height ?? double.infinity;
final child = Stack(
alignment: Alignment.center,
children: [
if (isLocal)
Image.file(File(cover),
width: width, height: height, fit: BoxFit.cover)
else
NetworkImageLoader(
imageUrl: cover, width: maskW, height: maskH, borderRadius: 0),
if (showMask)
Container(
width: maskW,
height: maskH,
color: Colors.black.withValues(alpha: 0.4)),
Image.asset('circle_play.webp'.videoPath, width: 30, height: 30),
],
);
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: ratio == null
? child
: AspectRatio(aspectRatio: ratio! > 0 ? ratio! : 1, child: child),
);
}
}