a274cc40c8
连接页额度卡头部一行放不下「今日剩余 X 分钟 + 免费版·每日10分钟」时,此前 Flexible 落在标签上,导致「今日剩余」被挤成「今日…」。改为:标签固定绝不省略,数值作最后收缩项; 右上角标签缩短为「免费版」(每日额度由卡片本身「今日剩余 X 分钟」表达,不再挤进胶囊)。 新增 planFreeTag l10n(zh 免费版 / en Free),quotaFree 仍保留供账户页 plan_badge_card。 golden 基线(quota_low/exhausted)在 Linux 权威环境重生成。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
167 lines
6.2 KiB
Dart
167 lines
6.2 KiB
Dart
// quota_card.dart — 免费版每日额度卡
|
|
//
|
|
// 三态展示(额度全账户共享):
|
|
// ① 连接中(countdown != null):显示剩余倒计时 mm:ss + 进度条随之收缩。
|
|
// ② 未连接·有余额:显示今日剩余分钟 + 进度条;移动端附「看广告加时」。
|
|
// ③ 未连接·已耗尽:显示「今日已用完」+ 移动端「看广告加时」/ 桌面「升级会员」。
|
|
// 状态由 quota_provider(剩余)+ connection_provider(倒计时)注入,本地仅展示。
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../l10n/app_text.dart';
|
|
import '../pangolin_theme.dart';
|
|
import '../state/quota_provider.dart';
|
|
import 'pangolin_icons.dart';
|
|
|
|
class QuotaCard extends StatelessWidget {
|
|
const QuotaCard({
|
|
super.key,
|
|
required this.quota,
|
|
required this.t,
|
|
required this.onWatchAd,
|
|
this.countdown,
|
|
this.isDesktop = false,
|
|
});
|
|
|
|
final FreeQuotaState quota;
|
|
final AppText t;
|
|
|
|
/// 额度耗尽时的加时入口(移动端占位广告 / 桌面升级提示)。
|
|
final VoidCallback onWatchAd;
|
|
|
|
/// 连接期剩余倒计时;null = 未连接(展示今日剩余分钟)。
|
|
final Duration? countdown;
|
|
|
|
/// 桌面端(Windows/macOS):免费硬 10 分钟不可延,不显示看广告按钮。
|
|
final bool isDesktop;
|
|
|
|
static String _mmss(Duration d) {
|
|
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
|
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
|
return '$m:$s';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = context.pangolin;
|
|
final connected = countdown != null;
|
|
final exhausted = quota.isExhausted && !connected;
|
|
|
|
// 进度与主值:连接期用倒计时,未连接用剩余分钟。
|
|
final double progress;
|
|
final String label;
|
|
final String value;
|
|
if (connected) {
|
|
final totalSec = quota.totalMinutes * 60;
|
|
progress = totalSec == 0 ? 0 : (countdown!.inSeconds / totalSec).clamp(0.0, 1.0);
|
|
label = t.quotaLeftLabel;
|
|
value = _mmss(countdown!);
|
|
} else {
|
|
progress = quota.progress;
|
|
label = exhausted ? t.quotaUsedUp : t.quotaToday;
|
|
value = exhausted ? '' : '${quota.remainingMinutes} ${t.minutes}';
|
|
}
|
|
final low = connected ? countdown!.inMinutes < 3 : quota.isLow;
|
|
final barColor = exhausted ? c.danger : (low ? c.warning : c.accent);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
|
decoration: BoxDecoration(
|
|
color: c.surface,
|
|
borderRadius: BorderRadius.circular(PangolinRadius.lg),
|
|
border: Border.all(color: c.border),
|
|
boxShadow: PangolinShadow.sm,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Row(children: [
|
|
Icon(PangolinIcons.clock, size: 15, color: exhausted ? c.danger : c.accent),
|
|
const SizedBox(width: 7),
|
|
// 标签固定、绝不省略(此前用 Flexible 让「今日剩余」被挤成「今日…」)。
|
|
Text(label,
|
|
style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600, fontSize: 12)),
|
|
if (value.isNotEmpty) ...[
|
|
const SizedBox(width: 7),
|
|
// 数值可作为最后收缩项(极端窄屏/三位数分钟才会省略)。
|
|
Flexible(
|
|
child: Text(value,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: PangolinText.mono.copyWith(color: c.fg1, fontSize: 14, fontWeight: FontWeight.w600)),
|
|
),
|
|
],
|
|
const SizedBox(width: 8),
|
|
const Spacer(),
|
|
// 短标签「免费版」——每日额度由卡片本身表达,不再挤进右上角。
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
|
|
decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)),
|
|
child: Text(t.planFreeTag,
|
|
style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w600, fontSize: 10.5)),
|
|
),
|
|
]),
|
|
const SizedBox(height: 9),
|
|
// 进度条:剩余比例;≤3 分钟 warning、耗尽 danger(满宽轨道 + 比例填充)。
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(3),
|
|
child: SizedBox(
|
|
height: 6,
|
|
child: Stack(children: [
|
|
Positioned.fill(child: ColoredBox(color: c.bgSubtle)),
|
|
FractionallySizedBox(
|
|
alignment: Alignment.centerLeft,
|
|
widthFactor: progress,
|
|
child: AnimatedContainer(
|
|
duration: PangolinMotion.base,
|
|
curve: PangolinMotion.easeOut,
|
|
decoration: BoxDecoration(color: barColor, borderRadius: BorderRadius.circular(3)),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
// 加时按钮:未连接时显示。移动端「看广告加时」;桌面仅耗尽时给「升级会员」。
|
|
if (!connected && (!isDesktop || exhausted)) ...[
|
|
const SizedBox(height: 11),
|
|
_ActionButton(
|
|
label: isDesktop ? t.upgrade : t.watchAdMore,
|
|
icon: isDesktop ? PangolinIcons.zap : PangolinIcons.playCircle,
|
|
onTap: onWatchAd,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ActionButton extends StatelessWidget {
|
|
const _ActionButton({required this.label, required this.icon, required this.onTap});
|
|
final String label;
|
|
final IconData icon;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = context.pangolin;
|
|
return Material(
|
|
color: c.accentSubtle,
|
|
shape: StadiumBorder(side: BorderSide(color: c.accentBorder, width: 1.5)),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
child: SizedBox(
|
|
height: 38,
|
|
child: Center(
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Icon(icon, size: 16, color: c.accent),
|
|
const SizedBox(width: 7),
|
|
Text(label,
|
|
style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w700, fontSize: 13)),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|