feat: Implement character avatar upload and refactor character form fields and API calls.
This commit is contained in:
@@ -2,30 +2,17 @@ import 'dart:io';
|
||||
|
||||
/// Supabase 配置
|
||||
///
|
||||
/// 这里配置了本地 Docker 部署的 Supabase 连接信息
|
||||
/// 生产环境部署时需要替换为实际的 URL 和 Key
|
||||
/// 这里配置了 Wei AI 线上项目的连接信息
|
||||
class SupabaseConfig {
|
||||
SupabaseConfig._();
|
||||
|
||||
/// Supabase API URL
|
||||
///
|
||||
/// - Android 模拟器: 使用 10.0.2.2 访问主机
|
||||
/// - iOS 模拟器/macOS: 使用 localhost
|
||||
/// - 真机测试: 需要替换为电脑的局域网 IP 地址
|
||||
static String get url {
|
||||
// 真机测试: Android 和 iOS 都使用电脑的局域网 IP 地址
|
||||
// 模拟器测试: Android 用 10.0.2.2,iOS 用 localhost
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
return 'http://192.168.3.118:8000';
|
||||
}
|
||||
// macOS 使用 localhost
|
||||
return 'http://localhost:8000';
|
||||
}
|
||||
static const String url = 'https://ovkwghuaorazlhijhmpj.supabase.co';
|
||||
|
||||
/// Supabase Anon Key
|
||||
/// 这个 key 是公开的,用于客户端访问
|
||||
/// Supabase Anon Key (Publishable Key)
|
||||
/// 这个 key 是公开的,用于客户端安全访问
|
||||
static const String anonKey =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY5NTk1NjI4LCJleHAiOjE5MjcyNzU2Mjh9.moV0JpCSx3Y1QTZmKZ5K-tQLaWcshxtxFlCoIBQFsEU';
|
||||
'sb_publishable_CXBzerv3x8xYMSV0hnfk6w_nuy5pWtE';
|
||||
|
||||
/// 是否启用调试模式
|
||||
static const bool debug = true;
|
||||
|
||||
@@ -71,8 +71,9 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
|
||||
void _scrollToBottom() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
// 使用 reverse: true 后,0.0 就是列表底部(最新消息处)
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
@@ -158,11 +159,20 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
|
||||
|
||||
final avatarUrl = CharacterRepository.getAvatarUrl(_character!.avatarPath);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF2E1065),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF2E1065), Color(0xFF0F172A)],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
flexibleSpace: ClipRRect(
|
||||
@@ -229,30 +239,27 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF2E1065),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF2E1065), Color(0xFF0F172A)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
body: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
reverse: true,
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.only(top: 120, bottom: 20, left: 16, right: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
itemCount: _messages.length + (_isTyping ? 1 : 0),
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 16),
|
||||
itemBuilder: (context, index) {
|
||||
// 如果是正在输入的消息
|
||||
if (_isTyping && index == _messages.length) {
|
||||
return _buildTypingBubble();
|
||||
// reverse: true 模式下,索引 0 是列表的最底部
|
||||
if (_isTyping) {
|
||||
if (index == 0) return _buildTypingBubble();
|
||||
final msg = _messages[_messages.length - index];
|
||||
return _buildMessageBubble(msg, avatarUrl);
|
||||
}
|
||||
|
||||
final msg = _messages[index];
|
||||
final msg = _messages[_messages.length - 1 - index];
|
||||
return _buildMessageBubble(msg, avatarUrl);
|
||||
},
|
||||
),
|
||||
@@ -263,15 +270,16 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
if (_isVoiceMode && _character != null)
|
||||
VoiceModeOverlay(
|
||||
character: _character!,
|
||||
onClose: () => setState(() => _isVoiceMode = false),
|
||||
),
|
||||
],
|
||||
|
||||
|
||||
if (_isVoiceMode && _character != null)
|
||||
VoiceModeOverlay(
|
||||
character: _character!,
|
||||
onClose: () => setState(() => _isVoiceMode = false),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -397,7 +405,10 @@ class _InteractionScreenState extends ConsumerState<InteractionScreen> {
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _isVoiceMode = true),
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
setState(() => _isVoiceMode = true);
|
||||
},
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
|
||||
@@ -32,13 +32,10 @@ CREATE TABLE IF NOT EXISTS characters (
|
||||
|
||||
-- 基本信息
|
||||
name TEXT NOT NULL, -- 角色名称
|
||||
tagline TEXT, -- 角色标语/副标题
|
||||
avatar_path TEXT, -- Storage 中的头像路径
|
||||
description TEXT, -- 角色描述
|
||||
|
||||
-- 状态信息
|
||||
compatibility REAL DEFAULT 0, -- 契合度 (0-100)
|
||||
status TEXT DEFAULT 'online', -- 状态: online, busy, offline
|
||||
is_locked BOOLEAN DEFAULT false, -- 是否锁定(会员限定)
|
||||
is_active BOOLEAN DEFAULT true, -- 是否上架显示
|
||||
sort_order INTEGER DEFAULT 0, -- 排序顺序
|
||||
@@ -76,7 +73,6 @@ CREATE INDEX IF NOT EXISTS idx_tags_category ON tags(category_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_characters_active ON characters(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_characters_locked ON characters(is_locked);
|
||||
CREATE INDEX IF NOT EXISTS idx_characters_status ON characters(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_characters_sort ON characters(sort_order);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_character_tags_character ON character_tags(character_id);
|
||||
|
||||
Reference in New Issue
Block a user