76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
4.1 KiB
Dart
126 lines
4.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../api/oa_client.dart';
|
|
import '../session/session.dart';
|
|
import '../widgets/common.dart';
|
|
import 'record_detail_page.dart';
|
|
|
|
class WorkAssignPage extends StatefulWidget {
|
|
const WorkAssignPage({super.key, required this.session, required this.api});
|
|
final SessionStore session;
|
|
final OaClient api;
|
|
|
|
@override
|
|
State<WorkAssignPage> createState() => _WorkAssignPageState();
|
|
}
|
|
|
|
class _WorkAssignPageState extends State<WorkAssignPage> {
|
|
List<Map<String, dynamic>> _items = [];
|
|
bool _loading = true;
|
|
|
|
bool get _canAssign {
|
|
const codes = ['admin', 'owner', 'biz_director', 'tech_director', 'rd_director', '3d_director', 'video_director', 'material_director', 'pm'];
|
|
return widget.session.roles.any(codes.contains);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
try {
|
|
final data = await widget.api.get('/office/work-assignments');
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_items = asMaps(data);
|
|
_loading = false;
|
|
});
|
|
} catch (_) {
|
|
if (mounted) setState(() => _loading = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _create() async {
|
|
final staff = asMaps(await widget.api.get('/staff'));
|
|
if (!mounted) return;
|
|
final title = TextEditingController();
|
|
final content = TextEditingController();
|
|
String? assignee;
|
|
final ok = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setSt) => AlertDialog(
|
|
title: const Text('安排工作'),
|
|
content: SizedBox(
|
|
width: 420,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(controller: title, decoration: const InputDecoration(labelText: '标题')),
|
|
const SizedBox(height: 8),
|
|
TextField(controller: content, decoration: const InputDecoration(labelText: '内容'), maxLines: 3),
|
|
const SizedBox(height: 8),
|
|
DropdownButtonFormField<String>(
|
|
decoration: const InputDecoration(labelText: '执行人'),
|
|
items: [
|
|
for (final s in staff)
|
|
DropdownMenuItem(value: '${s['id']}', child: Text('${s['name'] ?? ''} ${s['department'] ?? ''}')),
|
|
],
|
|
onChanged: (v) => setSt(() => assignee = v),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
|
|
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('下达')),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
if (ok != true) return;
|
|
try {
|
|
await widget.api.post('/office/work-assignments', {
|
|
'title': title.text.trim(),
|
|
'content': content.text.trim(),
|
|
'assigneeId': assignee,
|
|
});
|
|
await _load();
|
|
} catch (e) {
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('工作安排'),
|
|
actions: [
|
|
if (_canAssign) IconButton(onPressed: _create, icon: const Icon(Icons.add)),
|
|
],
|
|
),
|
|
body: _loading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _items.isEmpty
|
|
? const EmptyHint('没有工作安排')
|
|
: RefreshIndicator(
|
|
onRefresh: _load,
|
|
child: ListView(
|
|
children: [
|
|
for (final r in _items)
|
|
KvTile(
|
|
title: '${r['title'] ?? ''}',
|
|
subtitle: '${r['assignee'] is Map ? r['assignee']['displayName'] : ''} ${fmtTime(r['dueAt'] ?? r['createdAt'])}',
|
|
status: r['status'],
|
|
onTap: () => openRecord(context, widget.api, r),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|