327 lines
10 KiB
Dart
327 lines
10 KiB
Dart
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)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|