更新新增三个模块

This commit is contained in:
super
2026-03-22 11:20:09 +08:00
parent 74a5fb9ce1
commit 8e66926b9e
112 changed files with 5411 additions and 606 deletions

View File

@@ -12,7 +12,7 @@
<button type="button" class="opt-btn" @click="selectSplitFolder">选择文件夹</button>
</div>
<div class="selected-files clean-placeholder">
<div class="selected-files clean-placeholder split-selected-files">
<template v-if="splitSelectedPaths.length">
<span v-for="path in splitDisplayPaths" :key="path">{{ path }}</span>
<span v-if="splitSelectedPaths.length > splitDisplayPaths.length" class="more-line">
@@ -118,23 +118,27 @@
<div class="result-list-wrap">
<div class="result-list-header">
<span>拆分后的 Excel 列表</span>
<span>拆分结果压缩包列表</span>
</div>
<div v-if="splitResultItems.length === 0" class="empty-tasks">
暂无拆分结果完成数据拆分后会在这里展示输出文件
暂无拆分结果完成数据拆分后会在这里展示压缩包文件
</div>
<ul v-else class="task-list clean-result-list">
<li v-for="item in splitResultItems" :key="`${item.resultId || item.outputFilename || item.sourceFilename}-${item.rowCount || 0}`" class="task-item">
<div class="left">
<li v-for="item in splitResultItems" :key="`${item.resultId || item.outputFilename || item.sourceFilename}-${item.rowCount || 0}`" class="task-item split-result-item">
<div class="left split-result-main">
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
<div v-if="item.outputFilename" class="files">输出文件{{ item.outputFilename }}</div>
<div v-if="item.rowCount !== undefined" class="time">包含 {{ item.rowCount }} 条数据</div>
<div v-else-if="item.error" class="files">错误信息{{ item.error }}</div>
<div v-if="item.outputFilename" class="files">压缩包文件{{ item.outputFilename }}</div>
<div v-if="item.rowCount !== undefined" class="time">压缩包共包含 {{ item.rowCount }} 条数据</div>
<div v-if="item.entryCount && item.entryCount > 0" class="files">压缩包内共 {{ item.entryCount }} 个拆分文件</div>
<div v-if="item.entries && item.entries.length" class="files split-entry-list">
压缩包内容{{ formatSplitEntries(item.entries) }}
</div>
<div v-if="item.error" class="files">错误信息{{ item.error }}</div>
</div>
<div class="task-right">
<div class="task-right split-result-actions">
<span class="status" :class="item.success ? 'success' : 'failed'">
{{ item.success ? '已完成' : '失败' }}
</span>
@@ -144,7 +148,7 @@
class="download"
@click="downloadSplitResult(item)"
>
下载文件
下载压缩包
</button>
<button
v-if="item.resultId"
@@ -168,11 +172,12 @@
import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue'
import { expandBrandFolder } from '@/shared/api/brand'
import { expandBrandFolder, type BrandExpandFolderItem } from '@/shared/api/brand'
import { deleteSplitHistory, getExcelInfo, getSplitHistory, runSplit, type SplitResultItem, type SplitRunVo } from '@/shared/api/java-modules'
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
const splitSelectedPaths = ref<string[]>([])
const splitArchiveName = ref('')
const splitUploadedFiles = ref<UploadedJavaFile[]>([])
const splitAvailableColumns = ref<string[]>([])
const splitSelectedColumns = ref<string[]>([])
@@ -185,16 +190,27 @@ const splitResultItems = ref<SplitResultItem[]>([])
const splitSummary = ref<SplitRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
const splitDisplayPaths = computed(() => splitSelectedPaths.value.slice(0, 8))
async function uploadPathsToJava(paths: string[]) {
function updateSplitSummaryFromItems(items: SplitResultItem[]) {
splitSummary.value = {
total: items.length,
successCount: items.filter((item) => item.success).length,
failedCount: items.filter((item) => !item.success).length,
items,
}
}
async function uploadPathsToJava(paths: Array<string | BrandExpandFolderItem>) {
const api = getPywebviewApi()
if (!api?.upload_file_to_java) {
throw new Error('当前桌面端未提供文件上传桥接能力')
}
const uploaded: UploadedJavaFile[] = []
for (const path of paths) {
const result = await api.upload_file_to_java(path)
for (const item of paths) {
const filePath = typeof item === 'string' ? item : item.absolutePath
const relativePath = typeof item === 'string' ? undefined : item.relativePath
const result = await api.upload_file_to_java(filePath, relativePath)
if (!result?.success || !result.data) {
throw new Error(result?.error || result?.message || `上传失败:${path}`)
throw new Error(result?.error || result?.message || `上传失败:${filePath}`)
}
uploaded.push(result.data)
}
@@ -210,6 +226,7 @@ async function loadSplitInfo(fileKey: string) {
async function handleSelectedPaths(paths: string[], successMessage: string) {
splitSelectedPaths.value = paths
splitArchiveName.value = ''
splitUploadedFiles.value = await uploadPathsToJava(paths)
if (splitUploadedFiles.value.length > 0) {
await loadSplitInfo(splitUploadedFiles.value[0].fileKey)
@@ -243,10 +260,16 @@ async function selectSplitFolder() {
try {
const folder = await api.select_brand_folder()
if (!folder) return
splitArchiveName.value = folder.split(/[/\\]/).filter(Boolean).pop() || ''
const result = await expandBrandFolder(folder)
if (result.success && result.paths?.length) {
await handleSelectedPaths(result.paths, `已选择文件夹内 ${result.paths.length} 个 xlsx 文件`)
if (result.success && result.items?.length) {
splitSelectedPaths.value = result.items.map((item) => item.relativePath)
splitUploadedFiles.value = await uploadPathsToJava(result.items)
if (splitUploadedFiles.value.length > 0) {
await loadSplitInfo(splitUploadedFiles.value[0].fileKey)
}
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 xlsx 文件`)
return
}
@@ -277,11 +300,15 @@ async function submitSplitRun() {
try {
splitRunning.value = true
const result = await runSplit({
files: splitUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename })),
files: splitUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename, relativePath: item.relativePath })),
selectedColumns: splitSelectedColumns.value,
splitMode: splitMode.value,
rowsPerFile: splitMode.value === 'rows_per_file' ? splitRowsPerFile.value || undefined : undefined,
parts: splitMode.value === 'parts' ? splitParts.value || undefined : undefined,
archiveName:
splitUploadedFiles.value.some((item) => !!item.relativePath) && splitArchiveName.value
? splitArchiveName.value
: undefined,
})
splitSummary.value = result
splitResultItems.value = result.items || []
@@ -298,12 +325,7 @@ async function loadSplitHistory() {
try {
const response = await getSplitHistory()
splitResultItems.value = response.items || []
splitSummary.value = {
total: response.items?.length || 0,
successCount: response.items?.filter((item) => item.success).length || 0,
failedCount: response.items?.filter((item) => !item.success).length || 0,
items: response.items || [],
}
updateSplitSummaryFromItems(splitResultItems.value)
} catch {
// ignore history load errors
}
@@ -319,6 +341,17 @@ async function deleteSplitHistoryRecord(resultId: number) {
}
}
function formatSplitEntries(entries: NonNullable<SplitResultItem['entries']>) {
const preview = entries.slice(0, 4).map((entry) => {
if (entry.rowCount !== undefined) {
return `${entry.filename}${entry.rowCount}条)`
}
return entry.filename
})
const hiddenCount = entries.length - preview.length
return hiddenCount > 0 ? `${preview.join('、')}${entries.length} 个文件` : preview.join('、')
}
async function downloadSplitResult(item: SplitResultItem) {
const api = getPywebviewApi()
if (!item.downloadUrl) {
@@ -326,7 +359,7 @@ async function downloadSplitResult(item: SplitResultItem) {
return
}
const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.xlsx`
const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.zip`
if (api?.save_file_from_url) {
const result = await api.save_file_from_url(item.downloadUrl, filename)
if (result.success) {
@@ -362,6 +395,7 @@ onMounted(() => {
.selected-files { margin-top: 14px; font-size: 12px; color: #888; max-height: 112px; overflow-y: auto; text-align: left; }
.selected-files span { display: block; margin: 4px 0; word-break: break-all; }
.more-line { color: #b8c1cc; }
.split-selected-files { max-height: 120px; min-height: 72px; padding-right: 4px; }
.option-group { margin-bottom: 20px; }
.column-option-group { padding: 14px; border-radius: 10px; background: #252525; border: 1px solid #2a2a2a; }
.column-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
@@ -391,11 +425,15 @@ onMounted(() => {
.task-list-wrap { flex: 1; overflow-y: auto; padding: 16px; }
.task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 16px; border: 1px solid #2a2a2a; border-radius: 8px; margin-bottom: 10px; background: #1e1e1e; }
.split-result-item { align-items: flex-start; }
.left { flex: 1; min-width: 0; }
.split-result-main { display: flex; flex-direction: column; gap: 4px; }
.id { display: inline-block; font-weight: 600; color: #e0e0e0; font-size: 13px; }
.files { font-size: 12px; color: #888; margin-top: 4px; }
.time { font-size: 12px; color: #666; margin-top: 2px; }
.split-entry-list { line-height: 1.6; word-break: break-all; max-width: 100%; }
.task-right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
.split-result-actions { flex-shrink: 0; min-width: 180px; justify-content: flex-end; align-self: center; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
@@ -404,12 +442,12 @@ onMounted(() => {
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
.empty-tasks { color: #666; font-size: 13px; padding: 24px 8px; }
.clean-placeholder { max-height: none; }
.clean-placeholder { max-height: 112px; }
.clean-result-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 220px)); gap: 16px; margin-bottom: 18px; }
.summary-card { padding: 16px 18px; border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; }
.summary-card strong { display: block; margin-top: 8px; font-size: 24px; color: #eaf4ff; }
.summary-label { font-size: 12px; color: #8d8d8d; }
.result-list-wrap { border: 1px solid #2a2a2a; border-radius: 10px; background: #1e1e1e; min-height: 260px; }
.result-list-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #2a2a2a; font-size: 14px; color: #ddd; }
@media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .right-panel { min-height: 420px; } .task-item { flex-direction: column; align-items: flex-start; } .task-right { width: 100%; justify-content: flex-start; } .clean-result-summary { grid-template-columns: 1fr; } }
@media (max-width: 1100px) { .main-content { flex-direction: column; height: auto; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2a2a2a; } .right-panel { min-height: 420px; } .task-item { flex-direction: column; align-items: flex-start; } .task-right { width: 100%; justify-content: flex-start; } .split-result-actions { min-width: 0; align-self: flex-start; } .clean-result-summary { grid-template-columns: 1fr; } }
</style>