feat(web): AI 工作台附件改为卡片化展示并支持单项移除
- PersonalWorkbenchAiMode 附件区由计数条改为按类型图标/名称/类型标签的卡片列表,支持单项移除(removeAiModeFile),复用 buildFileIdentity 作为 key - resolveAiComposerFileType 按 pdf/图片/表格/文档/压缩包/文件归类,分别对应图标与色调 - .gitignore 补充忽略 server/storage/receipt_folder/ 运行时票据存储目录
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,6 +21,7 @@ server/.secrets/
|
||||
server/logs/
|
||||
server/storage/expense_claims/
|
||||
server/storage/finance_reports/
|
||||
server/storage/receipt_folder/
|
||||
test-results/
|
||||
.codex-remote-attachments/
|
||||
tmp-*.png
|
||||
|
||||
@@ -175,9 +175,25 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="selectedFiles.length" class="workbench-ai-file-strip">
|
||||
<span>已选择 {{ selectedFiles.length }} 份附件</span>
|
||||
<button type="button" :disabled="isAiModeInputLocked" @click="clearAiModeFiles">清空</button>
|
||||
<div v-if="selectedFileCards.length" class="workbench-ai-file-strip" aria-label="已选择附件">
|
||||
<article v-for="file in selectedFileCards" :key="file.key" class="workbench-ai-file-card">
|
||||
<span class="workbench-ai-file-card__icon" :class="`type-${file.tone}`" aria-hidden="true">
|
||||
<i :class="file.icon"></i>
|
||||
</span>
|
||||
<span class="workbench-ai-file-card__body">
|
||||
<strong :title="file.name">{{ file.name }}</strong>
|
||||
<small>{{ file.typeLabel }}</small>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="workbench-ai-file-card__remove"
|
||||
:disabled="isAiModeInputLocked"
|
||||
:aria-label="`移除附件 ${file.name}`"
|
||||
@click="removeAiModeFile(file.key)"
|
||||
>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="workbench-ai-quick-start-section">
|
||||
@@ -469,9 +485,25 @@
|
||||
</div>
|
||||
|
||||
<div class="workbench-ai-conversation-bottom">
|
||||
<div v-if="selectedFiles.length" class="workbench-ai-file-strip inline">
|
||||
<span>已选择 {{ selectedFiles.length }} 份附件</span>
|
||||
<button type="button" :disabled="isAiModeInputLocked" @click="clearAiModeFiles">清空</button>
|
||||
<div v-if="selectedFileCards.length" class="workbench-ai-file-strip inline" aria-label="已选择附件">
|
||||
<article v-for="file in selectedFileCards" :key="file.key" class="workbench-ai-file-card">
|
||||
<span class="workbench-ai-file-card__icon" :class="`type-${file.tone}`" aria-hidden="true">
|
||||
<i :class="file.icon"></i>
|
||||
</span>
|
||||
<span class="workbench-ai-file-card__body">
|
||||
<strong :title="file.name">{{ file.name }}</strong>
|
||||
<small>{{ file.typeLabel }}</small>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="workbench-ai-file-card__remove"
|
||||
:disabled="isAiModeInputLocked"
|
||||
:aria-label="`移除附件 ${file.name}`"
|
||||
@click="removeAiModeFile(file.key)"
|
||||
>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<form class="workbench-ai-composer workbench-ai-composer--inline" @submit.prevent="submitAiModePrompt">
|
||||
@@ -742,6 +774,7 @@ import {
|
||||
buildRequiredApplicationSelectionText,
|
||||
filterRequiredApplicationCandidates
|
||||
} from '../../views/scripts/travelReimbursementApplicationLinkModel.js'
|
||||
import { buildFileIdentity } from '../../views/scripts/travelReimbursementAttachmentModel.js'
|
||||
import {
|
||||
calculateTravelReimbursement,
|
||||
extractExpenseClaimItems,
|
||||
@@ -787,6 +820,14 @@ const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6
|
||||
const INLINE_ANSWER_STREAM_DELAY_MS = 24
|
||||
const INLINE_AUTO_SCROLL_THRESHOLD = 96
|
||||
const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260
|
||||
const AI_COMPOSER_FILE_TYPE_META = {
|
||||
pdf: { label: 'PDF', icon: 'mdi mdi-file-pdf-box', tone: 'pdf' },
|
||||
image: { label: '图片', icon: 'mdi mdi-file-image-outline', tone: 'image' },
|
||||
spreadsheet: { label: '表格', icon: 'mdi mdi-file-excel-outline', tone: 'spreadsheet' },
|
||||
document: { label: '文档', icon: 'mdi mdi-file-document-outline', tone: 'document' },
|
||||
archive: { label: '压缩包', icon: 'mdi mdi-folder-zip-outline', tone: 'archive' },
|
||||
file: { label: '文件', icon: 'mdi mdi-file-outline', tone: 'file' }
|
||||
}
|
||||
const {
|
||||
applicationPreviewEditor,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
@@ -854,6 +895,12 @@ const aiModeActionItems = [
|
||||
}
|
||||
]
|
||||
|
||||
const selectedFileCards = computed(() => selectedFiles.value.map((file) => ({
|
||||
key: buildFileIdentity(file),
|
||||
name: resolveAiComposerFileName(file),
|
||||
...resolveAiComposerFileType(file)
|
||||
})))
|
||||
|
||||
const displayUserName = computed(() => {
|
||||
const user = currentUser.value || {}
|
||||
return String(user.name || user.username || '同事').trim() || '同事'
|
||||
@@ -2815,6 +2862,32 @@ function markInlineMessageFeedback(message, feedback) {
|
||||
toast(feedback === 'up' ? '已记录有帮助反馈。' : '已记录需要改进反馈。')
|
||||
}
|
||||
|
||||
function resolveAiComposerFileName(file) {
|
||||
return String(file?.name || '未命名附件').trim() || '未命名附件'
|
||||
}
|
||||
|
||||
function resolveAiComposerFileType(file) {
|
||||
const fileName = resolveAiComposerFileName(file).toLowerCase()
|
||||
const mimeType = String(file?.type || '').toLowerCase()
|
||||
const extension = fileName.includes('.') ? fileName.split('.').pop() : ''
|
||||
if (extension === 'pdf' || mimeType.includes('pdf')) {
|
||||
return AI_COMPOSER_FILE_TYPE_META.pdf
|
||||
}
|
||||
if (/^(png|jpe?g|gif|webp|bmp|svg|heic)$/.test(extension) || mimeType.startsWith('image/')) {
|
||||
return AI_COMPOSER_FILE_TYPE_META.image
|
||||
}
|
||||
if (/^(xls|xlsx|csv|numbers)$/.test(extension) || mimeType.includes('spreadsheet') || mimeType.includes('excel')) {
|
||||
return AI_COMPOSER_FILE_TYPE_META.spreadsheet
|
||||
}
|
||||
if (/^(doc|docx|txt|md|pages)$/.test(extension) || mimeType.includes('word') || mimeType.includes('text')) {
|
||||
return AI_COMPOSER_FILE_TYPE_META.document
|
||||
}
|
||||
if (/^(zip|rar|7z|tar|gz)$/.test(extension) || mimeType.includes('zip') || mimeType.includes('compressed')) {
|
||||
return AI_COMPOSER_FILE_TYPE_META.archive
|
||||
}
|
||||
return AI_COMPOSER_FILE_TYPE_META.file
|
||||
}
|
||||
|
||||
function triggerAiModeFileUpload() {
|
||||
if (isAiModeInputLocked.value) {
|
||||
toast('请等待费用测算完成后再继续操作。')
|
||||
@@ -2831,6 +2904,14 @@ function handleAiModeFilesChange(event) {
|
||||
focusAiModeInput()
|
||||
}
|
||||
|
||||
function removeAiModeFile(fileKey) {
|
||||
selectedFiles.value = selectedFiles.value.filter((file) => buildFileIdentity(file) !== fileKey)
|
||||
if (!selectedFiles.value.length && fileInputRef.value) {
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
focusAiModeInput()
|
||||
}
|
||||
|
||||
function clearAiModeFiles() {
|
||||
selectedFiles.value = []
|
||||
if (fileInputRef.value) {
|
||||
|
||||
Reference in New Issue
Block a user