feat(web): update components and composables

- PersonalWorkbench.vue: update personal workbench component
- useAppShell.js: update app shell composable
This commit is contained in:
caoxiaozhu
2026-05-12 06:39:26 +00:00
parent c2f208da31
commit f6a5eeb620
2 changed files with 335 additions and 20 deletions

View File

@@ -35,14 +35,34 @@
/>
</div>
<div v-if="selectedFiles.length" class="assistant-file-strip">
<span class="assistant-file-note">已带入 {{ selectedFiles.length }} 份附件</span>
<span v-for="file in selectedFiles" :key="file.name" class="assistant-file-chip">{{ file.name }}</span>
<button type="button" class="assistant-file-clear" @click="clearSelectedFiles">清空</button>
</div>
<div class="assistant-tools">
<button type="button" class="ghost-action" @click="triggerFileUpload">
<button type="button" class="ghost-action" :disabled="Boolean(pendingAction) || confirmDialogBusy" @click="triggerFileUpload">
<i class="mdi mdi-upload-outline"></i>
<span>上传票据</span>
</button>
<button type="button" class="hero-action" @click="openAssistantWithDraft">
<i class="mdi mdi-magnify-scan"></i>
<span>开始识别</span>
<button
type="button"
class="secondary-action"
:disabled="Boolean(pendingAction) || confirmDialogBusy"
@click="handleContinueConversation"
>
<i :class="pendingAction === 'continue' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-history'"></i>
<span>{{ pendingAction === 'continue' ? '恢复中...' : '继续会话' }}</span>
</button>
<button
type="button"
class="hero-action"
:disabled="Boolean(pendingAction) || confirmDialogBusy"
@click="handleNewConversation"
>
<i :class="pendingAction === 'new' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-magnify-scan'"></i>
<span>{{ pendingAction === 'new' ? '处理中...' : '新建会话' }}</span>
</button>
</div>
</div>
@@ -125,26 +145,84 @@
</div>
</article>
</section>
<Teleport to="body">
<Transition name="session-confirm">
<div v-if="confirmDialogOpen" class="session-confirm-mask" @click.self="closeConfirmDialog">
<section class="session-confirm-card">
<span class="session-confirm-badge">新建会话</span>
<h4>开始新会话会清空当前历史对话</h4>
<p>检测到你还有未清除的会话记录确认后会删除当前用户的全部历史对话再开启新的智能体会话</p>
<div class="session-confirm-actions">
<button type="button" class="ghost-action confirm-cancel" :disabled="confirmDialogBusy" @click="closeConfirmDialog">
<span>取消</span>
</button>
<button type="button" class="hero-action confirm-submit" :disabled="confirmDialogBusy" @click="confirmStartNewConversation">
<i :class="confirmDialogBusy ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-delete-sweep-outline'"></i>
<span>{{ confirmDialogBusy ? '清空中...' : '确认删除并新建' }}</span>
</button>
</div>
</section>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
import PanelHead from '../shared/PanelHead.vue'
import robotAssistant from '../../assets/robot-helper.png'
import { useSystemState } from '../../composables/useSystemState.js'
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
defineProps({
showHeader: { type: Boolean, default: true }
})
const emit = defineEmits(['openAssistant'])
const { currentUser } = useSystemState()
const assistantDraft = ref('')
const fileInputRef = ref(null)
const selectedFiles = ref([])
const pendingAction = ref('')
const confirmDialogOpen = ref(false)
const confirmDialogBusy = ref(false)
const pendingAssistantPayload = ref(null)
function openAssistantWithDraft() {
emit('openAssistant', {
function resolveCurrentUserId() {
const user = currentUser.value || {}
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
}
function buildAssistantPayload() {
return {
prompt: assistantDraft.value.trim(),
source: 'workbench'
})
source: 'workbench',
files: Array.from(selectedFiles.value)
}
}
function clearSelectedFiles() {
selectedFiles.value = []
if (fileInputRef.value) {
fileInputRef.value.value = ''
}
}
function resetWorkbenchDraft() {
assistantDraft.value = ''
clearSelectedFiles()
}
function emitAssistant(payload) {
emit('openAssistant', payload)
resetWorkbenchDraft()
}
async function loadLatestConversation() {
const payload = await fetchLatestConversation(resolveCurrentUserId())
return payload?.found ? payload.conversation || null : null
}
function handleWorkbenchEnter(event) {
@@ -152,7 +230,7 @@ function handleWorkbenchEnter(event) {
return
}
openAssistantWithDraft()
handleContinueConversation()
}
function triggerFileUpload() {
@@ -160,16 +238,85 @@ function triggerFileUpload() {
}
function handleWorkbenchFilesChange(event) {
const files = Array.from(event.target.files ?? [])
if (!files.length) return
selectedFiles.value = Array.from(event.target.files ?? [])
}
emit('openAssistant', {
prompt: assistantDraft.value.trim(),
source: 'upload',
files
})
async function handleContinueConversation() {
if (pendingAction.value || confirmDialogBusy.value) {
return
}
event.target.value = ''
pendingAction.value = 'continue'
const nextPayload = buildAssistantPayload()
try {
const conversation = await loadLatestConversation()
emitAssistant({
...nextPayload,
conversation
})
} catch (error) {
console.warn('Failed to load latest conversation:', error)
emitAssistant(nextPayload)
} finally {
pendingAction.value = ''
}
}
async function handleNewConversation() {
if (pendingAction.value || confirmDialogBusy.value) {
return
}
pendingAction.value = 'new'
const nextPayload = buildAssistantPayload()
try {
const conversation = await loadLatestConversation()
if (!conversation) {
emitAssistant(nextPayload)
pendingAction.value = ''
return
}
pendingAssistantPayload.value = nextPayload
confirmDialogOpen.value = true
} catch (error) {
console.warn('Failed to inspect latest conversation before reset:', error)
emitAssistant(nextPayload)
pendingAction.value = ''
}
}
function closeConfirmDialog() {
if (confirmDialogBusy.value) {
return
}
confirmDialogOpen.value = false
pendingAssistantPayload.value = null
pendingAction.value = ''
}
async function confirmStartNewConversation() {
if (confirmDialogBusy.value) {
return
}
confirmDialogBusy.value = true
try {
await clearUserConversations(resolveCurrentUserId())
const nextPayload = pendingAssistantPayload.value || buildAssistantPayload()
confirmDialogOpen.value = false
pendingAssistantPayload.value = null
pendingAction.value = ''
emitAssistant(nextPayload)
} catch (error) {
window.alert(error?.message || '清空历史会话失败,请稍后重试。')
} finally {
confirmDialogBusy.value = false
}
}
const todoItems = [
@@ -434,6 +581,7 @@ const policyItems = [
}
.hero-action,
.secondary-action,
.ghost-action,
.row-action,
.link-action,
@@ -459,6 +607,7 @@ const policyItems = [
}
.hero-action .mdi,
.secondary-action .mdi,
.ghost-action .mdi {
display: inline-flex;
align-items: center;
@@ -468,6 +617,7 @@ const policyItems = [
}
.hero-action span,
.secondary-action span,
.ghost-action span {
display: inline-flex;
align-items: center;
@@ -481,6 +631,48 @@ const policyItems = [
flex-wrap: wrap;
}
.assistant-file-strip {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.assistant-file-note,
.assistant-file-chip {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.assistant-file-note {
background: rgba(16, 185, 129, 0.1);
color: #047857;
}
.assistant-file-chip {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border: 1px solid rgba(148, 163, 184, 0.24);
background: rgba(255, 255, 255, 0.9);
color: #475569;
}
.assistant-file-clear {
border: 0;
background: transparent;
color: #64748b;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.ghost-action {
height: 40px;
display: inline-flex;
@@ -503,6 +695,114 @@ const policyItems = [
color: #10b981;
}
.secondary-action {
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 16px;
border: 1px solid rgba(59, 130, 246, 0.18);
border-radius: 10px;
background: linear-gradient(180deg, rgba(244, 249, 255, 0.96), rgba(234, 244, 255, 0.9));
color: #1d4ed8;
font-size: 14px;
font-weight: 700;
white-space: nowrap;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.92),
0 6px 14px rgba(37, 99, 235, 0.08);
}
.secondary-action .mdi {
color: #2563eb;
}
.hero-action:disabled,
.secondary-action:disabled,
.ghost-action:disabled {
cursor: not-allowed;
opacity: 0.68;
box-shadow: none;
}
.session-confirm-mask {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 24px;
background: rgba(15, 23, 42, 0.32);
backdrop-filter: blur(10px);
}
.session-confirm-card {
width: min(480px, 100%);
display: grid;
gap: 14px;
padding: 24px;
border: 1px solid rgba(16, 185, 129, 0.14);
border-radius: 24px;
background:
radial-gradient(circle at top left, rgba(16, 185, 129, 0.12), transparent 36%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 252, 0.98));
box-shadow: 0 28px 56px rgba(15, 23, 42, 0.18);
}
.session-confirm-badge {
display: inline-flex;
width: fit-content;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
background: rgba(59, 130, 246, 0.12);
color: #1d4ed8;
font-size: 12px;
font-weight: 800;
}
.session-confirm-card h4 {
color: #0f172a;
font-size: 22px;
line-height: 1.35;
font-weight: 800;
}
.session-confirm-card p {
color: #5b6b83;
font-size: 14px;
line-height: 1.7;
}
.session-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.confirm-cancel,
.confirm-submit {
min-width: 140px;
}
.session-confirm-enter-active,
.session-confirm-leave-active {
transition: opacity 0.18s ease, transform 0.18s ease;
}
.session-confirm-enter-from,
.session-confirm-leave-to {
opacity: 0;
}
.session-confirm-enter-from .session-confirm-card,
.session-confirm-leave-to .session-confirm-card {
transform: translateY(8px) scale(0.98);
}
.workbench-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -826,12 +1126,26 @@ const policyItems = [
}
.hero-action,
.secondary-action,
.ghost-action,
.row-action {
width: 100%;
justify-content: center;
}
.assistant-file-chip {
max-width: 100%;
}
.session-confirm-actions {
justify-content: stretch;
}
.confirm-cancel,
.confirm-submit {
width: 100%;
}
.todo-row,
.progress-row {
grid-template-columns: 48px minmax(0, 1fr);

View File

@@ -14,7 +14,7 @@ export function useAppShell() {
const travelCreateMode = ref(false)
const smartEntryOpen = ref(false)
const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [] })
const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null })
const smartEntrySessionId = ref(0)
const { activeView, currentView, setView } = useNavigation()
@@ -93,7 +93,7 @@ export function useAppShell() {
function openTravelCreate() {
smartEntryOpen.value = true
travelCreateMode.value = false
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [] }
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null }
smartEntrySessionId.value += 1
}
@@ -105,7 +105,8 @@ export function useAppShell() {
prompt: payload.prompt ?? '',
source: payload.source ?? 'workbench',
request: payload.request ?? selectedTravelRequest.value,
files: Array.isArray(payload.files) ? payload.files : []
files: Array.isArray(payload.files) ? payload.files : [],
conversation: payload.conversation ?? null
}
smartEntrySessionId.value += 1
}