import 'package:flutter/material.dart'; import '../api/oa_client.dart'; import '../session/session.dart'; import '../theme.dart'; import 'wecom.dart'; const kDeskBg = Color(0xFFEDEDED); const kDeskSide = Color(0xFFF7F7F7); const kDeskPane = Colors.white; const kDeskActive = Color(0xFFE8F3FF); class DesktopSidebar extends StatelessWidget { const DesktopSidebar({ super.key, required this.session, required this.api, required this.index, required this.onChanged, required this.items, }); final SessionStore session; final OaClient api; final int index; final ValueChanged onChanged; final List<(IconData, IconData, String, int)> items; @override Widget build(BuildContext context) { return ColoredBox( color: kDeskSide, child: SafeArea( child: SizedBox( width: 210, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 10), child: Row( children: [ SquareAvatar( label: session.displayName, size: 36, fileId: '${session.user['avatarFileId'] ?? ''}', api: api, ), const SizedBox(width: 10), Expanded( child: Text( session.displayName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: kInk), ), ), ], ), ), const Divider(height: 1, color: kLine), const SizedBox(height: 6), Expanded( child: ListView( padding: const EdgeInsets.symmetric(horizontal: 10), children: [ for (var i = 0; i < items.length; i++) _NavItem( active: index == i, icon: index == i ? items[i].$2 : items[i].$1, label: items[i].$3, badge: items[i].$4, onTap: () => onChanged(i), ), ], ), ), ], ), ), ), ); } } class _NavItem extends StatelessWidget { const _NavItem({ required this.active, required this.icon, required this.label, required this.badge, required this.onTap, }); final bool active; final IconData icon; final String label; final int badge; final VoidCallback onTap; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 4), child: Material( color: active ? kDeskActive : Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ Stack( clipBehavior: Clip.none, children: [ Icon(icon, size: 20, color: active ? kBind : const Color(0xFF646464)), if (badge > 0) Positioned(right: -8, top: -4, child: UnreadBadge(badge)), ], ), const SizedBox(width: 10), Expanded( child: Text( label, style: TextStyle( fontSize: 14, color: active ? kBind : const Color(0xFF323232), fontWeight: active ? FontWeight.w600 : FontWeight.w400, ), ), ), ], ), ), ), ), ); } } class DesktopPaneHeader extends StatelessWidget { const DesktopPaneHeader({ super.key, required this.title, this.actions = const [], this.leading, this.bottom, }); final String title; final List actions; final Widget? leading; final Widget? bottom; @override Widget build(BuildContext context) { return ColoredBox( color: kDeskPane, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: 52, child: Row( children: [ if (leading != null) leading!, Padding( padding: EdgeInsets.only(left: leading == null ? 16 : 4), child: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: kInk)), ), const Spacer(), ...actions, const SizedBox(width: 8), ], ), ), if (bottom != null) bottom!, const Divider(height: 1, color: kLine), ], ), ); } } class DesktopSearchBar extends StatelessWidget { const DesktopSearchBar({super.key, required this.hint, this.onChanged, this.controller}); final String hint; final ValueChanged? onChanged; final TextEditingController? controller; @override Widget build(BuildContext context) { return Container( height: 34, decoration: BoxDecoration(color: const Color(0xFFF3F3F3), borderRadius: BorderRadius.circular(6)), padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( children: [ const Icon(Icons.search, size: 18, color: kMute), const SizedBox(width: 6), Expanded( child: TextField( controller: controller, onChanged: onChanged, style: const TextStyle(fontSize: 14), decoration: InputDecoration( hintText: hint, isDense: true, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, contentPadding: EdgeInsets.zero, hintStyle: const TextStyle(color: Color(0xFFB2B2B2), fontSize: 14), ), ), ), ], ), ); } } class DesktopAppCard extends StatelessWidget { const DesktopAppCard({ super.key, required this.label, required this.desc, required this.icon, required this.color, required this.onTap, }); final String label; final String desc; final IconData icon; final Color color; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: Colors.white, borderRadius: BorderRadius.circular(10), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Padding( padding: const EdgeInsets.fromLTRB(14, 14, 14, 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 40, height: 40, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(10)), child: Icon(icon, color: Colors.white, size: 22), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: kInk)), const SizedBox(height: 4), Text(desc, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: kMute, height: 1.35)), ], ), ), ], ), ), ), ); } } class DesktopEmptyPane extends StatelessWidget { const DesktopEmptyPane({super.key, this.hint = '选择左侧会话开始聊天'}); final String hint; @override Widget build(BuildContext context) { return ColoredBox( color: const Color(0xFFF5F5F5), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Opacity( opacity: 0.12, child: Container( width: 120, height: 120, decoration: BoxDecoration(color: kBind, borderRadius: BorderRadius.circular(28)), child: const Icon(Icons.apartment, size: 64, color: Colors.white), ), ), const SizedBox(height: 20), Text(hint, style: const TextStyle(color: kMute, fontSize: 14)), ], ), ), ); } } class DesktopTabBar extends StatelessWidget { const DesktopTabBar({ super.key, required this.tabs, required this.active, required this.onSelect, required this.onClose, }); final List<(String key, String title, IconData icon, Color color)> tabs; final int active; final ValueChanged onSelect; final ValueChanged onClose; @override Widget build(BuildContext context) { return ColoredBox( color: kDeskPane, child: Column( children: [ SizedBox( height: 44, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8), children: [ for (var i = 0; i < tabs.length; i++) _TabChip( title: tabs[i].$2, icon: tabs[i].$3, color: tabs[i].$4, active: active == i, closable: i > 0, onTap: () => onSelect(i), onClose: () => onClose(i), ), ], ), ), const Divider(height: 1, color: kLine), ], ), ); } } class _TabChip extends StatelessWidget { const _TabChip({ required this.title, required this.icon, required this.color, required this.active, required this.closable, required this.onTap, required this.onClose, }); final String title; final IconData icon; final Color color; final bool active; final bool closable; final VoidCallback onTap; final VoidCallback onClose; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 4, top: 6, bottom: 6), child: Material( color: active ? const Color(0xFFF3F6FB) : Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: EdgeInsets.fromLTRB(10, 6, closable ? 4 : 10, 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: color), const SizedBox(width: 6), Text(title, style: TextStyle(fontSize: 13, color: active ? kInk : kMute, fontWeight: active ? FontWeight.w600 : FontWeight.w400)), if (closable) ...[ const SizedBox(width: 4), InkWell( onTap: onClose, borderRadius: BorderRadius.circular(10), child: const Padding( padding: EdgeInsets.all(2), child: Icon(Icons.close, size: 14, color: kMute), ), ), ], ], ), ), ), ), ); } }