This commit is contained in:
owenchen committed 2026-10-21 20:50:55 +08:00
1 parent 211d2cd253
commit 7fb4503e0e
27 files changed
+2218 -1202

No files matched your search

@@ -0,0 +1,316 @@
<script setup lang="ts">
/**
* 文件选择弹层(音乐明细的封面 / 地址 / 歌词用):
* 复用 useDmsFiles 的「我的网盘」浏览链——目录下钻、返回上级、按名称搜索;
* 只列 accept 指定类型的文件(文件夹始终可进入用于下钻),点文件即回传整条 DMS 记录。
*/
import type { PropType } from "vue";
import type { DmsFileUploadViewDisplayData } from "@/data/auto/dmsFileUploadViewDisplayData";
import type { FileItem, FileKind } from "@/data/custom/fileBrowser";
import { useLocale } from "uview-pro";
import { computed, ref, watch } from "vue";
import { useDmsFiles, useThemeTokens } from "@/composables";
const props = defineProps({
/** 弹层是否展开 */
visible: {
type: Boolean,
default: false,
},
/** 可选文件类型(folder 不在此列也始终可进入) */
accept: {
type: Array as PropType<FileKind[]>,
default: () => ["image", "audio", "document"],
},
});
const emit = defineEmits<{
/** 关闭弹层(遮罩/关闭按钮/根目录点返回) */
(e: "close"): void;
/** 选中一个文件,回传完整记录(FileName 是提交值,Url 可直接预览) */
(e: "select", record: DmsFileUploadViewDisplayData): void;
}>();
const { t } = useLocale();
const { accentGradient } = useThemeTokens();
const files = useDmsFiles();
const keyword = ref("");
/** 类型过滤:文件夹保留用于下钻,文件只留 accept 内的类型 */
const rows = computed<FileItem[]>(() =>
files.files.value.filter(
file => file.kind === "folder" || props.accept.includes(file.kind),
),
);
const loading = files.loading;
/** 当前目录名(面包屑末项),用于顶部路径展示 */
const currName = computed(
() => files.pathStack.value[files.pathStack.value.length - 1]?.displayName || t("FilePage.myCloud"),
);
// 每次打开都从「我的网盘」根目录重新加载:目录可能被别的操作改过,也避免停在上一轮的下钻位置
watch(
() => props.visible,
(open) => {
if (!open) {
return;
}
keyword.value = "";
void files.init();
},
{ immediate: true },
);
/** 点文件夹下钻,点文件回传并关闭 */
async function onTap(row: FileItem): Promise<void> {
if (row.kind === "folder") {
await files.openFolder(row);
return;
}
const record = files.records.value.get(row.id);
if (!record) {
return;
}
emit("select", record);
emit("close");
}
/** 返回上级;已在根目录则关闭弹层 */
async function onBack(): Promise<void> {
const stayed = await files.back();
if (!stayed) {
emit("close");
}
}
async function onSearch(): Promise<void> {
await files.search(keyword.value);
}
/** 文件类型 -> 图标与配色(与 file-grid-list 同套;文件夹用 CSS 造型,图标库没有 folder) */
const kindVisual: Record<Exclude<FileKind, "folder">, { icon: string; color: string }> = {
image: { icon: "photo-fill", color: "#0ea5e9" },
video: { icon: "play-right-fill", color: "#8b5cf6" },
document: { icon: "file-text-fill", color: "#3b82f6" },
audio: { icon: "volume-up", color: "#f59e0b" },
};
function visualOf(kind: FileKind): { icon: string; color: string } {
return kind === "folder" ? { icon: "file-text-fill", color: "#8f959e" } : kindVisual[kind];
}
// 缩略图加载失败的行记住失败态,回退类型图标(与网盘宫格同一套处理,图被清理时不致白块)
const thumbFailed = ref(new Set<number>());
function onThumbError(row: FileItem): void {
thumbFailed.value.add(row.id);
}
/** 可显缩略图的地址:仅图片类型且后端 Url(Scale 压缩图)有值、未报错 */
function thumbSrcOf(row: FileItem): string {
return row.kind === "image" && row.thumbUrl && !thumbFailed.value.has(row.id) ? row.thumbUrl : "";
}
</script>
<template>
<!-- u-popup 只认 modelValue(v-model),绑 :show 会静默弹不出 -->
<u-popup :model-value="visible" mode="bottom" round="24" @close="emit('close')">
<view class="file-pick">
<view class="file-pick__head">
<text class="file-pick__title">{{ t("FilePage.pickFile") }}</text>
<u-icon name="close" size="32" color="#8f959e" @click="emit('close')" />
</view>
<!-- 当前目录 + 返回上级 -->
<view class="file-pick__bar">
<view class="file-pick__back" @tap="onBack">
<u-icon name="arrow-left" size="28" color="#5c626b" />
</view>
<text class="file-pick__path">{{ currName }}</text>
</view>
<!-- 按名称搜索当前目录(回车或点放大镜) -->
<view class="file-pick__search">
<input v-model="keyword" class="file-pick__search-input" :placeholder="t('FilePage.searchPlaceholder')"
confirm-type="search" @confirm="onSearch">
<view class="file-pick__search-btn" @tap="onSearch">
<u-icon name="search" size="32" color="#8f959e" />
</view>
</view>
<scroll-view scroll-y class="file-pick__list">
<view v-if="loading" class="file-pick__empty">
<text>{{ t("FilePage.loading") }}</text>
</view>
<view v-else-if="!rows.length" class="file-pick__empty">
<text>{{ t("FilePage.emptyText") }}</text>
</view>
<view v-for="row in rows" :key="row.id" class="file-pick__row" @tap="onTap(row)">
<view v-if="row.kind === 'folder'" class="file-pick__folder" :style="{ background: accentGradient }">
<view class="file-pick__folder-tab" :style="{ background: accentGradient }" />
</view>
<!-- 图片行:后端 Url 指向 Scale 压缩缩略(网盘宫格同源),失败回退类型图标 -->
<image v-else-if="thumbSrcOf(row)" class="file-pick__thumb" :src="thumbSrcOf(row)"
mode="aspectFill" @error="onThumbError(row)" />
<u-icon v-else :name="visualOf(row.kind).icon" size="36" :color="visualOf(row.kind).color" />
<view class="file-pick__row-main">
<text class="file-pick__row-name">{{ row.name }}</text>
<text v-if="row.sizeText" class="file-pick__row-meta">{{ row.sizeText }}</text>
</view>
<u-icon v-if="row.kind === 'folder'" name="arrow-right" size="24" color="#c0c4cc" />
</view>
</scroll-view>
<view class="file-pick__safe" />
</view>
</u-popup>
</template>
<style lang="scss" scoped>
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角行 */
.file-pick {
padding: 32rpx 24rpx 0;
background-color: #f2f3f5;
&__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8rpx 24rpx;
}
&__title {
font-size: 30rpx;
font-weight: 600;
color: #1f2329;
}
&__bar {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
border-radius: 16rpx;
background-color: #ffffff;
}
&__back {
display: flex;
align-items: center;
justify-content: center;
}
&__path {
flex: 1;
min-width: 0;
overflow: hidden;
font-size: 26rpx;
color: #5c626b;
text-overflow: ellipsis;
white-space: nowrap;
}
&__search {
display: flex;
align-items: center;
gap: 12rpx;
height: 76rpx;
margin-top: 16rpx;
padding: 0 24rpx;
border-radius: 16rpx;
background-color: #ffffff;
}
&__search-input {
flex: 1;
min-width: 0;
font-size: 28rpx;
color: #1f2329;
}
&__search-btn {
display: flex;
align-items: center;
justify-content: center;
}
&__list {
height: 52vh;
margin-top: 16rpx;
}
&__row {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 16rpx;
padding: 24rpx 28rpx;
border-radius: 16rpx;
background-color: #ffffff;
}
&__row-main {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
}
/* 图片预览缩略:尺寸与网盘宫格缩略一致 */
&__thumb {
flex-shrink: 0;
width: 96rpx;
height: 96rpx;
border-radius: 14rpx;
background-color: rgba(31, 35, 41, 0.04);
}
&__row-name {
overflow: hidden;
font-size: 28rpx;
color: #1f2329;
text-overflow: ellipsis;
white-space: nowrap;
}
&__row-meta {
margin-top: 6rpx;
font-size: 22rpx;
color: #a8adb5;
}
/* 文件夹:纯 CSS 造型(主体 + 左上页签) */
&__folder {
position: relative;
width: 44rpx;
height: 32rpx;
border-radius: 6rpx;
}
&__folder-tab {
position: absolute;
top: -8rpx;
left: 0;
width: 20rpx;
height: 10rpx;
border-radius: 4rpx 4rpx 0 0;
}
&__empty {
display: flex;
align-items: center;
justify-content: center;
padding: 80rpx 0;
font-size: 26rpx;
color: #a8adb5;
}
/* 底部留白:贴屏安全区 */
&__safe {
height: calc(32rpx + env(safe-area-inset-bottom));
}
}
</style>