76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
4.1 KiB
Dart
124 lines
4.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../theme.dart';
|
|
import '../labels.dart';
|
|
|
|
/// 与 Web `ApplyBucketBar` 对齐:Segmented + 选中态。
|
|
class BucketBar extends StatelessWidget {
|
|
const BucketBar({super.key, required this.value, required this.onChanged, this.extra = const []});
|
|
|
|
final String value;
|
|
final ValueChanged<String> onChanged;
|
|
final List<(String, String)> extra;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final items = <(String, String)>[
|
|
('', '全部'),
|
|
('pending', '待审批'),
|
|
('approved', '已通过'),
|
|
('rejected', '已驳回'),
|
|
...extra,
|
|
];
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
|
child: Row(
|
|
children: [
|
|
for (final it in items)
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: ChoiceChip(
|
|
label: Text(it.$2),
|
|
selected: value == it.$1,
|
|
showCheckmark: false,
|
|
visualDensity: VisualDensity.compact,
|
|
selectedColor: kBind.withValues(alpha: 0.16),
|
|
onSelected: (_) => onChanged(it.$1),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EmptyHint extends StatelessWidget {
|
|
const EmptyHint(this.text, {super.key});
|
|
final String text;
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Text(text, style: const TextStyle(color: kMute)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class KvTile extends StatelessWidget {
|
|
const KvTile({super.key, required this.title, this.subtitle, this.trailing, this.onTap, this.badge, this.status});
|
|
final String title;
|
|
final String? subtitle;
|
|
final Widget? trailing;
|
|
final VoidCallback? onTap;
|
|
final int? badge;
|
|
final dynamic status;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
color: Colors.white,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 12, 0),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: kLine, width: 0.5))),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, color: kInk)),
|
|
if (subtitle != null && subtitle!.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
child: Text(subtitle!, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13, color: kMute)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 2),
|
|
child: Column(
|
|
children: [
|
|
if (status != null) StatusDot(status),
|
|
if (badge != null && badge! > 0)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 6),
|
|
child: CircleAvatar(
|
|
radius: 10,
|
|
backgroundColor: kDanger,
|
|
child: Text('$badge', style: const TextStyle(color: Colors.white, fontSize: 10)),
|
|
),
|
|
),
|
|
if (trailing != null) trailing!,
|
|
if (onTap != null) const Icon(Icons.chevron_right, color: Color(0xFFC0C0C0), size: 20),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|