Files
Public/apps/native/lib/widgets/common.dart
T
daiyongkang 76f266645d Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。
排除 node_modules、构建产物、安装包与 .env 密钥。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 10:04:03 +00:00

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),
],
),
),
],
),
),
),
);
}
}