Commit 85482980 by 焦子成

1

parent d36acf7e
var apiHost = "";
var domain = "";
let env = process.env.NODE_ENV || "";
if (env == "production") {
apiHost = "https://taskh5.htangcloud.com";
} else if (env == "test") {
apiHost = "https://testapp.rongketech.com";
} else {
apiHost = "http://39.107.245.36:8081";
domain = '/api'
}
console.log(
`-----env[${env}]-----, apiHost[${apiHost}]`
);
const config = {
apiHost,
domain,
login: `${domain}/user/login`,
register: `${domain}/user/register`,
uploadBatchList: `${domain}/uploadBatch/pageList`,
uploadBatchQueryUpload: `${domain}/uploadBatch/queryUpload`,
uploadBatchDelete: `${domain}/uploadBatch/delete`,
uploadDetailList: `${domain}/uploadDetail/pageList`,
uploadBatchFileUpload: `${domain}/uploadBatch/fileUpload`,
uploadDetailTotalNum: `${domain}/uploadDetail/totalNum`,
uploadDetailDeleteAll: `${domain}/uploadDetail/deleteAll`,
uploadDetailDelete: `${domain}/uploadDetail/delete`,
downLoadStatus: `${domain}/uploadDetail/downLoadStatus`,
fileUploadDetail: `${domain}/uploadBatch/fileUploadDetail`,
};
export default config;
......@@ -7,6 +7,7 @@ import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
import './style.css'
import axios from 'axios'
const app = createApp(App)
const pinia = createPinia()
......@@ -16,5 +17,6 @@ app.use(router)
app.use(ElementPlus, {
locale: zhCn,
})
// app.prototype.$http = axios
app.mount('#app')
......@@ -42,21 +42,21 @@ const router = createRouter({
})
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
// router.beforeEach((to, from, next) => {
// const authStore = useAuthStore()
// 初始化认证状态
if (!authStore.isAuthenticated) {
authStore.initialize()
}
// // 初始化认证状态
// if (!authStore.isAuthenticated) {
// authStore.initialize()
// }
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next('/login')
} else if (to.path === '/login' && authStore.isAuthenticated) {
next('/downloader')
} else {
next()
}
})
// if (to.meta.requiresAuth && !authStore.isAuthenticated) {
// next('/login')
// } else if (to.path === '/login' && authStore.isAuthenticated) {
// next('/downloader')
// } else {
// next()
// }
// })
export default router
......@@ -58,18 +58,18 @@ export const useAuthStore = defineStore('auth', () => {
}
// 检查用户名是否已存在
const isUsernameExists = (username) => {
return users.value.some(user => user.username === username)
const isUsernameExists = (userName) => {
return users.value.some(user => user.userName === userName)
}
// 用户注册
const register = async (username, password, email = '') => {
const register = async (userName, password, email = '') => {
// 验证用户名
if (!username || username.trim().length < 3) {
if (!userName || userName.trim().length < 3) {
throw new Error('用户名至少需要3个字符')
}
if (isUsernameExists(username)) {
if (isUsernameExists(userName)) {
throw new Error('用户名已存在')
}
......@@ -82,7 +82,7 @@ export const useAuthStore = defineStore('auth', () => {
// 创建新用户
const newUser = {
id: Date.now().toString(),
username: username.trim(),
userName: userName.trim(),
password: btoa(password), // 简单的Base64编码(实际项目中应使用更安全的加密)
email: email.trim(),
createdAt: new Date().toISOString(),
......@@ -102,9 +102,9 @@ export const useAuthStore = defineStore('auth', () => {
}
// 用户登录
const login = async (username, password) => {
const login = async (userName, password) => {
try {
console.log('Auth store login 被调用,用户名:', username)
console.log('Auth store login 被调用,用户名:', userName)
// 确保用户数据已加载
if (users.value.length === 0) {
......@@ -112,17 +112,19 @@ export const useAuthStore = defineStore('auth', () => {
loadUsersFromStorage()
}
console.log('当前用户列表:', users.value.map(u => u.username))
console.log('当前用户列表:', users.value.map(u => u.userName))
// 查找用户
const user = users.value.find(u => u.username === username)
let user = users.value.find(u => u.userName === userName)
console.log('--->查找用户:', user)
if (!user) {
console.log('未找到用户:', username)
throw new Error('用户名或密码错误')
console.log('未找到用户:', userName)
// user.value.userName = userName
// user.value.password = password
// throw new Error('用户名或密码错误')
}
console.log('找到用户:', user.username)
console.log('找到用户:', user.userName)
console.log('输入的密码:', password)
console.log('输入的密码编码后:', btoa(password))
console.log('存储的密码:', user.password)
......@@ -130,7 +132,7 @@ export const useAuthStore = defineStore('auth', () => {
// 验证密码
if (user.password !== btoa(password)) {
console.log('密码不匹配')
throw new Error('用户名或密码错误')
// throw new Error('用户名或密码错误')
}
console.log('密码验证成功')
......@@ -272,7 +274,7 @@ export const useAuthStore = defineStore('auth', () => {
const adminPassword = '123456'
const adminUser = {
id: 'admin',
username: 'admin',
userName: 'admin',
password: btoa(adminPassword),
email: '',
createdAt: new Date().toISOString(),
......@@ -290,7 +292,7 @@ export const useAuthStore = defineStore('auth', () => {
console.log('管理员密码编码后:', btoa(adminPassword))
}
console.log('当前用户列表:', users.value.map(u => u.username))
console.log('当前用户列表:', users.value.map(u => u.userName))
// 尝试恢复当前用户会话
try {
......@@ -301,7 +303,7 @@ export const useAuthStore = defineStore('auth', () => {
const existingUser = users.value.find(u => u.id === userData.id)
if (existingUser) {
setCurrentUser(existingUser)
console.log('用户会话已恢复:', existingUser.username)
console.log('用户会话已恢复:', existingUser.userName)
}
}
} catch (error) {
......
// import Vue from 'vue'
import axios from 'axios'
export default {
post(url, params = {}) {
return new Promise((resolve, reject) => {
axios.post(url, params || {}, {
headers: {
'content-type': 'application/json',
'Authorization': sessionStorage.getItem("token") || '',
}
})
.then(res => {
resolve(res.data)
})
.catch(err => {
reject(err.data)
})
})
},
// get(url, params) {
// return new Promise((resolve, reject) => {
// axios.get(url, {
// params: params
// }).then(res => {
// resolve(res.data)
// }).catch(err => {
// reject(err.data)
// })
// })
// }
}
\ No newline at end of file
......@@ -3,15 +3,21 @@
<!-- 头部 -->
<header class="header">
<div class="logo">
<el-icon><Download /></el-icon>
<el-icon>
<Download />
</el-icon>
文件下载器
</div>
<div style="display: flex; align-items: center; gap: 20px;">
<el-dropdown @command="handleUserCommand">
<span class="user-info">
<el-icon><User /></el-icon>
{{ authStore.user?.username }}
<el-icon><ArrowDown /></el-icon>
<el-icon>
<User />
</el-icon>
{{ authStore.user?.userName }}
<el-icon>
<ArrowDown />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
......@@ -26,162 +32,127 @@
<!-- 主要内容 -->
<div class="main-content">
<!-- 配置区域(可折叠) -->
<el-collapse v-model="activeCollapse" class="config-collapse">
<el-collapse-item title="📁 Excel文件管理" name="upload">
<!-- Excel文件列表 -->
<div v-if="downloadStore.excelFiles.length > 0" style="margin-bottom: 20px;">
<h4 style="margin-bottom: 15px; color: #333;">已上传的Excel文件 ({{ downloadStore.excelFiles.length }}个)</h4>
<el-table :data="downloadStore.excelFiles" size="small" border style="width: 100%">
<el-table-column prop="fileName" label="文件名" />
<el-table-column prop="totalRows" label="总行数" width="100" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="180">
<template #default="{ row }">
{{ new Date(row.uploadTime).toLocaleString() }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template #default="{ row }">
<el-button @click="selectExcelFile(row.id)" type="primary" size="small">
选择
</el-button>
<el-button @click="deleteExcelFile(row.id)" type="danger" size="small">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 配置区域(可折叠) -->
<el-collapse v-model="activeCollapse" class="config-collapse">
<el-collapse-item title="📁 Excel文件管理" name="upload">
<!-- Excel文件列表 -->
<!-- v-if="downloadStore.excelFiles.length > 0" -->
<div v-if="upTotal > 0" style="margin-bottom: 20px;">
<h4 style="margin-bottom: 15px; color: #333;">已上传的Excel文件 ({{ upTotal }}个)</h4>
<el-table :data="upTableData" size="small" border style="width: 100%">
<el-table-column prop="uploadName" label="文件名" />
<el-table-column prop="totalRows" label="总行数" width="100" align="center" />
<el-table-column prop="createTime" label="上传时间" width="180">
<!-- <template #default="{ row }">
{{ new Date(row.uploadTime).toLocaleString() }}
</template> -->
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template #default="{ row }">
<el-button @click="selectExcelFile(row.batchId)" type="primary" size="small">
选择
</el-button>
<el-button @click="deleteExcelFile(row.batchId)" type="danger" size="small">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination v-model:current-page="upQuery.current" v-model:page-size="upQuery.pageSize"
:page-sizes="[10, 20, 50, 100]" :total="upTotal"
layout="total, sizes, prev, pager, next, jumper" @size-change="handleUpSizeChange"
@current-change="handleUpCurrentChange" />
</div>
<el-divider />
<!-- Excel文件上传配置 -->
<div class="upload-section">
<el-upload
ref="uploadRef"
:auto-upload="false"
:show-file-list="false"
accept=".xlsx,.xls"
:on-change="handleFileChange"
>
</div>
<el-divider />
<!-- Excel文件上传配置 -->
<div class="upload-section">
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls"
:on-change="handleFileChange">
<el-button type="primary" size="large">
<el-icon><Upload /></el-icon>
<el-icon>
<Upload />
</el-icon>
选择Excel文件
</el-button>
</el-upload>
<div v-if="selectedFile" style="margin-top: 15px;">
<el-alert
:title="`已选择文件: ${selectedFile.name}`"
type="success"
:closable="false"
/>
<el-alert :title="`已选择文件: ${selectedFile.name}`" type="success" :closable="false" />
</div>
<!-- Excel列选择 -->
<div v-if="excelColumns.length > 0" style="margin-top: 20px;">
<h4 style="margin-bottom: 15px; color: #333;">配置Excel列映射</h4>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="文件名列(可多选,用'-'连接)">
<el-select
v-model="columnMapping.fileNameColumns"
multiple
placeholder="选择文件名列"
style="width: 100%"
@change="handleColumnMappingChange"
>
<el-option
v-for="column in excelColumns"
:key="column"
:label="column"
:value="column"
/>
<el-select v-model="columnMapping.fileNameColumns" multiple placeholder="选择文件名列" style="width: 100%"
@change="handleColumnMappingChange">
<el-option v-for="column in excelColumns" :key="column" :label="column" :value="column" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="URL列">
<el-select
v-model="columnMapping.url"
placeholder="选择URL列"
style="width: 100%"
@change="handleColumnMappingChange"
>
<el-option
v-for="column in excelColumns"
:key="column"
:label="column"
:value="column"
/>
<el-select v-model="columnMapping.url" placeholder="选择URL列" style="width: 100%"
@change="handleColumnMappingChange">
<el-option v-for="column in excelColumns" :key="column" :label="column" :value="column" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 预览数据 -->
<div v-if="previewData.length > 0" style="margin-top: 15px;">
<h5 style="margin-bottom: 10px; color: #666;">
数据预览 (前5行) - 总计: {{ rawExcelData.length }} 行
</h5>
<el-table :data="previewData" size="small" border style="width: 100%">
<el-table-column prop="fileName" label="文件名" />
<el-table-column prop="url" label="URL" show-overflow-tooltip />
</el-table>
<div v-if="rawExcelData.length > 1000" style="margin-top: 10px;">
<el-alert
title="文件较大提示"
type="warning"
:closable="false"
show-icon
>
<template #default>
<div style="font-size: 12px; line-height: 1.5;">
<p>• 当前文件包含 {{ rawExcelData.length }} 行数据</p>
<p>• 已优化:只保留 {{ Object.keys(rawExcelData[0] || {}).length }} 个必要字段</p>
<p> 大文件处理可能需要较长时间,请耐心等待</p>
<p> 建议分批处理或使用较小的Excel文件</p>
</div>
</template>
</el-alert>
</div>
</div>
<!-- 预览数据 -->
<div v-if="previewData.length > 0" style="margin-top: 15px;">
<h5 style="margin-bottom: 10px; color: #666;">
数据预览 (前5行) - 总计: {{ rawExcelData.length }} 行
</h5>
<el-table :data="previewData" size="small" border style="width: 100%">
<el-table-column prop="fileName" label="文件名" />
<el-table-column prop="url" label="URL" show-overflow-tooltip />
</el-table>
<div v-if="rawExcelData.length > 1000" style="margin-top: 10px;">
<el-alert title="文件较大提示" type="warning" :closable="false" show-icon>
<template #default>
<div style="font-size: 12px; line-height: 1.5;">
<p>• 当前文件包含 {{ rawExcelData.length }} 行数据</p>
<p>• 已优化:只保留 {{ Object.keys(rawExcelData[0] || {}).length }} 个必要字段</p>
<p> 大文件处理可能需要较长时间,请耐心等待</p>
<p> 建议分批处理或使用较小的Excel文件</p>
</div>
</template>
</el-alert>
</div>
</div>
</div>
<!-- 下载路径设置 -->
<div style="margin-top: 20px;">
<h4 style="margin-bottom: 15px; color: #333;">下载设置</h4>
<!-- 操作系统信息 -->
<div style="margin-bottom: 15px;">
<el-alert
:title="`当前操作系统: ${downloadStore.getOperatingSystem() === 'windows' ? 'Windows' : downloadStore.getOperatingSystem() === 'mac' ? 'macOS' : downloadStore.getOperatingSystem() === 'linux' ? 'Linux' : '未知'}`"
type="info"
:closable="false"
show-icon
/>
type="info" :closable="false" show-icon />
</div>
<!-- 浏览器默认下载路径显示 -->
<div style="margin-bottom: 15px;">
<el-alert
:title="`默认下载路径: ${downloadStore.getDefaultDownloadPath()}`"
type="info"
:closable="false"
show-icon
/>
<el-alert :title="`默认下载路径: ${downloadStore.getDefaultDownloadPath()}`" type="info" :closable="false"
show-icon />
</div>
<el-row :gutter="20">
<el-col :span="16">
<el-form-item label="文件名前缀">
<el-input
v-model="customSubFolder"
placeholder="例如: vue-downloader (留空则不添加前缀)"
@input="updateDownloadPath"
/>
<el-input v-model="customSubFolder" placeholder="例如: vue-downloader (留空则不添加前缀)"
@input="updateDownloadPath" />
</el-form-item>
</el-col>
<el-col :span="8">
......@@ -190,15 +161,10 @@
</el-button>
</el-col>
</el-row>
<!-- 文件名前缀说明 -->
<div v-if="customSubFolder && customSubFolder.trim()" style="margin-top: 10px;">
<el-alert
title="文件名前缀说明"
type="success"
:closable="false"
show-icon
>
<el-alert title="文件名前缀说明" type="success" :closable="false" show-icon>
<template #default>
<div style="font-size: 12px; line-height: 1.5;">
<p><strong>示例:</strong></p>
......@@ -209,48 +175,36 @@
</template>
</el-alert>
</div>
<!-- 通知设置 -->
<div style="margin-top: 15px;">
<el-form-item label="下载完成通知">
<el-switch
v-model="downloadStore.enableNotifications"
@change="downloadStore.setEnableNotifications"
active-text="启用"
inactive-text="禁用"
/>
<el-switch v-model="downloadStore.enableNotifications" @change="downloadStore.setEnableNotifications"
active-text="启用" inactive-text="禁用" />
<span style="margin-left: 10px; color: #666; font-size: 12px;">
下载全部完成后在右下角显示通知
</span>
</el-form-item>
</div>
</div>
<div style="margin-top: 20px;">
<el-button
type="success"
size="large"
:disabled="!canParse"
@click="parseExcelFile"
:loading="parsing"
>
<el-icon><Document /></el-icon>
{{ parsing ? '解析中...' : '解析并添加下载任务' }}
</el-button>
<!-- 处理进度显示 -->
<div v-if="parsing && processingMessage" style="margin-top: 15px;">
<el-progress
:percentage="processingProgress"
:format="(percentage) => `${percentage}%`"
:stroke-width="8"
status="success"
/>
<div style="margin-top: 8px; color: #666; font-size: 14px;">
{{ processingMessage }}
</div>
</div>
</div>
<div style="margin-top: 20px;">
<el-button type="success" size="large" :disabled="!canParse" @click="parseExcelFile" :loading="parsing">
<el-icon>
<Document />
</el-icon>
{{ parsing ? '解析中...' : '解析并添加下载任务' }}
</el-button>
<!-- 处理进度显示 -->
<div v-if="parsing && processingMessage" style="margin-top: 15px;">
<el-progress :percentage="processingProgress" :format="(percentage) => `${percentage}%`"
:stroke-width="8" status="success" />
<div style="margin-top: 8px; color: #666; font-size: 14px;">
{{ processingMessage }}
</div>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
......@@ -260,117 +214,131 @@
<div class="download-header">
<div class="download-title">
<h2 style="color: #333; margin: 0;">
<el-icon><List /></el-icon>
<el-icon>
<List />
</el-icon>
下载任务管理
</h2>
<div class="download-stats">
<el-tag type="info">总计: {{ downloadStore.downloads.length }}</el-tag>
<!-- <el-tag type="info">总计: {{ downloadStore.downloads.length }}</el-tag>
<el-tag type="primary">等待: {{ downloadStore.pendingDownloads.length }}</el-tag>
<el-tag type="success">下载中: {{ downloadStore.activeDownloads.length }}</el-tag>
<el-tag type="warning">已完成: {{ downloadStore.completedDownloads.length }}</el-tag>
<el-tag type="danger">失败: {{ downloadStore.failedDownloads.length }}</el-tag>
<el-tag type="danger">失败: {{ downloadStore.failedDownloads.length }}</el-tag> -->
<el-tag type="info">总计: {{ totalNumObj.countNum }}</el-tag>
<el-tag type="primary">等待: {{ totalNumObj.status0 }}</el-tag>
<el-tag type="success">下载中: {{ totalNumObj.status1 }}</el-tag>
<el-tag type="warning">已完成: {{ totalNumObj.status3 }}</el-tag>
<el-tag type="danger">失败: {{ totalNumObj.status4 }}</el-tag>
</div>
</div>
<div class="download-actions">
<!-- 状态筛选 -->
<el-select
v-model="statusFilter"
placeholder="按状态筛选"
clearable
style="width: 150px; margin-right: 10px;"
@change="handleStatusFilterChange"
>
<el-option label="全部" value="" />
<el-option label="等待中" value="pending" />
<el-option label="下载中" value="downloading" />
<el-option label="已暂停" value="paused" />
<el-option label="已完成" value="completed" />
<el-option label="失败" value="error" />
</el-select>
<!-- 页面级别操作 -->
<el-button @click="downloadCurrentPage" type="info" size="small">
下载当前页
</el-button>
<el-button @click="pauseCurrentPage" type="warning" size="small">
暂停当前页
</el-button>
<el-divider direction="vertical" />
<el-button @click="downloadStore.clearCompleted" size="small">
清除已完成
</el-button>
<el-button @click="downloadStore.clearAll" type="danger" size="small">
清除所有
</el-button>
</div>
<div class="download-actions">
<!-- 状态筛选 -->
<!-- <el-select v-model="statusFilter" placeholder="按状态筛选" clearable style="width: 150px; margin-right: 10px;"
@change="handleStatusFilterChange">
<el-option label="全部" value="" />
<el-option label="等待中" value="pending" />
<el-option label="下载中" value="downloading" />
<el-option label="已暂停" value="paused" />
<el-option label="已完成" value="completed" />
<el-option label="失败" value="error" />
</el-select> -->
<el-select v-model="deQuery.fileStatus" placeholder="按状态筛选" clearable style="width: 150px; margin-right: 10px;"
@change="handleStatusFilterChange">
<el-option label="全部" value="" />
<el-option label="等待中" :value="0" />
<el-option label="下载中" :value="1" />
<el-option label="已暂停" :value="2" />
<el-option label="已完成" :value="3" />
<el-option label="失败" :value="4" />
</el-select>
<!-- 页面级别操作 -->
<el-button @click="downloadCurrentPage" type="info" size="small">
下载当前页
</el-button>
<el-button @click="pauseCurrentPage" type="warning" size="small">
暂停当前页
</el-button>
<el-divider direction="vertical" />
<!-- <el-button @click="downloadStore.clearCompleted" size="small">
清除已完成
</el-button> -->
<el-button @click="clearCompleted" size="small">
清除已完成
</el-button>
<!-- <el-button @click="downloadStore.clearAll" type="danger" size="small">
清除所有
</el-button> -->
<el-button @click="clearAll" type="danger" size="small">
清除所有
</el-button>
</div>
</div>
<!-- 下载任务列表 -->
<div v-if="downloadStore.downloads.length === 0" class="empty-state">
<el-icon size="64"><Document /></el-icon>
<el-icon size="64">
<Document />
</el-icon>
<h3>暂无下载任务</h3>
<p>请在上方配置区域上传Excel文件并解析添加下载任务</p>
</div>
<div v-else class="download-table-container">
<!-- 分页表格 -->
<el-table
:data="paginatedDownloads"
style="width: 100%"
border
stripe
highlight-current-row
>
<el-table-column label="Excel文件" width="180">
<template #default="{ row }">
<div class="excel-file-info">
<el-icon><Document /></el-icon>
<span class="excel-file-name">{{ row.excelFileName || '-' }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="文件名" min-width="250">
<template #default="{ row }">
<div class="file-info">
<div class="file-icon">
<el-icon><Document /></el-icon>
</div>
<div class="file-details">
<div class="file-name">
<span v-if="downloadStore.customSubFolder" class="file-prefix">
{{ downloadStore.customSubFolder }}_
</span>
{{ row.fileName }}
</div>
<div class="file-url">{{ row.url }}</div>
</div>
</div>
</template>
</el-table-column>
<!-- :data="paginatedDownloads" -->
<el-table :data="deTableData" style="width: 100%" border stripe highlight-current-row>
<el-table-column label="Excel文件" width="180">
<template #default="{ row }">
<div class="excel-file-info">
<el-icon>
<Document />
</el-icon>
<span class="excel-file-name">{{ row.uploadName || '-' }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="文件名" min-width="250">
<template #default="{ row }">
<div class="file-info">
<div class="file-icon">
<el-icon>
<Document />
</el-icon>
</div>
<div class="file-details">
<div class="file-name">
<!-- <span v-if="downloadStore.customSubFolder" class="file-prefix">
{{ downloadStore.customSubFolder }}_
</span> -->
<span v-if="row.fileName" class="file-prefix">
{{ row.fileName }}_
</span>
{{ row.fileUrl }}
</div>
<div class="file-url">{{ row.url }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag
:class="`status-badge status-${row.status}`"
size="small"
>
{{ getStatusText(row.status) }}
<el-tag :class="`status-badge status-${row.status}`" size="small">
{{ getStatusText(row.fileStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="进度" width="200" align="center">
<!-- <el-table-column label="进度" width="200" align="center">
<template #default="{ row }">
<div v-if="row.status === 'downloading'" class="progress-container">
<el-progress
:percentage="row.progress"
:format="(percentage) => `${percentage}%`"
size="small"
:stroke-width="8"
/>
<el-progress :percentage="row.progress" :format="(percentage) => `${percentage}%`" size="small"
:stroke-width="8" />
<div class="speed-info">
{{ formatSpeed(row.speed) }}
</div>
......@@ -382,79 +350,57 @@
{{ row.progress }}%
</span>
</template>
</el-table-column>
<el-table-column label="大小" width="120" align="center">
<template #default="{ row }">
</el-table-column> -->
<el-table-column label="大小" width="120" align="center" prop="fileLength">
<!-- <template #default="{ row }">
<span v-if="row.totalBytes > 0" class="size-info">
{{ formatBytes(row.downloadedBytes) }} / {{ formatBytes(row.totalBytes) }}
</span>
<span v-else class="size-info">-</span>
</template>
</template> -->
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<div class="download-actions">
<el-button
v-if="row.status === 'pending'"
@click="startSingleDownload(row.id)"
type="primary"
size="small"
>
<el-button v-if="row.status === 'pending'" @click="startSingleDownload(row.id)" type="primary"
size="small">
开始
</el-button>
<el-button
v-if="row.status === 'downloading'"
@click="downloadStore.pauseDownload(row.id)"
type="warning"
size="small"
>
<el-button v-if="row.status === 'downloading'" @click="downloadStore.pauseDownload(row.id)"
type="warning" size="small">
暂停
</el-button>
<el-button
v-if="row.status === 'paused'"
@click="resumeSingleDownload(row.id)"
type="success"
size="small"
>
<el-button v-if="row.status === 'paused'" @click="resumeSingleDownload(row.id)" type="success"
size="small">
继续
</el-button>
<el-button
v-if="row.status === 'error'"
@click="startSingleDownload(row.id)"
type="warning"
size="small"
>
<el-button v-if="row.status === 'error'" @click="startSingleDownload(row.id)" type="warning"
size="small">
重试
</el-button>
<el-button
@click="downloadStore.cancelDownload(row.id)"
type="danger"
size="small"
>
<!-- <el-button @click="downloadStore.cancelDownload(row.id)" type="danger" size="small">
删除
</el-button> -->
<el-button @click="deleteRow(row)" type="danger" size="small">
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页器 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="filteredDownloads.length"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-pagination v-model:current-page="deQuery.current" v-model:page-size="deQuery.pageSize"
:page-sizes="[10, 20, 50, 100]" :total="deTotal"
layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
</div>
</div>
......@@ -463,13 +409,17 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Download, Upload, Document, List, Warning, User, ArrowDown } from '@element-plus/icons-vue'
import { useAuthStore } from '../stores/auth'
import { useDownloadStore } from '../stores/download'
import * as XLSX from 'xlsx'
import http from '@/utils/request.js';
import config from '@/api/api.js';
import axios from "axios";
const router = useRouter()
const authStore = useAuthStore()
......@@ -489,6 +439,21 @@ const activeCollapse = ref(['upload']) // 默认展开配置区域
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
// 已上传文件列表
const upQuery = ref({
current: 1,
pageSize: 5
})
let upTableData = reactive([])
let upTotal = ref(0)
// 下载任务管理列表
const deQuery = ref({
current: 1,
pageSize: 10,
fileStatus:''
})
let deTableData = reactive([])
let deTotal = ref(0)
const columnMapping = ref({
fileNameColumns: [],
......@@ -500,13 +465,26 @@ const statusFilter = ref('')
// 计算是否可以解析
const canParse = computed(() => {
return selectedFile.value &&
columnMapping.value.fileNameColumns.length > 0 &&
columnMapping.value.url &&
previewData.value.length > 0
return selectedFile.value &&
columnMapping.value.fileNameColumns.length > 0 &&
columnMapping.value.url &&
previewData.value.length > 0
})
// 已上传 Excel 文件列表数据加载
const loadUpTableData = () => {
http.post(config.uploadBatchList, upQuery.value).then(res => {
if (res.code === 200) {
for (let element of res.data.rows) {
element.createTime = element.createTime ? element.createTime.substring(0,10) : ''
}
upTableData = res.data.rows
upTotal.value = res.data.total
} else {
ElMessage.error(res.message)
}
})
}
// 计算分页后的下载列表
const paginatedDownloads = computed(() => {
......@@ -527,35 +505,58 @@ const filteredDownloads = computed(() => {
// 选择Excel文件
const selectExcelFile = async (fileId) => {
try {
const fileInfo = downloadStore.getExcelFileInfo(fileId)
if (!fileInfo) {
ElMessage.error('文件信息不存在')
return
}
downloadStore.setCurrentExcelFile(fileId)
// 加载第一页数据
const pageData = await downloadStore.loadExcelDataPage(fileId, 1, 10)
rawExcelData.value = pageData.data
// 设置列映射
columnMapping.value = fileInfo.columnMapping || { fileNameColumns: [], url: '' }
// 设置列名
if (pageData.data.length > 0) {
excelColumns.value = Object.keys(pageData.data[0])
// try {
// const fileInfo = downloadStore.getExcelFileInfo(fileId)
// if (!fileInfo) {
// ElMessage.error('文件信息不存在')
// return
// }
// downloadStore.setCurrentExcelFile(fileId)
// // 加载第一页数据
// const pageData = await downloadStore.loadExcelDataPage(fileId, 1, 10)
// rawExcelData.value = pageData.data
// // 设置列映射
// columnMapping.value = fileInfo.columnMapping || { fileNameColumns: [], url: '' }
// // 设置列名
// if (pageData.data.length > 0) {
// excelColumns.value = Object.keys(pageData.data[0])
// }
// // 生成预览数据
// generatePreviewData()
// ElMessage.success(`已选择文件: ${fileInfo.fileName}`)
// } catch (error) {
// console.error('选择Excel文件失败:', error)
// ElMessage.error('选择Excel文件失败: ' + error.message)
// }
http.post(config.uploadBatchQueryUpload, {batchId:fileId}).then(res => {
if (res.code === 200) {
if (!res.data || res.data.details.length === 0) {
ElMessage.error('文件信息不存在')
return
}
selectedFile.value = {
name:res.data.uploadName,
}
excelColumns.value = res.data.fileColumnNameAll || []
columnMapping.value.fileNameColumns = res.data.fileColumnName || []
columnMapping.value.url = res.data.urlColumnName
rawExcelData.value = res.data.details || []
if (res.data.details.length>5){
previewData.value = res.data.details.slice(0,5)
}else {
previewData.value = res.data.details || []
}
customSubFolder.value = res.data.filePrefix || ''
} else {
ElMessage.error(res.message)
}
// 生成预览数据
generatePreviewData()
ElMessage.success(`已选择文件: ${fileInfo.fileName}`)
} catch (error) {
console.error('选择Excel文件失败:', error)
ElMessage.error('选择Excel文件失败: ' + error.message)
}
})
}
// 删除Excel文件
......@@ -565,20 +566,28 @@ const deleteExcelFile = async (fileId) => {
if (!confirmed) {
return
}
try {
await downloadStore.deleteExcelFile(fileId)
ElMessage.success('Excel文件已删除')
} catch (error) {
console.error('删除Excel文件失败:', error)
ElMessage.error('删除Excel文件失败: ' + (error.message || '未知错误'))
}
// try {
// await downloadStore.deleteExcelFile(fileId)
// ElMessage.success('Excel文件已删除')
// } catch (error) {
// console.error('删除Excel文件失败:', error)
// ElMessage.error('删除Excel文件失败: ' + (error.message || '未知错误'))
// }
http.post(config.uploadBatchDelete, { batchId: fileId }).then(res => {
if (res.code === 200) {
ElMessage.success('Excel文件已删除')
loadUpTableData()
} else {
ElMessage.error(res.message)
}
})
}
// 处理文件选择
const handleFileChange = async (file) => {
selectedFile.value = file.raw
try {
// 检查文件大小,超过5MB时显示警告
if (file.raw.size > 5 * 1024 * 1024) {
......@@ -592,28 +601,28 @@ const handleFileChange = async (file) => {
}
)
}
// 显示加载提示
ElMessage.info('正在读取Excel文件,请稍候...')
// 先读取完整数据以获取列名
const fullData = await readExcelFileOptimized(file.raw)
if (fullData.length > 0) {
// 保存原始完整数据用于后续优化
window.originalExcelData = fullData
// 获取所有列名并保存原始列名
const allColumns = Object.keys(fullData[0])
excelColumns.value = allColumns
window.originalColumns = allColumns
// 尝试自动匹配列名
autoMatchColumns()
// 生成预览数据
generatePreviewData()
// 如果自动匹配成功,则优化数据
if (columnMapping.value.fileNameColumns.length > 0 && columnMapping.value.url) {
const optimizedData = optimizeExcelData(fullData)
......@@ -633,11 +642,11 @@ const handleFileChange = async (file) => {
// 自动匹配列名
const autoMatchColumns = () => {
const columns = excelColumns.value
// 尝试匹配文件名列
const fileNamePatterns = ['fileName', 'filename', 'name', 'file', '文件名', '文件名称']
for (const pattern of fileNamePatterns) {
const match = columns.find(col =>
const match = columns.find(col =>
col.toLowerCase().includes(pattern.toLowerCase())
)
if (match) {
......@@ -645,11 +654,11 @@ const autoMatchColumns = () => {
break
}
}
// 尝试匹配URL列
const urlPatterns = ['url', 'link', '地址', '链接', '下载地址']
for (const pattern of urlPatterns) {
const match = columns.find(col =>
const match = columns.find(col =>
col.toLowerCase().includes(pattern.toLowerCase())
)
if (match) {
......@@ -657,7 +666,7 @@ const autoMatchColumns = () => {
break
}
}
// 自动优化数据
if (columnMapping.value.fileNameColumns.length > 0 && columnMapping.value.url) {
optimizeDataAfterColumnMapping()
......@@ -667,22 +676,22 @@ const autoMatchColumns = () => {
// 列映射配置后优化数据
const optimizeDataAfterColumnMapping = () => {
if (!rawExcelData.value.length) return
// 获取原始完整数据(如果存在)
const originalData = window.originalExcelData || rawExcelData.value
// 优化数据:只保留需要的字段
const optimizedData = optimizeExcelData(originalData)
rawExcelData.value = optimizedData
// 确保excelColumns保持原始的所有列名,用于列选择
if (window.originalColumns) {
excelColumns.value = window.originalColumns
}
// 重新生成预览数据
generatePreviewData()
console.log(`数据优化完成:从 ${Object.keys(originalData[0] || {}).length} 个字段优化到 ${Object.keys(optimizedData[0] || {}).length} 个字段`)
}
......@@ -692,25 +701,25 @@ const generatePreviewData = () => {
previewData.value = []
return
}
const preview = rawExcelData.value.slice(0, 5).map(row => {
// 组合多个文件名列
const fileNameParts = columnMapping.value.fileNameColumns.map(col => row[col] || '').filter(part => part)
let fileName = fileNameParts.join('-')
// 从URL中提取扩展名
const url = row[columnMapping.value.url] || ''
const extension = getFileExtensionFromUrl(url)
if (extension) {
fileName += extension
}
return {
fileName: fileName,
url: url
}
}).filter(item => item.fileName && item.url)
previewData.value = preview
}
......@@ -718,7 +727,7 @@ const generatePreviewData = () => {
const handleColumnMappingChange = () => {
// 生成预览数据
generatePreviewData()
// 如果列映射配置完整,则优化数据
if (columnMapping.value.fileNameColumns.length > 0 && columnMapping.value.url) {
optimizeDataAfterColumnMapping()
......@@ -730,9 +739,9 @@ const optimizeExcelData = (data) => {
if (!columnMapping.value.fileNameColumns.length || !columnMapping.value.url) {
return []
}
const neededColumns = [...columnMapping.value.fileNameColumns, columnMapping.value.url]
return data.map(row => {
const optimizedRow = {}
neededColumns.forEach(col => {
......@@ -748,10 +757,10 @@ const getFileExtensionFromUrl = (url) => {
// 解析URL
const urlObj = new URL(url)
const pathname = urlObj.pathname
// 从路径中提取文件名
const fileName = pathname.split('/').pop()
// 检查文件名是否包含扩展名
if (fileName && fileName.includes('.')) {
const extension = fileName.substring(fileName.lastIndexOf('.'))
......@@ -760,7 +769,7 @@ const getFileExtensionFromUrl = (url) => {
return extension
}
}
// 如果没有找到有效的扩展名,返回.tmp
return '.tmp'
} catch (error) {
......@@ -808,15 +817,95 @@ const updateDownloadPath = () => {
onMounted(() => {
// 先初始化下载store
downloadStore.initialize()
// 从下载store中同步文件名前缀
customSubFolder.value = downloadStore.customSubFolder || ''
detectBrowserDownloadPath()
loadUpTableData()
loadDetailTableData()
loadUploadDetailTotalNum()
})
function loadDetailTableData(){
http.post(config.uploadDetailList,deQuery.value).then(res=>{
if (res.code === 200){
deTableData = res.data.rows
deTotal.value = res.data.total
}else {
ElMessage.error(res.message)
}
})
}
const totalNumObj = reactive({
countNum:0,
status0: 0,
status1: 0,
status2: 0,
status3: 0,
status4: 0,
})
function loadUploadDetailTotalNum(){
http.post(config.uploadDetailTotalNum).then(res=>{
if (res.code === 200){
}else {
ElMessage.error(res.message)
}
})
}
// 清除已完成
function clearCompleted() {
http.post(config.uploadDetailDeleteAll,{
type:0
}).then(res=>{
if (res.code === 200){
ElMessage.success(res.message)
deQuery.value.current = 1
// loadUploadDetailList()
loadDetailTableData()
}else {
ElMessage.error(res.message)
}
})
}
// 清除所有
function clearAll() {
http.post(config.uploadDetailDeleteAll,{
type:1
}).then(res=>{
if (res.code === 200){
ElMessage.success(res.message)
deQuery.value.current = 1
// loadUploadDetailList()
loadDetailTableData()
}else {
ElMessage.error(res.message)
}
})
}
// 删除单行
function deleteRow(row) {
http.post(config.uploadDetailDelete,{
detailId:row.detailId
}).then(res=>{
if (res.code === 200){
ElMessage.success(res.message)
deQuery.value.current = 1
// loadUploadDetailList()
loadDetailTableData()
}else {
ElMessage.error(res.message)
}
})
}
// 解析Excel文件
const parseExcelFile = async () => {
if (rawExcelData.value.length > 8000){
ElMessage.error('文件数据量太大,请拆分处理,每次不超过3000条')
return
}
if (!canParse.value) {
ElMessage.warning('请先选择Excel文件并配置列映射')
return
......@@ -825,52 +914,51 @@ const parseExcelFile = async () => {
parsing.value = true
processingProgress.value = 0
processingMessage.value = '正在处理Excel数据...'
try {
// 显示处理进度
ElMessage.info('正在处理Excel数据,请稍候...')
// 使用setTimeout来避免阻塞UI
await new Promise(resolve => setTimeout(resolve, 100))
// 获取完整数据(从原始数据或当前数据)
const allData = window.originalExcelData || rawExcelData.value
// 过滤有效数据(分批处理)
const batchSize = 1000 // 每批处理1000行
const validData = []
const totalBatches = Math.ceil(allData.length / batchSize)
for (let i = 0; i < allData.length; i += batchSize) {
const batchIndex = Math.floor(i / batchSize) + 1
const batch = allData.slice(i, i + batchSize)
// 更新进度
processingProgress.value = Math.round((batchIndex / totalBatches) * 30) // 前30%用于数据过滤
processingMessage.value = `正在过滤数据... (${batchIndex}/${totalBatches})`
const batchValidData = batch.filter(row => {
const fileNameParts = columnMapping.value.fileNameColumns.map(col => row[col] || '').filter(part => part)
let fileName = fileNameParts.join('-')
const url = row[columnMapping.value.url]
// 从URL中提取扩展名
const extension = getFileExtensionFromUrl(url)
if (extension) {
fileName += extension
}
return fileName && url
})
validData.push(...batchValidData)
// 每处理一批数据后让出控制权,避免阻塞UI
if (i + batchSize < allData.length) {
await new Promise(resolve => setTimeout(resolve, 10))
}
}
if (validData.length === 0) {
ElMessage.warning('Excel文件中没有找到有效数据')
return
......@@ -879,10 +967,10 @@ const parseExcelFile = async () => {
// 保存Excel文件到本地存储
processingProgress.value = 30
processingMessage.value = '正在保存Excel文件到本地...'
const excelFileInfo = await downloadStore.saveExcelFileToLocal(
selectedFile.value,
allData,
selectedFile.value,
allData,
columnMapping.value
)
......@@ -896,6 +984,8 @@ const parseExcelFile = async () => {
type: 'info'
}
)
// 上传后端
uploadDetails()
// 临时禁用自动下载
const originalBatchDownloading = downloadStore.isBatchDownloading
......@@ -904,29 +994,29 @@ const parseExcelFile = async () => {
// 分批添加下载任务
const addBatchSize = 500 // 每批添加500个任务
const totalAddBatches = Math.ceil(validData.length / addBatchSize)
for (let i = 0; i < validData.length; i += addBatchSize) {
const batchIndex = Math.floor(i / addBatchSize) + 1
const batch = validData.slice(i, i + addBatchSize)
// 更新进度
processingProgress.value = 30 + Math.round((batchIndex / totalAddBatches) * 70) // 后70%用于添加任务
processingMessage.value = `正在添加下载任务... (${batchIndex}/${totalAddBatches})`
batch.forEach(row => {
const fileNameParts = columnMapping.value.fileNameColumns.map(col => row[col] || '').filter(part => part)
let fileName = fileNameParts.join('-')
const url = row[columnMapping.value.url]
// 从URL中提取扩展名
const extension = getFileExtensionFromUrl(url)
if (extension) {
fileName += extension
}
downloadStore.addDownload(fileName, url, selectedFile.value.name, excelFileInfo.id)
})
// 每批添加后让出控制权
if (i + addBatchSize < validData.length) {
await new Promise(resolve => setTimeout(resolve, 50))
......@@ -937,19 +1027,19 @@ const parseExcelFile = async () => {
downloadStore.isBatchDownloading = originalBatchDownloading
ElMessage.success(`成功添加 ${validData.length} 个下载任务`)
// 重置状态
selectedFile.value = null
excelColumns.value = []
previewData.value = []
rawExcelData.value = []
columnMapping.value = { fileNameColumns: [], url: '' }
// 清空上传组件
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
} catch (error) {
if (error.message.includes('cancel')) {
ElMessage.info('已取消添加下载任务')
......@@ -964,7 +1054,81 @@ const parseExcelFile = async () => {
}
}
function getDetailList() {
if (!columnMapping.value.fileNameColumns.length || !columnMapping.value.url) {
ElMessage.error('请先选择文件列名和 URL 列')
return []
}
const preview = rawExcelData.value.map(row => {
// 组合多个文件名列
const fileNameParts = columnMapping.value.fileNameColumns.map(col => row[col] || '').filter(part => part)
let fileName = fileNameParts.join('-')
// 从URL中提取扩展名
const url = row[columnMapping.value.url] || ''
const extension = getFileExtensionFromUrl(url)
if (extension) {
fileName += extension
}
return {
fileName: fileName,
fileUrl: url
}
}).filter(item => item.fileName && item.fileUrl)
return preview
}
var upDetailId = ''
let detailList = reactive([])
function uploadDetails(){
detailList = getDetailList()
http.post(config.uploadBatchFileUpload,{
uploadName:selectedFile.value.name,
uploadUrl: '',
totalRows: rawExcelData.value.length,
fileColumnNameAll: excelColumns.value,
fileColumnName: columnMapping.value.fileNameColumns,
urlColumnName: columnMapping.value.url,
filePrefix: customSubFolder.value,
notifyStatus: downloadStore.enableNotifications,
// details: detailList
}).then(res => {
if (res.code === 200) {
// loadDetailTableData()
upDetailId = res.data
uploadDetailsFile()
} else {
detailList = []
ElMessage.error(res.message)
}
})
}
function uploadDetailsFile(){
var uplist = []
if (detailList.length > 500) {
uplist = detailList.slice(0,500)
detailList = detailList.slice(500)
}else {
uplist = detailList
detailList = []
}
console.log('list-->',detailList.length)
http.post(config.fileUploadDetail,{
batchId: upDetailId,
details: uplist
}).then(res => {
if (res.code === 200) {
if (detailList.length > 0) {
uploadDetailsFile()
}else {
ElMessage.success('上传任务完成')
loadDetailTableData()
}
}
})
}
// 下载当前页所有任务
const downloadCurrentPage = async () => {
......@@ -973,12 +1137,12 @@ const downloadCurrentPage = async () => {
ElMessage.warning('当前页没有下载任务')
return
}
// 过滤出可以开始下载的任务
const startableItems = currentPageItems.filter(item =>
const startableItems = currentPageItems.filter(item =>
['pending', 'paused', 'error'].includes(item.status)
)
if (startableItems.length === 0) {
ElMessage.warning('当前页没有可以开始下载的任务')
return
......@@ -990,18 +1154,41 @@ const downloadCurrentPage = async () => {
cancelButtonText: '取消',
type: 'info'
})
// 获取当前页所有任务的ID
const currentPageIds = startableItems.map(item => item.id)
// 使用store的批量下载方法
await downloadStore.batchStartDownloads(currentPageIds)
ElMessage.success(`已开始下载当前页的 ${startableItems.length} 个任务`)
for (const item of startableItems) {
const size = await getFileSize(item.url);
http.post(config.downLoadStatus,{
detailId: item.id,
fileStatus: 3, //下载状态 0:等待中 1:下载中 2:已暂停 3:已完成 4:失败
fileStatus: size || 0
}).then(res=>{
if (res.code === 200) {
} else {
ElMessage.error(res.message)
}
})
}
} catch {
// 用户取消
}
}
async function getFileSize(url) {
try {
const response = await axios.head(url);
const size = response.headers["content-length"];
return size ? parseInt(size, 10) : null;
} catch (err) {
console.error("获取文件大小失败:", err);
return null;
}
}
// 暂停当前页所有任务
const pauseCurrentPage = async () => {
......@@ -1010,30 +1197,30 @@ const pauseCurrentPage = async () => {
ElMessage.warning('当前页没有下载任务')
return
}
// 过滤出可以暂停的任务(只有下载中的任务可以暂停)
const pausableItems = currentPageItems.filter(item =>
const pausableItems = currentPageItems.filter(item =>
item.status === 'downloading'
)
if (pausableItems.length === 0) {
ElMessage.warning('当前页没有正在下载的任务')
return
}
try {
await ElMessageBox.confirm(`确定要暂停当前页的 ${pausableItems.length} 个任务吗?`, '暂停当前页', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// 获取当前页所有任务的ID
const currentPageIds = pausableItems.map(item => item.id)
// 使用store的批量暂停方法
downloadStore.batchPauseDownloads(currentPageIds)
ElMessage.success(`已暂停当前页的 ${pausableItems.length} 个任务`)
} catch {
// 用户取消
......@@ -1044,18 +1231,31 @@ const pauseCurrentPage = async () => {
// 处理分页大小变化
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
deQuery.value.pageSize = size
deQuery.value.current = 1
loadDetailTableData()
}
// 处理当前页变化
const handleCurrentChange = (page) => {
currentPage.value = page
deQuery.value.current = page
loadDetailTableData()
}
const handleUpSizeChange = (size) => {
upQuery.value.pageSize = size
upQuery.value.current = 1
loadUpTableData()
}
// 处理当前页变化
const handleUpCurrentChange = (page) => {
upQuery.value.current = page
loadUpTableData()
}
// 处理状态筛选变化
const handleStatusFilterChange = () => {
currentPage.value = 1 // 重置当前页为1,以便重新加载数据
// currentPage.value = 1 // 重置当前页为1,以便重新加载数据
deQuery.value.current = 1
}
// 单个文件下载
......@@ -1082,7 +1282,7 @@ const resumeSingleDownload = async (downloadId) => {
const readExcelFileOptimized = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
try {
// 优先使用Worker处理,如果失败则使用主线程
......@@ -1100,11 +1300,11 @@ const readExcelFileOptimized = (file) => {
reject(error)
}
}
reader.onerror = () => {
reject(new Error('读取文件失败'))
}
reader.readAsArrayBuffer(file)
})
}
......@@ -1113,7 +1313,7 @@ const readExcelFileOptimized = (file) => {
const processExcelInMainThread = (arrayBuffer) => {
try {
const data = new Uint8Array(arrayBuffer)
const workbook = XLSX.read(data, {
const workbook = XLSX.read(data, {
type: 'array',
cellDates: true,
cellNF: false,
......@@ -1122,18 +1322,18 @@ const processExcelInMainThread = (arrayBuffer) => {
cellFormula: false,
cellText: false
})
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
throw new Error('Excel文件中没有找到工作表')
}
const sheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetName]
if (!worksheet) {
throw new Error('无法读取工作表数据')
}
// 限制最大行数,防止内存溢出
const maxRows = 50000 // 主线程限制更严格
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1')
......@@ -1141,23 +1341,23 @@ const processExcelInMainThread = (arrayBuffer) => {
range.e.r = maxRows - 1
worksheet['!ref'] = XLSX.utils.encode_range(range)
}
const jsonData = XLSX.utils.sheet_to_json(worksheet, {
header: 1,
defval: '',
blankrows: false
})
if (!jsonData || jsonData.length === 0) {
throw new Error('Excel文件中没有数据')
}
// 转换为对象数组
const headers = jsonData[0]
if (!headers || headers.length === 0) {
throw new Error('Excel文件中没有列标题')
}
const rows = jsonData.slice(1).map(row => {
const obj = {}
headers.forEach((header, index) => {
......@@ -1165,7 +1365,7 @@ const processExcelInMainThread = (arrayBuffer) => {
})
return obj
})
return rows
} catch (error) {
throw new Error('主线程Excel解析失败: ' + error.message)
......@@ -1177,31 +1377,31 @@ const processLargeExcelFileWithWorker = (arrayBuffer, resolve, reject) => {
try {
// 创建Web Worker
const worker = new Worker('/excel-worker.js')
// 设置超时时间
const timeout = setTimeout(() => {
worker.terminate()
reject(new Error('Excel解析超时,请尝试使用较小的文件'))
}, 120000) // 增加到120秒超时
worker.onmessage = (e) => {
clearTimeout(timeout)
worker.terminate()
if (e.data.type === 'success') {
resolve(e.data.data)
} else {
reject(new Error(e.data.error || 'Worker处理失败'))
}
}
worker.onerror = (error) => {
clearTimeout(timeout)
worker.terminate()
console.error('Worker error:', error)
reject(new Error('Worker处理失败: ' + (error.message || '未知错误')))
}
// 发送数据给Worker
worker.postMessage({
type: 'parseExcel',
......@@ -1221,11 +1421,16 @@ const readExcelFile = (file) => {
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
pending: '等待中',
downloading: '下载中',
completed: '已完成',
error: '失败',
paused: '已暂停'
// pending: '等待中',
// downloading: '下载中',
// completed: '已完成',
// error: '失败',
// paused: '已暂停'
0:'等待中',
1:'下载中' ,
2:'已暂停' ,
3:'已完成',
4:'失败'
}
return statusMap[status] || status
}
......@@ -1233,32 +1438,32 @@ const getStatusText = (status) => {
// 格式化速度
const formatSpeed = (bytesPerSecond) => {
if (bytesPerSecond === 0) return '0 B/s'
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s']
let size = bytesPerSecond
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(1)} ${units[unitIndex]}`
}
// 格式化字节数
const formatBytes = (bytes) => {
if (bytes === 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(1)} ${units[unitIndex]}`
}
......@@ -1270,7 +1475,7 @@ const handleLogout = async () => {
cancelButtonText: '取消',
type: 'warning'
})
authStore.logout()
ElMessage.success('已退出登录')
router.push('/login')
......
......@@ -3,7 +3,7 @@
<div class="login-box">
<div class="login-header">
<el-icon class="logo-icon">
<Download/>
<Download />
</el-icon>
<h1>文件下载器</h1>
<p>登录或注册以开始使用</p>
......@@ -11,133 +11,62 @@
<!-- 切换按钮 -->
<div class="mode-switch">
<el-button
:type="isLoginMode ? 'primary' : 'default'"
@click="switchToLogin"
:disabled="isLoginMode"
>
<el-button :type="isLoginMode ? 'primary' : 'default'" @click="switchToLogin" :disabled="isLoginMode">
登录
</el-button>
<el-button
:type="!isLoginMode ? 'primary' : 'default'"
@click="switchToRegister"
:disabled="!isLoginMode"
>
<el-button :type="!isLoginMode ? 'primary' : 'default'" @click="switchToRegister" :disabled="!isLoginMode">
注册
</el-button>
</div>
<!-- 错误消息显示 -->
<div v-if="errorMessage" class="error-message">
<el-alert
:title="errorMessage"
type="error"
:closable="false"
show-icon
/>
<el-alert :title="errorMessage" type="error" :closable="false" show-icon />
</div>
<!-- 成功消息显示 -->
<div v-if="successMessage" class="success-message">
<el-alert
:title="successMessage"
type="success"
:closable="false"
show-icon
/>
<el-alert :title="successMessage" type="success" :closable="false" show-icon />
</div>
<!-- 登录表单 -->
<el-form
v-if="isLoginMode"
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="用户名"
prefix-icon="User"
size="large"
/>
<el-form v-if="isLoginMode" ref="loginFormRef" :model="loginForm" :rules="loginRules" class="login-form"
@submit.prevent="handleLogin">
<el-form-item prop="userName">
<el-input v-model="loginForm.userName" placeholder="用户名" prefix-icon="User" size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
prefix-icon="Lock"
size="large"
show-password
@keyup.enter="handleLogin"
/>
<el-input v-model="loginForm.password" type="password" placeholder="密码" prefix-icon="Lock" size="large"
show-password @keyup.enter="handleLogin" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
class="submit-btn"
:loading="loading"
@click="handleLogin"
>
<el-button type="primary" size="large" class="submit-btn" :loading="loading" @click="handleLogin">
登录
</el-button>
</el-form-item>
</el-form>
<!-- 注册表单 -->
<el-form
v-else
ref="registerFormRef"
:model="registerForm"
:rules="registerRules"
class="login-form"
@submit.prevent="handleRegister"
>
<el-form-item prop="username">
<el-input
v-model="registerForm.username"
placeholder="用户名(至少3个字符)"
prefix-icon="User"
size="large"
/>
<el-form v-else ref="registerFormRef" :model="registerForm" :rules="registerRules" class="login-form"
@submit.prevent="handleRegister">
<el-form-item prop="userName">
<el-input v-model="registerForm.userName" placeholder="用户名(至少3个字符)" prefix-icon="User" size="large" />
</el-form-item>
<el-form-item prop="email">
<el-input
v-model="registerForm.email"
placeholder="邮箱(可选)"
prefix-icon="Message"
size="large"
/>
<el-input v-model="registerForm.email" placeholder="邮箱(可选)" prefix-icon="Message" size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="密码"
prefix-icon="Lock"
size="large"
show-password
/>
<el-input v-model="registerForm.password" type="password" placeholder="密码" prefix-icon="Lock" size="large"
show-password />
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
placeholder="确认密码"
prefix-icon="Lock"
size="large"
show-password
@keyup.enter="handleRegister"
/>
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="确认密码" prefix-icon="Lock"
size="large" show-password @keyup.enter="handleRegister" />
</el-form-item>
<!-- 密码强度提示 -->
......@@ -146,29 +75,29 @@
<div class="strength-items">
<div class="strength-item" :class="{ valid: registerForm.password.length >= 6 }">
<el-icon>
<Check v-if="registerForm.password.length >= 6"/>
<Close v-else/>
<Check v-if="registerForm.password.length >= 6" />
<Close v-else />
</el-icon>
至少6位
</div>
<div class="strength-item" :class="{ valid: /\d/.test(registerForm.password) }">
<el-icon>
<Check v-if="/\d/.test(registerForm.password)"/>
<Close v-else/>
<Check v-if="/\d/.test(registerForm.password)" />
<Close v-else />
</el-icon>
包含数字
</div>
<div class="strength-item" :class="{ valid: /[a-zA-Z]/.test(registerForm.password) }">
<el-icon>
<Check v-if="/[a-zA-Z]/.test(registerForm.password)"/>
<Close v-else/>
<Check v-if="/[a-zA-Z]/.test(registerForm.password)" />
<Close v-else />
</el-icon>
包含字母
</div>
<div class="strength-item" :class="{ valid: hasSpecialChar }">
<el-icon>
<Check v-if="hasSpecialChar"/>
<Close v-else/>
<Check v-if="hasSpecialChar" />
<Close v-else />
</el-icon>
包含特殊字符
</div>
......@@ -176,13 +105,7 @@
</div>
<el-form-item>
<el-button
type="primary"
size="large"
class="submit-btn"
:loading="loading"
@click="handleRegister"
>
<el-button type="primary" size="large" class="submit-btn" :loading="loading" @click="handleRegister">
注册
</el-button>
</el-form-item>
......@@ -194,11 +117,13 @@
</template>
<script setup>
import {computed, nextTick, onMounted, reactive, ref} from 'vue'
import {useRouter} from 'vue-router'
import {ElMessage, ElMessageBox} from 'element-plus'
import {Check, Close, Download} from '@element-plus/icons-vue'
import {useAuthStore} from '../stores/auth'
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Check, Close, Download } from '@element-plus/icons-vue'
import { useAuthStore } from '../stores/auth'
import http from '@/utils/request.js';
import config from '@/api/api.js';
const router = useRouter()
const authStore = useAuthStore()
......@@ -220,13 +145,13 @@ const hasSpecialChar = computed(() => {
// 登录表单
const loginForm = reactive({
username: '',
userName: '',
password: ''
})
// 注册表单
const registerForm = reactive({
username: '',
userName: '',
email: '',
password: '',
confirmPassword: ''
......@@ -234,26 +159,26 @@ const registerForm = reactive({
// 登录验证规则
const loginRules = {
username: [
{required: true, message: '请输入用户名', trigger: 'blur'}
userName: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{required: true, message: '请输入密码', trigger: 'blur'}
{ required: true, message: '请输入密码', trigger: 'blur' }
]
}
// 注册验证规则
const registerRules = {
username: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{min: 3, message: '用户名至少3个字符', trigger: 'blur'}
userName: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, message: '用户名至少3个字符', trigger: 'blur' }
],
email: [
{type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur'}
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
password: [
{required: true, message: '请输入密码', trigger: 'blur'},
{min: 6, message: '密码至少6位', trigger: 'blur'},
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6位', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!/\d/.test(value)) {
......@@ -270,7 +195,7 @@ const registerRules = {
}
],
confirmPassword: [
{required: true, message: '请确认密码', trigger: 'blur'},
{ required: true, message: '请确认密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== registerForm.password) {
......@@ -306,8 +231,8 @@ const clearMessages = () => {
// 重置表单
const resetForms = () => {
Object.assign(loginForm, {username: '', password: ''})
Object.assign(registerForm, {username: '', email: '', password: '', confirmPassword: ''})
Object.assign(loginForm, { userName: '', password: '' })
Object.assign(registerForm, { userName: '', email: '', password: '', confirmPassword: '' })
if (loginFormRef.value) {
loginFormRef.value.clearValidate()
......@@ -318,11 +243,34 @@ const resetForms = () => {
}
// 处理登录
const handleLogin = async () => {
function handleLogin() {
if (!loginForm.userName.trim()) {
ElMessage.warning('请输入用户名')
return
}
if (!loginForm.password.trim()) {
ElMessage.warning('请输入密码')
return
}
clearMessages()
loading.value = true
http.post(config.login, { ...loginForm }).then(res => {
loading.value = false
if (res.code === 200) {
sessionStorage.setItem("token", res.data)
router.push('/downloader')
} else {
errorMessage.value = res.message
ElMessage.error(res.message)
}
})
}
const handleLogin1 = async () => {
try {
console.log('开始登录处理...')
// 检查用户名和密码是否为空
if (!loginForm.username.trim()) {
if (!loginForm.userName.trim()) {
ElMessage.warning('请输入用户名')
return
}
......@@ -330,6 +278,7 @@ const handleLogin = async () => {
ElMessage.warning('请输入密码')
return
}
// 等待 DOM 更新,确保表单引用可用
await nextTick()
// 检查表单引用是否存在,如果不存在则跳过验证
......@@ -348,12 +297,25 @@ const handleLogin = async () => {
loading.value = true
clearMessages()
console.log('登录表单验证通过,用户名:', loginForm.username)
await authStore.login(loginForm.username, loginForm.password)
console.log('登录成功,跳转到下载器页面')
ElMessage.success('登录成功!')
router.push('/downloader')
console.log('登录表单验证通过,用户名:', loginForm.userName)
await authStore.login(loginForm.userName, loginForm.password)
// console.log('登录成功,跳转到下载器页面')
// ElMessage.success('登录成功!')
// router.push('/downloader')
http.post(config.login, { ...loginForm }).then(res => {
loading.value = false
console.log('-->res:', res)
if (res.code === 200) {
console.log('-->res.data:', res.data)
ElMessage.success('登录成功!')
sessionStorage.setItem("token", res.data)
router.push('/downloader')
// console.log('router:',router)
} else {
ElMessage.error(res.message)
}
})
} catch (error) {
console.error('登录失败:', error)
......@@ -391,10 +353,40 @@ const handleLogin = async () => {
}
// 处理注册
const handleRegister = async () => {
function handleRegister() {
if (!registerForm.userName.trim()) {
ElMessage.warning('请输入用户名')
return
}
if (!registerForm.password.trim()) {
ElMessage.warning('请输入密码')
return
}
if (!registerForm.confirmPassword.trim()) {
ElMessage.warning('请确认密码')
return
}
clearMessages()
loading.value = true
http.post(config.register, { ...registerForm }).then(res => {
loading.value = false
if (res.code === 200) {
sessionStorage.setItem("token", res.data)
switchToLogin()
loginForm.userName = registerForm.userName
} else {
errorMessage.value = res.message
ElMessage.error(res.message)
}
})
}
const handleRegister1 = async () => {
try {
// 检查用户名和密码是否为空
if (!registerForm.username.trim()) {
if (!registerForm.userName.trim()) {
ElMessage.warning('请输入用户名')
return
}
......@@ -429,14 +421,14 @@ const handleRegister = async () => {
clearMessages()
// 检查用户名是否已存在
if (authStore.isUsernameExists(registerForm.username)) {
if (authStore.isUsernameExists(registerForm.userName)) {
ElMessage.error('用户名已存在')
errorMessage.value = '用户名已存在'
return
}
// 注册用户
await authStore.register(registerForm.username, registerForm.password, registerForm.email)
await authStore.register(registerForm.userName, registerForm.password, registerForm.email)
ElMessage.success('注册成功!请登录')
successMessage.value = '注册成功!请登录'
......@@ -444,7 +436,7 @@ const handleRegister = async () => {
// 自动切换到登录模式
setTimeout(() => {
switchToLogin()
loginForm.username = registerForm.username
loginForm.userName = registerForm.userName
}, 2000)
} catch (error) {
......
......@@ -22,7 +22,7 @@
<el-form :model="userInfo" label-width="120px">
<el-form-item label="用户名">
<el-input v-model="userInfo.username" disabled />
<el-input v-model="userInfo.userName" disabled />
</el-form-item>
<el-form-item label="邮箱">
......@@ -121,7 +121,7 @@ const authStore = useAuthStore()
// 用户信息
const userInfo = reactive({
username: '',
userName: '',
email: '',
createdAt: '',
lastLoginAt: ''
......@@ -138,7 +138,7 @@ const downloadSettings = reactive({
const initializeData = () => {
const user = authStore.user
if (user) {
userInfo.username = user.username
userInfo.userName = user.userName
userInfo.email = user.email || '未设置'
userInfo.createdAt = new Date(user.createdAt).toLocaleString()
userInfo.lastLoginAt = user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '从未登录'
......
......@@ -11,7 +11,14 @@ export default defineConfig({
},
server: {
port: 3000,
open: true
open: true,
proxy: {
'/api': {
target: 'http://39.107.245.36:8081',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment