docs: 更新协议文档,删除废弃计划文档
- 更新 http-error-codes, user-points-chat-data-protocol - 更新 divination-run-protocol, profile-protocol - 删除废弃的后端和前端设计计划文档
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user