feat(web): update components
- PersonalWorkbench.vue: update personal workbench component - SidebarRail.vue: update sidebar rail component - TopBar.vue: update top bar component - ConfirmDialog.vue: update confirm dialog component
This commit is contained in:
@@ -42,27 +42,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="assistant-tools">
|
<div class="assistant-tools">
|
||||||
<button type="button" class="ghost-action" :disabled="Boolean(pendingAction) || confirmDialogBusy" @click="triggerFileUpload">
|
<button type="button" class="ghost-action" :disabled="Boolean(pendingAction)" @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="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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hero-action"
|
class="hero-action"
|
||||||
:disabled="Boolean(pendingAction) || confirmDialogBusy"
|
:disabled="Boolean(pendingAction)"
|
||||||
@click="handleNewConversation"
|
@click="handleExpenseConversationAction"
|
||||||
>
|
>
|
||||||
<i :class="pendingAction === 'new' ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-magnify-scan'"></i>
|
<i :class="pendingAction === 'expense' ? 'mdi mdi-loading mdi-spin' : expenseActionIcon"></i>
|
||||||
<span>{{ pendingAction === 'new' ? '处理中...' : '新建会话' }}</span>
|
<span>{{ pendingAction === 'expense' ? '处理中...' : expenseActionLabel }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,34 +137,19 @@
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
:open="confirmDialogOpen"
|
|
||||||
badge="新建会话"
|
|
||||||
badge-tone="info"
|
|
||||||
title="开始新会话会清空当前历史对话"
|
|
||||||
description="检测到你还有未清除的会话记录。确认后会删除当前用户的全部历史对话,再开启新的智能体会话。"
|
|
||||||
cancel-text="取消"
|
|
||||||
confirm-text="确认删除并新建"
|
|
||||||
busy-text="清空中..."
|
|
||||||
confirm-tone="primary"
|
|
||||||
confirm-icon="mdi mdi-delete-sweep-outline"
|
|
||||||
:busy="confirmDialogBusy"
|
|
||||||
@close="closeConfirmDialog"
|
|
||||||
@confirm="confirmStartNewConversation"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import PanelHead from '../shared/PanelHead.vue'
|
import PanelHead from '../shared/PanelHead.vue'
|
||||||
import ConfirmDialog from '../shared/ConfirmDialog.vue'
|
|
||||||
import robotAssistant from '../../assets/robot-helper.png'
|
import robotAssistant from '../../assets/robot-helper.png'
|
||||||
import { useSystemState } from '../../composables/useSystemState.js'
|
import { useSystemState } from '../../composables/useSystemState.js'
|
||||||
import { useToast } from '../../composables/useToast.js'
|
import { useToast } from '../../composables/useToast.js'
|
||||||
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
showHeader: { type: Boolean, default: true }
|
showHeader: { type: Boolean, default: true },
|
||||||
|
assistantModalOpen: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['openAssistant'])
|
const emit = defineEmits(['openAssistant'])
|
||||||
@@ -183,9 +159,48 @@ const assistantDraft = ref('')
|
|||||||
const fileInputRef = ref(null)
|
const fileInputRef = ref(null)
|
||||||
const selectedFiles = ref([])
|
const selectedFiles = ref([])
|
||||||
const pendingAction = ref('')
|
const pendingAction = ref('')
|
||||||
const confirmDialogOpen = ref(false)
|
const latestExpenseConversation = ref(null)
|
||||||
const confirmDialogBusy = ref(false)
|
const MAX_ATTACHMENTS = 10
|
||||||
const pendingAssistantPayload = ref(null)
|
const SESSION_TYPE_EXPENSE = 'expense'
|
||||||
|
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
||||||
|
|
||||||
|
const hasExpenseConversation = computed(() => Boolean(latestExpenseConversation.value?.conversation_id || latestExpenseConversation.value?.conversationId))
|
||||||
|
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
|
||||||
|
const expenseActionIcon = computed(() => (hasExpenseConversation.value ? 'mdi mdi-history' : 'mdi mdi-magnify-scan'))
|
||||||
|
|
||||||
|
function buildSelectedFileKey(file) {
|
||||||
|
return [file?.name, file?.size, file?.lastModified, file?.type].join('__')
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSelectedFiles(existingFiles, incomingFiles) {
|
||||||
|
const nextFiles = []
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
for (const file of existingFiles) {
|
||||||
|
const key = buildSelectedFileKey(file)
|
||||||
|
if (seen.has(key)) continue
|
||||||
|
seen.add(key)
|
||||||
|
nextFiles.push(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
let overflowCount = 0
|
||||||
|
|
||||||
|
for (const file of incomingFiles) {
|
||||||
|
const key = buildSelectedFileKey(file)
|
||||||
|
if (seen.has(key)) continue
|
||||||
|
if (nextFiles.length >= MAX_ATTACHMENTS) {
|
||||||
|
overflowCount += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen.add(key)
|
||||||
|
nextFiles.push(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: nextFiles,
|
||||||
|
overflowCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveCurrentUserId() {
|
function resolveCurrentUserId() {
|
||||||
const user = currentUser.value || {}
|
const user = currentUser.value || {}
|
||||||
@@ -218,7 +233,7 @@ function emitAssistant(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadLatestConversation() {
|
async function loadLatestConversation() {
|
||||||
const payload = await fetchLatestConversation(resolveCurrentUserId())
|
const payload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE)
|
||||||
return payload?.found ? payload.conversation || null : null
|
return payload?.found ? payload.conversation || null : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +242,7 @@ function handleWorkbenchEnter(event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handleContinueConversation()
|
handleExpenseConversationAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerFileUpload() {
|
function triggerFileUpload() {
|
||||||
@@ -235,87 +250,53 @@ function triggerFileUpload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWorkbenchFilesChange(event) {
|
function handleWorkbenchFilesChange(event) {
|
||||||
selectedFiles.value = Array.from(event.target.files ?? [])
|
const mergeResult = mergeSelectedFiles(selectedFiles.value, Array.from(event.target.files ?? []))
|
||||||
|
selectedFiles.value = mergeResult.files
|
||||||
|
if (mergeResult.overflowCount > 0) {
|
||||||
|
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
||||||
|
}
|
||||||
|
if (fileInputRef.value) {
|
||||||
|
fileInputRef.value.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleContinueConversation() {
|
async function refreshLatestExpenseConversation() {
|
||||||
if (pendingAction.value || confirmDialogBusy.value) {
|
try {
|
||||||
|
latestExpenseConversation.value = await loadLatestConversation()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to refresh latest expense conversation:', error)
|
||||||
|
latestExpenseConversation.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearKnowledgeHistoryBeforeExpense() {
|
||||||
|
await clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExpenseConversationAction() {
|
||||||
|
if (pendingAction.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingAction.value = 'continue'
|
pendingAction.value = 'expense'
|
||||||
const nextPayload = buildAssistantPayload()
|
const nextPayload = buildAssistantPayload()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await clearKnowledgeHistoryBeforeExpense()
|
||||||
const conversation = await loadLatestConversation()
|
const conversation = await loadLatestConversation()
|
||||||
|
latestExpenseConversation.value = conversation
|
||||||
emitAssistant({
|
emitAssistant({
|
||||||
...nextPayload,
|
...nextPayload,
|
||||||
conversation
|
conversation
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load latest conversation:', error)
|
console.warn('Failed to open expense conversation:', error)
|
||||||
emitAssistant(nextPayload)
|
toast(error?.message || '打开报销会话失败,请稍后重试。')
|
||||||
} finally {
|
} finally {
|
||||||
pendingAction.value = ''
|
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) {
|
|
||||||
toast(error?.message || '清空历史会话失败,请稍后重试。')
|
|
||||||
} finally {
|
|
||||||
confirmDialogBusy.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const todoItems = [
|
const todoItems = [
|
||||||
{
|
{
|
||||||
title: '业务招待报销建议补参与人员',
|
title: '业务招待报销建议补参与人员',
|
||||||
@@ -402,6 +383,19 @@ const policyItems = [
|
|||||||
date: '2026-04-25'
|
date: '2026-04-25'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshLatestExpenseConversation()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.assistantModalOpen,
|
||||||
|
(open, previous) => {
|
||||||
|
if (previous && !open) {
|
||||||
|
refreshLatestExpenseConversation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const sidebarMeta = {
|
|||||||
workbench: { label: '个人工作台' },
|
workbench: { label: '个人工作台' },
|
||||||
requests: { label: '个人报销' },
|
requests: { label: '个人报销' },
|
||||||
approval: { label: '审批中心', badge: '12' },
|
approval: { label: '审批中心', badge: '12' },
|
||||||
chat: { label: 'AI 助手' },
|
chat: { label: '财务知识问答' },
|
||||||
policies: { label: '知识管理' },
|
policies: { label: '知识管理' },
|
||||||
audit: { label: '任务规则中心' },
|
audit: { label: '任务规则中心' },
|
||||||
employees: { label: '员工管理' },
|
employees: { label: '员工管理' },
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function handleCancel() {
|
|||||||
.shared-confirm-mask {
|
.shared-confirm-mask {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 1200;
|
z-index: 10020;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
@@ -201,8 +201,8 @@ function handleCancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn.confirm.danger {
|
.shared-confirm-btn.confirm.danger {
|
||||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||||
box-shadow: 0 12px 24px rgba(234, 88, 12, 0.22);
|
box-shadow: 0 12px 24px rgba(220, 38, 38, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn.cancel:hover:not(:disabled) {
|
.shared-confirm-btn.cancel:hover:not(:disabled) {
|
||||||
|
|||||||
Reference in New Issue
Block a user