feat(ui06): App line A0-A7 — nav pill removal, light segments, bare summary, token alignment to app-preview
- tokens:次文字 #69778E、soft/card-border/seg 色组、输入圆角 10(kRadiusField) - theme:导航 indicatorColor 透明+显式选中蓝;卡片去阴影改 1px #EDF0F5 边框;输入边框同步 - 新组件 SegmentTabs(#EDF1F6 轨道/选中实蓝/light 浅蓝变体/null 无高亮)替换 M3 SegmentedButton - 上传中心:摘要去白卡直排+主数字 32/40;completed 次级筛选无高亮语义保留 - 相册卡 scrim 30% 起 #0C1728E8;导航角标红底白字白边;登录主标题 27/700 - theme_test 三处旧规格断言随 UI06 值更新;397/397 全绿、analyze 0 issues - 真机证据:MuMu + 测试服务器 API,登录/相册/详情/上传 4 页截图确认达标 - 登记偏差:语义色保持对比度合规深色版;视口矩阵随 G2;详见验收记录 §6 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import '../features/uploads/queue_controller.dart';
|
||||
import '../features/uploads/queue_l10n.dart';
|
||||
import '../features/uploads/task_state.dart';
|
||||
import '../features/uploads/task_store.dart';
|
||||
import '../ui/components/segment_tabs.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../ui/components/empty_state.dart';
|
||||
import '../ui/components/error_state.dart';
|
||||
@@ -102,63 +103,50 @@ class _SummaryCard extends ConsumerWidget {
|
||||
].join(' · ');
|
||||
|
||||
final light = theme.brightness == Brightness.light;
|
||||
return Card(
|
||||
margin: const EdgeInsets.fromLTRB(kSpace16, kSpace12, kSpace16, 0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(kSpace16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.summarize_outlined,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: kSpace8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
scope,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: kSpace8),
|
||||
// 已完成突出(§4 页面 14:摘要突出已完成数)。
|
||||
_SummaryCount(
|
||||
value: completedSummaryOf(counts),
|
||||
label: l10n.uploadsCompleted,
|
||||
color: light ? kColorSuccess : kColorSuccessDark,
|
||||
emphasized: true,
|
||||
),
|
||||
const SizedBox(height: kSpace4),
|
||||
Wrap(
|
||||
spacing: kSpace16,
|
||||
runSpacing: kSpace4,
|
||||
children: [
|
||||
_SummaryCount(
|
||||
value: uploadingSummaryOf(counts),
|
||||
label: l10n.uploadsUploading,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
_SummaryCount(
|
||||
value: waitingSummaryOf(counts),
|
||||
label: l10n.uploadsWaiting,
|
||||
color: light ? kColorWaiting : kColorWaitingDark,
|
||||
),
|
||||
_SummaryCount(
|
||||
value: failedCountOf(counts),
|
||||
label: l10n.queueSummaryNeedAttention,
|
||||
color: light ? kColorError : kColorErrorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// UI06:摘要直接排页背景(无白卡包裹);范围行 13px、主数字 32px、
|
||||
// 副行 13px 中性(app-preview 示例 14)。
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(kSpace16, kSpace16, kSpace16, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
scope,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: kSpace8),
|
||||
// 已完成突出(§4 页面 14:摘要突出已完成数)。
|
||||
_SummaryCount(
|
||||
value: completedSummaryOf(counts),
|
||||
label: l10n.uploadsCompleted,
|
||||
color: light ? kColorSuccess : kColorSuccessDark,
|
||||
emphasized: true,
|
||||
),
|
||||
const SizedBox(height: kSpace4),
|
||||
Wrap(
|
||||
spacing: kSpace16,
|
||||
runSpacing: kSpace4,
|
||||
children: [
|
||||
_SummaryCount(
|
||||
value: uploadingSummaryOf(counts),
|
||||
label: l10n.uploadsUploading,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
_SummaryCount(
|
||||
value: waitingSummaryOf(counts),
|
||||
label: l10n.uploadsWaiting,
|
||||
color: light ? kColorWaiting : kColorWaitingDark,
|
||||
),
|
||||
_SummaryCount(
|
||||
value: failedCountOf(counts),
|
||||
label: l10n.queueSummaryNeedAttention,
|
||||
color: light ? kColorError : kColorErrorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -189,12 +177,19 @@ class _SummaryCount extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'$value',
|
||||
style: (emphasized
|
||||
? theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
)
|
||||
: theme.textTheme.titleMedium)
|
||||
?.copyWith(fontFeatures: const [FontFeature.tabularFigures()]),
|
||||
style:
|
||||
(emphasized
|
||||
// UI06:主数字 32/40(app-preview 示例 14 .metric)
|
||||
? theme.textTheme.titleLarge?.copyWith(
|
||||
fontSize: 32,
|
||||
height: 40 / 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.6,
|
||||
)
|
||||
: theme.textTheme.titleMedium)
|
||||
?.copyWith(
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: kSpace4),
|
||||
Text(label, style: theme.textTheme.bodySmall?.copyWith(color: color)),
|
||||
@@ -232,34 +227,20 @@ class _PrimaryFilterBar extends ConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SegmentedButton<QueueStateFilter>(
|
||||
// 已完成经筛选菜单选择时三段不亮(属次级筛选,非主段)。
|
||||
selected: filter.state == QueueStateFilter.completed
|
||||
? const <QueueStateFilter>{}
|
||||
: <QueueStateFilter>{filter.state},
|
||||
emptySelectionAllowed: true,
|
||||
showSelectedIcon: false,
|
||||
style: SegmentedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(44),
|
||||
padding: const EdgeInsets.symmetric(horizontal: kSpace12),
|
||||
),
|
||||
// UI06:轻量分段(app-preview .tabs——#EDF1F6 轨道/选中实蓝,
|
||||
// 替换 M3 SegmentedButton 黑边胶囊)。
|
||||
// 已完成经筛选菜单选择时三段不亮(属次级筛选,非主段)。
|
||||
child: SegmentTabs<QueueStateFilter>(
|
||||
value: filter.state == QueueStateFilter.completed
|
||||
? null
|
||||
: filter.state,
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: QueueStateFilter.all,
|
||||
label: Text(l10n.uploadsAll),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: QueueStateFilter.inProgress,
|
||||
label: Text(l10n.uploadsActive),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: QueueStateFilter.needsAttention,
|
||||
label: Text(l10n.uploadsNeedsAttention),
|
||||
),
|
||||
(QueueStateFilter.all, l10n.uploadsAll),
|
||||
(QueueStateFilter.inProgress, l10n.uploadsActive),
|
||||
(QueueStateFilter.needsAttention, l10n.uploadsNeedsAttention),
|
||||
],
|
||||
onSelectionChanged: (selection) => ref
|
||||
.read(queueFilterProvider.notifier)
|
||||
.setStateGroup(selection.first),
|
||||
onChanged: (value) =>
|
||||
ref.read(queueFilterProvider.notifier).setStateGroup(value),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
@@ -295,8 +276,9 @@ class _FilterSheet extends ConsumerWidget {
|
||||
|
||||
void close() => Navigator.of(context).pop();
|
||||
// 相册组值:'' = 全部相册(选中相册不在列表时回退全部)。
|
||||
final albumValue =
|
||||
albums.any((a) => a.id == filter.albumId) ? (filter.albumId ?? '') : '';
|
||||
final albumValue = albums.any((a) => a.id == filter.albumId)
|
||||
? (filter.albumId ?? '')
|
||||
: '';
|
||||
return SafeArea(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
@@ -369,7 +351,9 @@ class _FilterSheet extends ConsumerWidget {
|
||||
),
|
||||
title: Text(l10n.queueStateCompleted),
|
||||
onTap: () {
|
||||
ref.read(queueFilterProvider.notifier).setStateGroup(
|
||||
ref
|
||||
.read(queueFilterProvider.notifier)
|
||||
.setStateGroup(
|
||||
filter.state == QueueStateFilter.completed
|
||||
? QueueStateFilter.all
|
||||
: QueueStateFilter.completed,
|
||||
@@ -488,9 +472,7 @@ class _TaskTile extends ConsumerWidget {
|
||||
return Padding(
|
||||
// 行键(task.id)供 widget 测试行级锚定(状态徽章/动作图标定位)。
|
||||
key: ValueKey('queue-task-${task.id}'),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
kSpace16, kSpace8, kSpace16, kSpace8,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(kSpace16, kSpace8, kSpace16, kSpace8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -504,8 +486,9 @@ class _TaskTile extends ConsumerWidget {
|
||||
task.filename,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
@@ -563,8 +546,8 @@ class _TaskTile extends ConsumerWidget {
|
||||
task.state == UploadTaskState.uploadedConfirming)
|
||||
_ProgressLine(
|
||||
taskId: task.id,
|
||||
confirming: task.state ==
|
||||
UploadTaskState.uploadedConfirming,
|
||||
confirming:
|
||||
task.state == UploadTaskState.uploadedConfirming,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -577,18 +560,17 @@ class _TaskTile extends ConsumerWidget {
|
||||
|
||||
/// 徽章语义变体(§4 页面 14:进行=主色、等待=琥珀、失败=红、完成=绿、
|
||||
/// 取消=灰)。
|
||||
static BadgeVariant _badgeVariant(UploadTaskState state) =>
|
||||
switch (state) {
|
||||
UploadTaskState.preparing ||
|
||||
UploadTaskState.uploading ||
|
||||
UploadTaskState.uploadedConfirming => BadgeVariant.info,
|
||||
UploadTaskState.queued ||
|
||||
UploadTaskState.waitingNetwork => BadgeVariant.warning,
|
||||
UploadTaskState.preparationFailed ||
|
||||
UploadTaskState.retryableFailed => BadgeVariant.error,
|
||||
UploadTaskState.completed => BadgeVariant.success,
|
||||
UploadTaskState.cancelled => BadgeVariant.neutral,
|
||||
};
|
||||
static BadgeVariant _badgeVariant(UploadTaskState state) => switch (state) {
|
||||
UploadTaskState.preparing ||
|
||||
UploadTaskState.uploading ||
|
||||
UploadTaskState.uploadedConfirming => BadgeVariant.info,
|
||||
UploadTaskState.queued ||
|
||||
UploadTaskState.waitingNetwork => BadgeVariant.warning,
|
||||
UploadTaskState.preparationFailed ||
|
||||
UploadTaskState.retryableFailed => BadgeVariant.error,
|
||||
UploadTaskState.completed => BadgeVariant.success,
|
||||
UploadTaskState.cancelled => BadgeVariant.neutral,
|
||||
};
|
||||
|
||||
/// 徽章文案 = §4.4 界面含义列(确认中不显示「已公开」等超集语义)。
|
||||
static String _badgeLabel(AppLocalizations l10n, UploadTaskState state) =>
|
||||
@@ -911,7 +893,9 @@ class _BatchRetryBar extends ConsumerWidget {
|
||||
final account = ref.read(sessionAccountProvider).value;
|
||||
if (account == null) return;
|
||||
final engine = ref.read(uploadEngineProvider);
|
||||
final tasks = await ref.read(taskStoreProvider).listByAccount(
|
||||
final tasks = await ref
|
||||
.read(taskStoreProvider)
|
||||
.listByAccount(
|
||||
account,
|
||||
states: const {UploadTaskState.retryableFailed},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user