20260802220544

This commit is contained in:
oneao committed 2026-08-02 22:05:44 +08:00
1 parent c00329b5a4
commit 4fc9040a8a
60 files changed
+6273 -908

No files matched your search

@@ -0,0 +1,277 @@
<script setup>
import { computed } from "vue";
import { Trash2, Download, Eye, MoreHorizontal } from "@lucide/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 || ""));
function onCheck(e) {
emit("select", props.file, e.target.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>
<a-checkbox :checked="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>
<a-tag v-if="isPending" class="fcard__tag">待上传</a-tag>
</div>
<div class="fcard__meta">{{ formatSize(file.mx_filesize) }}</div>
</div>
<!-- 操作栏:图标按钮(无文字,语言无关,避免多语言下文字长度撑破布局) -->
<div class="fcard__footer" @click.stop>
<a-button type="text" size="small" :disabled="isPending" @click="onDownload">
<template #icon><Download :size="15" /></template>
</a-button>
<span class="fcard__divider" />
<a-button type="text" size="small" danger @click="onDelete">
<template #icon><Trash2 :size="15" /></template>
</a-button>
<span class="fcard__divider" />
<a-dropdown :trigger="['click']">
<a-button type="text" size="small" :disabled="isPending">
<template #icon><MoreHorizontal :size="15" /></template>
</a-button>
<template #overlay>
<a-menu @click="onMoreClick">
<a-menu-item key="preview">
<Eye :size="13" /> 预览
</a-menu-item>
</a-menu>
</template>
</a-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-surface);
overflow: hidden;
cursor: pointer;
&--selected {
border-color: var(--fms-primary);
}
&--pending {
cursor: default;
}
}
/* 缩略图 */
.fcard__thumb {
position: relative;
width: 100%;
height: 84px;
background: var(--fms-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-surface);
:deep(.ant-btn) {
flex: 1;
box-sizing: border-box;
height: 32px;
margin: 0;
padding: 0 4px;
/* 预留 1px 透明边框,保证 hover 时宽度恒定,不再因边框出现而收缩 */
border: 1px solid transparent;
border-radius: 0;
box-shadow: none;
outline: none;
color: var(--fms-text-secondary);
/* 仅过渡颜色,避免 hover 时因 all 过渡引发的闪烁/位移 */
transition: background-color 0.15s, color 0.15s;
/* 关闭 antdv 按钮自带的波纹伪元素,消除 hover/点击时的边框状残影与闪烁 */
&::before,
&::after {
display: none !important;
}
&:hover:not(:disabled) {
color: var(--fms-primary);
background: var(--fms-hover);
border-color: transparent;
}
&.ant-btn-dangerous:hover:not(:disabled) {
color: var(--fms-danger);
background: color-mix(in srgb, var(--fms-danger) 10%, #fff);
}
/* 明确的禁用态:覆盖 antdv 默认 + 上面无条件 color 覆盖,
让 pending 时的下载/更多按钮图标明显变淡灰、鼠标显示禁止手势,
lucide 图标用 currentColor 自动跟随。 */
&:disabled,
&[disabled] {
color: var(--fms-disabled);
background: transparent;
border-color: transparent;
cursor: not-allowed;
opacity: 1;
}
}
}
/* 操作栏按钮间的竖向分隔线:独立元素,不依赖按钮类名 / 禁用态,
避免「更多」按钮被 a-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;
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>