add
This commit is contained in:
@@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user