426 lines
11 KiB
Plaintext
426 lines
11 KiB
Plaintext
|
|
<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>
|