ow
This commit is contained in:
1 parent
ceb3e7beec
commit
f6f3735ab3
29 files changed
+8376
-140
No files matched your search
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 音乐厅页(对齐旧 WebVue views/master/PreviewAudio.vue):
|
||||
* 黑胶唱机播放器外壳。入参经 query 传:mode=list&key=<目录路径> 播放当前目录音频;
|
||||
* 或 key=<专辑编号> 播放某音乐专辑;无 key 走默认专辑。取数逻辑见 composables/useMediaShow。
|
||||
* 隐藏控制区时(v-model:controls)本壳顺带隐掉导航/系统栏做整页沉浸(H5 元素全屏在组件内处理)。
|
||||
*/
|
||||
import type { VinylTrack } from "@/components/vinyl-audio-player/vinyl-audio-player.vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { ref } from "vue";
|
||||
import { useMediaShow } from "@/composables";
|
||||
|
||||
const { loadTracks } = useMediaShow();
|
||||
|
||||
const tracks = ref<VinylTrack[]>([]);
|
||||
const controls = ref(true);
|
||||
const loading = ref(true);
|
||||
const errorText = ref("");
|
||||
|
||||
function closeMe(): void {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
uni.switchTab({
|
||||
url: "/pages/tab/media",
|
||||
fail: () => uni.reLaunch({ url: "/pages/tab/media" }),
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(async (query) => {
|
||||
const mode = String(query?.mode || "");
|
||||
const key = decodeURIComponent(String(query?.key || ""));
|
||||
try {
|
||||
const items = await loadTracks(mode, key);
|
||||
tracks.value = items.map(t => ({
|
||||
name: t.name || t.title || "",
|
||||
author: "有家相册",
|
||||
url: t.url,
|
||||
cover: t.pic || undefined,
|
||||
lrc: t.lrc || undefined,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
errorText.value = error?.message || "加载失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :hide-nav="true" :show-tabbar="false">
|
||||
<view v-if="loading" class="show-loading">
|
||||
<text class="show-loading__text">加载中…</text>
|
||||
</view>
|
||||
<view v-else-if="errorText" class="show-loading" @tap="closeMe">
|
||||
<text class="show-loading__text">{{ errorText }}</text>
|
||||
</view>
|
||||
<vinyl-audio-player v-else v-model:controls="controls" :tracks="tracks" @close="closeMe" />
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.show-loading {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #0b0d10;
|
||||
}
|
||||
|
||||
.show-loading__text {
|
||||
font-size: 28rpx;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 相册展览页(对齐旧 WebVue views/master/Preview.vue):
|
||||
* 全屏幻灯播放器外壳。入参经 query 传:mode=list&key=<目录路径> 展览当前目录图片;
|
||||
* 或 key=<专辑编号> 展览某相册;无 key 走默认相册。取数逻辑见 composables/useMediaShow。
|
||||
* 隐藏控制区时(v-model:controls)本壳顺带隐掉导航/系统栏做整页沉浸(H5 元素全屏在组件内处理)。
|
||||
*/
|
||||
import type { SlideItem } from "@/composables";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { ref } from "vue";
|
||||
import { useMediaShow } from "@/composables";
|
||||
|
||||
const { loadPhotos } = useMediaShow();
|
||||
|
||||
const items = ref<SlideItem[]>([]);
|
||||
const controls = ref(true);
|
||||
const loading = ref(true);
|
||||
const errorText = ref("");
|
||||
|
||||
function closeMe(): void {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
uni.switchTab({
|
||||
url: "/pages/tab/media",
|
||||
fail: () => uni.reLaunch({ url: "/pages/tab/media" }),
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(async (query) => {
|
||||
const mode = String(query?.mode || "");
|
||||
const key = decodeURIComponent(String(query?.key || ""));
|
||||
try {
|
||||
items.value = await loadPhotos(mode, key);
|
||||
} catch (error: any) {
|
||||
errorText.value = error?.message || "加载失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :hide-nav="true" :show-tabbar="false">
|
||||
<view v-if="loading" class="show-loading">
|
||||
<text class="show-loading__text">加载中…</text>
|
||||
</view>
|
||||
<view v-else-if="errorText" class="show-loading" @tap="closeMe">
|
||||
<text class="show-loading__text">{{ errorText }}</text>
|
||||
</view>
|
||||
<photo-slide-player v-else v-model:controls="controls" :items="items" @close="closeMe" />
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.show-loading {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.show-loading__text {
|
||||
font-size: 28rpx;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 影视厅页(对齐旧 WebVue views/master/PreviewVideo.vue):
|
||||
* 自绘控制播放器外壳。入参经 query 传:mode=list&key=<目录路径> 播放当前目录视频;
|
||||
* 或 key=<专辑编号> 播放某影视专辑;无 key 走默认专辑。取数逻辑见 composables/useMediaShow。
|
||||
* 隐藏控制区时(v-model:controls)本壳顺带隐掉导航/系统栏做整页沉浸。
|
||||
*/
|
||||
import type { VideoSource } from "@/components/video-player/video-player.vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { ref } from "vue";
|
||||
import { useMediaShow } from "@/composables";
|
||||
|
||||
const { loadVideos } = useMediaShow();
|
||||
|
||||
const videos = ref<VideoSource[]>([]);
|
||||
const controls = ref(true);
|
||||
const loading = ref(true);
|
||||
const errorText = ref("");
|
||||
|
||||
function closeMe(): void {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
uni.switchTab({
|
||||
url: "/pages/tab/media",
|
||||
fail: () => uni.reLaunch({ url: "/pages/tab/media" }),
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(async (query) => {
|
||||
const mode = String(query?.mode || "");
|
||||
const key = decodeURIComponent(String(query?.key || ""));
|
||||
try {
|
||||
const items = await loadVideos(mode, key);
|
||||
videos.value = items.map(v => ({
|
||||
name: v.name || v.title || "",
|
||||
url: v.url,
|
||||
m3u8: v.m3u8,
|
||||
pic: v.pic,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
errorText.value = error?.message || "加载失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :hide-nav="true" :show-tabbar="false">
|
||||
<view v-if="loading" class="show-loading">
|
||||
<text class="show-loading__text">加载中…</text>
|
||||
</view>
|
||||
<view v-else-if="errorText" class="show-loading" @tap="closeMe">
|
||||
<text class="show-loading__text">{{ errorText }}</text>
|
||||
</view>
|
||||
<video-player v-else v-model:controls="controls" :videos="videos" @close="closeMe" />
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.show-loading {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #0b0d10;
|
||||
}
|
||||
|
||||
.show-loading__text {
|
||||
font-size: 28rpx;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,485 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 共享大厅页(短地址 /share-hall,从文件页快捷入口「共享大厅」跳入):
|
||||
* 两态结构——顶层大厅只列家人群组(每张卡=一个共享目录,无任何写操作),
|
||||
* 点群组进组后才出现搜索/分类/排序/新建文件夹(旧系统 FamilyGroupShare 语义:
|
||||
* 大厅顶层不建目录,进组才挂 CreateFamilyDmsFolder)。
|
||||
* 组内条目动作按类型分流:目录下钻(无权限提示)、图片预览、视频/音频播放、
|
||||
* 下载、取消共享(DeleteFamilyDmsFolder 只摘映射不动原文件)。
|
||||
* 数据链在 composables/useShareHall;权限沿用 DmsFile(routes.ts),useAuthGuard 兜冷启动直达。
|
||||
*/
|
||||
import type { FileCategory, FileItem, FileTabItem } from "@/data/custom/fileBrowser";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { useAuthGuard, useShareHall, useThemeGradient } from "@/composables";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import { fetchFileBytes, saveFileBytes, toPlayableUrl } from "@/utils/dmsBytes";
|
||||
import { clearPlayback, setPlayback } from "@/utils/playbackBus";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
// 页面底渐变跟随主题(与文件页同一派生逻辑)
|
||||
const { themeBackground } = useThemeGradient();
|
||||
|
||||
const {
|
||||
groups,
|
||||
files,
|
||||
records,
|
||||
loading,
|
||||
errorText,
|
||||
pathStack,
|
||||
inGroup,
|
||||
init,
|
||||
enterGroup,
|
||||
exitGroup,
|
||||
openFolder,
|
||||
back,
|
||||
goTo,
|
||||
loadFolder,
|
||||
createFolder,
|
||||
unshare,
|
||||
} = useShareHall();
|
||||
|
||||
// 每次回到页面重拉:大厅群组与组内目录都可能被别的端改过
|
||||
useAuthGuard(async () => {
|
||||
if (inGroup.value) {
|
||||
await loadFolder(pathStack.value[pathStack.value.length - 1]?.value ?? "");
|
||||
}
|
||||
else {
|
||||
await init();
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 页面状态 =====
|
||||
const keyword = ref("");
|
||||
const category = ref<FileCategory>("all");
|
||||
const viewMode = ref<"grid" | "list">("list");
|
||||
/** 排序模式:0 时间降序 / 1 升序 / 2 名称(与文件页同套) */
|
||||
const sortMode = ref(0);
|
||||
const showSearch = ref(true);
|
||||
/** 新建文件夹对话框(folder-name-dialog 复用文件页组件) */
|
||||
const folderDialogVisible = ref(false);
|
||||
const folderSubmitting = ref(false);
|
||||
/** 动作提交中标记:showLoading 期间防连点 */
|
||||
const busy = ref(false);
|
||||
|
||||
/** 导航标题:大厅顶层「共享大厅」,进组后跟随当前层名(组根=群组名) */
|
||||
const navTitle = computed(() =>
|
||||
inGroup.value
|
||||
? pathStack.value[pathStack.value.length - 1]?.displayName || t("ShareHallPage.title")
|
||||
: t("ShareHallPage.title"),
|
||||
);
|
||||
|
||||
const tabs = computed<FileTabItem[]>(() => [
|
||||
{ key: "all", label: t("FilePage.catAll") },
|
||||
{ key: "image", label: t("FilePage.catImage") },
|
||||
{ key: "audio", label: t("FilePage.catAudio") },
|
||||
{ key: "video", label: t("FilePage.catVideo") },
|
||||
{ key: "document", label: t("FilePage.catDocument") },
|
||||
]);
|
||||
|
||||
/** 分类/关键字本地过滤(组内一次拉全,切标签不打接口) */
|
||||
const visibleFiles = computed<FileItem[]>(() => {
|
||||
const kw = keyword.value.trim().toLowerCase();
|
||||
const list = (inGroup.value ? files.value : groups.value).filter(
|
||||
f => (category.value === "all" || f.kind === category.value)
|
||||
&& (!kw || f.name.toLowerCase().includes(kw)),
|
||||
);
|
||||
return [...list].sort((a, b) => {
|
||||
if (sortMode.value === 2) {
|
||||
return a.name.localeCompare(b.name, "zh-CN");
|
||||
}
|
||||
return sortMode.value === 1 ? a.sortTime - b.sortTime : b.sortTime - a.sortTime;
|
||||
});
|
||||
});
|
||||
|
||||
function toast(title: string): void {
|
||||
uni.showToast({ title, icon: "none" });
|
||||
}
|
||||
|
||||
/** showModal 包 Promise(取消共享确认) */
|
||||
function confirmDialog(title: string, content: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title,
|
||||
content,
|
||||
success: res => resolve(!!res.confirm),
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 动作防重 + loading 包裹(与文件页 withBusy 同套路) */
|
||||
async function withBusy(task: () => Promise<void>, tip?: string): Promise<void> {
|
||||
if (busy.value) {
|
||||
return;
|
||||
}
|
||||
busy.value = true;
|
||||
uni.showLoading({ title: tip || t("FilePage.pleaseWait"), mask: true });
|
||||
try {
|
||||
await task();
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message || t("FilePage.actionFailed"));
|
||||
}
|
||||
finally {
|
||||
uni.hideLoading();
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 导航 / 搜索 / 分类 =====
|
||||
async function onBack(): Promise<void> {
|
||||
// 组内:退一层目录,组根再退回大厅;顶层:退出页面
|
||||
if (inGroup.value) {
|
||||
keyword.value = "";
|
||||
if (!(await back())) {
|
||||
await exitGroup();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 直链直达 /share-hall 时没有返回栈:兜回文件页
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
uni.reLaunch({ url: pageUrl("fileTab") });
|
||||
}
|
||||
|
||||
function onToggleSearch(): void {
|
||||
showSearch.value = !showSearch.value;
|
||||
}
|
||||
|
||||
/** 关键字确认键走服务端 contain 重查(仅组内;大厅顶层本地过滤群组名) */
|
||||
async function onSearch(): Promise<void> {
|
||||
if (inGroup.value) {
|
||||
await loadFolder(pathStack.value[pathStack.value.length - 1]?.value ?? "", keyword.value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 失败态重试:组内重查当前层,顶层重拉群组列表 */
|
||||
async function onRetry(): Promise<void> {
|
||||
if (inGroup.value) {
|
||||
await onSearch();
|
||||
return;
|
||||
}
|
||||
await init();
|
||||
}
|
||||
|
||||
function onCategoryChange(key: FileCategory): void {
|
||||
category.value = key;
|
||||
}
|
||||
|
||||
function onSort(): void {
|
||||
sortMode.value = (sortMode.value + 1) % 3;
|
||||
}
|
||||
|
||||
function onToggleView(): void {
|
||||
viewMode.value = viewMode.value === "grid" ? "list" : "grid";
|
||||
}
|
||||
|
||||
/** 面包屑点某段:截断到该层(组根段回组根而非出组,出组走返回箭头) */
|
||||
async function onCrumbNav(index: number): Promise<void> {
|
||||
keyword.value = "";
|
||||
await goTo(index);
|
||||
}
|
||||
|
||||
// ===== 条目动作层 =====
|
||||
|
||||
/** 点目录:有权限下钻,无权限由 openFolder 抛文案 toast */
|
||||
async function enterFolder(file: FileItem): Promise<void> {
|
||||
try {
|
||||
await openFolder(file);
|
||||
keyword.value = "";
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message || t("FilePage.actionFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function doPreview(file: FileItem): Promise<void> {
|
||||
const record = records.value.get(file.id);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
await withBusy(async () => {
|
||||
const urls = [toPlayableUrl(await fetchFileBytes(record))];
|
||||
uni.previewImage({ urls, current: urls[0] });
|
||||
});
|
||||
}
|
||||
|
||||
async function doDownload(file: FileItem): Promise<void> {
|
||||
const record = records.value.get(file.id);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
await withBusy(async () => {
|
||||
await saveFileBytes(await fetchFileBytes(record));
|
||||
toast(t("FilePage.downloadDone"));
|
||||
}, t("FilePage.downloading"));
|
||||
}
|
||||
|
||||
/** 视频/音频播放:Url 直链直接进播放页,否则字节拼整后本地播(与文件页同策略) */
|
||||
async function doPlay(file: FileItem): Promise<void> {
|
||||
const record = records.value.get(file.id);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
const kind = file.kind === "video" ? "video" : "audio";
|
||||
if (kind === "video" && record.Url) {
|
||||
goPlay(kind, file.name, record.Url);
|
||||
return;
|
||||
}
|
||||
await withBusy(async () => {
|
||||
const payload = await fetchFileBytes(record);
|
||||
goPlay(kind, file.name, toPlayableUrl(payload));
|
||||
}, t("FilePage.loadingMedia"));
|
||||
}
|
||||
|
||||
function goPlay(kind: "video" | "audio", name: string, url: string): void {
|
||||
// 播放地址走模块级总线传递,query 只带类型与展示名(文件页同款:blob 地址经 query 二次编码会跳转失败)
|
||||
setPlayback({ type: kind, name, url });
|
||||
uni.navigateTo({
|
||||
url: `${pageUrl("mediaPlay")}?type=${kind}&name=${encodeURIComponent(name)}`,
|
||||
fail: (err: any) => {
|
||||
clearPlayback();
|
||||
toast(err?.errMsg || t("FilePage.actionFailed"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function doUnshare(file: FileItem): Promise<void> {
|
||||
const ok = await confirmDialog(
|
||||
t("ShareHallPage.unshare"),
|
||||
t("ShareHallPage.unshareConfirmText").replace("{name}", file.name),
|
||||
);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
await withBusy(async () => {
|
||||
await unshare(file);
|
||||
toast(t("ShareHallPage.unshareDone"));
|
||||
});
|
||||
}
|
||||
|
||||
/** 按条目类型拼动作集(系统动作面板),取消共享任何类型都有 */
|
||||
function sheetActions(file: FileItem): Array<{ key: string; label: string }> {
|
||||
const actions: Array<{ key: string; label: string }> = [];
|
||||
if (file.kind === "folder") {
|
||||
actions.push({ key: "enter", label: t("ShareHallPage.enter") });
|
||||
}
|
||||
if (file.kind === "image") {
|
||||
actions.push({ key: "preview", label: t("ShareHallPage.preview") });
|
||||
}
|
||||
if (file.kind === "video" || file.kind === "audio") {
|
||||
actions.push({ key: "play", label: t("ShareHallPage.play") });
|
||||
}
|
||||
if (file.kind !== "folder") {
|
||||
actions.push({ key: "download", label: t("ShareHallPage.download") });
|
||||
}
|
||||
// 大厅顶层的群组卡没有共享记录,不给取消共享
|
||||
if (inGroup.value) {
|
||||
actions.push({ key: "unshare", label: t("ShareHallPage.unshare") });
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
function openSheet(file: FileItem): void {
|
||||
const actions = sheetActions(file);
|
||||
uni.showActionSheet({
|
||||
itemList: actions.map(a => a.label),
|
||||
success: (res) => {
|
||||
const action = actions[res.tapIndex];
|
||||
if (action.key === "enter") {
|
||||
// 顶层群组卡的「打开」是进组,组内目录卡的「打开」才是下钻
|
||||
if (inGroup.value) {
|
||||
enterFolder(file);
|
||||
}
|
||||
else {
|
||||
enterTopGroup(file);
|
||||
}
|
||||
}
|
||||
else if (action.key === "preview") {
|
||||
doPreview(file);
|
||||
}
|
||||
else if (action.key === "play") {
|
||||
doPlay(file);
|
||||
}
|
||||
else if (action.key === "download") {
|
||||
doDownload(file);
|
||||
}
|
||||
else if (action.key === "unshare") {
|
||||
doUnshare(file);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 单击条目:目录进组/下钻,图片预览,音视频播放,文档走动作面板(无预览能力) */
|
||||
function onOpen(file: FileItem): void {
|
||||
if (!inGroup.value) {
|
||||
enterTopGroup(file);
|
||||
return;
|
||||
}
|
||||
if (file.kind === "folder") {
|
||||
enterFolder(file);
|
||||
}
|
||||
else if (file.kind === "image") {
|
||||
doPreview(file);
|
||||
}
|
||||
else if (file.kind === "video" || file.kind === "audio") {
|
||||
doPlay(file);
|
||||
}
|
||||
else {
|
||||
openSheet(file);
|
||||
}
|
||||
}
|
||||
|
||||
/** 大厅顶层点群组卡:进组 */
|
||||
async function enterTopGroup(group: FileItem): Promise<void> {
|
||||
await withBusy(async () => {
|
||||
await enterGroup(group);
|
||||
category.value = "all";
|
||||
keyword.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 新建文件夹(仅组内) =====
|
||||
function onNewFolder(): void {
|
||||
folderDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function onCreateFolder(name: string): Promise<void> {
|
||||
folderSubmitting.value = true;
|
||||
try {
|
||||
await createFolder(name);
|
||||
toast(t("ShareHallPage.createDone"));
|
||||
folderDialogVisible.value = false;
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message || t("FilePage.actionFailed"));
|
||||
}
|
||||
finally {
|
||||
folderSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 二级页不带 tabbar(app-page 默认关) -->
|
||||
<app-page :hide-nav="true">
|
||||
<view class="hall-page" :style="{ background: themeBackground }">
|
||||
<view class="hall-page__head">
|
||||
<!-- 导航:标题随层级变化;返回箭头常显(顶层退出回文件页,组内退层);搜索图标仅组内;传输/快捷入口开关关闭 -->
|
||||
<page-navbar :title="navTitle" :show-back="true" :search-on="showSearch" :show-transfer="false"
|
||||
:show-quick-toggle="false" :show-search-toggle="inGroup" @back="onBack"
|
||||
@toggle-search="onToggleSearch" />
|
||||
<!-- 组内才有的路径条:两层以上显示(组根不显示,与文件页惯例一致) -->
|
||||
<view v-if="inGroup && pathStack.length > 1" class="hall-page__crumb">
|
||||
<file-breadcrumb :items="pathStack" @nav="onCrumbNav" />
|
||||
</view>
|
||||
<search-input v-if="inGroup && showSearch" v-model="keyword"
|
||||
:placeholder="t('FilePage.searchPlaceholder')" @search="onSearch" />
|
||||
<!-- 分类栏:新建文件夹仅组内出现(大厅顶层无写操作),共享大厅不做批量多选 -->
|
||||
<file-tab-filter v-if="inGroup" :tabs="tabs" :active="category" :view-mode="viewMode"
|
||||
:show-new-folder="true" :show-select="false" @change="onCategoryChange" @sort="onSort"
|
||||
@toggle-view="onToggleView" @new-folder="onNewFolder" />
|
||||
</view>
|
||||
<!-- 滚动窗口外层包装=卡片本体:margin/圆角/白底/裁剪在这层(与文件页同因) -->
|
||||
<view class="hall-page__scroll-wrap">
|
||||
<scroll-view scroll-y class="hall-page__scroll">
|
||||
<view class="hall-page__card">
|
||||
<view v-if="loading" class="hall-page__state">
|
||||
<text class="hall-page__state-text">
|
||||
{{ t("FilePage.loading") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-else-if="errorText" class="hall-page__state">
|
||||
<text class="hall-page__state-text hall-page__state-text--error">
|
||||
{{ errorText }}
|
||||
</text>
|
||||
<text class="hall-page__retry" @click="onRetry()">
|
||||
{{ t("FilePage.retryBut") }}
|
||||
</text>
|
||||
</view>
|
||||
<file-grid-list v-else :files="visibleFiles" :view-mode="viewMode" :selected-ids="[]"
|
||||
:empty-text="inGroup ? t('ShareHallPage.emptyText') : t('ShareHallPage.emptyGroups')"
|
||||
@open="onOpen" @more="openSheet" />
|
||||
</view>
|
||||
<view class="hall-page__bottom-space" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 新建文件夹:复用文件页命名对话框(默认文案就是新建) -->
|
||||
<folder-name-dialog :visible="folderDialogVisible" :submitting="folderSubmitting"
|
||||
@close="folderDialogVisible = false" @confirm="onCreateFolder" />
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 骨架样式与文件页/回收站同套(fixed 全屏 + head/scroll-wrap 卡片分区),前缀换 hall-page */
|
||||
.hall-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
/* 渐变底由 script 的 themeBackground 按主题 primary 动态计算(内联 style 下发) */
|
||||
}
|
||||
|
||||
.hall-page__head {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 面包屑分区:与文件页同节奏的间隔(面包屑自身就是白卡片) */
|
||||
.hall-page__crumb {
|
||||
padding: 20rpx 24rpx 0;
|
||||
}
|
||||
|
||||
/* 滚动窗口外层包装=卡片本体 */
|
||||
.hall-page__scroll-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 20rpx 20rpx 0;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* scroll-view 只当纯滚动容器垫满 wrap,滚动内容被 wrap 的圆角窗口裁剪 */
|
||||
.hall-page__scroll {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hall-page__bottom-space {
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.hall-page__state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24rpx;
|
||||
padding: 64rpx 32rpx;
|
||||
}
|
||||
|
||||
.hall-page__state-text {
|
||||
font-size: 26rpx;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
|
||||
.hall-page__state-text--error {
|
||||
color: #f0413e;
|
||||
}
|
||||
|
||||
.hall-page__retry {
|
||||
padding: 8rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
color: #2b8fff;
|
||||
background-color: #eaf4ff;
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@
|
||||
* 权限暂用 UserOperation(见 routes.ts 的 PAGE_PERMISSIONS);useAuthGuard 兜冷启动直达。
|
||||
*/
|
||||
import type { FolderRow } from "@/components/folder-move-picker/folder-move-picker.vue";
|
||||
import type { ShareTarget } from "@/components/folder-share-picker/folder-share-picker.vue";
|
||||
import type { PickerOption } from "@/components/option-picker/option-picker.vue";
|
||||
import type { FileCategory, FileItem, FileKind, FileTabItem, QuickEntryItem } from "@/data/custom/fileBrowser";
|
||||
import { useLocale } from "uview-pro";
|
||||
@@ -22,14 +23,12 @@ import { computed, ref, watch } from "vue";
|
||||
import { useAuthGuard, useDmsFiles, useThemeGradient } from "@/composables";
|
||||
import { DmsFileEntity } from "@/data/auto/dmsFileEntity";
|
||||
import { FamilyGroupFolderEntity } from "@/data/auto/familyGroupFolderEntity";
|
||||
import { FamilyGroupManagerUserSearchCriteria } from "@/data/auto/familyGroupManagerUserSearchCriteria";
|
||||
import { PublishAudioSearchCriteria } from "@/data/auto/publishAudioSearchCriteria";
|
||||
import { PublishData } from "@/data/auto/publishData";
|
||||
import { PublishPhotoSearchCriteria } from "@/data/auto/publishPhotoSearchCriteria";
|
||||
import { PublishVideoSearchCriteria } from "@/data/auto/publishVideoSearchCriteria";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import dmsFileService from "@/service/master/dmsFileService";
|
||||
import familyGroupService from "@/service/master/familyGroupService";
|
||||
import publishAudioService from "@/service/master/publishAudioService";
|
||||
import publishPhotoService from "@/service/master/publishPhotoService";
|
||||
import publishVideoService from "@/service/master/publishVideoService";
|
||||
@@ -180,12 +179,37 @@ function onSearch(value: string): void {
|
||||
search(value);
|
||||
}
|
||||
|
||||
/** 展览/播放三入口:以当前目录为上下文,mode=list + key=目录路径跳对应播放器页 */
|
||||
function goShowWithFolder(name: "photoShow" | "audioHall" | "videoHall"): void {
|
||||
const folder = pathStack.value[pathStack.value.length - 1]?.value ?? "";
|
||||
uni.navigateTo({
|
||||
url: `${pageUrl(name)}?mode=list&key=${encodeURIComponent(folder)}`,
|
||||
fail: (err: any) => toast(err?.errMsg || t("FilePage.actionFailed")),
|
||||
});
|
||||
}
|
||||
|
||||
function onQuickClick(key: string): void {
|
||||
// 回收站已落地(pages/trash);其余入口仍是占位提示
|
||||
// 回收站/共享大厅(pages/trash、pages/share-hall);相册/音乐厅/影视厅展览当前目录
|
||||
if (key === "trash") {
|
||||
uni.navigateTo({ url: pageUrl("trash") });
|
||||
return;
|
||||
}
|
||||
if (key === "shareCenter") {
|
||||
uni.navigateTo({ url: pageUrl("shareHall") });
|
||||
return;
|
||||
}
|
||||
if (key === "album") {
|
||||
goShowWithFolder("photoShow");
|
||||
return;
|
||||
}
|
||||
if (key === "musicHall") {
|
||||
goShowWithFolder("audioHall");
|
||||
return;
|
||||
}
|
||||
if (key === "videoHall") {
|
||||
goShowWithFolder("videoHall");
|
||||
return;
|
||||
}
|
||||
const entry = quickEntries.value.find(e => e.key === key);
|
||||
toast(entry ? `${entry.label} · ${t("FilePage.todoTip")}` : t("FilePage.todoTip"));
|
||||
}
|
||||
@@ -600,15 +624,12 @@ function goPlay(type: "video" | "audio", name: string, url: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 共享家人:选收藏夹里的家人目录,组装 FamilyGroupFolderEntity 批量建(旧 handleSelectShare)-----
|
||||
// ----- 共享家人:共享大厅式抽屉选目标目录(群组卡→逐级下钻→共享到此),
|
||||
// 多选文件一次批量建(CreateFamilyGroupFolder 数组入参,旧 handleSelectShare 同口径)-----
|
||||
const shareVisible = ref(false);
|
||||
const shareLoading = ref(false);
|
||||
const shareItems = ref<PickerOption[]>([]);
|
||||
/** 展示用 items 之外的组装字段(FamilyName/TopFolderName 提交才需要) */
|
||||
const shareFolders = ref<Array<{ key: string; familyName: string; topFolderName: string }>>([]);
|
||||
const shareTargets = ref<FileItem[]>([]);
|
||||
|
||||
async function actionShare(targets: FileItem[]): Promise<void> {
|
||||
function actionShare(targets: FileItem[]): void {
|
||||
const picked = targets.filter(f => f.kind !== "folder");
|
||||
if (!picked.length) {
|
||||
toast(t("FilePage.shareNoFile"));
|
||||
@@ -616,41 +637,11 @@ async function actionShare(targets: FileItem[]): Promise<void> {
|
||||
}
|
||||
shareTargets.value = picked;
|
||||
shareVisible.value = true;
|
||||
shareLoading.value = true;
|
||||
try {
|
||||
const criteria = new FamilyGroupManagerUserSearchCriteria();
|
||||
criteria.Filter = [];
|
||||
criteria.limit = 999;
|
||||
criteria.page = 1;
|
||||
criteria.GridColumns = [];
|
||||
const data = await familyGroupService.FamilyFolderFavoriteBySearchCriteria(criteria);
|
||||
const favorites = data?.Favorites ?? [];
|
||||
shareFolders.value = favorites.map(f => ({
|
||||
key: f.FolderName,
|
||||
familyName: f.FamilyName,
|
||||
topFolderName: f.TopFolderName,
|
||||
}));
|
||||
shareItems.value = favorites.map(f => ({
|
||||
key: f.FolderName,
|
||||
label: `${f.FamilyLocalName} / ${f.FolderLocalName}`,
|
||||
sub: f.Notice,
|
||||
}));
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message || t("FilePage.actionFailed"));
|
||||
}
|
||||
finally {
|
||||
shareLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onShareConfirm(key: string): Promise<void> {
|
||||
const target = shareFolders.value.find(f => f.key === key);
|
||||
shareVisible.value = false;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
async function onShareConfirm(target: ShareTarget): Promise<void> {
|
||||
const filesToShare = shareTargets.value;
|
||||
shareVisible.value = false;
|
||||
await withBusy(async () => {
|
||||
const rows = filesToShare.map((file) => {
|
||||
const record = recordOf(file);
|
||||
@@ -660,7 +651,7 @@ async function onShareConfirm(key: string): Promise<void> {
|
||||
// 旧系统组装:FolderName 文件系统名、FolderLocalName 原始名、FilePath 目标共享目录
|
||||
fgp.FolderName = record?.FileName ?? file.path;
|
||||
fgp.FolderLocalName = record?.OrgFileName ?? file.name;
|
||||
fgp.FilePath = target.key;
|
||||
fgp.FilePath = target.filePath;
|
||||
return fgp;
|
||||
});
|
||||
await dmsFileService.CreateFamilyGroupFolder(rows);
|
||||
@@ -701,7 +692,8 @@ async function actionPublish(targets: FileItem[]): Promise<void> {
|
||||
publishVisible.value = true;
|
||||
publishLoading.value = true;
|
||||
try {
|
||||
let names: string[] = [];
|
||||
// 拉到的专辑行含名称(PublishLocalName)/描述(Notice)/编号(PublishName),不能只留编号
|
||||
let rows: Array<{ PublishName?: string; PublishLocalName?: string; Notice?: string }> = [];
|
||||
if (first.kind === "image") {
|
||||
const criteria = new PublishPhotoSearchCriteria();
|
||||
criteria.Filter = [];
|
||||
@@ -709,7 +701,7 @@ async function actionPublish(targets: FileItem[]): Promise<void> {
|
||||
criteria.page = 1;
|
||||
criteria.GridColumns = [];
|
||||
const data = await publishPhotoService.FindBySearchCriteria(criteria);
|
||||
names = (data?.Publishs ?? []).map(p => p.PublishName);
|
||||
rows = data?.Publishs ?? [];
|
||||
}
|
||||
else if (first.kind === "video") {
|
||||
const criteria = new PublishVideoSearchCriteria();
|
||||
@@ -718,7 +710,7 @@ async function actionPublish(targets: FileItem[]): Promise<void> {
|
||||
criteria.page = 1;
|
||||
criteria.GridColumns = [];
|
||||
const data = await publishVideoService.FindBySearchCriteria(criteria);
|
||||
names = (data?.Datas ?? []).map(p => p.PublishName);
|
||||
rows = data?.Datas ?? [];
|
||||
}
|
||||
else {
|
||||
const criteria = new PublishAudioSearchCriteria();
|
||||
@@ -727,9 +719,24 @@ async function actionPublish(targets: FileItem[]): Promise<void> {
|
||||
criteria.page = 1;
|
||||
criteria.GridColumns = [];
|
||||
const data = await publishAudioService.FindBySearchCriteria(criteria);
|
||||
names = (data?.Datas ?? []).map(p => p.PublishName);
|
||||
rows = data?.Datas ?? [];
|
||||
}
|
||||
publishItems.value = names.filter(Boolean).map(n => ({ key: n, label: n }));
|
||||
// 主行显示专辑名称(无名称回落编号),副行显示描述 + 编号;key 仍用编号供 onPublishConfirm 发布
|
||||
publishItems.value = rows
|
||||
.filter(p => p?.PublishName)
|
||||
.map((p) => {
|
||||
const code = p.PublishName || "";
|
||||
const hasName = !!p.PublishLocalName;
|
||||
const label = p.PublishLocalName || code;
|
||||
const parts: string[] = [];
|
||||
if (p.Notice) {
|
||||
parts.push(p.Notice);
|
||||
}
|
||||
if (hasName) {
|
||||
parts.push(code);
|
||||
}
|
||||
return { key: code, label, sub: parts.join(" · ") };
|
||||
});
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message || t("FilePage.actionFailed"));
|
||||
@@ -966,9 +973,8 @@ const transferIconWhite = transferGlyph("#ffffff");
|
||||
<!-- 移动:网盘式逐级下钻选择器(进层浏览,「移动到此」提交当前层后直接调移动接口) -->
|
||||
<folder-move-picker :visible="moveVisible" :exclude-name="moveExcludeName" :source-path="moveSourcePath"
|
||||
:selected-count="moveTargets.length" @close="moveVisible = false" @confirm="onMovePick" />
|
||||
<!-- 共享家人:选家人目录 -->
|
||||
<option-picker :visible="shareVisible" :title="t('FilePage.shareTitle')" :items="shareItems"
|
||||
:loading="shareLoading" :empty-text="t('FilePage.pickerEmpty')" @close="shareVisible = false"
|
||||
<!-- 共享家人:共享大厅式抽屉(群组卡→逐级下钻→「共享到此」),多选文件一次批量共享 -->
|
||||
<folder-share-picker :visible="shareVisible" :selected-count="shareTargets.length" @close="shareVisible = false"
|
||||
@confirm="onShareConfirm" />
|
||||
<!-- 发布:选专辑 -->
|
||||
<option-picker :visible="publishVisible" :title="publishTitle" :items="publishItems" :loading="publishLoading"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user