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,125 @@
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user