ow
This commit is contained in:
1 parent
3194f98ac0
commit
e740ec77ec
24 files changed
+1561
-65
No files matched your search
@@ -6,9 +6,9 @@
|
||||
* file-breadcrumb 目录面包屑(仅子目录态:左起排列,放不下折成 根+…+尾部段)
|
||||
* search-input 搜索框(v-model 即时过滤,确认键触发服务端搜索)
|
||||
* quick-entry 快捷功能入口(横向滚动图标组)
|
||||
* file-tab-filter 分类标签 + 排序 + 视图切换 + 多选开关
|
||||
* file-tab-filter 分类标签 + 新建文件夹 + 排序 + 视图切换 + 多选开关
|
||||
* file-grid-list 文件网格/列表卡片
|
||||
* float-add-btn 右下角悬浮加号
|
||||
* float-add-btn 右下角悬浮加号(直接选文件上传)
|
||||
* 数据来自后端 DMS 接口(composables/useDmsFiles,调用链对齐旧 WebVue DmsFileEnquiry.vue);
|
||||
* 组件契约(data/custom/fileBrowser.ts)不关心数据源,演示数据到真实接口只动页面层。
|
||||
* 目录导航:点文件夹下钻;子目录态隐藏底部 tabbar,返回箭头/面包屑退层级。
|
||||
@@ -18,24 +18,38 @@ import type { FileCategory, FileItem, FileKind, FileTabItem, QuickEntryItem } fr
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useAuthGuard, useDmsFiles, useThemeGradient } from "@/composables";
|
||||
import { DmsFileEntity } from "@/data/auto/dmsFileEntity";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import dmsFileService from "@/service/master/dmsFileService";
|
||||
import { useUploadQueueStore } from "@/stores/uploadQueue";
|
||||
import { chooseFiles } from "@/utils/chooseFiles";
|
||||
import { transferGlyph } from "@/utils/transferGlyph";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
// 页面底渐变跟随主题(派生逻辑在 composables/useThemeGradient,多页共用)
|
||||
const { themeBackground } = useThemeGradient();
|
||||
|
||||
const { files, loading, errorText, pathStack, atRoot, init, retry, openFolder, back, goTo, search } = useDmsFiles();
|
||||
const { files, loading, errorText, pathStack, atRoot, init, openTrail, retry, openFolder, back, goTo, search } = useDmsFiles();
|
||||
|
||||
// 首次显示拉根目录,之后每次回到页面只刷新当前目录(重进 tab 不该丢位置重新拉全链)
|
||||
// 上传队列(全局 store):入队在本页、胶囊读它的进行中计数、守卫回调消费它的面包屑导航请求
|
||||
const uploadQueue = useUploadQueueStore();
|
||||
|
||||
// 首次显示拉根目录,之后每次回到页面只刷新当前目录(重进 tab 不该丢位置重新拉全链);
|
||||
// 回页末尾消费传输页的面包屑导航请求(reLaunch 重建后恢复到目标目录)
|
||||
let started = false;
|
||||
useAuthGuard(() => {
|
||||
useAuthGuard(async () => {
|
||||
if (started) {
|
||||
retry();
|
||||
return;
|
||||
await retry();
|
||||
}
|
||||
else {
|
||||
started = true;
|
||||
await init();
|
||||
}
|
||||
const trail = uploadQueue.takeNav();
|
||||
if (trail) {
|
||||
await openTrail(trail);
|
||||
}
|
||||
started = true;
|
||||
init();
|
||||
});
|
||||
|
||||
// ===== 页面状态(全部单向下发,组件只上报意图) =====
|
||||
@@ -102,11 +116,6 @@ function toast(title: string): void {
|
||||
uni.showToast({ title, icon: "none" });
|
||||
}
|
||||
|
||||
/** 待接入真实功能时的统一占位提示 */
|
||||
function todoTip(): void {
|
||||
toast(t("FilePage.todoTip"));
|
||||
}
|
||||
|
||||
// ===== 导航栏 / 面包屑 =====
|
||||
function onToggleSearch(): void {
|
||||
showSearch.value = !showSearch.value;
|
||||
@@ -334,20 +343,83 @@ function onSheetAction(key: string, file: FileItem): void {
|
||||
toast(`${file.name} · ${key} · ${t("FilePage.todoTip")}`);
|
||||
}
|
||||
|
||||
// 新建文件夹命名对话框(CreateDmsFolder 在途标记防连点)
|
||||
const folderDialogVisible = ref(false);
|
||||
const folderSubmitting = ref(false);
|
||||
|
||||
// 上传队列状态见文件头部的 uploadQueue store
|
||||
|
||||
/** 悬浮「+」:新建文件夹已拆到工具栏图标,这里直接选文件上传 */
|
||||
function onAdd(): void {
|
||||
todoTip();
|
||||
pickAndUpload();
|
||||
}
|
||||
|
||||
/** 工具栏「文件夹+」图标:弹新建文件夹命名对话框 */
|
||||
function onNewFolder(): void {
|
||||
folderDialogVisible.value = true;
|
||||
}
|
||||
|
||||
/** 名称限制对齐后端目录模型:不能含路径分隔符(FolderLocalName 直接参与路径拼接) */
|
||||
async function onCreateFolder(name: string): Promise<void> {
|
||||
if (/[\\/]/.test(name)) {
|
||||
toast(t("FilePage.folderInvalid"));
|
||||
return;
|
||||
}
|
||||
folderSubmitting.value = true;
|
||||
try {
|
||||
const entity = new DmsFileEntity();
|
||||
entity.FileName = name;
|
||||
entity.FilePath = pathStack.value[pathStack.value.length - 1]?.value ?? "";
|
||||
await dmsFileService.CreateDmsFolder(entity);
|
||||
folderDialogVisible.value = false;
|
||||
toast(t("FilePage.folderCreated"));
|
||||
// 新建完直接刷当前目录,不等切回页面才重查
|
||||
retry();
|
||||
}
|
||||
catch (error: any) {
|
||||
// 业务异常主拦截器已弹模态,这里只兜网络层失败
|
||||
if (error?.errMsg) {
|
||||
toast(error.errMsg);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
folderSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选文件→入队(快照面包屑链供传输页展示/跳回)→跳传输列表;之后用户跳走不影响上传 */
|
||||
async function pickAndUpload(): Promise<void> {
|
||||
try {
|
||||
const picked = await chooseFiles();
|
||||
if (!picked.length) {
|
||||
return;
|
||||
}
|
||||
uploadQueue.addFiles(picked, [...pathStack.value]);
|
||||
uni.navigateTo({ url: pageUrl("transport") });
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.errMsg || error?.message || t("FilePage.todoTip"));
|
||||
}
|
||||
}
|
||||
|
||||
function goTransport(): void {
|
||||
uni.navigateTo({ url: pageUrl("transport") });
|
||||
}
|
||||
|
||||
/** 胶囊内的自绘传输字形(白字蓝底,颜色固定,模块级生成一次) */
|
||||
const transferIconWhite = transferGlyph("#ffffff");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 操作面板/多选操作栏展开时收起 tabbar:tabbar 在页面 fixed 层叠上下文之外,
|
||||
弹层 z-index 再高也压不住它,直接隐藏最干净(主流云盘同款交互) -->
|
||||
<app-page :hide-nav="true" :show-tabbar="atRoot && !actionFile && !selectMode">
|
||||
<app-page :hide-nav="true" :show-tabbar="atRoot && !actionFile && !selectMode && !folderDialogVisible">
|
||||
<view class="file-page" :style="{ background: themeBackground }">
|
||||
<!-- 上半屏整体固定,只有文件列表所在的 scroll-view 内部滚动(整页不滚) -->
|
||||
<view class="file-page__head">
|
||||
<page-navbar :title="navTitle" :show-back="!atRoot" :search-on="showSearch" :quick-on="showQuick" @back="onBack"
|
||||
@toggle-search="onToggleSearch" @toggle-quick="onToggleQuick" />
|
||||
<page-navbar :title="navTitle" :show-back="!atRoot" :search-on="showSearch" :quick-on="showQuick"
|
||||
:transferring="uploadQueue.uploadingCount" @back="onBack" @toggle-search="onToggleSearch"
|
||||
@toggle-quick="onToggleQuick" @transfer="goTransport" />
|
||||
<!-- 子目录态才有的路径条:白色衬底包住面包屑(外面不再是灰页面底),左起全量显示,溢出折叠 -->
|
||||
<view v-if="!atRoot" class="file-page__crumb">
|
||||
<file-breadcrumb :items="pathStack" @nav="onCrumbNav" />
|
||||
@@ -360,7 +432,8 @@ function onAdd(): void {
|
||||
<quick-entry :items="quickEntries" @click="onQuickClick" />
|
||||
</view>
|
||||
<file-tab-filter :tabs="tabs" :active="category" :view-mode="viewMode" :select-mode="selectMode"
|
||||
@change="onCategoryChange" @sort="onSort" @toggle-view="onToggleView" @toggle-select="onToggleSelect" />
|
||||
@change="onCategoryChange" @sort="onSort" @toggle-view="onToggleView" @toggle-select="onToggleSelect"
|
||||
@new-folder="onNewFolder" />
|
||||
<view v-if="selectMode" class="file-page__selection">
|
||||
<text class="file-page__selection-text">
|
||||
{{ t("FilePage.selectedCount", { count: selectedIds.length }) }}
|
||||
@@ -410,8 +483,17 @@ function onAdd(): void {
|
||||
</view>
|
||||
<!-- 多选态加号让位底部操作栏,避免两个悬浮物打架 -->
|
||||
<float-add-btn v-if="!selectMode" @click="onAdd" />
|
||||
<!-- 上传中悬浮胶囊:任意目录下一键回传输列表(任务在 store 里,跳走不中断) -->
|
||||
<view v-if="uploadQueue.uploadingCount && !selectMode" class="file-page__transfer" @click="goTransport">
|
||||
<image class="file-page__transfer-img" :src="transferIconWhite" mode="aspectFit" />
|
||||
<text class="file-page__transfer-text">
|
||||
{{ uploadQueue.uploadingCount }} {{ t("TransportPage.transferring") }}
|
||||
</text>
|
||||
</view>
|
||||
<file-action-sheet :visible="!!actionFile" :file="actionFile" @close="actionFile = null"
|
||||
@action="onSheetAction" />
|
||||
<folder-name-dialog :visible="folderDialogVisible" :submitting="folderSubmitting"
|
||||
@close="folderDialogVisible = false" @confirm="onCreateFolder" />
|
||||
<!-- 多选底部操作栏:常驻非模态(无遮罩),勾选与动作可交替进行;
|
||||
动作集是旧系统多选子集——共享/发布按同媒体类型条件,重命名/移动仅单选 -->
|
||||
<view v-if="selectMode" class="file-page__actions">
|
||||
@@ -493,6 +575,32 @@ function onAdd(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/* 上传中悬浮胶囊:左下角常驻(避开右侧可拖动加号),点击回传输列表 */
|
||||
.file-page__transfer {
|
||||
position: fixed;
|
||||
bottom: 220rpx;
|
||||
left: 24rpx;
|
||||
z-index: 8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 16rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background-color: rgba(41, 121, 255, 0.9);
|
||||
box-shadow: 0 6rpx 16rpx rgba(41, 121, 255, 0.35);
|
||||
}
|
||||
|
||||
.file-page__transfer-text {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 胶囊内自绘传输字形:与原文案图标同尺寸(26rpx) */
|
||||
.file-page__transfer-img {
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
}
|
||||
|
||||
.file-page__selection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 传输列表页:uploadQueue store 的视图层。
|
||||
*
|
||||
* 任务状态全部住在 store 里,本页只渲染与派发操作——上传中切走、回来看进度不断;
|
||||
* 与旧系统 OperatorStation 的 upload-progress/success/interrupted 事件链同一语义,
|
||||
* 只是把「页面持有状态」翻转成了「store 持有状态」。
|
||||
*/
|
||||
import type { UploadItem } from "@/stores/uploadQueue";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { useAuthGuard, useThemeGradient } from "@/composables";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import { useUploadQueueStore } from "@/stores/uploadQueue";
|
||||
|
||||
const { t } = useLocale();
|
||||
const { themeBackground } = useThemeGradient();
|
||||
const uploadQueue = useUploadQueueStore();
|
||||
|
||||
// 本页无后端数据要拉,守卫只做登录拦截(未登录直接送去登录页)
|
||||
useAuthGuard();
|
||||
|
||||
/** 字节数可读化:0/拿不到显示 -- */
|
||||
function formatSize(bytes: number): string {
|
||||
if (!bytes || bytes < 0) {
|
||||
return "--";
|
||||
}
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let value = bytes;
|
||||
let index = 0;
|
||||
while (value >= 1024 && index < units.length - 1) {
|
||||
value /= 1024;
|
||||
index += 1;
|
||||
}
|
||||
return `${value.toFixed(index === 0 || value >= 100 ? 0 : 1)} ${units[index]}`;
|
||||
}
|
||||
|
||||
/** 状态行:上传中带速度与百分比,失败附错误摘要 */
|
||||
function statusLine(item: UploadItem): string {
|
||||
switch (item.status) {
|
||||
case "queued":
|
||||
return t("TransportPage.queued");
|
||||
case "uploading":
|
||||
return `${t("TransportPage.uploading")} · ${formatSize(item.speed)}/s · ${item.progress}%`;
|
||||
case "success":
|
||||
return t("TransportPage.success");
|
||||
case "cancelled":
|
||||
return t("TransportPage.cancelled");
|
||||
default:
|
||||
return `${t("TransportPage.failed")}${item.error ? ` · ${item.error}` : ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** 按扩展名挑图标(字形均已核实存在于 uview-pro iconfont) */
|
||||
function kindIcon(name: string): string {
|
||||
const ext = name.split(".").pop()?.toLowerCase() || "";
|
||||
if (["jpg", "jpeg", "png", "gif", "webp", "bmp", "heic"].includes(ext)) {
|
||||
return "photo";
|
||||
}
|
||||
if (["mp4", "mov", "avi", "mkv", "webm"].includes(ext)) {
|
||||
return "play-circle";
|
||||
}
|
||||
if (["mp3", "wav", "m4a", "aac", "flac"].includes(ext)) {
|
||||
return "volume";
|
||||
}
|
||||
return "file-text";
|
||||
}
|
||||
|
||||
function statusColor(item: UploadItem): string {
|
||||
if (item.status === "failed") {
|
||||
return "#f0413e";
|
||||
}
|
||||
if (item.status === "success") {
|
||||
return "#19be6b";
|
||||
}
|
||||
return "#8f959e";
|
||||
}
|
||||
|
||||
/**
|
||||
* 点条目面包屑:登记目标目录链后 reLaunch 回文件 tab(与 app-tabbar 同款跳法);
|
||||
* 点任意段都去该任务的末段目录(入队时的目标目录),文件页 onShow 消费请求后恢复位置
|
||||
*/
|
||||
function goTargetFolder(item: UploadItem): void {
|
||||
uploadQueue.requestNav(item.targetTrail);
|
||||
uni.reLaunch({ url: pageUrl("fileTab") });
|
||||
}
|
||||
|
||||
/** 返回键:F5 刷新后历史栈已空,navigateBack 无效——无栈时兜底回文件 tab(本页就是文件页的附属页) */
|
||||
function onBack(): void {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
uni.reLaunch({ url: pageUrl("fileTab") });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :nav-title="t('TransportPage.title')" :custom-back="onBack">
|
||||
<view class="transport-page" :style="{ background: themeBackground }">
|
||||
<view v-if="!uploadQueue.items.length" class="transport-page__empty">
|
||||
<u-empty mode="list" :text="t('TransportPage.empty')" icon-size="120" />
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<!-- 进行中队(排队 + 上传中) -->
|
||||
<view v-if="uploadQueue.activeItems.length" class="transport-page__group">
|
||||
<text class="transport-page__group-title">
|
||||
{{ t("TransportPage.active") }} ({{ uploadQueue.activeItems.length }})
|
||||
</text>
|
||||
<view v-for="item in uploadQueue.activeItems" :key="item.id" class="transport-item">
|
||||
<view class="transport-item__row">
|
||||
<u-icon :name="kindIcon(item.name)" size="44" color="#2b8fff" />
|
||||
<view class="transport-item__main">
|
||||
<text class="transport-item__name">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="transport-item__status">
|
||||
{{ statusLine(item) }} · {{ formatSize(item.size) }}
|
||||
</text>
|
||||
<u-line-progress v-if="item.status === 'uploading'" :percent="item.progress" height="8"
|
||||
:show-percent="false" active-color="#2b8fff" />
|
||||
</view>
|
||||
<view class="transport-item__btn" @tap="uploadQueue.cancel(item.id)">
|
||||
{{ t("TransportPage.cancel") }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.targetTrail.length" class="transport-item__crumb" @tap="goTargetFolder(item)">
|
||||
<file-breadcrumb :items="item.targetTrail" show-single />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已结束队(成功 / 失败 / 取消) -->
|
||||
<view v-if="uploadQueue.doneItems.length" class="transport-page__group">
|
||||
<view class="transport-page__group-head">
|
||||
<text class="transport-page__group-title">
|
||||
{{ t("TransportPage.done") }} ({{ uploadQueue.doneItems.length }})
|
||||
</text>
|
||||
<text class="transport-page__clear" @tap="uploadQueue.clearDone()">
|
||||
{{ t("TransportPage.clearDone") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="item in uploadQueue.doneItems" :key="item.id" class="transport-item">
|
||||
<view class="transport-item__row">
|
||||
<u-icon :name="kindIcon(item.name)" size="44" :color="statusColor(item)" />
|
||||
<view class="transport-item__main">
|
||||
<text class="transport-item__name">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="transport-item__status" :style="{ color: statusColor(item) }">
|
||||
{{ statusLine(item) }} · {{ formatSize(item.size) }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="item.status !== 'success'" class="transport-item__btn" @tap="uploadQueue.retry(item.id)">
|
||||
{{ t("TransportPage.retry") }}
|
||||
</view>
|
||||
<view class="transport-item__btn transport-item__btn--plain" @tap="uploadQueue.remove(item.id)">
|
||||
{{ t("TransportPage.remove") }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.targetTrail.length" class="transport-item__crumb" @tap="goTargetFolder(item)">
|
||||
<file-breadcrumb :items="item.targetTrail" show-single />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.transport-page {
|
||||
min-height: calc(100vh - var(--window-top, 0px));
|
||||
padding: 24rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
|
||||
&__empty {
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
|
||||
&__group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
&__group-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__group-title {
|
||||
display: block;
|
||||
padding: 8rpx 8rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__clear {
|
||||
padding: 8rpx 8rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #2979ff;
|
||||
}
|
||||
}
|
||||
|
||||
.transport-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
|
||||
/* 行主体:图标 + 文本区 + 动作按钮横向排列(面包屑另占一行) */
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 目标目录面包屑行:整行可点,点任意段都跳回文件页的目标目录(单段也走同款卡片) */
|
||||
&__crumb {
|
||||
padding-left: 64rpx;
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 22rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
flex-shrink: 0;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
color: #2979ff;
|
||||
background-color: #eaf4ff;
|
||||
|
||||
&--plain {
|
||||
color: #8f959e;
|
||||
background-color: #f5f6f8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user