20260820223837

This commit is contained in:
oneao committed 2026-08-20 22:38:37 +08:00
1 parent 68ac5db038
commit 45854b0570
48 files changed
+7545 -650

No files matched your search

@@ -0,0 +1,295 @@
<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>