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