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:
2026-09-02 10:04:03 +00:00
commit 76f266645d
507 changed files with 99891 additions and 0 deletions
+125
View File
@@ -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),
),
],
),
),
);
}
}