Initial commit: AI chat assistant with workflow chat, workspace, and profile tabs

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 17:23:00 +08:00
commit 3fc394921e
34 changed files with 3449 additions and 0 deletions

425
pages/index/index.uvue Normal file
View File

@@ -0,0 +1,425 @@
<template>
<view class="app-root">
<!-- ========== Tab: Workspace ========== -->
<view v-if="activeTab === 'workspace'" class="app-page">
<view class="app-page__header" :style="{ paddingTop: statusBarHeight + 'px' }">
<text class="app-page__title">工作空间</text>
</view>
<scroll-view class="app-page__body" scroll-y="true" show-scrollbar="false">
<view v-if="workspaceItems.length === 0" class="app-empty">
<text class="app-empty__icon">📂</text>
<text class="app-empty__text">暂无工作结果</text>
<text class="app-empty__hint">使用工作流对话生成的结果会显示在这里</text>
</view>
<view v-else class="app-page__content">
<WorkspaceItemComponent
v-for="item in workspaceItems"
:key="item.id"
:item="item"
@tap="onWorkspaceItemTap(item.id)"
/>
</view>
</scroll-view>
</view>
<!-- ========== Tab: Chat ========== -->
<view v-if="activeTab === 'chat'" class="app-page">
<!-- Chat Top Bar -->
<view class="chat-topbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="chat-topbar__inner">
<text class="chat-topbar__title">{{ topBarTitle }}</text>
</view>
</view>
<!-- Messages -->
<MessageList
:messages="messages"
:isLoading="isLoading"
:emptyTitle="emptyTitle"
:emptySubtitle="emptySubtitle"
@retry="onRetryMessage"
/>
<!-- Workflow Selector -->
<WorkflowSelector
:workflows="workflows"
:activeId="activeWorkflowId"
@select="onWorkflowChange"
/>
<!-- Input -->
<InputArea
:disabled="isLoading"
:placeholder="inputPlaceholder"
@send="onSendMessage"
/>
</view>
<!-- ========== Tab: Profile ========== -->
<view v-if="activeTab === 'profile'" class="app-page">
<view class="app-page__header" :style="{ paddingTop: statusBarHeight + 'px' }">
<text class="app-page__title">我的</text>
</view>
<scroll-view class="app-page__body" scroll-y="true" show-scrollbar="false">
<!-- Avatar -->
<view class="profile-avatar-area">
<view class="profile-avatar">
<text class="profile-avatar-icon">🧑‍💻</text>
</view>
<text class="profile-name">用户</text>
<text class="profile-id">AI助手</text>
</view>
<!-- Stats -->
<view class="profile-stats">
<view class="profile-stat">
<text class="profile-stat-val">{{ conversations.length }}</text>
<text class="profile-stat-lbl">对话数</text>
</view>
<view class="profile-stat-div"></view>
<view class="profile-stat">
<text class="profile-stat-val">{{ workspaceItems.length }}</text>
<text class="profile-stat-lbl">工作结果</text>
</view>
</view>
<!-- Menu -->
<view class="profile-menu">
<view class="profile-menu-item" @tap="onProfileMenu('settings')">
<text class="profile-menu-icon">⚙️</text>
<text class="profile-menu-label">设置</text>
<text class="profile-menu-arrow"></text>
</view>
<view class="profile-menu-item" @tap="onProfileMenu('about')">
<text class="profile-menu-icon"></text>
<text class="profile-menu-label">关于</text>
<text class="profile-menu-arrow"></text>
</view>
<view class="profile-menu-item" @tap="onProfileMenu('clear')">
<text class="profile-menu-icon">🗑</text>
<text class="profile-menu-label">清除数据</text>
<text class="profile-menu-arrow"></text>
</view>
</view>
<text class="profile-version">AI助手 v1.0.0</text>
</scroll-view>
</view>
<!-- ========== Bottom Tab Bar ========== -->
<BottomTabBar :activeKey="activeTab" @change="activeTab = $event" />
</view>
</template>
<script setup lang="uts">
import type { WorkflowId, TabKey } from '@/types/index'
import { useWorkflow } from '@/composables/useWorkflow'
import { useConversations } from '@/composables/useConversations'
import { useConversation } from '@/composables/useConversation'
import { useWorkspace } from '@/composables/useWorkspace'
import BottomTabBar from '@/components/BottomTabBar.uvue'
import WorkflowSelector from '@/components/WorkflowSelector.uvue'
import MessageList from '@/components/MessageList.uvue'
import InputArea from '@/components/InputArea.uvue'
import WorkspaceItemComponent from '@/components/WorkspaceItem.uvue'
// ---- Tab state ----
const activeTab = ref<TabKey>('chat')
// ---- Status bar ----
const statusBarHeight = ref(24)
uni.getSystemInfo({
success: (res) => { statusBarHeight.value = res.statusBarHeight ?? 24 },
fail: () => { statusBarHeight.value = 24 },
})
// ---- Composables ----
const { activeWorkflowId, workflows, activeWorkflow, selectWorkflow } = useWorkflow()
const { conversations, deleteConversation, getConversation, saveConversation, createConversation } = useConversations()
const { workspaceItems, addWorkspaceItem } = useWorkspace()
const {
messages,
isLoading,
sendMessage,
retryMessage,
startNewConversation,
} = useConversation(
{ getConversation, saveConversation, createConversation },
() => activeWorkflowId.value,
addWorkspaceItem
)
// ---- Chat tab computed ----
const topBarTitle = computed<string>(() => activeWorkflow.value.name)
const inputPlaceholder = computed<string>(() => {
const m: Record<string, string> = {
general: '输入消息...', code: '描述你的编程问题...',
document: '输入写作需求...', data: '输入数据分析需求...', creative: '描述你的创意想法...',
}
return m[activeWorkflowId.value] ?? '输入消息...'
})
const emptyTitle = computed<string>(() => {
const m: Record<string, string> = {
general: '开始对话', code: '代码助手', document: '文档写作', data: '数据分析', creative: '创意生成',
}
return m[activeWorkflowId.value] ?? '开始对话'
})
const emptySubtitle = computed<string>(() => {
const m: Record<string, string> = {
general: '提出任何问题AI 将为你详细解答',
code: '提出编程问题,获取代码帮助与调试建议',
document: '告诉我你想写什么,我来帮你撰写与润色',
data: '分享你的数据需求,获取分析洞察与建议',
creative: '描述你的目标,一起头脑风暴创意方案',
}
return m[activeWorkflowId.value] ?? '开始与 AI 对话'
})
// ---- Chat handlers ----
function onWorkflowChange(id: WorkflowId): void {
selectWorkflow(id)
startNewConversation(id)
}
function onSendMessage(text: string): void { sendMessage(text) }
function onRetryMessage(messageId: string): void { retryMessage(messageId) }
// ---- Workspace handlers ----
function onWorkspaceItemTap(id: string): void {
const item = workspaceItems.value.find((w) => w.id === id)
if (item) {
uni.showToast({ title: `查看: ${item.title}`, icon: 'none' })
}
}
// ---- Profile handlers ----
function onProfileMenu(key: string): void {
if (key === 'clear') {
uni.showModal({
title: '确认清除',
content: '将清除所有对话历史和工作空间数据,此操作不可恢复。',
success: (res: any) => {
if (res.confirm) {
uni.clearStorageSync()
uni.showToast({ title: '数据已清除', icon: 'success' })
}
},
})
} else {
uni.showToast({ title: `功能开发中: ${key}`, icon: 'none' })
}
}
</script>
<style scoped>
/* ========== Root ========== */
.app-root {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #0f0f1a;
overflow: hidden;
}
/* ========== Page wrapper ========== */
.app-page {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ========== Generic header ========== */
.app-page__header {
padding: 16rpx 32rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.app-page__title {
font-size: 36rpx;
font-weight: 700;
color: rgba(255, 255, 255, 0.92);
}
.app-page__body {
flex: 1;
}
.app-page__content {
padding: 24rpx;
}
/* ========== Empty state ========== */
.app-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64rpx;
}
.app-empty__icon {
font-size: 80rpx;
margin-bottom: 24rpx;
opacity: 0.5;
}
.app-empty__text {
font-size: 30rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 12rpx;
}
.app-empty__hint {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.3);
text-align: center;
}
/* ========== Chat top bar ========== */
.chat-topbar {
background: rgba(15, 15, 26, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.chat-topbar__inner {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
}
.chat-topbar__title {
font-size: 32rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
/* ========== Profile ========== */
.profile-avatar-area {
display: flex;
flex-direction: column;
align-items: center;
padding: 56rpx 0 32rpx;
}
.profile-avatar {
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(108, 92, 231, 0.3);
}
.profile-avatar-icon {
font-size: 56rpx;
}
.profile-name {
font-size: 36rpx;
font-weight: 700;
color: rgba(255, 255, 255, 0.92);
margin-bottom: 6rpx;
}
.profile-id {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.35);
}
.profile-stats {
display: flex;
flex-direction: row;
align-items: center;
padding: 28rpx 48rpx;
margin: 0 24rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.profile-stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.profile-stat-val {
font-size: 40rpx;
font-weight: 700;
color: #a29bfe;
margin-bottom: 4rpx;
}
.profile-stat-lbl {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
}
.profile-stat-div {
width: 1px;
height: 48rpx;
background: rgba(255, 255, 255, 0.1);
}
.profile-menu {
margin: 24rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
overflow: hidden;
}
.profile-menu-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 28rpx 24rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.profile-menu-item:last-child {
border-bottom: none;
}
.profile-menu-item:active {
background: rgba(255, 255, 255, 0.04);
}
.profile-menu-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.profile-menu-label {
flex: 1;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
.profile-menu-arrow {
font-size: 36rpx;
color: rgba(255, 255, 255, 0.25);
}
.profile-version {
text-align: center;
padding: 16rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.2);
display: block;
}
</style>