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:
caoxiaozhu
2026-05-13 13:12:28 +00:00
parent 97b0851e26
commit eec4efe207
4 changed files with 129 additions and 135 deletions

View File

@@ -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>

View File

@@ -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: '员工管理' },

View File

@@ -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) {