feat(web): update components and composables
- PersonalWorkbench.vue: update personal workbench component - useAppShell.js: update app shell composable
This commit is contained in:
@@ -35,14 +35,34 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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>
|
<i class="mdi mdi-upload-outline"></i>
|
||||||
<span>上传票据</span>
|
<span>上传票据</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="hero-action" @click="openAssistantWithDraft">
|
<button
|
||||||
<i class="mdi mdi-magnify-scan"></i>
|
type="button"
|
||||||
<span>开始识别</span>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,26 +145,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import PanelHead from '../shared/PanelHead.vue'
|
import PanelHead from '../shared/PanelHead.vue'
|
||||||
import robotAssistant from '../../assets/robot-helper.png'
|
import robotAssistant from '../../assets/robot-helper.png'
|
||||||
|
import { useSystemState } from '../../composables/useSystemState.js'
|
||||||
|
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
showHeader: { type: Boolean, default: true }
|
showHeader: { type: Boolean, default: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['openAssistant'])
|
const emit = defineEmits(['openAssistant'])
|
||||||
|
const { currentUser } = useSystemState()
|
||||||
const assistantDraft = ref('')
|
const assistantDraft = ref('')
|
||||||
const fileInputRef = ref(null)
|
const fileInputRef = ref(null)
|
||||||
|
const selectedFiles = ref([])
|
||||||
|
const pendingAction = ref('')
|
||||||
|
const confirmDialogOpen = ref(false)
|
||||||
|
const confirmDialogBusy = ref(false)
|
||||||
|
const pendingAssistantPayload = ref(null)
|
||||||
|
|
||||||
function openAssistantWithDraft() {
|
function resolveCurrentUserId() {
|
||||||
emit('openAssistant', {
|
const user = currentUser.value || {}
|
||||||
|
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssistantPayload() {
|
||||||
|
return {
|
||||||
prompt: assistantDraft.value.trim(),
|
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) {
|
function handleWorkbenchEnter(event) {
|
||||||
@@ -152,7 +230,7 @@ function handleWorkbenchEnter(event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
openAssistantWithDraft()
|
handleContinueConversation()
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerFileUpload() {
|
function triggerFileUpload() {
|
||||||
@@ -160,16 +238,85 @@ function triggerFileUpload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWorkbenchFilesChange(event) {
|
function handleWorkbenchFilesChange(event) {
|
||||||
const files = Array.from(event.target.files ?? [])
|
selectedFiles.value = Array.from(event.target.files ?? [])
|
||||||
if (!files.length) return
|
}
|
||||||
|
|
||||||
emit('openAssistant', {
|
async function handleContinueConversation() {
|
||||||
prompt: assistantDraft.value.trim(),
|
if (pendingAction.value || confirmDialogBusy.value) {
|
||||||
source: 'upload',
|
return
|
||||||
files
|
}
|
||||||
})
|
|
||||||
|
|
||||||
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 = [
|
const todoItems = [
|
||||||
@@ -434,6 +581,7 @@ const policyItems = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-action,
|
.hero-action,
|
||||||
|
.secondary-action,
|
||||||
.ghost-action,
|
.ghost-action,
|
||||||
.row-action,
|
.row-action,
|
||||||
.link-action,
|
.link-action,
|
||||||
@@ -459,6 +607,7 @@ const policyItems = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-action .mdi,
|
.hero-action .mdi,
|
||||||
|
.secondary-action .mdi,
|
||||||
.ghost-action .mdi {
|
.ghost-action .mdi {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -468,6 +617,7 @@ const policyItems = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-action span,
|
.hero-action span,
|
||||||
|
.secondary-action span,
|
||||||
.ghost-action span {
|
.ghost-action span {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -481,6 +631,48 @@ const policyItems = [
|
|||||||
flex-wrap: wrap;
|
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 {
|
.ghost-action {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -503,6 +695,114 @@ const policyItems = [
|
|||||||
color: #10b981;
|
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 {
|
.workbench-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -826,12 +1126,26 @@ const policyItems = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-action,
|
.hero-action,
|
||||||
|
.secondary-action,
|
||||||
.ghost-action,
|
.ghost-action,
|
||||||
.row-action {
|
.row-action {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assistant-file-chip {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-confirm-actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-cancel,
|
||||||
|
.confirm-submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.todo-row,
|
.todo-row,
|
||||||
.progress-row {
|
.progress-row {
|
||||||
grid-template-columns: 48px minmax(0, 1fr);
|
grid-template-columns: 48px minmax(0, 1fr);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function useAppShell() {
|
|||||||
|
|
||||||
const travelCreateMode = ref(false)
|
const travelCreateMode = ref(false)
|
||||||
const smartEntryOpen = 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 smartEntrySessionId = ref(0)
|
||||||
|
|
||||||
const { activeView, currentView, setView } = useNavigation()
|
const { activeView, currentView, setView } = useNavigation()
|
||||||
@@ -93,7 +93,7 @@ export function useAppShell() {
|
|||||||
function openTravelCreate() {
|
function openTravelCreate() {
|
||||||
smartEntryOpen.value = true
|
smartEntryOpen.value = true
|
||||||
travelCreateMode.value = false
|
travelCreateMode.value = false
|
||||||
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [] }
|
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null }
|
||||||
smartEntrySessionId.value += 1
|
smartEntrySessionId.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,8 @@ export function useAppShell() {
|
|||||||
prompt: payload.prompt ?? '',
|
prompt: payload.prompt ?? '',
|
||||||
source: payload.source ?? 'workbench',
|
source: payload.source ?? 'workbench',
|
||||||
request: payload.request ?? selectedTravelRequest.value,
|
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
|
smartEntrySessionId.value += 1
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user