docs: 更新协议文档,删除废弃计划文档

- 更新 http-error-codes, user-points-chat-data-protocol
- 更新 divination-run-protocol, profile-protocol
- 删除废弃的后端和前端设计计划文档
This commit is contained in:
qzl
2026-04-08 17:23:02 +08:00
parent 49fc9a116f
commit e80a82bef4
57 changed files with 4117 additions and 2269 deletions
@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import '../../theme/design_tokens.dart';
class DivinationSummaryTagData {
const DivinationSummaryTagData({
required this.label,
required this.background,
required this.foreground,
});
final String label;
final Color background;
final Color foreground;
}
class DivinationSummaryCard extends StatelessWidget {
const DivinationSummaryCard({
super.key,
required this.question,
required this.leading,
required this.tags,
this.leadingBackgroundColor,
this.onTap,
this.questionMaxLines = 1,
});
final String question;
final Widget leading;
final List<DivinationSummaryTagData> tags;
final Color? leadingBackgroundColor;
final VoidCallback? onTap;
final int questionMaxLines;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final card = Card(
margin: EdgeInsets.zero,
color: colors.surface,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color:
leadingBackgroundColor ??
colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(child: leading),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Text(
question,
maxLines: questionMaxLines,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
if (tags.isNotEmpty) ...[
const SizedBox(height: AppSpacing.sm),
Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: tags
.map(
(tag) => _DivinationSummaryTag(
label: tag.label,
background: tag.background,
foreground: tag.foreground,
),
)
.toList(growable: false),
),
],
],
),
),
);
if (onTap == null) {
return SizedBox(width: double.infinity, child: card);
}
return SizedBox(
width: double.infinity,
child: Material(
color: colors.surface.withValues(alpha: 0),
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.md),
onTap: onTap,
child: card,
),
),
);
}
}
class _DivinationSummaryTag extends StatelessWidget {
const _DivinationSummaryTag({
required this.label,
required this.background,
required this.foreground,
});
final String label;
final Color background;
final Color foreground;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
label,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: foreground),
),
);
}
}
@@ -1,4 +1,5 @@
import '../../../features/divination/data/models/divination_params.dart';
import '../../../l10n/app_localizations.dart';
abstract final class DivinationTerms {
static const yaoNames = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];
@@ -45,6 +46,32 @@ abstract final class DivinationTerms {
static const yaoXiang = '爻象';
static const qiGua = '起卦';
static const jieGua = '解卦';
static String yaoName(AppLocalizations l10n, int index) {
return switch (index) {
0 => l10n.yaoNameFirst,
1 => l10n.yaoNameSecond,
2 => l10n.yaoNameThird,
3 => l10n.yaoNameFourth,
4 => l10n.yaoNameFifth,
5 => l10n.yaoNameTop,
_ => '',
};
}
static String yinYangLabel(AppLocalizations l10n, bool isYang) {
return isYang ? l10n.yaoYang : l10n.yaoYin;
}
static String yaoTypeLabel(AppLocalizations l10n, YaoType type) {
return switch (type) {
YaoType.youngYang => l10n.yaoYoungYang,
YaoType.youngYin => l10n.yaoYoungYin,
YaoType.oldYang => l10n.yaoOldYang,
YaoType.oldYin => l10n.yaoOldYin,
YaoType.undetermined => '',
};
}
}
enum YaoTypeLabel { youngYang, youngYin, oldYang, oldYin }
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../../../features/divination/data/models/divination_params.dart';
import '../../../l10n/app_localizations.dart';
import '../../theme/design_tokens.dart';
import 'divination_terms.dart';
@@ -8,6 +10,7 @@ class YaoLegend extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final style = Theme.of(context).textTheme.bodySmall;
final mutedTextColor = Theme.of(
context,
@@ -17,19 +20,19 @@ class YaoLegend extends StatelessWidget {
runSpacing: AppSpacing.xs,
children: [
Text(
'\u2014 ${DivinationTerms.yinYang[true]}',
'\u2014 ${DivinationTerms.yinYangLabel(l10n, true)}',
style: style?.copyWith(color: mutedTextColor),
),
Text(
'-- ${DivinationTerms.yinYang[false]}',
'-- ${DivinationTerms.yinYangLabel(l10n, false)}',
style: style?.copyWith(color: mutedTextColor),
),
Text(
'${DivinationTerms.changeMarkOldYang} ${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYang]}(变)',
'${DivinationTerms.changeMarkOldYang} ${DivinationTerms.yaoTypeLabel(l10n, YaoType.oldYang)}${l10n.yaoMovingSuffix}',
style: style?.copyWith(color: mutedTextColor),
),
Text(
'${DivinationTerms.changeMarkOldYin} ${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYin]}(变)',
'${DivinationTerms.changeMarkOldYin} ${DivinationTerms.yaoTypeLabel(l10n, YaoType.oldYin)}${l10n.yaoMovingSuffix}',
style: style?.copyWith(color: mutedTextColor),
),
],
@@ -0,0 +1,249 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../theme/design_tokens.dart';
import 'app_loading_indicator.dart';
enum MessageComposerMode { text, holdToSpeak }
enum MessageComposerProcess { idle, recording, transcribing }
class MessageComposer extends StatelessWidget {
const MessageComposer({
super.key,
required this.mode,
required this.process,
required this.hasMessage,
required this.isWaitingAgent,
required this.iconSize,
required this.composerMinHeight,
required this.onTapRightAction,
required this.onHoldToSpeakStart,
required this.onHoldToSpeakEnd,
required this.onHoldToSpeakMoveUpdate,
required this.onHoldToSpeakCancel,
required this.textInputChild,
required this.holdToSpeakText,
required this.recordingText,
required this.transcribingText,
required this.recordingHintText,
this.showRecordingInlineFeedback = true,
});
final MessageComposerMode mode;
final MessageComposerProcess process;
final bool hasMessage;
final bool isWaitingAgent;
final double iconSize;
final double composerMinHeight;
final VoidCallback onTapRightAction;
final VoidCallback onHoldToSpeakStart;
final VoidCallback onHoldToSpeakEnd;
final ValueChanged<LongPressMoveUpdateDetails> onHoldToSpeakMoveUpdate;
final VoidCallback onHoldToSpeakCancel;
final Widget textInputChild;
final String holdToSpeakText;
final String recordingText;
final String transcribingText;
final String recordingHintText;
final bool showRecordingInlineFeedback;
bool get _isHoldMode => mode == MessageComposerMode.holdToSpeak;
bool get _isRecording => process == MessageComposerProcess.recording;
bool get _isTranscribing => process == MessageComposerProcess.transcribing;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: colorScheme.outlineVariant, width: 0.5),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: _buildCenterArea(colorScheme)),
const SizedBox(width: AppSpacing.sm),
_buildRightAction(colorScheme),
],
),
);
}
Widget _buildRightAction(ColorScheme colorScheme) {
if (_isTranscribing) {
return SizedBox(
width: iconSize,
height: iconSize,
child: AppLoadingIndicator(
variant: AppLoadingVariant.inline,
size: iconSize,
strokeWidth: AppSpacing.xs / 2,
color: colorScheme.primary,
trackColor: colorScheme.primaryContainer,
),
);
}
if (_isRecording) {
return Container(
width: iconSize,
height: iconSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.error.withValues(alpha: 0.1),
),
child: Icon(
Icons.fiber_manual_record,
size: iconSize * 0.6,
color: colorScheme.error,
),
);
}
return IconButton(
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: BoxConstraints(minWidth: iconSize, minHeight: iconSize),
onPressed: onTapRightAction,
icon: Icon(
_resolveRightIcon(),
size: iconSize,
color: _resolveRightIconColor(colorScheme),
),
);
}
Widget _buildCenterArea(ColorScheme colorScheme) {
return SizedBox(
height: composerMinHeight,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeOut,
child: _isHoldMode
? _buildHoldToSpeakArea(
key: const ValueKey('hold_mode'),
colorScheme: colorScheme,
)
: _buildTextInputArea(key: const ValueKey('text_mode')),
),
);
}
Widget _buildTextInputArea({required Key key}) {
return SizedBox(key: key, height: composerMinHeight, child: textInputChild);
}
Widget _buildHoldToSpeakArea({
required Key key,
required ColorScheme colorScheme,
}) {
return RawGestureDetector(
behavior: HitTestBehavior.opaque,
gestures: {
LongPressGestureRecognizer:
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
duration: const Duration(milliseconds: 120),
),
(instance) {
instance.onLongPressStart = (details) => onHoldToSpeakStart();
instance.onLongPressEnd = (details) => onHoldToSpeakEnd();
instance.onLongPressMoveUpdate = onHoldToSpeakMoveUpdate;
instance.onLongPressCancel = onHoldToSpeakCancel;
},
),
},
child: Container(
key: key,
width: double.infinity,
height: composerMinHeight,
alignment: Alignment.center,
child: _buildHoldToSpeakContent(colorScheme),
),
);
}
Widget _buildHoldToSpeakContent(ColorScheme colorScheme) {
if (_isRecording) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.error,
),
),
const SizedBox(width: AppSpacing.sm),
Text(
recordingText,
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
],
);
}
if (_isTranscribing) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
color: colorScheme.primary,
),
),
const SizedBox(width: AppSpacing.sm),
Text(
transcribingText,
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
],
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.mic, size: 16, color: colorScheme.onSurfaceVariant),
const SizedBox(width: AppSpacing.sm),
Text(
holdToSpeakText,
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
],
);
}
IconData _resolveRightIcon() {
if (isWaitingAgent) {
return Icons.stop_rounded;
}
if (hasMessage) {
return Icons.send_rounded;
}
return _isHoldMode ? Icons.keyboard_rounded : Icons.mic_rounded;
}
Color _resolveRightIconColor(ColorScheme colorScheme) {
if (isWaitingAgent || hasMessage) {
return colorScheme.primary;
}
return colorScheme.onSurfaceVariant;
}
}