新增影视分享的几种样式

This commit is contained in:
DelLevin-Home
2026-06-19 00:28:13 +08:00
parent 56dee60782
commit 67b77f14e0
12 changed files with 2076 additions and 557 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
assets/images/ticket/qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -1,5 +1,4 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -9,10 +8,8 @@ import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
import '../../widgets/fade_in_local_image.dart';
/// 影视分享海报页面
class MovieSharePage extends StatefulWidget {
final Movie movie;
const MovieSharePage({super.key, required this.movie});
@override
@@ -22,6 +19,72 @@ class MovieSharePage extends StatefulWidget {
class _MovieSharePageState extends State<MovieSharePage> {
final GlobalKey _posterKey = GlobalKey();
bool _isGenerating = false;
int _currentStyle = 0;
static const _styleNames = ['海报', '经典票根', '喵眼电影票根', '收藏版票根'];
// 影院信息(可编辑)
late TextEditingController _cinemaCtrl;
late TextEditingController _hallCtrl;
late TextEditingController _seatRowCtrl;
late TextEditingController _seatNumCtrl;
late TextEditingController _screeningDateCtrl;
late TextEditingController _screeningTimeCtrl;
late TextEditingController _priceCtrl;
late TextEditingController _serviceFeeCtrl;
late TextEditingController _issueTimeCtrl;
late TextEditingController _platformCtrl;
late TextEditingController _ticketCodeCtrl;
@override
void initState() {
super.initState();
final now = DateTime.now();
final dateStr = '${now.year}${now.month}${now.day}';
final timeStr = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
final issueStr = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
final codeStr = 'MK${now.millisecondsSinceEpoch.toString().substring(1)}';
_cinemaCtrl = TextEditingController(text: '万达影城');
_hallCtrl = TextEditingController(text: '3号厅');
_seatRowCtrl = TextEditingController(text: '8');
_seatNumCtrl = TextEditingController(text: '12');
_screeningDateCtrl = TextEditingController(text: dateStr);
_screeningTimeCtrl = TextEditingController(text: timeStr);
_priceCtrl = TextEditingController(text: '39.90');
_serviceFeeCtrl = TextEditingController(text: '5.00');
_issueTimeCtrl = TextEditingController(text: issueStr);
_platformCtrl = TextEditingController(text: '喵眼电影');
_ticketCodeCtrl = TextEditingController(text: codeStr);
}
@override
void dispose() {
_cinemaCtrl.dispose();
_hallCtrl.dispose();
_seatRowCtrl.dispose();
_seatNumCtrl.dispose();
_screeningDateCtrl.dispose();
_screeningTimeCtrl.dispose();
_priceCtrl.dispose();
_serviceFeeCtrl.dispose();
_issueTimeCtrl.dispose();
_platformCtrl.dispose();
_ticketCodeCtrl.dispose();
super.dispose();
}
bool get _isCinemaStyle => _currentStyle >= 2;
Widget _buildBody() {
switch (_currentStyle) {
case 0: return _buildPosterWidget();
case 1: return _buildTicketClassic();
case 2: return _buildTicketCinema();
case 3: return _buildTicketCollectible();
default: return _buildPosterWidget();
}
}
@override
Widget build(BuildContext context) {
@@ -31,53 +94,232 @@ class _MovieSharePageState extends State<MovieSharePage> {
appBar: AppBar(
backgroundColor: colors.surface,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.close, color: colors.onSurface),
onPressed: () => Navigator.pop(context),
),
title: Text(
'分享海报',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
leading: IconButton(icon: Icon(Icons.close, color: colors.onSurface), onPressed: () => Navigator.pop(context)),
title: Text('分享海报', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
centerTitle: true,
actions: [
IconButton(icon: Icon(Icons.palette_outlined, color: colors.onSurface, size: 22), tooltip: '选择样式', onPressed: _showStylePicker),
TextButton(
onPressed: _isGenerating ? null : _generateAndShare,
child: _isGenerating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(
'分享',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: Text('分享', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
),
const SizedBox(width: 8),
const SizedBox(width: 4),
],
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: RepaintBoundary(
key: _posterKey,
child: _buildPosterWidget(),
body: Column(children: [
// 预览
Expanded(child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: RepaintBoundary(key: _posterKey, child: _buildBody()),
),
)),
// 底部编辑按钮(仅票根样式)
if (_isCinemaStyle) _buildBottomEditBar(colors),
]),
);
}
// ─── 样式选择 ───
void _showStylePicker() {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('选择样式', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(height: 16),
Wrap(spacing: 10, runSpacing: 10, children: List.generate(_styleNames.length, (i) {
final selected = _currentStyle == i;
return GestureDetector(
onTap: () { setState(() => _currentStyle = i); Navigator.pop(ctx); },
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
decoration: BoxDecoration(
color: selected ? colors.primary : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: selected ? colors.primary : colors.outline, width: 0.5),
),
child: Text(_styleNames[i], style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6))),
),
);
})),
]),
),
);
}
// ─── 底部编辑栏 ───
Widget _buildBottomEditBar(ColorScheme colors) {
return Container(
padding: EdgeInsets.only(left: 24, right: 24, bottom: MediaQuery.of(context).padding.bottom + 12, top: 8),
color: colors.surface,
child: SizedBox(
width: double.infinity,
height: 44,
child: ElevatedButton.icon(
onPressed: _showEditSheet,
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('编辑票根信息'),
style: ElevatedButton.styleFrom(
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
),
);
}
/// 构建海报 Widget
void _showEditSheet() {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => Padding(
padding: EdgeInsets.fromLTRB(20, 16, 20, MediaQuery.of(ctx).viewInsets.bottom + 20),
child: SingleChildScrollView(child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('编辑票根信息', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
const SizedBox(height: 16),
_sheetField('影院名称', _cinemaCtrl),
_sheetField('影厅', _hallCtrl),
Row(children: [
Expanded(child: _sheetField('', _seatRowCtrl)),
const SizedBox(width: 12),
Expanded(child: _sheetField('', _seatNumCtrl)),
]),
Row(children: [
Expanded(child: _sheetField('票价 (¥)', _priceCtrl)),
const SizedBox(width: 12),
Expanded(child: _sheetField('服务费 (¥)', _serviceFeeCtrl)),
]),
_sheetDatePicker('出票日期', _issueTimeCtrl, ctx),
_sheetCodeField(),
const SizedBox(height: 16),
SizedBox(
width: double.infinity, height: 44,
child: ElevatedButton(
onPressed: () { setState(() {}); Navigator.pop(ctx); },
style: ElevatedButton.styleFrom(
backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('完成'),
),
),
],
)),
),
);
}
Widget _sheetField(String label, TextEditingController ctrl) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: ctrl,
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5)),
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
filled: true, fillColor: colors.surfaceContainerHighest,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: colors.primary, width: 1)),
),
),
);
}
Widget _sheetDatePicker(String label, TextEditingController ctrl, BuildContext sheetCtx) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: GestureDetector(
onTap: () async {
final now = DateTime.now();
final picked = await showDatePicker(
context: sheetCtx,
initialDate: now,
firstDate: DateTime(1900),
lastDate: DateTime(now.year + 10),
locale: const Locale('zh'),
);
if (picked != null) {
ctrl.text = '${picked.year}-${picked.month.toString().padLeft(2, '0')}-${picked.day.toString().padLeft(2, '0')}';
}
},
child: AbsorbPointer(child: TextField(
controller: ctrl,
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5)),
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
filled: true, fillColor: colors.surfaceContainerHighest,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: colors.primary, width: 1)),
suffixIcon: Icon(Icons.calendar_today_outlined, size: 18, color: colors.onSurface.withValues(alpha: 0.4)),
),
)),
),
);
}
Widget _sheetCodeField() {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(children: [
Expanded(child: TextField(
controller: _ticketCodeCtrl,
style: TextStyle(fontSize: 14, color: colors.onSurface, fontFamily: 'monospace'),
decoration: InputDecoration(
labelText: '编码',
labelStyle: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5)),
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
filled: true, fillColor: colors.surfaceContainerHighest,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: colors.primary, width: 1)),
),
)),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
_ticketCodeCtrl.text = 'MK${DateTime.now().millisecondsSinceEpoch.toString().substring(1)}';
},
child: Container(
width: 40, height: 40,
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)),
child: Icon(Icons.casino_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
),
),
]),
);
}
// ═══════════════════════════════════════════════════════════
// 样式 0海报
// ═══════════════════════════════════════════════════════════
Widget _buildPosterWidget() {
final colors = Theme.of(context).colorScheme;
final movie = widget.movie;
@@ -85,240 +327,389 @@ class _MovieSharePageState extends State<MovieSharePage> {
return Container(
width: 320,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(16),
decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))]),
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
if (hasPoster)
ClipRRect(borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
child: FadeInLocalImage(path: movie.posterPath, width: 320, height: 200, fit: BoxFit.cover)),
Padding(padding: const EdgeInsets.all(20), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(movie.title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: colors.onSurface)),
if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 8),
Text(movie.alternateTitles.join(' / '), style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
],
const SizedBox(height: 16),
if (movie.rating != null && movie.rating! > 0) ...[
Row(children: [
const Icon(Icons.star, size: 18, color: Color(0xFFFFB800)),
const SizedBox(width: 4),
Text(movie.rating!.toStringAsFixed(1), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFFFFB800))),
const SizedBox(width: 4),
Text('/ 10', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
const SizedBox(height: 12),
],
if (movie.directors.isNotEmpty) _infoRow('导演', movie.directors.join(' / '), colors),
if (movie.writers.isNotEmpty) _infoRow('编剧', movie.writers.join(' / '), colors),
if (movie.actors.isNotEmpty) _infoRow('主演', movie.actors.take(3).join(' / '), colors),
if (movie.genres.isNotEmpty) _infoRow('类型', movie.genres.join(' / '), colors),
if (movie.releaseDate != null) _infoRow('上映', _fmtDate(movie.releaseDate!), colors),
if (movie.summary != null && movie.summary!.isNotEmpty) ...[
const SizedBox(height: 16),
Text('简介', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 8),
Text(movie.summary!, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.6), height: 1.6), maxLines: 5, overflow: TextOverflow.ellipsis),
],
const SizedBox(height: 20),
Divider(height: 1, color: colors.outline),
const SizedBox(height: 12),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.movie_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.5)),
const SizedBox(width: 6),
Text('来自 MookNote', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.5))),
]),
])),
]),
);
}
Widget _infoRow(String label, String value, ColorScheme colors) {
return Padding(padding: const EdgeInsets.only(bottom: 8), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('$label', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
Expanded(child: Text(value, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.75)))),
]));
}
// ═══════════════════════════════════════════════════════════
// 样式 1经典票根
// ═══════════════════════════════════════════════════════════
Widget _buildTicketClassic() {
final movie = widget.movie;
final hasPoster = movie.posterPath != null && movie.posterPath!.isNotEmpty;
const c = Color(0xFF2D2D2D);
return Container(
width: 300,
decoration: BoxDecoration(color: const Color(0xFFFFFBF5), borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 16, offset: const Offset(0, 6))]),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Padding(padding: const EdgeInsets.all(16), child: Column(children: [
if (hasPoster) ClipRRect(borderRadius: BorderRadius.circular(4),
child: FadeInLocalImage(path: movie.posterPath, width: 268, height: 160, fit: BoxFit.cover)),
const SizedBox(height: 12),
Text(movie.title, textAlign: TextAlign.center, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: c, letterSpacing: 1)),
if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 4),
Text(movie.alternateTitles.join(' / '), textAlign: TextAlign.center, style: TextStyle(fontSize: 11, color: c.withValues(alpha: 0.4))),
],
])),
_dashedLine(c.withValues(alpha: 0.15)),
Padding(padding: const EdgeInsets.fromLTRB(20, 14, 20, 16), child: Column(children: [
_classicRow('DIRECTED BY', movie.directors.isNotEmpty ? movie.directors.join(', ') : '--'),
const SizedBox(height: 10),
_classicRow('STARRING', movie.actors.isNotEmpty ? movie.actors.take(3).join(', ') : '--'),
const SizedBox(height: 10),
_classicRow('GENRE', movie.genres.isNotEmpty ? movie.genres.join(' / ') : '--'),
const SizedBox(height: 10),
_classicRow('RELEASE', movie.releaseDate != null ? _fmtDate(movie.releaseDate!) : '--'),
if (movie.rating != null && movie.rating! > 0) ...[
const SizedBox(height: 10),
_classicRow('RATING', '${movie.rating!.toStringAsFixed(1)} / 10'),
],
const SizedBox(height: 14),
Row(children: [
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(border: Border.all(color: c.withValues(alpha: 0.2), width: 0.5)),
child: Text(_statusEN(movie.status), style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, letterSpacing: 2, color: c.withValues(alpha: 0.5)))),
const Spacer(),
Icon(Icons.movie_outlined, size: 12, color: c.withValues(alpha: 0.3)),
const SizedBox(width: 4),
Text('MookNote', style: TextStyle(fontSize: 9, letterSpacing: 1, color: c.withValues(alpha: 0.3))),
]),
])),
]),
);
}
Widget _classicRow(String label, String value) {
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(width: 80, child: Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, letterSpacing: 1.5, color: const Color(0xFF2D2D2D).withValues(alpha: 0.35)))),
Expanded(child: Text(value, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF2D2D2D), height: 1.4))),
]);
}
// ═══════════════════════════════════════════════════════════
// 样式 2电影票参考票根.html
// ═══════════════════════════════════════════════════════════
Widget _buildTicketCinema() {
final movie = widget.movie;
const bg = Color(0xFFFFFDF7);
const text = Color(0xFF1A1A1A);
const muted = Color(0xFF999999);
return Container(
width: 340,
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 24, offset: const Offset(0, 6))]),
child: Column(mainAxisSize: MainAxisSize.min, children: [
// Header
Padding(padding: const EdgeInsets.fromLTRB(20, 16, 20, 14), child: Row(children: [
Image.asset('assets/images/ticket/maoyan.png', width: 36, height: 36, errorBuilder: (_, __, ___) => Icon(Icons.local_movies, size: 36, color: text)),
const SizedBox(width: 10),
Text(_platformCtrl.text.isNotEmpty ? _platformCtrl.text : 'MookNote', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: text, letterSpacing: 0.5)),
])),
Container(height: 0.5, color: const Color(0xFFF0EDE5)),
// Body
Padding(padding: const EdgeInsets.fromLTRB(20, 18, 20, 14), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
// 影院
Text(_cinemaCtrl.text, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: text, letterSpacing: 0.5)),
const SizedBox(height: 14),
// 片名
Text(movie.title, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: text, letterSpacing: 0.5, height: 1.3)),
const SizedBox(height: 4),
Text(_subtitleText(movie), style: const TextStyle(fontSize: 12, color: Color(0xFF666666), letterSpacing: 0.5)),
const SizedBox(height: 16),
// 场次
Row(children: [
Icon(Icons.desktop_windows_outlined, size: 15, color: text.withValues(alpha: 0.6)),
const SizedBox(width: 8),
Text(_hallCtrl.text, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: text.withValues(alpha: 0.8))),
]),
const SizedBox(height: 8),
Row(children: [
Text('${_seatRowCtrl.text}${_seatNumCtrl.text}', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: text.withValues(alpha: 0.8))),
Padding(padding: const EdgeInsets.symmetric(horizontal: 8), child: Text('|', style: TextStyle(color: const Color(0xFFD0CCC0), fontWeight: FontWeight.w300))),
Text(widget.movie.releaseDate != null ? '${widget.movie.releaseDate!.year}${widget.movie.releaseDate!.month}${widget.movie.releaseDate!.day}' : '待定', style: TextStyle(fontSize: 14, color: text.withValues(alpha: 0.6))),
]),
])),
// 撕票线
_tearScissors(bg),
// Footer
Padding(padding: const EdgeInsets.fromLTRB(20, 10, 20, 16), child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Container(width: 100, height: 100, decoration: BoxDecoration(border: Border.all(color: const Color(0xFFE8E4DA), width: 0.5)),
child: Image.asset('assets/images/ticket/qr.png', fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const SizedBox.shrink())),
const SizedBox(width: 16),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_ticketDetail('票价', _priceCtrl.text, isPrice: true),
_ticketDetail('服务费', _serviceFeeCtrl.text),
_ticketDetail('出票日期', _issueTimeCtrl.text),
_ticketDetail(_platformCtrl.text.isNotEmpty ? _platformCtrl.text : '平台', '已出票'),
const SizedBox(height: 4),
Text(_ticketCodeCtrl.text, style: TextStyle(fontFamily: 'monospace', fontSize: 9, letterSpacing: 0.8, color: muted)),
])),
])),
]),
);
}
Widget _ticketDetail(String label, String value, {bool isPrice = false}) {
return Padding(padding: const EdgeInsets.only(bottom: 4), child: Row(children: [
Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
const Spacer(),
Text(value, style: TextStyle(
fontSize: isPrice ? 16 : 12,
fontWeight: isPrice ? FontWeight.w700 : FontWeight.w500,
color: isPrice ? const Color(0xFFE5342F) : const Color(0xFF1A1A1A),
)),
]));
}
// ═══════════════════════════════════════════════════════════
// 样式 3收藏版票根参考票根收藏版.html
// ═══════════════════════════════════════════════════════════
Widget _buildTicketCollectible() {
final movie = widget.movie;
const red = Color(0xFFC0392B);
const text = Color(0xFF1A1A1A);
const light = Color(0xFF8C8C8C);
const line = Color(0xFFE8E2D8);
return Container(
width: 360,
decoration: BoxDecoration(color: const Color(0xFFFFFFFB),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 12, offset: const Offset(0, 2)),
BoxShadow(color: Colors.black.withValues(alpha: 0.10), blurRadius: 40, offset: const Offset(0, 8)),
]),
child: Column(mainAxisSize: MainAxisSize.min, children: [
// 红色头部
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
color: red,
child: Row(children: [
const Text('MookNote', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white, letterSpacing: 0.5)),
const Spacer(),
Text('收藏纪念\nCOLLECTIBLE', textAlign: TextAlign.right,
style: TextStyle(fontSize: 8, color: Colors.white.withValues(alpha: 0.7), letterSpacing: 0.5, height: 1.5)),
]),
),
// 主体
Padding(padding: const EdgeInsets.fromLTRB(24, 22, 24, 0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
// 影院
Row(children: [
Icon(Icons.location_on_outlined, size: 14, color: red),
const SizedBox(width: 4),
Text(_cinemaCtrl.text, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF6B6B6B), letterSpacing: 0.3)),
]),
const SizedBox(height: 16),
// 标题
Text(movie.title, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: text, letterSpacing: 0.5, height: 1.15)),
const SizedBox(height: 6),
// 标签
Wrap(spacing: 8, runSpacing: 6, children: [
if (movie.genres.isNotEmpty) ...movie.genres.take(3).map((g) => _tagChip(g)),
if (movie.releaseDate != null) _tagChip(_fmtDate(movie.releaseDate!)),
]),
const SizedBox(height: 20),
// 三列网格
Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: line, width: 0.5), bottom: BorderSide(color: line, width: 0.5))),
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(children: [
_gridCol('影厅', _hallCtrl.text, red),
Container(width: 0.5, height: 36, color: const Color(0xFFF2EDE5)),
_gridCol('座位', '${_seatRowCtrl.text}${_seatNumCtrl.text}', text),
Container(width: 0.5, height: 36, color: const Color(0xFFF2EDE5)),
_gridCol('上映', movie.releaseDate != null ? _fmtDate(movie.releaseDate!) : '--', red),
]),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 海报图片
if (hasPoster)
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
child: FadeInLocalImage(
path: movie.posterPath,
width: 320,
height: 200,
fit: BoxFit.cover,
),
),
])),
// 内容区域
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
movie.title,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: colors.onSurface,
),
),
// 撕票线(圆形缺口)
_tearCircles(),
// 别名
if (movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
movie.alternateTitles.join(' / '),
style: TextStyle(
fontSize: 14,
color: colors.onSurface.withValues(alpha: 0.6),
),
),
],
// 底部信息
Padding(padding: const EdgeInsets.fromLTRB(24, 10, 24, 0), child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Container(width: 120, height: 120, decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFE8E2D8), width: 0.5)),
child: Image.asset('assets/images/ticket/qr.png', fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const SizedBox.shrink())),
const SizedBox(width: 16),
Expanded(child: Column(children: [
_collectInfo('票价', '¥${_priceCtrl.text}', isPrice: true),
_collectInfo('服务费', '¥${_serviceFeeCtrl.text}'),
_collectInfo('时间', _issueTimeCtrl.text),
_collectInfo('平台', 'MookNote'),
_collectInfo('编码', _ticketCodeCtrl.text, isCode: true),
])),
])),
const SizedBox(height: 16),
// 评分
if (movie.rating != null && movie.rating! > 0) ...[
Row(
children: [
const Icon(
Icons.star,
size: 18,
color: Color(0xFFFFB800),
),
const SizedBox(width: 4),
Text(
movie.rating!.toStringAsFixed(1),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFFFFB800),
),
),
const SizedBox(width: 4),
Text(
'/ 10',
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
],
),
const SizedBox(height: 12),
],
// 导演
if (movie.directors.isNotEmpty)
_buildInfoRow('导演', movie.directors.join(' / '), colors),
// 编剧
if (movie.writers.isNotEmpty)
_buildInfoRow('编剧', movie.writers.join(' / '), colors),
// 主演
if (movie.actors.isNotEmpty)
_buildInfoRow('主演', movie.actors.take(3).join(' / '), colors),
// 类型
if (movie.genres.isNotEmpty)
_buildInfoRow('类型', movie.genres.join(' / '), colors),
// 上映日期
if (movie.releaseDate != null)
_buildInfoRow(
'上映',
'${movie.releaseDate!.year}.${movie.releaseDate!.month.toString().padLeft(2, '0')}.${movie.releaseDate!.day.toString().padLeft(2, '0')}',
colors,
),
const SizedBox(height: 16),
// 简介
if (movie.summary != null && movie.summary!.isNotEmpty) ...[
Text(
'简介',
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(height: 8),
Text(
movie.summary!,
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.6),
height: 1.6,
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 20),
// 底部标识
Divider(height: 1, color: colors.outline),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.movie_outlined,
size: 14,
color: colors.onSurface.withValues(alpha: 0.5),
),
const SizedBox(width: 6),
Text(
'来自 MookNote',
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withValues(alpha: 0.5),
),
),
],
),
],
),
),
],
),
// 底部条
Padding(padding: const EdgeInsets.fromLTRB(24, 12, 24, 14), child: Row(children: [
Text('MOOKNOTE MOVIE · TICKET', style: TextStyle(fontSize: 9, letterSpacing: 1, color: light.withValues(alpha: 0.7))),
const Spacer(),
Text(_ticketCodeCtrl.text, style: TextStyle(fontSize: 9, letterSpacing: 0.5, color: light.withValues(alpha: 0.7))),
])),
]),
);
}
/// 构建信息行
Widget _buildInfoRow(String label, String value, ColorScheme colors) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label',
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.75),
),
),
),
],
),
);
Widget _tagChip(String text) {
return Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFEBE5D9), width: 0.5), borderRadius: BorderRadius.circular(3)),
child: Text(text, style: const TextStyle(fontSize: 10, color: Color(0xFF6B6B6B), letterSpacing: 0.5)));
}
/// 生成并分享海报
Widget _gridCol(String label, String value, Color valueColor) {
return Expanded(child: Column(children: [
Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFF8C8C8C), letterSpacing: 1)),
const SizedBox(height: 4),
Text(value, textAlign: TextAlign.center, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: valueColor, letterSpacing: 0.5)),
]));
}
Widget _collectInfo(String label, String value, {bool isPrice = false, bool isCode = false}) {
return Padding(padding: const EdgeInsets.only(bottom: 3), child: Row(children: [
SizedBox(width: 32, child: Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFF8C8C8C), letterSpacing: 0.5))),
Expanded(child: Text(value, textAlign: TextAlign.right, style: TextStyle(
fontSize: isPrice ? 15 : 10,
fontWeight: isPrice ? FontWeight.w700 : FontWeight.w500,
color: isPrice ? const Color(0xFFC0392B) : const Color(0xFF1A1A1A).withValues(alpha: 0.8),
fontFamily: isCode ? 'monospace' : null,
letterSpacing: isCode ? 0.5 : 0,
))),
]));
}
// ─── 通用组件 ───
Widget _dashedLine(Color color) {
return Padding(padding: const EdgeInsets.symmetric(horizontal: 8),
child: CustomPaint(size: const Size(double.infinity, 1), painter: _DashedLinePainter(color: color)));
}
Widget _tearScissors(Color bg) {
return SizedBox(height: 28, child: Stack(children: [
Positioned(left: 20, right: 20, top: 13, child: CustomPaint(
size: const Size(double.infinity, 1), painter: _DashedLinePainter(color: const Color(0xFFC8C4B8), dashWidth: 6, dashSpace: 8))),
Positioned(left: 2, top: 4, child: Icon(Icons.content_cut, size: 16, color: const Color(0xFF999999))),
]));
}
Widget _tearCircles() {
return Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: SizedBox(height: 24, child: Stack(children: [
Positioned(left: 0, right: 0, top: 11, child: CustomPaint(
size: const Size(double.infinity, 1), painter: _DashedLinePainter(color: const Color(0xFFD8D0C0), dashWidth: 9, dashSpace: 9))),
Positioned(left: -30, top: 5.5, child: Container(width: 13, height: 13, decoration: BoxDecoration(color: const Color(0xFFEBE5DB), shape: BoxShape.circle))),
Positioned(right: -30, top: 5.5, child: Container(width: 13, height: 13, decoration: BoxDecoration(color: const Color(0xFFEBE5DB), shape: BoxShape.circle))),
])));
}
String _fmtDate(DateTime d) => '${d.year}.${d.month.toString().padLeft(2, '0')}.${d.day.toString().padLeft(2, '0')}';
String _subtitleText(Movie m) {
final parts = <String>[];
if (m.genres.isNotEmpty) parts.add(m.genres.join(' / '));
return parts.join(' · ');
}
String _statusEN(String s) {
switch (s) { case 'watched': return 'ADMITTED'; case 'watching': return 'NOW PLAYING'; default: return 'RESERVED'; }
}
// ─── 生成分享 ───
Future<void> _generateAndShare() async {
setState(() => _isGenerating = true);
try {
// 捕获海报图片
final boundary = _posterKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) {
throw Exception('无法获取海报边界');
}
final boundary = _posterKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
if (boundary == null) throw Exception('无法获取海报边界');
final image = await boundary.toImage(pixelRatio: 3.0);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
throw Exception('无法生成图片数据');
}
final bytes = byteData.buffer.asUint8List();
// 保存到临时文件
if (byteData == null) throw Exception('无法生成图片数据');
final tempDir = await getTemporaryDirectory();
final fileName = 'movie_poster_${DateTime.now().millisecondsSinceEpoch}.png';
final file = File('${tempDir.path}/$fileName');
await file.writeAsBytes(bytes);
// 分享
await Share.shareXFiles(
[XFile(file.path)],
text: '分享影视:${widget.movie.title}',
);
final file = File('${tempDir.path}/movie_poster_${DateTime.now().millisecondsSinceEpoch}.png');
await file.writeAsBytes(byteData.buffer.asUint8List());
await Share.shareXFiles([XFile(file.path)], text: '分享影视:${widget.movie.title}');
} catch (e) {
if (mounted) {
ToastUtil.show(context, '生成海报失败:$e');
}
if (mounted) ToastUtil.show(context, '生成海报失败:$e');
} finally {
if (mounted) {
setState(() => _isGenerating = false);
}
if (mounted) setState(() => _isGenerating = false);
}
}
}
class _DashedLinePainter extends CustomPainter {
final Color color;
final double dashWidth;
final double dashSpace;
_DashedLinePainter({required this.color, this.dashWidth = 4, this.dashSpace = 4});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color..strokeWidth = 1..style = PaintingStyle.stroke;
double x = 0;
while (x < size.width) { canvas.drawLine(Offset(x, 0), Offset(x + dashWidth, 0), paint); x += dashWidth + dashSpace; }
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
/// 阅读状态选择栏 - 水滴滑动动画
/// 阅读状态选择栏 - 交叉渐变动画
class BookStatusBar extends StatelessWidget {
const BookStatusBar({super.key});
@@ -15,9 +15,7 @@ class BookStatusBar extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: colors.surface,
border: Border(
bottom: BorderSide(color: colors.outline, width: 0.5),
),
border: Border(bottom: BorderSide(color: colors.outline, width: 0.5)),
),
child: Container(
padding: const EdgeInsets.all(4),
@@ -25,66 +23,13 @@ class BookStatusBar extends StatelessWidget {
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(24),
),
child: LayoutBuilder(
builder: (context, constraints) {
final tabWidth = constraints.maxWidth / 3;
return SizedBox(
height: 40,
child: Stack(
children: [
AnimatedPositioned(
duration: const Duration(milliseconds: 800),
curve: Curves.elasticOut,
left: provider.bookStatusIndex * tabWidth,
top: 0,
bottom: 0,
width: tabWidth,
child: Padding(
padding: const EdgeInsets.all(3),
child: Container(
decoration: BoxDecoration(
color: colors.primary,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
),
),
),
Row(
children: [
_buildTab(
colors: colors,
label: '已读',
icon: Icons.check_circle_outline,
isSelected: provider.bookStatusIndex == 0,
onTap: () => provider.setBookStatusIndex(0),
),
_buildTab(
colors: colors,
label: '在读',
icon: Icons.menu_book_outlined,
isSelected: provider.bookStatusIndex == 1,
onTap: () => provider.setBookStatusIndex(1),
),
_buildTab(
colors: colors,
label: '想读',
icon: Icons.bookmark_outlined,
isSelected: provider.bookStatusIndex == 2,
onTap: () => provider.setBookStatusIndex(2),
),
],
),
],
),
);
},
child: SizedBox(
height: 40,
child: Row(children: [
_buildTab(colors, '已读', Icons.check_circle_outline, provider.bookStatusIndex == 0, () => provider.setBookStatusIndex(0)),
_buildTab(colors, '在读', Icons.menu_book_outlined, provider.bookStatusIndex == 1, () => provider.setBookStatusIndex(1)),
_buildTab(colors, '想读', Icons.bookmark_outlined, provider.bookStatusIndex == 2, () => provider.setBookStatusIndex(2)),
]),
),
),
);
@@ -92,39 +37,45 @@ class BookStatusBar extends StatelessWidget {
);
}
Widget _buildTab({
required ColorScheme colors,
required String label,
required IconData icon,
required bool isSelected,
required VoidCallback onTap,
}) {
Widget _buildTab(ColorScheme colors, String label, IconData icon, bool isSelected, VoidCallback onTap) {
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: double.infinity,
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
margin: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: isSelected ? colors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(20),
boxShadow: isSelected ? [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2))] : null,
),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
child: Icon(icon, key: ValueKey(isSelected), size: 16,
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6)),
),
),
],
const SizedBox(width: 6),
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
child: Text(label, key: ValueKey('$label$isSelected')),
),
],
),
),
),
),

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
/// 观影状态选择栏 - 水滴滑动动画
/// 观影状态选择栏 - 交叉渐变动画
class MovieStatusBar extends StatelessWidget {
const MovieStatusBar({super.key});
@@ -15,9 +15,7 @@ class MovieStatusBar extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: colors.surface,
border: Border(
bottom: BorderSide(color: colors.outline, width: 0.5),
),
border: Border(bottom: BorderSide(color: colors.outline, width: 0.5)),
),
child: Container(
padding: const EdgeInsets.all(4),
@@ -25,66 +23,13 @@ class MovieStatusBar extends StatelessWidget {
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(24),
),
child: LayoutBuilder(
builder: (context, constraints) {
final tabWidth = constraints.maxWidth / 3;
return SizedBox(
height: 40,
child: Stack(
children: [
AnimatedPositioned(
duration: const Duration(milliseconds: 800),
curve: Curves.elasticOut,
left: provider.movieStatusIndex * tabWidth,
top: 0,
bottom: 0,
width: tabWidth,
child: Padding(
padding: const EdgeInsets.all(3),
child: Container(
decoration: BoxDecoration(
color: colors.primary,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
),
),
),
Row(
children: [
_buildTab(
colors: colors,
label: '已看',
icon: Icons.check_circle_outline,
isSelected: provider.movieStatusIndex == 0,
onTap: () => provider.setMovieStatusIndex(0),
),
_buildTab(
colors: colors,
label: '在看',
icon: Icons.play_circle_outline,
isSelected: provider.movieStatusIndex == 1,
onTap: () => provider.setMovieStatusIndex(1),
),
_buildTab(
colors: colors,
label: '想看',
icon: Icons.bookmark_outline,
isSelected: provider.movieStatusIndex == 2,
onTap: () => provider.setMovieStatusIndex(2),
),
],
),
],
),
);
},
child: SizedBox(
height: 40,
child: Row(children: [
_buildTab(colors, '已看', Icons.check_circle_outline, provider.movieStatusIndex == 0, () => provider.setMovieStatusIndex(0)),
_buildTab(colors, '在看', Icons.play_circle_outline, provider.movieStatusIndex == 1, () => provider.setMovieStatusIndex(1)),
_buildTab(colors, '想看', Icons.bookmark_outline, provider.movieStatusIndex == 2, () => provider.setMovieStatusIndex(2)),
]),
),
),
);
@@ -92,39 +37,45 @@ class MovieStatusBar extends StatelessWidget {
);
}
Widget _buildTab({
required ColorScheme colors,
required String label,
required IconData icon,
required bool isSelected,
required VoidCallback onTap,
}) {
Widget _buildTab(ColorScheme colors, String label, IconData icon, bool isSelected, VoidCallback onTap) {
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: double.infinity,
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
margin: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: isSelected ? colors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(20),
boxShadow: isSelected ? [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2))] : null,
),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
child: Icon(icon, key: ValueKey(isSelected), size: 16,
color: isSelected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6)),
),
),
],
const SizedBox(width: 6),
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
child: Text(label, key: ValueKey('$label$isSelected')),
),
],
),
),
),
),

View File

@@ -6,7 +6,7 @@ packages:
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.6.1"
args:
@@ -14,7 +14,7 @@ packages:
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async:
@@ -22,7 +22,7 @@ packages:
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.13.1"
boolean_selector:
@@ -30,7 +30,7 @@ packages:
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
characters:
@@ -38,7 +38,7 @@ packages:
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
checked_yaml:
@@ -46,7 +46,7 @@ packages:
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.4"
cli_util:
@@ -54,7 +54,7 @@ packages:
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.4.2"
clock:
@@ -62,7 +62,7 @@ packages:
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
code_assets:
@@ -70,7 +70,7 @@ packages:
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
collection:
@@ -78,7 +78,7 @@ packages:
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
cross_file:
@@ -86,7 +86,7 @@ packages:
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.5+2"
crypto:
@@ -94,7 +94,7 @@ packages:
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.7"
cupertino_icons:
@@ -102,7 +102,7 @@ packages:
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.9"
equatable:
@@ -110,7 +110,7 @@ packages:
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.8"
extended_text_field:
@@ -118,7 +118,7 @@ packages:
description:
name: extended_text_field
sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "16.0.2"
extended_text_library:
@@ -126,7 +126,7 @@ packages:
description:
name: extended_text_library
sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "12.0.1"
fake_async:
@@ -134,7 +134,7 @@ packages:
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.3"
ffi:
@@ -142,7 +142,7 @@ packages:
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
file:
@@ -150,7 +150,7 @@ packages:
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
file_picker:
@@ -158,7 +158,7 @@ packages:
description:
name: file_picker
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.3.7"
file_selector_linux:
@@ -166,7 +166,7 @@ packages:
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.4"
file_selector_macos:
@@ -174,7 +174,7 @@ packages:
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
@@ -182,7 +182,7 @@ packages:
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
file_selector_windows:
@@ -190,7 +190,7 @@ packages:
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.3+5"
fixnum:
@@ -198,7 +198,7 @@ packages:
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
fl_chart:
@@ -206,7 +206,7 @@ packages:
description:
name: fl_chart
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.69.2"
flutter:
@@ -219,7 +219,7 @@ packages:
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.13.1"
flutter_lints:
@@ -227,7 +227,7 @@ packages:
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
flutter_localizations:
@@ -240,7 +240,7 @@ packages:
description:
name: flutter_markdown_plus
sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.7"
flutter_plugin_android_lifecycle:
@@ -248,7 +248,7 @@ packages:
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.35"
flutter_staggered_grid_view:
@@ -256,7 +256,7 @@ packages:
description:
name: flutter_staggered_grid_view
sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.0"
flutter_test:
@@ -274,7 +274,7 @@ packages:
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
http:
@@ -282,7 +282,7 @@ packages:
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.6.0"
http_parser:
@@ -290,7 +290,7 @@ packages:
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
image:
@@ -298,7 +298,7 @@ packages:
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.3.0"
image_picker:
@@ -306,7 +306,7 @@ packages:
description:
name: image_picker
sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
image_picker_android:
@@ -314,7 +314,7 @@ packages:
description:
name: image_picker_android
sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.8.13+17"
image_picker_for_web:
@@ -322,7 +322,7 @@ packages:
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.1"
image_picker_ios:
@@ -330,7 +330,7 @@ packages:
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.8.13+6"
image_picker_linux:
@@ -338,7 +338,7 @@ packages:
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
image_picker_macos:
@@ -346,7 +346,7 @@ packages:
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
@@ -354,7 +354,7 @@ packages:
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.1"
image_picker_windows:
@@ -362,7 +362,7 @@ packages:
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
intl:
@@ -370,7 +370,7 @@ packages:
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.20.2"
jni:
@@ -378,7 +378,7 @@ packages:
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
jni_flutter:
@@ -386,7 +386,7 @@ packages:
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.1"
json_annotation:
@@ -394,7 +394,7 @@ packages:
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.12.0"
leak_tracker:
@@ -402,7 +402,7 @@ packages:
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
@@ -410,7 +410,7 @@ packages:
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.10"
leak_tracker_testing:
@@ -418,7 +418,7 @@ packages:
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
lints:
@@ -426,7 +426,7 @@ packages:
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
logging:
@@ -434,7 +434,7 @@ packages:
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
markdown:
@@ -442,7 +442,7 @@ packages:
description:
name: markdown
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.3.1"
matcher:
@@ -450,7 +450,7 @@ packages:
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.19"
material_color_utilities:
@@ -458,7 +458,7 @@ packages:
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.13.0"
meta:
@@ -466,7 +466,7 @@ packages:
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.17.0"
mime:
@@ -474,7 +474,7 @@ packages:
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
nested:
@@ -482,7 +482,7 @@ packages:
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
objective_c:
@@ -490,7 +490,7 @@ packages:
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.4.1"
package_config:
@@ -498,7 +498,7 @@ packages:
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
package_info_plus:
@@ -506,7 +506,7 @@ packages:
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.3.1"
package_info_plus_platform_interface:
@@ -514,7 +514,7 @@ packages:
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.1"
path:
@@ -522,7 +522,7 @@ packages:
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
path_provider:
@@ -530,7 +530,7 @@ packages:
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.6"
path_provider_android:
@@ -538,7 +538,7 @@ packages:
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.1"
path_provider_foundation:
@@ -546,7 +546,7 @@ packages:
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.0"
path_provider_linux:
@@ -554,7 +554,7 @@ packages:
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
@@ -562,7 +562,7 @@ packages:
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
path_provider_windows:
@@ -570,7 +570,7 @@ packages:
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
permission_handler:
@@ -578,7 +578,7 @@ packages:
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.4.0"
permission_handler_android:
@@ -586,7 +586,7 @@ packages:
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "12.1.0"
permission_handler_apple:
@@ -594,7 +594,7 @@ packages:
description:
name: permission_handler_apple
sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.4.10"
permission_handler_html:
@@ -602,7 +602,7 @@ packages:
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
@@ -610,7 +610,7 @@ packages:
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.3.0"
permission_handler_windows:
@@ -618,7 +618,7 @@ packages:
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
petitparser:
@@ -626,7 +626,7 @@ packages:
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.2"
platform:
@@ -634,7 +634,7 @@ packages:
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
@@ -642,7 +642,7 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
provider:
@@ -650,7 +650,7 @@ packages:
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.5+1"
pub_semver:
@@ -658,7 +658,7 @@ packages:
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
record_use:
@@ -666,7 +666,7 @@ packages:
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.0"
share_plus:
@@ -674,7 +674,7 @@ packages:
description:
name: share_plus
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.1.4"
share_plus_platform_interface:
@@ -682,7 +682,7 @@ packages:
description:
name: share_plus_platform_interface
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.2"
shared_preferences:
@@ -690,7 +690,7 @@ packages:
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.5"
shared_preferences_android:
@@ -698,7 +698,7 @@ packages:
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
@@ -706,7 +706,7 @@ packages:
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.6"
shared_preferences_linux:
@@ -714,7 +714,7 @@ packages:
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
@@ -722,7 +722,7 @@ packages:
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
shared_preferences_web:
@@ -730,7 +730,7 @@ packages:
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.3"
shared_preferences_windows:
@@ -738,7 +738,7 @@ packages:
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
sky_engine:
@@ -751,7 +751,7 @@ packages:
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.2"
sqflite:
@@ -759,7 +759,7 @@ packages:
description:
name: sqflite
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2+1"
sqflite_android:
@@ -767,7 +767,7 @@ packages:
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2+3"
sqflite_common:
@@ -775,7 +775,7 @@ packages:
description:
name: sqflite_common
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.8"
sqflite_darwin:
@@ -783,7 +783,7 @@ packages:
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
@@ -791,7 +791,7 @@ packages:
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
stack_trace:
@@ -799,7 +799,7 @@ packages:
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.1"
stream_channel:
@@ -807,7 +807,7 @@ packages:
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
string_scanner:
@@ -815,7 +815,7 @@ packages:
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
synchronized:
@@ -823,7 +823,7 @@ packages:
description:
name: synchronized
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.0+1"
term_glyph:
@@ -831,7 +831,7 @@ packages:
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
test_api:
@@ -839,7 +839,7 @@ packages:
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.10"
typed_data:
@@ -847,7 +847,7 @@ packages:
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
url_launcher:
@@ -855,7 +855,7 @@ packages:
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.3.2"
url_launcher_android:
@@ -863,7 +863,7 @@ packages:
description:
name: url_launcher_android
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.3.30"
url_launcher_ios:
@@ -871,7 +871,7 @@ packages:
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.4.1"
url_launcher_linux:
@@ -879,7 +879,7 @@ packages:
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.2"
url_launcher_macos:
@@ -887,7 +887,7 @@ packages:
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
@@ -895,7 +895,7 @@ packages:
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
url_launcher_web:
@@ -903,7 +903,7 @@ packages:
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.3"
url_launcher_windows:
@@ -911,7 +911,7 @@ packages:
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.5"
uuid:
@@ -919,7 +919,7 @@ packages:
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.5.3"
vector_math:
@@ -927,7 +927,7 @@ packages:
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
vm_service:
@@ -935,7 +935,7 @@ packages:
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "15.2.0"
web:
@@ -943,7 +943,7 @@ packages:
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
webview_flutter:
@@ -951,7 +951,7 @@ packages:
description:
name: webview_flutter
sha256: e4d3474277c5043f5f3100ed27d94ad693abfd5c602b0221c719a4497e4ba1e4
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.14.0"
webview_flutter_android:
@@ -959,7 +959,7 @@ packages:
description:
name: webview_flutter_android
sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.12.0"
webview_flutter_platform_interface:
@@ -967,7 +967,7 @@ packages:
description:
name: webview_flutter_platform_interface
sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.15.1"
webview_flutter_wkwebview:
@@ -975,7 +975,7 @@ packages:
description:
name: webview_flutter_wkwebview
sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.25.1"
win32:
@@ -983,7 +983,7 @@ packages:
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.15.0"
xdg_directories:
@@ -991,7 +991,7 @@ packages:
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
xml:
@@ -999,7 +999,7 @@ packages:
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.6.1"
yaml:
@@ -1007,7 +1007,7 @@ packages:
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
sdks:

View File

@@ -53,3 +53,4 @@ flutter:
assets:
- assets/images/
- assets/icon/
- assets/images/ticket/

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猫眼电影 · 票根</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Noto+Serif+SC:wght@400;600;700&display=swap');
:root {
--maoyan-red: #E5342F;
--maoyan-red-dark: #C41F1A;
--ticket-bg: #FFFDF7;
--text-primary: #1A1A1A;
--text-secondary: #666666;
--text-muted: #999999;
--divider: #E8E4DA;
--bg-dark: #1E1610;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #F5F0E6;
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
padding: 20px;
}
/* ── Ticket Wrapper ── */
.ticket-wrapper {
position: relative;
z-index: 1;
width: 100%;
max-width: 400px;
filter: drop-shadow(0 4px 24px rgba(0,0,0,0.12));
transition: transform 0.3s ease;
animation: ticketEntrance 0.6s cubic-bezier(0.34,1.56,0.64,1) forwards;
}
.ticket-wrapper:hover {
transform: translateY(-4px);
}
@keyframes ticketEntrance {
0% { opacity: 0; transform: translateY(30px) scale(0.96); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
/* ── Main Ticket ── */
.ticket {
background: var(--ticket-bg);
border-radius: 12px;
overflow: hidden;
position: relative;
box-shadow: 0 2px 0 0 rgba(255,255,255,0.06) inset;
}
/* Subtle print texture */
.ticket::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(0,0,0,0.003) 1px,
rgba(0,0,0,0.003) 2px
);
border-radius: 12px;
z-index: 10;
}
/* ── Header (票头) ── */
.ticket-header {
padding: 18px 24px 16px;
display: flex;
align-items: center;
gap: 14px;
position: relative;
border-bottom: 1px solid #F0EDE5;
}
.maoyan-logo {
width: 42px; height: 42px;
flex-shrink: 0;
position: relative; z-index: 1;
}
.logo-text {
font-size: 24px; font-weight: 900;
color: #1A1A1A;
letter-spacing: 0.06em;
position: relative; z-index: 1;
}
/* ── Body (票身) ── */
.ticket-body {
padding: 22px 24px 18px;
position: relative;
}
/* Cinema */
.cinema-info {
margin-bottom: 20px;
padding-bottom: 0;
border-bottom: none;
}
.cinema-name {
font-size: 15px; font-weight: 600;
color: var(--text-primary);
letter-spacing: 0.02em;
margin-bottom: 4px;
}
.cinema-address {
font-size: 12px; color: var(--text-muted);
line-height: 1.5;
}
/* Movie title */
.movie-title-section {
margin-bottom: 18px;
}
.movie-title {
font-family: 'Noto Serif SC', 'STSong', 'SimSun', serif;
font-size: 28px; font-weight: 700;
color: var(--text-primary);
line-height: 1.3; letter-spacing: 0.04em;
}
.movie-subtitle {
font-size: 13px; color: var(--text-secondary);
margin-top: 3px; letter-spacing: 0.03em;
}
/* Hall & seat */
.session-info {
margin-bottom: 10px;
}
.hall-badge {
display: inline-flex;
align-items: center; gap: 6px;
color: var(--text-primary);
font-size: 14px; font-weight: 500;
letter-spacing: 0.02em;
}
.hall-badge svg {
width: 16px; height: 16px;
flex-shrink: 0;
}
.seat-datetime {
font-size: 14px; color: var(--text-primary);
font-weight: 500;
display: flex; align-items: center;
gap: 8px;
}
.separator {
color: #D0CCC0; font-weight: 300;
user-select: none;
}
.datetime-text {
color: var(--text-secondary); font-weight: 400;
}
/* ── Tear Line (撕票虚线) ── */
.tear-line {
position: relative;
height: 32px;
display: flex;
align-items: center;
margin: 0 12px;
overflow: visible;
}
.tear-line::before {
content: '';
position: absolute;
left: 18px; right: 18px;
top: 50%; transform: translateY(-50%);
height: 1px;
background: repeating-linear-gradient(
to right,
#C8C4B8 0px, #C8C4B8 6px,
transparent 6px, transparent 14px
);
}
.scissors-icon {
position: absolute;
left: 0; top: 50%;
transform: translateY(-50%);
font-size: 14px; color: #B8B4A8;
z-index: 1;
background: var(--ticket-bg);
padding-right: 6px;
user-select: none;
}
/* ── Footer (票尾) ── */
.ticket-footer {
padding: 10px 24px 16px;
display: flex;
gap: 18px;
align-items: stretch;
}
/* QR code */
.qr-section {
flex-shrink: 0;
display: flex;
align-items: stretch;
}
.qr-code {
width: 130px;
}
.qr-code img {
width: 100%;
display: block;
}
.qr-label {
font-size: 10px; color: var(--text-muted);
letter-spacing: 0.04em;
}
/* Detail rows */
.details-section {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
justify-content: center;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 13px;
}
.detail-label {
color: var(--text-muted);
flex-shrink: 0;
margin-right: 8px;
font-size: 12px;
}
.detail-value {
color: var(--text-primary);
font-weight: 500;
text-align: right;
white-space: nowrap;
}
.price-value {
font-size: 18px; font-weight: 700;
color: var(--maoyan-red);
}
.price-symbol {
font-size: 12px; font-weight: 500;
}
.service-fee {
font-size: 14px; font-weight: 600;
color: var(--text-primary);
}
.platform-badge {
display: inline-flex;
align-items: center;
gap: 4px;
justify-content: flex-end;
}
.platform-text {
font-size: 11px; color: var(--text-muted);
letter-spacing: 0.05em; font-weight: 500;
}
/* ── Responsive ── */
@media (max-width: 440px) {
.ticket-wrapper { max-width: 100%; }
.ticket-header { padding: 14px 18px 12px; }
.ticket-body { padding: 16px 18px 14px; }
.ticket-footer { padding: 10px 18px 14px; gap: 12px; }
.movie-title { font-size: 24px; }
.qr-code { width: 110px; }
}
</style>
</head>
<body>
<div class="ticket-wrapper">
<div class="ticket">
<!-- ═══════════ HEADER ═══════════ -->
<div class="ticket-header">
<div class="maoyan-logo">
<img src="maoyan.png" alt="猫眼电影" style="width:100%;height:100%;object-fit:contain;">
</div>
<div class="logo-text">猫眼电影</div>
</div>
<!-- ═══════════ BODY ═══════════ -->
<div class="ticket-body">
<div class="cinema-info">
<div class="cinema-name">万达影城朝阳大悦城IMAX店</div>
</div>
<div class="movie-title-section">
<div class="movie-title">流浪地球3</div>
<div class="movie-subtitle">科幻 / 冒险 / 3D IMAX · 片长148分钟</div>
</div>
<div class="session-info">
<div class="hall-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="20" height="8" rx="1"/>
<path d="M4 10v10h16V10"/>
<path d="M6 14h4v6H6z"/><path d="M14 14h4v6h-4z"/>
</svg>
3号IMAX厅
</div>
</div>
<div class="seat-datetime">
<span>8排12座</span>
<span class="separator">|</span>
<span class="datetime-text">2026年6月18日 19:30</span>
</div>
</div>
<!-- ═══════════ TEAR LINE ═══════════ -->
<div class="tear-line">
<span class="scissors-icon"></span>
</div>
<!-- ═══════════ FOOTER ═══════════ -->
<div class="ticket-footer">
<div class="qr-section">
<div class="qr-code">
<img src="mookiletter.png"
alt="验票二维码" style="width:100%;height:100%;object-fit:contain;">
</div>
<span class="qr-label"></span>
</div>
<div class="details-section">
<div class="detail-row">
<span class="detail-label">票价</span>
<span class="detail-value price-value"><span class="price-symbol">¥</span>49.90</span>
</div>
<div class="detail-row">
<span class="detail-label">服务费</span>
<span class="detail-value service-fee">¥5.00</span>
</div>
<div class="detail-row">
<span class="detail-label">出票时间</span>
<span class="detail-value">2026-06-18 10:32:15</span>
</div>
<div class="detail-row">
<span class="detail-label">淘票票</span>
<span class="detail-value platform-badge">
<span class="platform-text">已出票</span>
</span>
</div>
<div class="detail-row">
<span class="detail-label">编码</span>
<span class="detail-value" style="font-family: monospace; letter-spacing:0.05em;">8602 3100 0618 7251</span>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,444 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猫眼电影 · 票根(收藏版)</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Noto+Serif+SC:wght@400;600;700&display=swap');
:root {
--red: #D4342E;
--gold: #C9A96E;
--bg: #F8F5F0;
--card: #FFFFFF;
--text: #1C1C1C;
--text-secondary: #6B6B6B;
--text-muted: #A0A0A0;
--border: #EBE5D9;
--divider: #EEE8DA;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(160deg, #EBE4D8 0%, #F2EDE4 40%, #EEF0EF 100%);
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
padding: 40px 20px;
}
/* Ambient */
body::before, body::after {
content: '';
position: fixed;
border-radius: 50%;
pointer-events: none;
z-index: 0;
opacity: 0.15;
}
body::before {
width: 420px; height: 420px;
background: radial-gradient(circle, #D4342E 0%, transparent 70%);
top: -120px; right: -120px;
}
body::after {
width: 300px; height: 300px;
background: radial-gradient(circle, #C9A96E 0%, transparent 70%);
bottom: -80px; left: -80px;
}
.card {
position: relative;
z-index: 1;
width: 400px;
background: var(--card);
border-radius: 16px;
box-shadow:
0 1px 0 rgba(0,0,0,0.03),
0 2px 8px rgba(0,0,0,0.04),
0 12px 40px rgba(0,0,0,0.10);
overflow: hidden;
animation: cardIn 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes cardIn {
0% { opacity: 0; transform: translateY(40px) scale(0.94); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
.card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 4px;
background: linear-gradient(90deg, var(--red) 0%, #E85D59 30%, var(--red) 60%, #C1272D 100%);
z-index: 2;
}
/* Header */
.header {
padding: 28px 28px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.header-left {
display: flex;
align-items: center;
gap: 10px;
}
.logo {
width: 34px; height: 34px;
flex-shrink: 0;
}
.logo img {
width: 100%; height: 100%;
object-fit: contain;
}
.brand {
font-size: 20px; font-weight: 800;
color: var(--text);
letter-spacing: 0.03em;
}
.header-right {
font-size: 10px; color: var(--text-muted);
text-align: right;
letter-spacing: 0.05em;
line-height: 1.6;
}
/* Body */
.body {
padding: 24px 28px 0;
}
.cinema {
font-size: 13px; font-weight: 500;
color: var(--text-secondary);
margin-bottom: 18px;
letter-spacing: 0.03em;
display: flex;
align-items: center;
gap: 6px;
}
.cinema svg {
width: 14px; height: 14px;
flex-shrink: 0;
color: var(--red);
}
.movie-title {
font-family: 'Noto Serif SC', 'STSong', 'SimSun', serif;
font-size: 34px; font-weight: 700;
color: var(--text);
line-height: 1.15;
letter-spacing: 0.06em;
margin-bottom: 4px;
}
.movie-meta-row {
font-size: 12px; color: var(--text-muted);
letter-spacing: 0.04em;
margin-bottom: 24px;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.movie-meta-row .tag {
display: inline-block;
padding: 3px 10px;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 11px;
color: var(--text-secondary);
letter-spacing: 0.04em;
}
/* Session grid */
.session-grid {
display: flex;
gap: 20px;
padding: 16px 0;
margin-bottom: 0;
}
.session-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.session-item .label {
font-size: 10px; color: var(--text-muted);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.session-item .value {
font-size: 13px; font-weight: 600;
color: var(--text);
letter-spacing: 0.03em;
white-space: nowrap;
}
.session-item .value.large {
font-size: 14px;
}
/* Tear line */
.tear-section {
padding: 0 28px;
}
.tear-line {
position: relative;
height: 28px;
display: flex;
align-items: center;
}
.tear-line::before {
content: '';
position: absolute;
left: 0; right: 0;
top: 50%;
height: 1px;
background: repeating-linear-gradient(
to right,
#D5CFC0 0px, #D5CFC0 7px,
transparent 7px, transparent 15px
);
}
.tear-dot {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
z-index: 1;
}
.tear-dot.left { left: -2px; }
.tear-dot.right { right: -2px; }
.tear-dot svg {
width: 12px; height: 12px;
color: #C5BFAE;
}
/* Footer */
.footer {
padding: 10px 28px 28px;
display: flex;
gap: 20px;
align-items: stretch;
}
.footer-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 7px;
min-width: 0;
justify-content: center;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 12px;
}
.info-row .key {
color: var(--text-muted);
font-size: 11px;
letter-spacing: 0.04em;
}
.info-row .val {
color: var(--text);
font-weight: 500;
}
.info-row .val.price {
font-size: 22px; font-weight: 700;
color: var(--red);
}
.info-row .val.code {
font-family: 'SF Mono', 'Cascadia Code', 'Courier New', monospace;
font-size: 11px; letter-spacing: 0.06em;
color: var(--text-secondary);
}
/* QR */
.qr-wrap {
flex-shrink: 0;
width: 150px;
display: flex;
align-items: stretch;
}
.qr-img {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
}
.qr-img img {
width: 100%;
display: block;
}
/* Bottom strip */
.footer-strip {
margin: 0 28px;
padding: 12px 0;
border-top: 1px solid var(--divider);
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-strip .left {
font-size: 10px; color: var(--text-muted);
letter-spacing: 0.05em;
}
.footer-strip .right {
font-size: 10px; color: var(--text-muted);
letter-spacing: 0.04em;
}
/* Print */
@media print {
@page { margin: 14mm; size: auto; }
body {
background: #FFF;
padding: 0;
}
body::before, body::after { display: none; }
.card {
box-shadow: none;
border: 1px solid #D8D3C5;
border-radius: 10px;
animation: none;
width: 360px;
margin: 0 auto;
}
.card::before { height: 3px; }
.header { padding: 22px 24px 0; }
.body { padding: 18px 24px 0; }
.tear-section { padding: 0 24px; }
.footer { padding: 8px 24px 24px; }
.footer-strip { margin: 0 24px; }
}
@media (max-width: 440px) {
.card { width: 100%; max-width: 400px; }
.header { padding: 22px 20px 0; }
.body { padding: 18px 20px 0; }
.tear-section { padding: 0 20px; }
.footer { padding: 8px 20px 24px; gap: 14px; }
.footer-strip { margin: 0 20px; }
.movie-title { font-size: 28px; }
.session-grid { gap: 14px; }
.qr-img { width: 120px; height: auto; }
}
</style>
</head>
<body>
<div class="card">
<div class="header">
<div class="header-left">
<div class="logo">
<img src="maoyan.png" alt="">
</div>
<span class="brand">猫眼电影</span>
</div>
<div class="header-right">收藏纪念<br>PASSENGER COPY</div>
</div>
<div class="body">
<div class="cinema">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/>
</svg>
万达影城朝阳大悦城IMAX店
</div>
<div class="movie-title">流浪地球3</div>
<div class="movie-meta-row">
<span class="tag">3D IMAX</span>
<span class="tag">科幻 / 冒险</span>
<span class="tag">片长148分钟</span>
</div>
<div class="session-grid">
<div class="session-item">
<span class="label">影厅</span>
<span class="value large">3号IMAX厅</span>
</div>
<div class="session-item">
<span class="label">座位</span>
<span class="value large">8排12座</span>
</div>
<div class="session-item">
<span class="label">时间</span>
<span class="value">2026/06/18 19:30</span>
</div>
</div>
</div>
<div class="tear-section">
<div class="tear-line">
<span class="tear-dot left">
<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10"/></svg>
</span>
<span class="tear-dot right">
<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10"/></svg>
</span>
</div>
</div>
<div class="footer">
<div class="qr-wrap">
<div class="qr-img">
<img src="mookiletter.png" alt="二维码">
</div>
</div>
<div class="footer-right">
<div class="info-row">
<span class="key">票价</span>
<span class="val price">¥49.90</span>
</div>
<div class="info-row">
<span class="key">服务费</span>
<span class="val">¥5.00</span>
</div>
<div class="divider-thin"></div>
<div class="info-row">
<span class="key">出票时间</span>
<span class="val">2026-06-18 10:32</span>
</div>
<div class="info-row">
<span class="key">淘票票</span>
<span class="val">已出票</span>
</div>
<div class="info-row">
<span class="key">编码</span>
<span class="val code">8602 3100 0618 7251</span>
</div>
</div>
</div>
<div class="footer-strip">
<span class="left">M AOYAN MOVIE · TICKET</span>
<span class="right">MY202606181930</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电影票根 · 收藏版</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Noto+Serif+SC:wght@400;600;700&family=DM+Serif+Display:ital@0;1&display=swap');
:root {
--bg: #FAF7F2;
--card: #FFFFFF;
--red: #C0392B;
--red-light: #E74C3C;
--text: #1A1A1A;
--text-mid: #555555;
--text-light: #8C8C8C;
--line: #E8E2D8;
--line-light: #F2EDE5;
--stamp: #E8D5C4;
--accent: #2C5F8A;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #EBE5DB;
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
padding: 40px 20px;
}
.ticket {
width: 420px;
background: #FFFFFB;
box-shadow:
0 0 0 1px rgba(0,0,0,0.04),
0 2px 12px rgba(0,0,0,0.05),
0 8px 40px rgba(0,0,0,0.10);
animation: in 0.65s cubic-bezier(0.22, 1, 0.36, 1) forwards;
position: relative;
}
@keyframes in {
0% { opacity: 0; transform: translateY(32px); }
100% { opacity: 1; transform: translateY(0); }
}
/* ── Red header strip ── */
.hdr {
background: var(--red);
padding: 14px 28px;
display: flex;
align-items: center;
gap: 10px;
}
.hdr-title {
font-size: 18px; font-weight: 700;
color: #FFF;
letter-spacing: 0.06em;
}
.hdr-sub {
margin-left: auto;
font-size: 9px; color: rgba(255,255,255,0.7);
text-align: right;
letter-spacing: 0.08em;
line-height: 1.4;
}
/* ── Main content ── */
.main {
padding: 26px 32px 0;
}
.cinema-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 22px;
}
.cinema-icon {
width: 18px; height: 18px;
flex-shrink: 0;
color: var(--red);
}
.cinema-name {
font-size: 13px; font-weight: 500;
color: var(--text-mid);
letter-spacing: 0.03em;
}
.film-title {
font-family: 'Noto Serif SC', 'STSong', 'SimSun', serif;
font-size: 38px; font-weight: 700;
color: var(--text);
line-height: 1.12;
letter-spacing: 0.06em;
margin-bottom: 8px;
}
.film-sub {
font-size: 12px; color: var(--text-light);
letter-spacing: 0.04em;
margin-bottom: 26px;
display: flex;
gap: 16px;
}
.film-sub span {
display: inline-block;
padding: 3px 0;
border-bottom: 1px solid var(--line);
}
/* ── Three-column session ── */
.sess {
display: flex;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
padding: 16px 0;
margin-bottom: 0;
}
.sess-col {
flex: 1;
text-align: center;
border-right: 1px solid var(--line-light);
padding: 4px 8px;
}
.sess-col:last-child {
border-right: none;
}
.sess-col .sess-label {
font-size: 9px; color: var(--text-light);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 6px;
display: block;
}
.sess-col .sess-val {
font-size: 15px; font-weight: 600;
color: var(--text);
letter-spacing: 0.03em;
white-space: nowrap;
}
.sess-col .sess-val.hi {
color: var(--red);
}
/* ── Tear ── */
.tear {
margin: 0 32px;
height: 28px;
position: relative;
display: flex;
align-items: center;
}
.tear::before {
content: '';
position: absolute;
left: 0; right: 0;
top: 50%;
height: 1px;
background: repeating-linear-gradient(
to right,
#D8D0C0 0px, #D8D0C0 9px,
transparent 9px, transparent 18px
);
}
.tear-circle {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 13px; height: 13px;
background: #EBE5DB;
border-radius: 50%;
}
.tear-circle.l { left: -38px; }
.tear-circle.r { right: -38px; }
/* ── Footer ── */
.ftr {
padding: 14px 32px 26px;
display: flex;
gap: 28px;
align-items: center;
}
.ftr-l {
flex-shrink: 0;
display: flex;
align-items: center;
}
.qr {
width: 150px;
height: 150px;
}
.qr img {
width: 100%; height: 100%;
display: block;
object-fit: contain;
}
.ftr-r {
flex: 1;
display: flex;
flex-direction: column;
gap: 5px;
min-width: 0;
}
.row {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.row .lbl {
font-size: 11px; color: var(--text-light);
letter-spacing: 0.04em;
}
.row .val {
font-size: 13px; color: var(--text);
font-weight: 500;
text-align: right;
}
.row .val.big {
font-size: 26px; font-weight: 700;
color: var(--red);
font-family: 'Noto Serif SC', serif;
}
.row .val.code {
font-family: 'SF Mono', 'Courier New', monospace;
font-size: 10px; letter-spacing: 0.06em;
color: var(--text-mid);
}
.split {
height: 1px;
background: var(--line);
margin: 4px 0;
}
.status {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 500;
}
.status::before {
content: '';
width: 6px; height: 6px;
background: #27AE60;
border-radius: 50%;
}
/* ── Ticket stub number ── */
.stub {
margin: 0 32px;
padding: 14px 0;
border-top: 1px solid var(--line);
display: flex;
justify-content: space-between;
}
.stub span {
font-family: 'SF Mono', 'Courier New', monospace;
font-size: 10px; color: var(--text-light);
letter-spacing: 0.08em;
}
/* ── Print ── */
@media print {
@page { margin: 12mm; size: auto; }
body { background: #FFF; padding: 0; }
.ticket {
box-shadow: none;
border: 1px solid #D0CBC0;
width: 370px;
margin: 0 auto;
animation: none;
}
.hdr { background: var(--red); -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.hdr-title, .hdr-sub { color: #FFF; }
.tear-circle { background: #FFF; }
}
@media (max-width: 460px) {
.ticket { width: 100%; }
.main, .ftr, .stub { padding-left: 20px; padding-right: 20px; }
.tear { margin: 0 20px; }
.stub { margin: 0 20px; }
.film-title { font-size: 32px; }
.qr { width: 120px; height: 120px; }
}
</style>
</head>
<body>
<div class="ticket">
<!-- Header -->
<div class="hdr">
<span class="hdr-title">猫眼电影</span>
<span class="hdr-sub">收藏纪念<br>COLLECTIBLE</span>
</div>
<!-- Main -->
<div class="main">
<div class="cinema-row">
<svg class="cinema-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/>
</svg>
<span class="cinema-name">万达影城朝阳大悦城IMAX店</span>
</div>
<div class="film-title">流浪地球3</div>
<div class="film-sub">
<span>3D IMAX</span>
<span>科幻 / 冒险</span>
<span>片长148分钟</span>
</div>
<div class="sess">
<div class="sess-col">
<span class="sess-label">影厅</span>
<span class="sess-val hi">3号IMAX厅</span>
</div>
<div class="sess-col">
<span class="sess-label">座位</span>
<span class="sess-val">8排12座</span>
</div>
<div class="sess-col">
<span class="sess-label">时间</span>
<span class="sess-val">2026.06.18 19:30</span>
</div>
</div>
</div>
<!-- Tear line -->
<div class="tear">
<span class="tear-circle l"></span>
<span class="tear-circle r"></span>
</div>
<!-- Footer -->
<div class="ftr">
<div class="ftr-l">
<div class="qr">
<img src="mookiletter.png" alt="二维码">
</div>
</div>
<div class="ftr-r">
<div class="row">
<span class="lbl">票价</span>
<span class="val big">¥49.90</span>
</div>
<div class="row">
<span class="lbl">服务费</span>
<span class="val">¥5.00</span>
</div>
<div class="split"></div>
<div class="row">
<span class="lbl">出票时间</span>
<span class="val">2026-06-18 10:32</span>
</div>
<div class="row">
<span class="lbl">淘票票</span>
<span class="val status">已出票</span>
</div>
<div class="row">
<span class="lbl">取票码</span>
<span class="val code">8602 3100 0618 7251</span>
</div>
</div>
</div>
<!-- Stub -->
<div class="stub">
<span>TICKET · COLLECTIBLE</span>
<span>NO. MY202606181930</span>
</div>
</div>
</body>
</html>