This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
+326
View File
@@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/auth_provider.dart';
import '../../providers/upload_provider.dart';
import '../../services/upload_service.dart';
import '../feed/widgets/feed_overlays.dart';
/// 发布作品页(《03-接口文档》5.5/5.6):
/// 三步:选视频 → 填信息(标题/描述/分类/标签) → 发布结果(进我的作品查看审核状态)
class UploadScreen extends ConsumerStatefulWidget {
const UploadScreen({super.key});
@override
ConsumerState<UploadScreen> createState() => _UploadScreenState();
}
class _UploadScreenState extends ConsumerState<UploadScreen> {
final _titleCtrl = TextEditingController();
final _descCtrl = TextEditingController();
final _tagsCtrl = TextEditingController();
int? _categoryId;
@override
void dispose() {
_titleCtrl.dispose();
_descCtrl.dispose();
_tagsCtrl.dispose();
super.dispose();
}
/// 解析标签输入:空格/逗号/顿号分隔,去重,最多 5 个
List<String> _parseTags() {
return _tagsCtrl.text
.split(RegExp(r'[\s,,、]+'))
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toSet()
.take(5)
.toList();
}
void _submit() {
final title = _titleCtrl.text.trim();
if (title.isEmpty) {
showTopToast(context, '请填写标题');
return;
}
if (_categoryId == null) {
showTopToast(context, '请选择分类');
return;
}
ref.read(uploadProvider.notifier).submit(
title: title,
description: _descCtrl.text.trim(),
categoryId: _categoryId!,
tags: _parseTags(),
);
}
@override
Widget build(BuildContext context) {
final isLoggedIn = ref.watch(authProvider).isLoggedIn;
if (!isLoggedIn) {
return Scaffold(
appBar: AppBar(title: const Text('发布作品')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.video_call_outlined,
size: 64, color: AppColors.textWeak),
const SizedBox(height: 12),
const Text('登录后发布你的作品',
style: TextStyle(color: AppColors.textSecondary)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.push('/login'),
style:
ElevatedButton.styleFrom(minimumSize: const Size(160, 44)),
child: const Text('去登录 / 注册'),
),
],
),
),
);
}
// 发布失败:停留表单并提示服务端文案
ref.listen(uploadProvider, (prev, next) {
if (next.error.isNotEmpty && next.error != prev?.error) {
showTopToast(context, next.error);
}
});
final state = ref.watch(uploadProvider);
return Scaffold(
appBar: AppBar(title: const Text('发布作品')),
body: switch (state.step) {
UploadStep.pick => _PickStep(
onPick: () => ref.read(uploadProvider.notifier).pickVideo(),
),
UploadStep.edit || UploadStep.publishing => _EditStep(
state: state,
titleCtrl: _titleCtrl,
descCtrl: _descCtrl,
tagsCtrl: _tagsCtrl,
categoryId: _categoryId,
onCategoryChanged: (id) => setState(() => _categoryId = id),
onReselect: () => ref.read(uploadProvider.notifier).reset(),
onSubmit: _submit,
),
UploadStep.done => _DoneStep(result: state.result),
},
);
}
}
/// 第一步:选择视频
class _PickStep extends StatelessWidget {
const _PickStep({required this.onPick});
final VoidCallback onPick;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_upload_outlined,
size: 72, color: AppColors.textWeak),
const SizedBox(height: 16),
const Text('选择相册中的视频',
style: TextStyle(color: AppColors.textSecondary)),
const SizedBox(height: 4),
const Text('支持 mp4 / mov 等常见格式,不超过 500MB',
style: TextStyle(color: AppColors.textWeak, fontSize: 12)),
const SizedBox(height: 24),
ElevatedButton(
onPressed: onPick,
style: ElevatedButton.styleFrom(minimumSize: const Size(180, 48)),
child: const Text('选择视频'),
),
],
),
);
}
}
/// 第二步:填写作品信息(发布中显示进度)
class _EditStep extends ConsumerWidget {
const _EditStep({
required this.state,
required this.titleCtrl,
required this.descCtrl,
required this.tagsCtrl,
required this.categoryId,
required this.onCategoryChanged,
required this.onReselect,
required this.onSubmit,
});
final UploadState state;
final TextEditingController titleCtrl;
final TextEditingController descCtrl;
final TextEditingController tagsCtrl;
final int? categoryId;
final ValueChanged<int> onCategoryChanged;
final VoidCallback onReselect;
final VoidCallback onSubmit;
@override
Widget build(BuildContext context, WidgetRef ref) {
final publishing = state.publishing;
final fileName = state.file?.name ?? '';
final sizeMB = (state.fileSize / 1024 / 1024).toStringAsFixed(1);
final asyncCategories = ref.watch(uploadCategoriesProvider);
return ListView(
padding: const EdgeInsets.all(16),
children: [
// 已选视频信息
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.videocam_outlined,
color: AppColors.textSecondary),
const SizedBox(width: 8),
Expanded(
child: Text(
'$fileName($sizeMB MB)',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 13),
),
),
if (!publishing)
TextButton(
onPressed: onReselect, child: const Text('重新选择')),
],
),
),
const SizedBox(height: 16),
TextField(
controller: titleCtrl,
enabled: !publishing,
maxLength: 100,
decoration: const InputDecoration(
labelText: '标题',
hintText: '给你的作品起个标题(必填)',
),
),
const SizedBox(height: 8),
TextField(
controller: descCtrl,
enabled: !publishing,
maxLength: 500,
maxLines: 3,
decoration: const InputDecoration(
labelText: '简介',
hintText: '补充介绍一下(选填)',
),
),
const SizedBox(height: 8),
const Text('分类', style: TextStyle(color: AppColors.textSecondary)),
const SizedBox(height: 8),
asyncCategories.when(
loading: () => const LinearProgressIndicator(),
error: (_, __) => const Text('分类加载失败',
style: TextStyle(color: AppColors.textWeak, fontSize: 12)),
data: (categories) => Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final c in categories)
ChoiceChip(
label: Text(c.name),
selected: categoryId == c.categoryId,
onSelected:
publishing ? null : (_) => onCategoryChanged(c.categoryId),
),
],
),
),
const SizedBox(height: 16),
TextField(
controller: tagsCtrl,
enabled: !publishing,
decoration: const InputDecoration(
labelText: '标签',
hintText: '空格分隔,最多 5 个(选填)',
),
),
const SizedBox(height: 24),
if (publishing) ...[
LinearProgressIndicator(value: state.progress / 100),
const SizedBox(height: 8),
Text(
state.progress < 100 ? '上传中 ${state.progress}%' : '发布中…',
textAlign: TextAlign.center,
style:
const TextStyle(color: AppColors.textSecondary, fontSize: 12),
),
] else
ElevatedButton(
onPressed: onSubmit,
style: ElevatedButton.styleFrom(minimumSize: const Size(0, 48)),
child: const Text('发布'),
),
],
);
}
}
/// 第三步:发布结果(引导去我的作品查看审核进度)
class _DoneStep extends StatelessWidget {
const _DoneStep({required this.result});
final PublishResult? result;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check_circle_outline,
size: 72, color: AppColors.secondary),
const SizedBox(height: 16),
const Text('发布成功',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
const SizedBox(height: 8),
Text(
'当前状态:${result?.statusText ?? '待审核'},审核通过后自动上架',
textAlign: TextAlign.center,
style:
const TextStyle(color: AppColors.textSecondary, fontSize: 13),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.pushReplacement('/mine/works'),
style:
ElevatedButton.styleFrom(minimumSize: const Size(180, 44)),
child: const Text('查看我的作品'),
),
TextButton(
onPressed: () => Navigator.of(context).maybePop(),
child: const Text('返回',
style: TextStyle(color: AppColors.textSecondary)),
),
],
),
),
);
}
}