Files
ai-contract-platform/mobile/lib/features/contracts/presentation/contract_builder_screen.dart
T
yuqei 3df85fdb04 feat: land phase15 legal docs and commercial ops
Includes legal document generation workflows, prompt governance runtime wiring, legal change impact scans, operation KPI dashboards, invoice files, sales follow-up events, mobile review polish, deployment automation, and updated project docs.
2026-05-21 14:42:44 +08:00

392 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import '../../../shared/widgets/app_scaffold_page.dart';
import '../../../shared/widgets/app_section_card.dart';
import '../application/contract_generation_controller.dart';
import '../domain/contract_models.dart';
import '../domain/contract_review_context.dart';
import 'generated_contract_result_screen.dart';
class ContractBuilderScreen extends StatefulWidget {
const ContractBuilderScreen({
super.key,
required this.template,
this.reviewContext,
this.initialVariableValues,
this.initialExportFormat = 'docx',
});
final ContractTemplateItem template;
final ContractReviewContext? reviewContext;
final Map<String, String>? initialVariableValues;
final String initialExportFormat;
@override
State<ContractBuilderScreen> createState() => _ContractBuilderScreenState();
}
class _ContractBuilderScreenState extends State<ContractBuilderScreen> {
final ContractGenerationController _controller =
ContractGenerationController();
final Map<String, TextEditingController> _fieldControllers =
<String, TextEditingController>{};
late String _exportFormat;
@override
void initState() {
super.initState();
_exportFormat = widget.initialExportFormat;
for (final variable in widget.template.variables) {
_fieldControllers[variable.name] = TextEditingController(
text: _buildInitialValue(variable),
);
}
}
String _buildInitialValue(ContractTemplateVariable variable) {
final restoredValue = widget.initialVariableValues?[variable.name];
if (restoredValue != null && restoredValue.trim().isNotEmpty) {
return restoredValue;
}
final reviewContextValue = buildReviewContextVariableValue(
variable.name,
variable.label,
variable.description,
widget.reviewContext,
);
if (reviewContextValue.isNotEmpty) {
return reviewContextValue;
}
return variable.defaultValue;
}
@override
void dispose() {
_controller.dispose();
for (final controller in _fieldControllers.values) {
controller.dispose();
}
super.dispose();
}
Map<String, String> get _variableValues {
return _fieldControllers.map(
(key, value) => MapEntry(key, value.text.trim()),
);
}
String? _validateRequiredFields() {
for (final variable in widget.template.variables) {
if (variable.required) {
final value = _fieldControllers[variable.name]?.text.trim() ?? '';
if (value.isEmpty) {
return '请先填写“${variable.label}”。';
}
}
}
return null;
}
Future<void> _preview() async {
final validationError = _validateRequiredFields();
if (validationError != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(validationError)),
);
return;
}
await _controller.loadPreview(
templateId: widget.template.id,
variableValues: _variableValues,
);
final error = _controller.errorMessage.value;
if (!mounted || error == null || error.isEmpty) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error)),
);
}
Future<void> _generate() async {
final validationError = _validateRequiredFields();
if (validationError != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(validationError)),
);
return;
}
try {
final contract = await _controller.generate(
templateId: widget.template.id,
variableValues: _variableValues,
exportFormat: _exportFormat,
);
if (!mounted) {
return;
}
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => GeneratedContractResultScreen(contract: contract),
),
);
} on ContractGenerationException catch (error) {
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error.message)),
);
}
}
@override
Widget build(BuildContext context) {
final template = widget.template;
return Scaffold(
appBar: AppBar(
title: Text(template.name),
),
body: ListView(
padding: EdgeInsets.zero,
children: [
AppScaffoldPage(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppSectionCard(
title: '模板信息',
subtitle: '先确认模板说明,再填写变量并预览生成结果。',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_InfoLine(label: '合同类型', value: template.contractType),
if (template.description.isNotEmpty) ...[
const SizedBox(height: 12),
_InfoLine(label: '模板说明', value: template.description),
],
if (template.previewExcerpt.isNotEmpty) ...[
const SizedBox(height: 12),
_InfoLine(
label: '预览摘要', value: template.previewExcerpt),
],
],
),
),
if (widget.reviewContext != null) ...[
const SizedBox(height: 16),
AppSectionCard(
title: '审查上下文已带入',
subtitle: '系统已尝试把审查意见、风险项和建议动作预填到匹配变量中,你可以继续编辑后再生成。',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_InfoLine(
label: '审查任务',
value: widget.reviewContext!.reviewTitle ?? '-'),
const SizedBox(height: 12),
_InfoLine(
label: '合同类型',
value: widget.reviewContext!.contractType ?? '-'),
const SizedBox(height: 12),
_InfoLine(
label: '总体风险',
value: widget.reviewContext!.overallRisk ?? '-'),
if (widget.reviewContext!.reviewOpinion != null &&
widget
.reviewContext!.reviewOpinion!.isNotEmpty) ...[
const SizedBox(height: 12),
_InfoLine(
label: '审查意见',
value: widget.reviewContext!.reviewOpinion!),
],
],
),
),
],
const SizedBox(height: 16),
AppSectionCard(
title: '变量填写',
subtitle: '优先填写必填变量,确认导出格式后再预览和生成。',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final variable in template.variables) ...[
TextField(
controller: _fieldControllers[variable.name],
minLines: variable.description.length > 24 ? 2 : 1,
maxLines: variable.description.length > 24 ? 3 : 1,
decoration: InputDecoration(
labelText: variable.required
? '${variable.label} *'
: variable.label,
hintText: variable.description.isNotEmpty
? variable.description
: null,
),
),
if (variable != template.variables.last)
const SizedBox(height: 12),
],
const SizedBox(height: 16),
DropdownButtonFormField<String>(
initialValue: _exportFormat,
decoration: const InputDecoration(
labelText: '导出格式',
),
items: const <DropdownMenuItem<String>>[
DropdownMenuItem<String>(
value: 'docx', child: Text('Word (.docx)')),
DropdownMenuItem<String>(
value: 'pdf', child: Text('PDF (.pdf)')),
DropdownMenuItem<String>(
value: 'html', child: Text('HTML (.html)')),
DropdownMenuItem<String>(
value: 'txt', child: Text('文本 (.txt)')),
],
onChanged: (value) {
if (value == null) {
return;
}
setState(() {
_exportFormat = value;
});
},
),
],
),
),
const SizedBox(height: 16),
ValueListenableBuilder<ContractPreviewItem?>(
valueListenable: _controller.preview,
builder: (context, preview, _) {
if (preview == null) {
return const SizedBox.shrink();
}
return Column(
children: [
AppSectionCard(
title: '合同预览',
subtitle: '这里只是生成前预览,不会写入正式合同记录。',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (preview.variableSummary.isNotEmpty) ...[
for (final item in preview.variableSummary) ...[
_InfoLine(
label: item.label,
value: item.valuePreview.isEmpty
? '(空)'
: item.valuePreview,
),
if (item != preview.variableSummary.last)
const SizedBox(height: 12),
],
const SizedBox(height: 16),
],
Text(preview.previewContent),
const SizedBox(height: 16),
_InfoLine(
label: '当前导出格式',
value: _exportFormat.toUpperCase()),
],
),
),
const SizedBox(height: 16),
],
);
},
),
ValueListenableBuilder<String?>(
valueListenable: _controller.errorMessage,
builder: (context, message, _) {
if (message == null || message.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
message,
style: TextStyle(
color: Theme.of(context).colorScheme.error),
),
);
},
),
Row(
children: [
Expanded(
child: ValueListenableBuilder<bool>(
valueListenable: _controller.isPreviewing,
builder: (context, isPreviewing, _) {
return OutlinedButton.icon(
onPressed: isPreviewing ? null : _preview,
icon: isPreviewing
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Icon(Icons.visibility_outlined),
label: Text(isPreviewing ? '预览中...' : '预览合同'),
);
},
),
),
const SizedBox(width: 12),
Expanded(
child: ValueListenableBuilder<bool>(
valueListenable: _controller.isGenerating,
builder: (context, isGenerating, _) {
return FilledButton.icon(
onPressed: isGenerating ? null : _generate,
icon: isGenerating
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Icon(Icons.auto_awesome),
label: Text(isGenerating ? '生成中...' : '生成合同'),
);
},
),
),
],
),
],
),
),
],
),
);
}
}
class _InfoLine extends StatelessWidget {
const _InfoLine({
required this.label,
required this.value,
});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 4),
Text(value.isEmpty ? '-' : value),
],
);
}
}