Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user