实现删除附件的功能

This commit is contained in:
jingrow 2025-10-19 15:53:16 +08:00
parent ebd6ceb8e7
commit e9c271ba80
4 changed files with 335 additions and 35 deletions

View File

@ -1,33 +1,94 @@
<template>
<div class="media-section" v-if="hasAttachments">
<h4>{{ t('Attachments') }}</h4>
<div class="attachment-list">
<div class="media-section" v-if="hasAttachments || canAddAttachments">
<div class="section-header">
<h4>{{ t('Attachments') }}</h4>
<n-button
v-if="canAddAttachments"
type="default"
size="small"
text
@click="addAttachment"
:title="t('Add Attachment')"
class="add-attachment-btn"
>
<template #icon>
<n-icon><Icon icon="tabler:plus" /></n-icon>
</template>
</n-button>
</div>
<div class="attachment-list" v-if="hasAttachments">
<div
v-for="(attachment, index) in attachmentFields"
:key="index"
class="attachment-card"
@click="downloadAttachment(attachment.file_url)"
:class="['attachment-item', { 'is-image': isImageAttachment(attachment) }]"
@click="handleAttachmentClick(attachment)"
>
<div class="attachment-icon">
<Icon :icon="getFileIcon(attachment.file_url)" />
<!-- 图片附件显示 -->
<div v-if="isImageAttachment(attachment)" class="image-attachment">
<img :src="attachment.file_url" :alt="attachment.file_name" />
<div class="image-overlay">
<button
@click.stop="deleteAttachment(attachment)"
:title="t('Delete Attachment')"
class="delete-btn"
>
<Icon icon="tabler:x" />
</button>
</div>
<div class="image-filename">
<span class="file-name">{{ attachment.file_name }}</span>
</div>
</div>
<div class="attachment-info">
<span class="file-name">{{ attachment.file_name || getFileName(attachment.file_url) }}</span>
<span class="field-name">{{ attachment.is_private ? 'Private' : 'Public' }}</span>
<!-- 普通附件显示 -->
<div v-else class="file-attachment">
<div class="attachment-icon">
<Icon :icon="getFileIcon(attachment.file_url)" />
</div>
<div class="attachment-info">
<span class="file-name">{{ attachment.file_name }}</span>
</div>
<div class="file-overlay">
<button
@click.stop="deleteAttachment(attachment)"
:title="t('Delete Attachment')"
class="delete-btn"
>
<Icon icon="tabler:x" />
</button>
</div>
</div>
</div>
</div>
<!-- 添加附件按钮当没有附件时显示 -->
<div v-if="!hasAttachments && canAddAttachments" class="add-attachment-placeholder">
<n-button type="default" @click="addAttachment" class="add-button">
<template #icon>
<n-icon><Icon icon="tabler:plus" /></n-icon>
</template>
{{ t('Add Attachment') }}
</n-button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { NButton, NIcon } from 'naive-ui'
import { Icon } from '@iconify/vue'
import { t } from '@/shared/i18n'
const props = defineProps<{
metaFields: any[]
record: Record<string, any>
canEdit?: boolean
}>()
const emit = defineEmits<{
'add-attachment': []
'delete-attachment': [attachment: any]
}>()
// - Jingrow
@ -44,6 +105,19 @@ const attachmentFields = computed(() => {
})
const hasAttachments = computed(() => attachmentFields.value.length > 0)
const canAddAttachments = computed(() => props.canEdit !== false)
//
const isImageAttachment = (attachment: any) => {
const url = attachment.file_url || ''
const fileName = attachment.file_name || ''
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg']
return imageExtensions.some(ext =>
url.toLowerCase().includes(ext) ||
fileName.toLowerCase().includes(ext)
)
}
//
const getFileIcon = (url: string) => {
@ -67,6 +141,17 @@ const getFileName = (url: string) => {
return url.split('/').pop() || 'Unknown'
}
//
const handleAttachmentClick = (attachment: any) => {
if (isImageAttachment(attachment)) {
//
window.open(attachment.file_url, '_blank')
} else {
//
downloadAttachment(attachment.file_url)
}
}
const downloadAttachment = (url: string) => {
//
if (!url) return
@ -77,6 +162,16 @@ const downloadAttachment = (url: string) => {
link.target = '_blank'
link.click()
}
//
const addAttachment = () => {
emit('add-attachment')
}
//
const deleteAttachment = (attachment: any) => {
emit('delete-attachment', attachment)
}
</script>
<style scoped>
@ -89,44 +184,146 @@ const downloadAttachment = (url: string) => {
margin-bottom: 0;
}
.media-section h4 {
margin: 0 0 12px 0;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.section-header h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #374151;
}
/* 附件列表 */
.add-attachment-btn {
color: #6b7280;
transition: color 0.2s ease;
}
.add-attachment-btn:hover {
color: #374151;
}
/* 附件列表 - 网格布局一行2个 */
.attachment-list {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.attachment-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f8fafc;
border-radius: 6px;
.attachment-item {
cursor: pointer;
transition: background-color 0.2s ease;
border-radius: 6px;
overflow: hidden;
transition: transform 0.2s ease;
}
.attachment-card:hover {
background: #e2e8f0;
.attachment-item:hover {
transform: scale(1.02);
}
/* 图片附件样式 */
.image-attachment {
position: relative;
width: 100%;
height: 120px;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f8fafc;
border-radius: 6px;
}
.image-attachment img {
max-width: 100%;
max-height: 80px;
width: auto;
height: auto;
object-fit: contain;
}
.image-overlay {
position: absolute;
top: 4px;
right: 4px;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 10;
}
.image-attachment:hover .image-overlay {
opacity: 1;
}
.delete-btn {
width: 20px;
height: 20px;
border: none;
border-radius: 50%;
background: rgba(239, 68, 68, 0.9);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
.delete-btn:hover {
background: rgba(220, 38, 38, 0.95);
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
.image-filename {
position: absolute;
bottom: 4px;
left: 4px;
right: 4px;
text-align: center;
}
.image-filename .file-name {
font-size: 10px;
color: #6b7280;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background: rgba(255, 255, 255, 0.9);
padding: 2px 4px;
border-radius: 3px;
backdrop-filter: blur(4px);
}
/* 普通附件样式 */
.file-attachment {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f8fafc;
border-radius: 6px;
height: 120px;
}
.attachment-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
width: 24px;
height: 24px;
background: #e5e7eb;
border-radius: 4px;
color: #6b7280;
flex-shrink: 0;
}
.attachment-info {
@ -136,7 +333,7 @@ const downloadAttachment = (url: string) => {
.file-name {
display: block;
font-size: 13px;
font-size: 12px;
font-weight: 500;
color: #1f2937;
white-space: nowrap;
@ -144,10 +341,35 @@ const downloadAttachment = (url: string) => {
text-overflow: ellipsis;
}
.field-name {
display: block;
font-size: 11px;
color: #6b7280;
margin-top: 2px;
/* 普通附件悬浮删除样式 */
.file-overlay {
position: absolute;
top: 4px;
right: 4px;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 10;
}
.file-attachment:hover .file-overlay {
opacity: 1;
}
/* 添加附件占位符 */
.add-attachment-placeholder {
display: flex;
justify-content: center;
padding: 20px;
}
.add-button {
width: 100%;
}
/* 响应式设计 */
@media (max-width: 768px) {
.attachment-list {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -65,6 +65,9 @@
<AttachmentSection
:meta-fields="metaFields"
:record="record"
:can-edit="canEdit"
@add-attachment="handleAddAttachment"
@delete-attachment="handleDeleteAttachment"
/>
<TagSection
:meta-fields="metaFields"
@ -167,7 +170,7 @@ import TagSection from '@/core/components/form/sidebar/TagSection.vue'
import axios from 'axios'
import { t } from '@/shared/i18n'
import { get_session_api_headers } from '@/shared/api/auth'
import { updateRecord, getRecord, getRecordAttachments } from '@/shared/api/common'
import { updateRecord, getRecord, getRecordAttachments, deleteAttachment } from '@/shared/api/common'
import { downloadImageToLocal } from '@/shared/api/common'
import { usePageTypeSlug } from '@/shared/utils/slug'
import { resolvePagetypeDetailOverride, resolvePagetypeToolbarOverride } from '@/core/registry/pagetypeOverride'
@ -453,6 +456,54 @@ const toggleSidebarCollapse = () => {
saveSidebarPreferences()
}
//
function handleAddAttachment() {
//
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
input.accept = '*/*'
input.onchange = async (e) => {
const files = (e.target as HTMLInputElement).files
if (!files || files.length === 0) return
try {
// API
//
message.success(t('Attachment upload started'))
// TODO:
console.log('Uploading files:', Array.from(files).map(f => f.name))
//
// await loadDetail()
} catch (error) {
message.error(t('Failed to upload attachment'))
console.error('Upload error:', error)
}
}
input.click()
}
//
async function handleDeleteAttachment(attachment: any) {
try {
const result = await deleteAttachment(attachment.name)
if (result.success) {
message.success(t('Attachment deleted successfully'))
//
await loadDetail()
} else {
message.error(result.message || t('Failed to delete attachment'))
}
} catch (error) {
message.error(t('Failed to delete attachment'))
console.error('Delete attachment error:', error)
}
}
function toggleSeenBy() {
seenByExpanded.value = !seenByExpanded.value
}

View File

@ -680,5 +680,16 @@
"Backend": "后端",
"Create Frontend": "创建前端",
"Create Backend": "创建后端",
"Please select PageType": "请选择页面类型"
"Please select PageType": "请选择页面类型",
"Media Resources": "媒体资源",
"Attachments": "附件",
"Add Attachment": "添加附件",
"Delete Attachment": "删除附件",
"Attachment upload started": "附件上传已开始",
"Failed to upload attachment": "附件上传失败",
"Attachment deleted successfully": "附件删除成功",
"Failed to delete attachment": "删除附件失败",
"Images": "图片",
"Tags": "标签"
}

View File

@ -118,6 +118,22 @@ export const getRecordAttachments = async (pagetype: string, name: string): Prom
}
}
// 删除附件
export const deleteAttachment = async (attachmentName: string): Promise<{ success: boolean; message?: string }> => {
try {
await axios.delete(
`${jingrowServerUrl}/api/data/File/${attachmentName}`,
{
headers: get_session_api_headers(),
withCredentials: true
}
)
return { success: true, message: '附件删除成功' }
} catch (error: any) {
return { success: false, message: error.response?.data?.message || error.message || '删除附件失败' }
}
}
// 获取 Workspace 配置
export const getWorkspace = async (name: string): Promise<{ success: boolean; data?: any; message?: string }> => {
return getRecord('Workspace', name)