296 lines
7.5 KiB
Vue
296 lines
7.5 KiB
Vue
<script setup>
|
||
import { computed, h } from 'vue'
|
||
import { Trash2, Download, Eye, MoreHorizontal } from '@lucide/vue'
|
||
import Button from '@/components/ui/button/button.vue'
|
||
import Checkbox from '@/components/ui/checkbox/checkbox.vue'
|
||
import Tag from '@/components/ui/tag/tag.vue'
|
||
import Dropdown from '@/components/ui/dropdown/dropdown.vue'
|
||
import { getPreviewUrl } from '@/services/fileService'
|
||
import {
|
||
extFromName,
|
||
extColor,
|
||
isImageType,
|
||
formatSize,
|
||
} from './fileVisual'
|
||
|
||
const props = defineProps({
|
||
file: { type: Object, required: true },
|
||
selected: { type: Boolean, default: false },
|
||
})
|
||
|
||
const emit = defineEmits(['select', 'preview', 'download', 'delete'])
|
||
|
||
const isPending = computed(() => !!props.file?.pending)
|
||
const isImage = computed(() => isImageType(props.file?.mx_filename || ''))
|
||
|
||
const thumbnailUrl = computed(() => {
|
||
if (!isImage.value) return ''
|
||
return props.file?.blobUrl || getPreviewUrl(props.file) || ''
|
||
})
|
||
|
||
const extname = computed(() => extFromName(props.file?.mx_filename || ''))
|
||
const extnameUpper = computed(() => extname.value.toUpperCase() || '?')
|
||
const typeColor = computed(() => extColor(props.file?.mx_filename || ''))
|
||
|
||
const moreMenuItems = computed(() => [
|
||
{ key: 'preview', label: '预览', icon: h(Eye, { size: 13 }) },
|
||
])
|
||
|
||
function onCheck(checked) {
|
||
emit('select', props.file, checked)
|
||
}
|
||
|
||
function onCardClick() {
|
||
if (!isPending.value) emit('preview', props.file)
|
||
}
|
||
|
||
function onDownload() {
|
||
if (!isPending.value) emit('download', props.file)
|
||
}
|
||
|
||
function onDelete() {
|
||
emit('delete', props.file)
|
||
}
|
||
|
||
function onMoreClick(info) {
|
||
if (info?.key === 'preview' && !isPending.value) emit('preview', props.file)
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
class="fcard"
|
||
:class="{ 'fcard--selected': selected, 'fcard--pending': isPending }"
|
||
:title="file.mx_filename"
|
||
@click="onCardClick"
|
||
>
|
||
<!-- 缩略图区域 -->
|
||
<div class="fcard__thumb">
|
||
<img v-if="isImage && thumbnailUrl" :src="thumbnailUrl" alt="" />
|
||
<div v-else class="fcard__type" :style="{ '--c': typeColor }">
|
||
{{ extnameUpper }}
|
||
</div>
|
||
|
||
<!-- 左上 checkbox(始终可见) -->
|
||
<span class="fcard__check" @click.stop>
|
||
<Checkbox :model-value="selected" @change="onCheck" />
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 文件信息 -->
|
||
<div class="fcard__info">
|
||
<div class="fcard__name">
|
||
<span class="fcard__title" :title="file.mx_filename">{{
|
||
file.mx_filename
|
||
}}</span>
|
||
<Tag v-if="isPending" variant="outline" class="fcard__tag">待上传</Tag>
|
||
</div>
|
||
<div class="fcard__meta">{{ formatSize(file.mx_filesize) }}</div>
|
||
</div>
|
||
|
||
<!-- 操作栏:图标按钮(无文字,语言无关,避免多语言下文字长度撑破布局) -->
|
||
<div class="fcard__footer" @click.stop>
|
||
<Button type="ghost" :disabled="isPending" @click="onDownload">
|
||
<Download />
|
||
</Button>
|
||
<span class="fcard__divider" />
|
||
<Button type="ghost" class="fcard__btn-danger" @click="onDelete">
|
||
<Trash2 />
|
||
</Button>
|
||
<span class="fcard__divider" />
|
||
<Dropdown :items="moreMenuItems" :disabled="isPending" @click="onMoreClick">
|
||
<Button type="ghost" :disabled="isPending">
|
||
<MoreHorizontal />
|
||
</Button>
|
||
</Dropdown>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.fcard {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid var(--fms-border);
|
||
border-radius: 6px;
|
||
background: var(--fms-card);
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
|
||
&--selected {
|
||
border-color: var(--fms-primary);
|
||
}
|
||
|
||
&--pending {
|
||
cursor: default;
|
||
}
|
||
}
|
||
|
||
/* 缩略图 */
|
||
.fcard__thumb {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 84px;
|
||
background: var(--fms-secondary-hover);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
}
|
||
|
||
/* 彩色类型方块(签名元素) */
|
||
.fcard__type {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 46px;
|
||
height: 46px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
color: var(--c);
|
||
background: color-mix(in srgb, var(--c) 14%, #fff);
|
||
border: 1px solid color-mix(in srgb, var(--c) 32%, #fff);
|
||
}
|
||
|
||
/* checkbox:直接浮在缩略图上,不加底块/阴影,保持轻量 */
|
||
.fcard__check {
|
||
position: absolute;
|
||
top: 4px;
|
||
left: 4px;
|
||
z-index: 3;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* 常驻操作栏:下载 / 删除 / 更多 */
|
||
.fcard__footer {
|
||
display: flex;
|
||
align-items: stretch;
|
||
box-sizing: border-box;
|
||
border-top: 1px solid var(--fms-border);
|
||
background: var(--fms-card);
|
||
|
||
:deep(.button) {
|
||
flex: 1;
|
||
box-sizing: border-box;
|
||
min-height: 0;
|
||
height: 32px;
|
||
margin: 0;
|
||
padding: 0;
|
||
/* 底部三个操作按钮必须是等宽 flex 子项:在 flex-basis: 0% 下浏览器按
|
||
内容盒均分剩余空间,任何一方额外的 padding / border 都会让其 border-box
|
||
更宽。故这里与 .popover-trigger 统一 padding:0 + border:none,
|
||
让三个按钮 border-box 完全相等 */
|
||
border: none;
|
||
border-radius: 0;
|
||
color: var(--fms-text-secondary);
|
||
/* 仅过渡颜色,避免 hover 时因 all 过渡引发的闪烁/位移 */
|
||
transition: background-color 0.15s, color 0.15s;
|
||
|
||
&:hover:not(:disabled) {
|
||
color: var(--fms-primary);
|
||
background: var(--fms-secondary-hover);
|
||
border-color: transparent;
|
||
}
|
||
|
||
&.fcard__btn-danger:hover:not(:disabled) {
|
||
color: var(--fms-danger);
|
||
background: color-mix(in srgb, var(--fms-danger) 10%, #fff);
|
||
}
|
||
|
||
/* 明确的禁用态:覆盖按钮库默认透明度,
|
||
让 pending 时的下载/更多按钮图标明显变淡灰、鼠标显示禁止手势,
|
||
lucide 图标用 currentColor 自动跟随。 */
|
||
&:disabled {
|
||
color: var(--fms-disabled-text);
|
||
background: transparent;
|
||
border-color: transparent;
|
||
cursor: not-allowed;
|
||
opacity: 1;
|
||
}
|
||
|
||
&:active {
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
/* 「更多」按钮被 Dropdown 包裹:Popover 的 .popover-trigger 才是 flex 子项,
|
||
必须让它与两个直接按钮一样 flex:1 且 padding/border 保持一致,
|
||
三个操作按钮宽度才会一致 */
|
||
:deep(.popover-trigger) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
justify-self: auto;
|
||
box-sizing: border-box;
|
||
padding: 0;
|
||
border: none;
|
||
|
||
.button {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 操作栏按钮间的竖向分隔线:独立元素,不依赖按钮类名 / 禁用态,
|
||
避免「更多」按钮被 dropdown 包裹且禁用时 border 被抹掉导致分隔线消失。 */
|
||
.fcard__divider {
|
||
flex: none;
|
||
width: 1px;
|
||
align-self: stretch;
|
||
background: var(--fms-border);
|
||
}
|
||
|
||
/* 文件信息 */
|
||
.fcard__info {
|
||
padding: 7px 10px 9px;
|
||
}
|
||
|
||
.fcard__name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.fcard__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--fms-text);
|
||
line-height: 1.5;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.fcard__tag {
|
||
flex: none;
|
||
margin: 0;
|
||
padding: 0 5px;
|
||
font-size: 11px;
|
||
line-height: 16px;
|
||
height: 18px;
|
||
color: var(--fms-warning);
|
||
background: color-mix(in srgb, var(--fms-warning) 12%, #fff);
|
||
border: 1px solid color-mix(in srgb, var(--fms-warning) 35%, #fff);
|
||
}
|
||
|
||
.fcard__meta {
|
||
margin-top: 2px;
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
color: var(--fms-text-secondary);
|
||
line-height: 1.4;
|
||
}
|
||
</style>
|