Files
assistant/pages/index/index.uvue

426 lines
11 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>