mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
update: 完善图片上传
This commit is contained in:
22
web/components/ProxyImage.vue
Normal file
22
web/components/ProxyImage.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<n-image
|
||||
:src="proxyUrl"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useImageUrl } from '~/composables/useImageUrl'
|
||||
|
||||
interface Props {
|
||||
src: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { getImageUrl } = useImageUrl()
|
||||
|
||||
const proxyUrl = computed(() => {
|
||||
return getImageUrl(props.src)
|
||||
})
|
||||
</script>
|
||||
@@ -28,7 +28,7 @@ export const parseApiResponse = <T>(response: any): T => {
|
||||
if (response.success) {
|
||||
// 特殊处理登录接口,直接返回data部分(包含token和user)
|
||||
if (response.data && response.data.token && response.data.user) {
|
||||
console.log('parseApiResponse - 登录接口处理,返回data:', response.data)
|
||||
// console.log('parseApiResponse - 登录接口处理,返回data:', response.data)
|
||||
return response.data
|
||||
}
|
||||
// 特殊处理删除操作响应,直接返回data部分
|
||||
|
||||
@@ -22,11 +22,11 @@ export function useApiFetch<T = any>(
|
||||
...options,
|
||||
headers,
|
||||
onResponse({ response }) {
|
||||
console.log('API响应:', {
|
||||
status: response.status,
|
||||
data: response._data,
|
||||
url: url
|
||||
})
|
||||
// console.log('API响应:', {
|
||||
// status: response.status,
|
||||
// data: response._data,
|
||||
// url: url
|
||||
// })
|
||||
|
||||
// 处理401认证错误
|
||||
if (response.status === 401 ||
|
||||
|
||||
25
web/composables/useImageUrl.ts
Normal file
25
web/composables/useImageUrl.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const useImageUrl = () => {
|
||||
const getImageUrl = (url: string) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整URL,直接返回
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 如果是相对路径,在开发环境中添加后端地址
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const fullUrl = `http://localhost:8080${url}`
|
||||
// console.log('useImageUrl - 开发环境:', { original: url, processed: fullUrl })
|
||||
return fullUrl
|
||||
}
|
||||
|
||||
// 生产环境中直接返回相对路径(通过Nginx代理)
|
||||
// console.log('useImageUrl - 生产环境:', { original: url, processed: url })
|
||||
return url
|
||||
}
|
||||
|
||||
return {
|
||||
getImageUrl
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,16 @@ export default defineNuxtConfig({
|
||||
optimizeDeps: {
|
||||
include: ['vueuc', 'date-fns'],
|
||||
exclude: ["oxc-parser"] // 强制使用 WASM 版本
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/uploads': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
|
||||
|
||||
@@ -97,52 +97,59 @@
|
||||
:class="{ 'is-image': isImageFile(file) }"
|
||||
>
|
||||
<!-- 图片文件显示预览 -->
|
||||
<div v-if="isImageFile(file)" class="image-preview">
|
||||
<n-image
|
||||
:src="file.access_url"
|
||||
:alt="file.original_name"
|
||||
:lazy="true"
|
||||
:intersection-observer-options="{
|
||||
root: null,
|
||||
rootMargin: '50px',
|
||||
threshold: 0.1
|
||||
}"
|
||||
object-fit="cover"
|
||||
class="preview-image"
|
||||
/>
|
||||
<div class="delete-button">
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
circle
|
||||
@click="deleteFile(file)"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
<div v-if="isImageFile(file)" class="file-item cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg p-3 transition-colors border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<div class="image-preview relative">
|
||||
<n-image
|
||||
:src="getImageUrl(file.access_url)"
|
||||
:alt="file.original_name"
|
||||
:lazy="false"
|
||||
object-fit="cover"
|
||||
class="preview-image rounded"
|
||||
@error="handleImageError"
|
||||
@load="handleImageLoad"
|
||||
/>
|
||||
<div class="delete-button">
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
circle
|
||||
@click="confirmDelete(file)"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-info">
|
||||
<div class="file-name">{{ file.original_name }}</div>
|
||||
<div class="file-size">{{ formatFileSize(file.file_size) }}</div>
|
||||
<div class="image-info mt-2">
|
||||
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{{ file.original_name }}
|
||||
</div>
|
||||
<div class="file-size text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatFileSize(file.file_size) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 非图片文件显示图标 -->
|
||||
<div v-else class="file-item">
|
||||
<div v-else class="file-item cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg p-3 transition-colors border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-600 relative">
|
||||
<div class="file-icon">
|
||||
<i :class="getFileIconClass(file.file_type)"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">{{ file.original_name }}</div>
|
||||
<div class="file-size">{{ formatFileSize(file.file_size) }}</div>
|
||||
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{{ file.original_name }}
|
||||
</div>
|
||||
<div class="file-size text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatFileSize(file.file_size) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="delete-button">
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
circle
|
||||
@click="deleteFile(file)"
|
||||
@click="confirmDelete(file)"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-trash"></i>
|
||||
@@ -180,6 +187,22 @@
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<n-modal v-model:show="showDeleteModal" preset="card" title="确认删除" style="width: 400px">
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500 text-4xl mb-4"></i>
|
||||
<p class="text-lg font-medium mb-2">确定要删除这个文件吗?</p>
|
||||
<p class="text-gray-600 mb-4">{{ fileToDelete?.original_name }}</p>
|
||||
<p class="text-sm text-gray-500">此操作不可撤销,文件将被永久删除。</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showDeleteModal = false">取消</n-button>
|
||||
<n-button type="error" @click="handleConfirmDelete">确认删除</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -187,6 +210,8 @@
|
||||
import { ref, onMounted, h } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useFileApi } from '~/composables/useFileApi'
|
||||
import { useImageUrl } from '~/composables/useImageUrl'
|
||||
|
||||
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
@@ -213,6 +238,7 @@ interface FileItem {
|
||||
|
||||
const message = useMessage()
|
||||
const fileApi = useFileApi()
|
||||
const { getImageUrl } = useImageUrl()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -224,6 +250,10 @@ const showUploadModal = ref(false)
|
||||
const fileUploadRef = ref()
|
||||
const uploadModalKey = ref(0)
|
||||
|
||||
// 删除确认相关
|
||||
const showDeleteModal = ref(false)
|
||||
const fileToDelete = ref<FileItem | null>(null)
|
||||
|
||||
// 分页
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
@@ -239,12 +269,7 @@ const total = computed(() => pagination.value.total)
|
||||
// 选项
|
||||
const fileTypeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: 'JPEG', value: 'jpeg' },
|
||||
{ label: 'PNG', value: 'png' },
|
||||
{ label: 'GIF', value: 'gif' },
|
||||
{ label: 'WebP', value: 'webp' },
|
||||
{ label: 'BMP', value: 'bmp' },
|
||||
{ label: 'SVG', value: 'svg' }
|
||||
{ label: '图片', value: 'image' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
@@ -271,6 +296,17 @@ const loadFileList = async () => {
|
||||
const response = await fileApi.getFileList(params)
|
||||
fileList.value = response.data.files || []
|
||||
pagination.value.total = response.data.total || 0
|
||||
|
||||
console.log('文件列表加载完成:', {
|
||||
total: pagination.value.total,
|
||||
files: fileList.value.map(f => ({
|
||||
id: f.id,
|
||||
name: f.original_name,
|
||||
type: f.file_type,
|
||||
url: f.access_url,
|
||||
isImage: isImageFile(f)
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载文件列表失败:', error)
|
||||
message.error('加载文件列表失败')
|
||||
@@ -331,6 +367,26 @@ const toggleFilePublic = async (file: FileItem) => {
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = (file: FileItem) => {
|
||||
fileToDelete.value = file
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!fileToDelete.value) return
|
||||
|
||||
try {
|
||||
await fileApi.deleteFiles([fileToDelete.value.id])
|
||||
message.success('文件删除成功')
|
||||
showDeleteModal.value = false
|
||||
fileToDelete.value = null
|
||||
loadFileList()
|
||||
} catch (error) {
|
||||
console.error('删除文件失败:', error)
|
||||
message.error('删除文件失败')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFile = async (file: FileItem) => {
|
||||
try {
|
||||
await fileApi.deleteFiles([file.id])
|
||||
@@ -372,6 +428,7 @@ const handleModalClose = (show: boolean) => {
|
||||
|
||||
const getFileIconClass = (fileType: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
'image': 'fas fa-image text-blue-500',
|
||||
'jpeg': 'fas fa-image text-blue-500',
|
||||
'jpg': 'fas fa-image text-blue-500',
|
||||
'png': 'fas fa-image text-green-500',
|
||||
@@ -405,8 +462,42 @@ const formatFileSize = (bytes: number) => {
|
||||
}
|
||||
|
||||
const isImageFile = (file: FileItem) => {
|
||||
const imageTypes = ['jpeg', 'jpg', 'png', 'gif', 'webp', 'bmp', 'svg']
|
||||
return imageTypes.includes(file.file_type.toLowerCase())
|
||||
// 后端返回的 file_type 是 "image",所以直接检查这个值
|
||||
const isImageByType = file.file_type.toLowerCase() === 'image'
|
||||
|
||||
// 检查文件名扩展名
|
||||
const imageExtensions = ['jpeg', 'jpg', 'png', 'gif', 'webp', 'bmp', 'svg']
|
||||
const fileNameLower = file.original_name.toLowerCase()
|
||||
const hasImageExtension = imageExtensions.some(ext => fileNameLower.endsWith(`.${ext}`))
|
||||
|
||||
// 检查 MIME 类型
|
||||
const mimeTypeLower = (file.mime_type || '').toLowerCase()
|
||||
const isImageByMime = mimeTypeLower.startsWith('image/')
|
||||
|
||||
// 综合判断
|
||||
const isImage = isImageByType || hasImageExtension || isImageByMime
|
||||
|
||||
console.log('isImageFile 详细检查:', {
|
||||
fileName: file.original_name,
|
||||
fileType: file.file_type,
|
||||
mimeType: file.mime_type,
|
||||
isImageByType: isImageByType,
|
||||
hasImageExtension: hasImageExtension,
|
||||
isImageByMime: isImageByMime,
|
||||
finalResult: isImage,
|
||||
accessUrl: file.access_url,
|
||||
processedUrl: getImageUrl(file.access_url)
|
||||
})
|
||||
|
||||
return isImage
|
||||
}
|
||||
|
||||
const handleImageError = (event: any) => {
|
||||
console.error('图片加载失败:', event)
|
||||
}
|
||||
|
||||
const handleImageLoad = (event: any) => {
|
||||
console.log('图片加载成功:', event)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
@@ -417,46 +508,26 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
/* 文件管理页面样式 */
|
||||
.image-preview-container {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
gap: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.image-item:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
position: relative;
|
||||
height: 240px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #f3f4f6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@@ -474,28 +545,29 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.delete-button .n-button {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.delete-button .n-button:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
background: rgba(239, 68, 68, 1);
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.image-info {
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.delete-button .n-button i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.image-info .file-name {
|
||||
|
||||
|
||||
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
@@ -505,20 +577,12 @@ onMounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.image-info .file-size {
|
||||
.file-size {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 240px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.file-icon {
|
||||
font-size: 48px;
|
||||
@@ -536,10 +600,7 @@ onMounted(() => {
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<div class="flex items-center space-x-4">
|
||||
<div v-if="configForm.site_logo" class="flex-shrink-0">
|
||||
<n-image
|
||||
:src="configForm.site_logo"
|
||||
:src="getImageUrl(configForm.site_logo)"
|
||||
alt="网站Logo"
|
||||
width="80"
|
||||
height="80"
|
||||
@@ -222,17 +222,15 @@
|
||||
>
|
||||
<div class="image-preview">
|
||||
<n-image
|
||||
:src="file.access_url"
|
||||
:src="getImageUrl(file.access_url)"
|
||||
:alt="file.original_name"
|
||||
:lazy="true"
|
||||
:intersection-observer-options="{
|
||||
root: null,
|
||||
rootMargin: '50px',
|
||||
threshold: 0.1
|
||||
}"
|
||||
:lazy="false"
|
||||
object-fit="cover"
|
||||
class="preview-image rounded"
|
||||
@error="handleImageError"
|
||||
@load="handleImageLoad"
|
||||
/>
|
||||
|
||||
<div class="image-info mt-2">
|
||||
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{{ file.original_name }}
|
||||
@@ -282,7 +280,11 @@ definePageMeta({
|
||||
ssr: false
|
||||
})
|
||||
|
||||
|
||||
import { useImageUrl } from '~/composables/useImageUrl'
|
||||
|
||||
const notification = useNotification()
|
||||
const { getImageUrl } = useImageUrl()
|
||||
const formRef = ref()
|
||||
const saving = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
@@ -443,6 +445,18 @@ const loadFileList = async () => {
|
||||
fileList.value = response.data.files || []
|
||||
pagination.value.total = response.data.total || 0
|
||||
console.log('获取到的图片文件:', fileList.value) // 调试信息
|
||||
|
||||
// 添加图片URL处理调试
|
||||
fileList.value.forEach(file => {
|
||||
console.log('图片文件详情:', {
|
||||
id: file.id,
|
||||
name: file.original_name,
|
||||
accessUrl: file.access_url,
|
||||
processedUrl: getImageUrl(file.access_url),
|
||||
fileType: file.file_type,
|
||||
mimeType: file.mime_type
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error)
|
||||
@@ -493,6 +507,14 @@ const formatFileSize = (size: number) => {
|
||||
return (size / (1024 * 1024 * 1024)).toFixed(1) + ' GB'
|
||||
}
|
||||
|
||||
const handleImageError = (event: any) => {
|
||||
console.error('图片加载失败:', event)
|
||||
}
|
||||
|
||||
const handleImageLoad = (event: any) => {
|
||||
console.log('图片加载成功:', event)
|
||||
}
|
||||
|
||||
// 页面加载时获取配置
|
||||
onMounted(() => {
|
||||
fetchConfig()
|
||||
@@ -525,8 +547,12 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -66,7 +66,7 @@ export const useTaskStore = defineStore('task', () => {
|
||||
const fetchTaskStats = async () => {
|
||||
try {
|
||||
const response = await taskApi.getTasks() as any
|
||||
console.log('原始任务API响应:', response)
|
||||
// console.log('原始任务API响应:', response)
|
||||
|
||||
// 处理API响应格式
|
||||
let tasks: TaskInfo[] = []
|
||||
@@ -76,7 +76,7 @@ export const useTaskStore = defineStore('task', () => {
|
||||
tasks = response
|
||||
}
|
||||
|
||||
console.log('解析后的任务列表:', tasks)
|
||||
// console.log('解析后的任务列表:', tasks)
|
||||
|
||||
if (tasks && tasks.length >= 0) {
|
||||
// 重置统计
|
||||
@@ -94,7 +94,7 @@ export const useTaskStore = defineStore('task', () => {
|
||||
|
||||
// 统计各种状态的任务
|
||||
tasks.forEach((task: TaskInfo) => {
|
||||
console.log('处理任务:', task.id, '状态:', task.status, '是否运行中:', task.is_running)
|
||||
// console.log('处理任务:', task.id, '状态:', task.status, '是否运行中:', task.is_running)
|
||||
|
||||
// 如果任务标记为运行中,优先使用running状态
|
||||
let currentStatus = task.status
|
||||
@@ -130,9 +130,9 @@ export const useTaskStore = defineStore('task', () => {
|
||||
runningTasks.value = running
|
||||
incompleteTasks.value = incomplete
|
||||
|
||||
console.log('任务统计更新:', stats)
|
||||
console.log('运行中的任务:', running)
|
||||
console.log('未完成的任务:', incomplete)
|
||||
// console.log('任务统计更新:', stats)
|
||||
// console.log('运行中的任务:', running)
|
||||
// console.log('未完成的任务:', incomplete)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务统计失败:', error)
|
||||
|
||||
Reference in New Issue
Block a user