ow
This commit is contained in:
1 parent
fd67fa8f63
commit
211d2cd253
31 files changed
+1830
-454
No files matched your search
@@ -45,6 +45,9 @@ import { copyIntoAppUploadDir } from "@/utils/appUploadFile";
|
||||
// 不含 H5 运行时补的短地址别名,故路径比较只能用真实路径,跳转 URL 才走 pageUrl()
|
||||
const loginPage = toRealPath("login");
|
||||
const timeout = 60000;
|
||||
// 常规接口超时:断网/线路不可达时 60 秒才会失败,App 里表现为按钮一直转(“一直停留”),
|
||||
// 收到 20 秒快速收敛;分片上传仍用上面的 timeout(大文件需要更长时间)
|
||||
const requestTimeout = 20000;
|
||||
|
||||
// ==================== 登录态(JWT) ====================
|
||||
|
||||
@@ -290,7 +293,7 @@ const httpRequestConfig: RequestConfig = {
|
||||
header: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
timeout,
|
||||
timeout: requestTimeout,
|
||||
meta: {
|
||||
originalData: true, // 拦截器收到完整响应(含 statusCode/header),自行解包
|
||||
toast: true, // 出错默认提示,单次请求可传 meta.toast=false 关闭
|
||||
|
||||
@@ -117,4 +117,20 @@ function onChange(index: number): void {
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 -6rpx 28rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
/*
|
||||
* 中间「多媒体」tab 的文字与其它 tab 文字水平对齐。
|
||||
* u-tabbar 原逻辑:普通项内容=[图标 40rpx + gap 8rpx + 文字] 在容器内垂直居中;
|
||||
* 中间项图标绝对定位凸出、不占流,组件给容器硬加 padding-top=图标半径+8rpx(内联 style),
|
||||
* 文字于是单独在剩余空间里居中,与普通项文字上下错位。
|
||||
* 修法:压掉内联 padding-top,给文字补上与普通项等高的前置占位(iconSize 40 + gap 8 = 48rpx),
|
||||
* 两边内容总高一致、同一条基线。若以后改 iconSize/gap,这里的 48rpx 要同步。
|
||||
*/
|
||||
:deep(.u-tabbar__content__circle__container) {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.u-tabbar__content__circle__container .u-tabbar__content__item__text) {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -70,7 +70,8 @@ const gridActions = computed<ActionDef[]>(() => {
|
||||
return actions;
|
||||
});
|
||||
|
||||
/** 行式区:媒体预览/播放(按类型取一)+ 共享/发布(仅文件,目录在旧系统被过滤) */
|
||||
/** 行式区:媒体预览/播放(按类型取一)+ 共享/发布(仅文件,目录在旧系统被过滤;
|
||||
* 发布只服务图片/视频/音频,文档没有发布功能不显示) */
|
||||
const listActions = computed<ActionDef[]>(() => {
|
||||
const file = props.file;
|
||||
if (!file) {
|
||||
@@ -89,9 +90,10 @@ const listActions = computed<ActionDef[]>(() => {
|
||||
}
|
||||
if (file.kind !== "folder") {
|
||||
actions.push({ key: "share", label: t("FilePage.actionShare"), icon: "share-fill" });
|
||||
// 发布命名:媒体类型统一「发布专辑」(不超 4 字),文档仍「发布」
|
||||
const isMedia = file.kind === "image" || file.kind === "video" || file.kind === "audio";
|
||||
actions.push({ key: "publish", label: isMedia ? t("FilePage.actionPublishAlbum") : t("FilePage.actionPublish"), icon: "volume-fill" });
|
||||
// 发布命名:媒体类型统一「发布专辑」(不超 4 字);文档不显示发布
|
||||
if (file.kind !== "document") {
|
||||
actions.push({ key: "publish", label: t("FilePage.actionPublishAlbum"), icon: "volume-fill" });
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 共享家人目标选择器(底部抽屉,共享大厅同款两态):顶层列家人群组卡片
|
||||
* 共享到群组目录目标选择器(底部抽屉,共享大厅同款两态):顶层列家人群组卡片
|
||||
* (图2 样式),点群组进组后逐级浏览目录,「共享到此」提交当前所在层。
|
||||
* 替代旧的收藏夹单选列表——目标不再限定收藏过的目录,群组根与任何有权限
|
||||
* 的目录都能接收共享(后端 CreateFamilyGroupFolder 纯插入不校验目录)。
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* 页面注入的 loadFull(分片拼整 + 落盘),就绪后立即原地替换为高清图。
|
||||
* 顶部工具组(左旋/右旋/放大/缩小/高清/关闭):旋转与缩放是每张图独立状态,
|
||||
* 换图后各自恢复;旋转字形为自绘 SVG(utils/rotateGlyph,图标库无旋转语义)。
|
||||
* 手势:双指捏合缩放(围绕两指中点,未放大也能捏),放大后单指拖动平移;
|
||||
* 捏合/放大期间 swiper 翻页让位,小图状态单指横扫才切上/下一张。
|
||||
* 缩略图缺失(老文件没有 Scale 图)时打开即自动拉原图兜底。
|
||||
* 关闭时回收 H5 blob: 地址(App 端是落盘文件路径,留着下次预览复用)。
|
||||
* 组件不认识 service/记录类型:原图加载器由页面以函数注入(与 admin 组件「不认识 service」约定一致)。
|
||||
@@ -44,8 +46,10 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
items?: ImageViewerItem[];
|
||||
/** 打开时定位的起始图(连览整个列表时从点击那张开始),越界自动夹取 */
|
||||
startIndex?: number;
|
||||
}>(),
|
||||
{ items: () => [] },
|
||||
{ items: () => [], startIndex: 0 },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -69,7 +73,6 @@ watch(
|
||||
() => props.visible,
|
||||
(v) => {
|
||||
if (v) {
|
||||
index.value = 0;
|
||||
slots.value = (props.items ?? []).map(item => ({
|
||||
src: item.thumb || "",
|
||||
full: undefined,
|
||||
@@ -80,6 +83,8 @@ watch(
|
||||
y: 0,
|
||||
dragging: false,
|
||||
}));
|
||||
// 先建槽后定位:起始索引按实际条数夹取,越界落回第一张
|
||||
index.value = Math.min(Math.max(0, props.startIndex), Math.max(0, slots.value.length - 1));
|
||||
// 没有缩略图的老文件:打开即自动拉原图兜底,不留白等用户点按钮
|
||||
slots.value.forEach((_, i) => {
|
||||
if (!slots.value[i].src) {
|
||||
@@ -136,11 +141,41 @@ let dragStartY = 0;
|
||||
let dragOriginX = 0;
|
||||
let dragOriginY = 0;
|
||||
|
||||
/** 放大后才启用单指平移:小图状态单指手势留给 swiper 翻页 */
|
||||
// 双指捏合会话:起始间距与起始倍率/偏移,松到一指后交还单指拖动
|
||||
const pinching = ref(false);
|
||||
let pinchStartDist = 0;
|
||||
let pinchStartScale = 1;
|
||||
let pinchStartX = 0;
|
||||
let pinchStartY = 0;
|
||||
let pinchFocusX = 0;
|
||||
let pinchFocusY = 0;
|
||||
|
||||
/** 两指间距(px):捏合倍率按它的变化比例算 */
|
||||
function touchDistance(a: any, b: any): number {
|
||||
return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
||||
}
|
||||
|
||||
/** 手势开始:双指进捏合会话(未放大也能捏),单指仅在放大后拖动,小图状态把手势留给 swiper 翻页 */
|
||||
function onImageTouchStart(e: any): void {
|
||||
const slot = slots.value[index.value];
|
||||
const touch = e.touches?.[0];
|
||||
if (!slot || !touch || slot.scale <= 1) {
|
||||
if (!slot) {
|
||||
return;
|
||||
}
|
||||
const touches = e.touches ?? [];
|
||||
if (touches.length >= 2) {
|
||||
pinching.value = true;
|
||||
// 沿用 dragging 关掉 transform 过渡,捏合才有跟手感
|
||||
slot.dragging = true;
|
||||
pinchStartDist = Math.max(1, touchDistance(touches[0], touches[1]));
|
||||
pinchStartScale = slot.scale;
|
||||
pinchStartX = slot.x;
|
||||
pinchStartY = slot.y;
|
||||
pinchFocusX = (touches[0].clientX + touches[1].clientX) / 2;
|
||||
pinchFocusY = (touches[0].clientY + touches[1].clientY) / 2;
|
||||
return;
|
||||
}
|
||||
const touch = touches[0];
|
||||
if (!touch || slot.scale <= 1) {
|
||||
return;
|
||||
}
|
||||
slot.dragging = true;
|
||||
@@ -150,11 +185,27 @@ function onImageTouchStart(e: any): void {
|
||||
dragOriginY = slot.y;
|
||||
}
|
||||
|
||||
/** 拖动平移:transform 里 translate 在最外层,位移沿屏幕轴、与旋转角无关,始终跟手 */
|
||||
/** 手势移动:双指按间距比例缩放(围绕两指中点,图片不会从手指下跑掉);单指平移沿屏幕轴,与旋转角无关 */
|
||||
function onImageTouchMove(e: any): void {
|
||||
const slot = slots.value[index.value];
|
||||
const touch = e.touches?.[0];
|
||||
if (!slot || !slot.dragging || !touch) {
|
||||
if (!slot) {
|
||||
return;
|
||||
}
|
||||
const touches = e.touches ?? [];
|
||||
if (pinching.value && touches.length >= 2) {
|
||||
const next = Math.min(4, Math.max(0.6, pinchStartScale * touchDistance(touches[0], touches[1]) / pinchStartDist));
|
||||
// 围绕焦点缩放:把焦点相对图片中心的偏移按倍率差缩放,替代单纯改 scale(否则总向屏幕中心收缩)
|
||||
const k = next / pinchStartScale;
|
||||
const fx = pinchFocusX - viewWidth / 2;
|
||||
const fy = pinchFocusY - viewHeight / 2;
|
||||
slot.scale = next;
|
||||
slot.x = fx - k * (fx - pinchStartX);
|
||||
slot.y = fy - k * (fy - pinchStartY);
|
||||
clampOffset(slot);
|
||||
return;
|
||||
}
|
||||
const touch = touches[0];
|
||||
if (!slot.dragging || !touch || slot.scale <= 1) {
|
||||
return;
|
||||
}
|
||||
slot.x = dragOriginX + touch.clientX - dragStartX;
|
||||
@@ -162,15 +213,29 @@ function onImageTouchMove(e: any): void {
|
||||
clampOffset(slot);
|
||||
}
|
||||
|
||||
function onImageTouchEnd(): void {
|
||||
/** 手势结束:松完则收尾;双指松掉一根时用剩下那根就地接续拖动,避免图片跳一下 */
|
||||
function onImageTouchEnd(e?: any): void {
|
||||
const slot = slots.value[index.value];
|
||||
if (slot) {
|
||||
if (!slot) {
|
||||
return;
|
||||
}
|
||||
const touches = e?.touches ?? [];
|
||||
if (touches.length === 0) {
|
||||
pinching.value = false;
|
||||
slot.dragging = false;
|
||||
return;
|
||||
}
|
||||
if (pinching.value) {
|
||||
pinching.value = false;
|
||||
dragStartX = touches[0].clientX;
|
||||
dragStartY = touches[0].clientY;
|
||||
dragOriginX = slot.x;
|
||||
dragOriginY = slot.y;
|
||||
}
|
||||
}
|
||||
|
||||
/** 放大(>1 倍)时禁用 swiper 翻页,把单指手势让给图片平移 */
|
||||
const panEnabled = computed(() => (slots.value[index.value]?.scale ?? 1) > 1);
|
||||
/** 双指捏合或放大(>1 倍)时禁用 swiper 翻页,把单指手势让给图片平移 */
|
||||
const panEnabled = computed(() => pinching.value || (slots.value[index.value]?.scale ?? 1) > 1);
|
||||
|
||||
/** 放大/缩小:倍率夹在 0.6~4,超出范围按钮照点不生效(变换仍平滑);缩放后收一下平移量 */
|
||||
function zoomCurrent(factor: number): void {
|
||||
@@ -389,7 +454,7 @@ function onImageError(i: number): void {
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
/* 拖动中关掉过渡:否则图片会追着手指爬,跟手感全无 */
|
||||
/* 手势会话中(拖动/捏合)关掉过渡:否则图片会追着手指爬,跟手感全无 */
|
||||
.iv-img--dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 通用条目动作面板:头部文件卡(类型图标 + 名称/元信息)+ 宫格动作 + 取消通栏。
|
||||
* file-action-sheet 的通用版——动作集由调用方按业务喂进来(共享中心的下载/取消共享等),
|
||||
* 组件只渲染与派发,不认识 service;视觉与网盘动作面板保持一致,替代系统 showActionSheet。
|
||||
*/
|
||||
import type { PropType } from "vue";
|
||||
import type { FileItem, FileKind } from "@/data/custom/fileBrowser";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
|
||||
export interface ItemSheetAction {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
/** 危险动作(取消共享/删除):图标与文字标红 */
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
/** 面板是否展开 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 目标条目;null 时渲染空面板(正常情况下 visible=true 必有值) */
|
||||
file: {
|
||||
type: Object as PropType<FileItem | null>,
|
||||
default: null,
|
||||
},
|
||||
/** 本次条目可用的动作集(调用方按类型派生) */
|
||||
actions: {
|
||||
type: Array as PropType<ItemSheetAction[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 面板关闭(遮罩/取消按钮) */
|
||||
(e: "close"): void;
|
||||
/** 点了某个动作,原样带上目标条目 */
|
||||
(e: "action", key: string, file: FileItem): void;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
// 文件夹图形随主题(与 file-action-sheet 同一强调渐变)
|
||||
const { accentGradient, accentSoft } = useThemeTokens();
|
||||
|
||||
// 类型 -> 头部图标与配色,与 file-grid-list 的 kindVisual 同一套
|
||||
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 onAction(action: ItemSheetAction): void {
|
||||
if (props.file) {
|
||||
emit("action", action.key, props.file);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- u-popup 只认 modelValue(v-model),绑 :show 会静默弹不出 -->
|
||||
<u-popup :model-value="visible" mode="bottom" round="24" @close="emit('close')">
|
||||
<view class="item-action-sheet">
|
||||
<!-- 头部白卡:类型图标 + 名称/元信息 -->
|
||||
<view class="item-action-sheet__head">
|
||||
<view v-if="file && file.kind === 'folder'" class="item-action-sheet__folder">
|
||||
<view class="item-action-sheet__folder-tab" :style="{ background: accentSoft }" />
|
||||
<view class="item-action-sheet__folder-body" :style="{ background: accentGradient }" />
|
||||
</view>
|
||||
<u-icon v-else-if="file" :name="kindVisual[file.kind as Exclude<FileKind, 'folder'>].icon" size="64"
|
||||
:color="kindVisual[file.kind as Exclude<FileKind, 'folder'>].color" />
|
||||
<view class="item-action-sheet__meta">
|
||||
<text class="item-action-sheet__name">
|
||||
{{ file?.name }}
|
||||
</text>
|
||||
<text class="item-action-sheet__sub">
|
||||
{{ file && file.kind !== 'folder' ? `${file.sizeText} · ` : '' }}{{ file?.modifiedAt }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 宫格动作:白卡上图标下文字,危险动作标红 -->
|
||||
<view class="item-action-sheet__grid">
|
||||
<view v-for="action in actions" :key="action.key" class="item-action-sheet__grid-btn"
|
||||
@tap="onAction(action)">
|
||||
<u-icon :name="action.icon" size="44" :color="action.danger ? '#f0413e' : '#1f2329'" />
|
||||
<text class="item-action-sheet__grid-text"
|
||||
:style="action.danger ? { color: '#f0413e' } : undefined">
|
||||
{{ action.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 取消通栏:退出锚点 -->
|
||||
<view class="item-action-sheet__cancel" @tap="emit('close')">
|
||||
<text class="item-action-sheet__cancel-text">
|
||||
{{ t("FilePage.folderCancel") }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="item-action-sheet__safe" />
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格(对齐 file-action-sheet):浅灰面板底 + 白色圆角卡分块 */
|
||||
.item-action-sheet {
|
||||
padding: 32rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 文件夹造型与 file-grid-list 同源:主体 + 左上页签 */
|
||||
&__folder {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 84rpx;
|
||||
height: 68rpx;
|
||||
}
|
||||
|
||||
&__folder-tab {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 36rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 8rpx 8rpx 0 0;
|
||||
}
|
||||
|
||||
&__folder-body {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
overflow: hidden;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 24rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* 宫格钮:白卡、上图标下文字居中,动作数 ≤4 时平分一行 */
|
||||
&__grid-btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 28rpx 0;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__grid-text {
|
||||
margin-top: 14rpx;
|
||||
font-size: 24rpx;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
/* 取消通栏:与内容隔一段灰底收尾,防误点动作区 */
|
||||
&__cancel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 28rpx;
|
||||
padding: 26rpx 0;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__cancel-text {
|
||||
font-size: 28rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
|
||||
/* 底部留白:贴屏安全区(H5 无 env 时也有 32rpx 呼吸) */
|
||||
&__safe {
|
||||
height: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 通用单选列表弹层(底部抽屉):标题 + 可滚动选项行 + 确认按钮。
|
||||
* 「共享家人」选目标群组目录、「发布」选专辑都吃它——数据由页面拉好喂进来,组件不发请求。
|
||||
* 「共享到群组目录」选目标群组目录、「发布」选专辑都吃它——数据由页面拉好喂进来,组件不发请求。
|
||||
* 行为:展开时重置为「未选中」(防上次残留误提交);点行仅选中,点确认才 emit confirm(key);
|
||||
* loading 显示转圈占位,items 空且不在 loading 时显示 emptyText。
|
||||
*/
|
||||
|
||||
@@ -83,6 +83,14 @@ export function useFamilyGroups() {
|
||||
),
|
||||
);
|
||||
|
||||
/** 我的申请是否待审:非管理员时后端把「我自己的 ValidIndc=N 申请」放进 FamilyGroupRequests
|
||||
* (管理员视角这里才是全群待审名单,两种视角前端都只用 UserName 比对我自己) */
|
||||
const isPending = computed(() =>
|
||||
(detail.value.FamilyGroupRequests ?? []).some(
|
||||
(r) => r.UserName === myName.value,
|
||||
),
|
||||
);
|
||||
|
||||
function setMyUser(name: string): void {
|
||||
myName.value = name;
|
||||
}
|
||||
@@ -257,12 +265,26 @@ export function useFamilyGroups() {
|
||||
await openGroup(familyNameOf());
|
||||
}
|
||||
|
||||
/** 移除某成员对某目录的权限:摘掉明细后整单提交,后端 SavePermissions 按「缺失即删」落库
|
||||
* (无单条 Delete 接口;未展示的管理员记录仍在明细里,不会被连带删掉) */
|
||||
async function removePermission(
|
||||
folderName: string,
|
||||
userName: string,
|
||||
): Promise<void> {
|
||||
detail.value.FamilyGroupPermissions = (
|
||||
detail.value.FamilyGroupPermissions ?? []
|
||||
).filter((p) => !(p.FolderName === folderName && p.UserName === userName));
|
||||
await save();
|
||||
}
|
||||
|
||||
/** 申请加入当前群(非成员,公开且可申请的群才会露这个入口) */
|
||||
async function applyJoin(notice: string): Promise<void> {
|
||||
const entity = new FamilyGroupManagerEntity();
|
||||
entity.FamilyName = familyNameOf();
|
||||
entity.Notice = notice;
|
||||
await familyGroupService.InsertFamilyGroupManager(entity);
|
||||
// 提交后刷新详情:后端会把我的待审申请放进 FamilyGroupRequests,头卡据此显示「已申请,待审核」
|
||||
await openGroup(familyNameOf());
|
||||
}
|
||||
|
||||
// ===== 保存 / 解散 =====
|
||||
@@ -317,6 +339,7 @@ export function useFamilyGroups() {
|
||||
isEdit,
|
||||
isManager,
|
||||
isMember,
|
||||
isPending,
|
||||
setMyUser,
|
||||
// list
|
||||
loadList,
|
||||
@@ -334,6 +357,7 @@ export function useFamilyGroups() {
|
||||
removeMember,
|
||||
toggleManager,
|
||||
assignPermission,
|
||||
removePermission,
|
||||
applyJoin,
|
||||
// save / dissolve
|
||||
save,
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
* 展览播放器数据装载:把旧 WebVue 的 Preview.vue / PreviewAudio.vue / PreviewVideo.vue
|
||||
* 三页 onMounted 的取数逻辑收敛成一个 kind 参数化 composable,供三个展示页复用。
|
||||
*
|
||||
* 三态取数(照抄旧页语义):
|
||||
* 取数模式(照抄旧页语义 + 共享中心扩展):
|
||||
* 1. mode=list:key 当作「目录路径」,走 DMS 查询 [FilePath single=key] + [FileType single=P/A/V]
|
||||
* -> 文件页 album/musicHall/videoHall 快捷入口用,展览当前目录里的全部该类文件;
|
||||
* 2. 否则 key 非空:按专辑编号取明细(photo 用 FindPublishPhotoDetailByPublishName,
|
||||
* 2. mode=share:key 为「群组系统名|共享目录路径」,走 FindFamilyBySearchCriteria 拉共享目录
|
||||
* (共享中心的媒体不存在自己网盘里,list 模式拉不到),再按 kindOf 在本地筛出媒体;
|
||||
* 3. 否则 key 非空:按专辑编号取明细(photo 用 FindPublishPhotoDetailByPublishName,
|
||||
* audio/video 无该方法 -> FindByKey(key).Details);
|
||||
* 3. 否则(无 key):取默认专辑(photo FindPublishPhotoDetail 全量,audio/video FindPublishXxxDefault)。
|
||||
* 4. 否则(无 key):取默认专辑(photo FindPublishPhotoDetail 全量,audio/video FindPublishXxxDefault)。
|
||||
*
|
||||
* 后端把同一素材给两个地址:缩略图/转码给 Formatted*,原图/原码给 Url/FileName。
|
||||
* 幻灯轮播与列表用缩略图(加载快),精看/下载/播放用原图原码。视频原码 Url 对专辑明细
|
||||
@@ -15,12 +17,14 @@
|
||||
*/
|
||||
import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria";
|
||||
import { DmsFileUploadViewDisplayData as UploadView } from "@/data/auto/dmsFileUploadViewDisplayData";
|
||||
import { FamilyDmsFileViewDisplayData as FamilyView } from "@/data/auto/familyDmsFileViewDisplayData";
|
||||
import { SearchFilter } from "@/data/auto/searchFilter";
|
||||
import { FileTypePolicy } from "@/policy/custom/fileTypePolicy";
|
||||
import dmsFileService from "@/service/master/dmsFileService";
|
||||
import publishAudioService from "@/service/master/publishAudioService";
|
||||
import publishPhotoService from "@/service/master/publishPhotoService";
|
||||
import publishVideoService from "@/service/master/publishVideoService";
|
||||
import { kindOf } from "./useDmsFiles";
|
||||
|
||||
const FIND_LIMIT = 999999;
|
||||
|
||||
@@ -89,6 +93,38 @@ async function listRows(fileType: string, path: string): Promise<any[]> {
|
||||
return data?.Datas ?? [];
|
||||
}
|
||||
|
||||
/** 拆「群组系统名|共享目录路径」:组根路径为空(与 useShareHall 组根语义一致) */
|
||||
function splitShareKey(key: string): [string, string] {
|
||||
const idx = key.indexOf("|");
|
||||
return idx < 0 ? [key, ""] : [key.slice(0, idx), key.slice(idx + 1)];
|
||||
}
|
||||
|
||||
/** mode=share:拉共享目录全量,媒体类型本地用 kindOf 筛(后端共享接口不做类型过滤) */
|
||||
async function shareRows(key: string, kind: "audio" | "video"): Promise<any[]> {
|
||||
const [familyName, path] = splitShareKey(key);
|
||||
const criteria = new DmsFileSearchCriteria();
|
||||
const filters: SearchFilter[] = [];
|
||||
const familyMatch = new SearchFilter();
|
||||
familyMatch.Column = FamilyView.DB_FAMILY_NAME;
|
||||
familyMatch.Type = "SingleSelection";
|
||||
familyMatch.Operator = "single";
|
||||
familyMatch.Value = familyName;
|
||||
filters.push(familyMatch);
|
||||
const pathMatch = new SearchFilter();
|
||||
pathMatch.Column = FamilyView.DB_FILE_PATH;
|
||||
pathMatch.Type = "SingleSelection";
|
||||
pathMatch.Operator = "single";
|
||||
// 组根直属记录的 FilePath 存的是群组编号(useShareHall.loadFolder 同口径)
|
||||
pathMatch.Value = path || familyName;
|
||||
filters.push(pathMatch);
|
||||
criteria.Filter = filters;
|
||||
criteria.limit = FIND_LIMIT;
|
||||
criteria.page = 1;
|
||||
criteria.GridColumns = [];
|
||||
const data = await dmsFileService.FindFamilyBySearchCriteria(criteria);
|
||||
return (data?.Families ?? []).filter((r) => kindOf(r) === kind);
|
||||
}
|
||||
|
||||
export function useMediaShow() {
|
||||
/** 相册展览条目 */
|
||||
async function loadPhotos(mode: string, key: string): Promise<SlideItem[]> {
|
||||
@@ -128,6 +164,17 @@ export function useMediaShow() {
|
||||
name: baseName(r.OrgFileName || r.Url),
|
||||
}));
|
||||
}
|
||||
if (mode === "share") {
|
||||
// 共享中心跳厅:以共享目录为播放列表(与文件页 list 同形态,数据源换共享接口)
|
||||
const rows = await shareRows(key, "audio");
|
||||
return rows.map((r: any) => ({
|
||||
url: r.Url,
|
||||
id: r.FileId,
|
||||
pic: r.FormattedPic || "",
|
||||
title: r.OrgFileName || baseName(r.Url),
|
||||
name: baseName(r.OrgFileName || r.Url),
|
||||
}));
|
||||
}
|
||||
const data = key
|
||||
? await publishAudioService.FindByKey(key)
|
||||
: await publishAudioService.FindPublishAudioDefault();
|
||||
@@ -154,6 +201,18 @@ export function useMediaShow() {
|
||||
name: baseName(r.OrgFileName || r.Url),
|
||||
}));
|
||||
}
|
||||
if (mode === "share") {
|
||||
// 共享中心跳厅:以共享目录为片源列表(与文件页 list 同形态,数据源换共享接口)
|
||||
const rows = await shareRows(key, "video");
|
||||
return rows.map((r: any) => ({
|
||||
url: r.Url,
|
||||
id: r.FileId,
|
||||
m3u8: r.FormattedM3u8 || "",
|
||||
pic: r.FormattedPic || "",
|
||||
title: r.OrgFileName || baseName(r.Url),
|
||||
name: baseName(r.OrgFileName || r.Url),
|
||||
}));
|
||||
}
|
||||
const data = key
|
||||
? await publishVideoService.FindByKey(key)
|
||||
: await publishVideoService.FindPublishVideoDefault();
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
* 无权限的目录点进去要提示;
|
||||
* 4. 组内新建目录:CreateFamilyDmsFolder({ FilePath, FolderLocalName, FamilyName, Notice })
|
||||
* (旧 DmsFolderModal 的 isFamilyGroup 分支,不是文件页的 CreateDmsFolder);
|
||||
* 5. 取消共享:DeleteFamilyDmsFolder(record)(旧 onDelete,摘掉的是共享映射不动原文件)。
|
||||
* 5. 取消共享:DeleteFamilyDmsFolder(record)(旧 onDelete,摘掉的是共享映射不动原文件);
|
||||
* 后端不校验操作人身份,显隐规则在前端收口:仅群管理员或该条共享的创建者(CreatedBy)可见;
|
||||
* 6. 管理员身份:进组时 familyGroupService.FindByKey(family).IsManager 权威回填。
|
||||
*/
|
||||
import type { FamilyDmsFileViewDisplayData } from "@/data/auto/familyDmsFileViewDisplayData";
|
||||
import type { FileItem, PathNode } from "@/data/custom/fileBrowser";
|
||||
@@ -23,6 +25,7 @@ import { FamilyDmsFileViewDisplayData as FamilyView } from "@/data/auto/familyDm
|
||||
import { FamilyDmsFileViewEntity } from "@/data/auto/familyDmsFileViewEntity";
|
||||
import { SearchFilter } from "@/data/auto/searchFilter";
|
||||
import dmsFileService from "@/service/master/dmsFileService";
|
||||
import familyGroupService from "@/service/master/familyGroupService";
|
||||
import {
|
||||
formatModifiedLabel,
|
||||
formatSize,
|
||||
@@ -47,6 +50,10 @@ export function useShareHall() {
|
||||
const errorText = ref("");
|
||||
const currPath = ref("");
|
||||
const pathStack = ref<PathNode[]>([]);
|
||||
/** 当前登录名:判断「这条共享是不是我共享进来的」(记录 CreatedBy 就是共享操作人) */
|
||||
const myUser = ref("");
|
||||
/** 我是否当前群管理员:进组时 FindByKey 权威回填(非成员/非管理员为 false) */
|
||||
const isManager = ref(false);
|
||||
|
||||
/** 是否在组内(组根也算):决定导航返回、面包屑与「新建文件夹」按钮显隐 */
|
||||
const inGroup = computed(() => !!currentFamily.value);
|
||||
@@ -162,11 +169,62 @@ export function useShareHall() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 记录当前登录名(页面登录态就绪后回填) */
|
||||
function setMyUser(name: string): void {
|
||||
myUser.value = name;
|
||||
}
|
||||
|
||||
/** 进组后回填管理员身份:失败只当非管理员(少露一个取消共享,不阻断浏览) */
|
||||
async function loadManagerState(): Promise<void> {
|
||||
if (!currentFamily.value) {
|
||||
isManager.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await familyGroupService.FindByKey(currentFamily.value);
|
||||
isManager.value = !!data?.IsManager;
|
||||
} catch {
|
||||
isManager.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 「取消共享」显隐:群管理员,或这条共享的创建者本人(CreatedBy=把文件共享进群的人);
|
||||
* 其他成员即使能浏览也不能取消,前端收口(后端 DeleteFamilyDmsFolder 无操作人校验) */
|
||||
function canUnshare(file: FileItem): boolean {
|
||||
if (isManager.value) {
|
||||
return true;
|
||||
}
|
||||
const record = records.value.get(file.id);
|
||||
return !!record && !!myUser.value && record.CreatedBy === myUser.value;
|
||||
}
|
||||
|
||||
/** 进组:路径栈重置为组根(旧系统 rootPath="" 的组根语义),value 空串供面包屑回组根 */
|
||||
async function enterGroup(group: FileItem): Promise<void> {
|
||||
currentFamily.value = group.path;
|
||||
pathStack.value = [{ value: "", displayName: group.name }];
|
||||
await loadFolder("");
|
||||
// 取消共享的显隐依赖管理员身份:进组时一并回填
|
||||
await loadManagerState();
|
||||
}
|
||||
|
||||
/** 群组详情「共享目录」行直达:跳过大厅直接开到指定群组的指定目录(folder 空=组根);
|
||||
* 目录路径语义同 openFolder 下钻:FolderName 即 family_dms_file_view 的 file_name */
|
||||
async function openAt(
|
||||
familyName: string,
|
||||
familyLabel: string,
|
||||
folder: string,
|
||||
folderLabel: string,
|
||||
): Promise<void> {
|
||||
currentFamily.value = familyName;
|
||||
pathStack.value = [{ value: "", displayName: familyLabel || familyName }];
|
||||
if (folder) {
|
||||
pathStack.value.push({
|
||||
value: folder,
|
||||
displayName: folderLabel || folder,
|
||||
});
|
||||
}
|
||||
await loadFolder(folder);
|
||||
await loadManagerState();
|
||||
}
|
||||
|
||||
/** 退出到大厅:清组与列表,重拉群组(组内可能刚建过目录) */
|
||||
@@ -176,6 +234,7 @@ export function useShareHall() {
|
||||
records.value = new Map();
|
||||
pathStack.value = [];
|
||||
currPath.value = "";
|
||||
isManager.value = false;
|
||||
await init();
|
||||
}
|
||||
|
||||
@@ -248,8 +307,12 @@ export function useShareHall() {
|
||||
pathStack,
|
||||
currentFamily,
|
||||
inGroup,
|
||||
isManager,
|
||||
setMyUser,
|
||||
canUnshare,
|
||||
init,
|
||||
enterGroup,
|
||||
openAt,
|
||||
exitGroup,
|
||||
openFolder,
|
||||
back,
|
||||
|
||||
@@ -9,6 +9,7 @@ export class FamilyGroupData
|
||||
public IsManager: boolean =false;
|
||||
public IsOpen: boolean =false;
|
||||
public IsAdd: boolean =false;
|
||||
public IsOpenDir: boolean =false;
|
||||
public FamilyGroupFolders: FamilyGroupFolderDisplayData[] = [];
|
||||
public FamilyGroupManagers: FamilyGroupManagerDisplayData[] = [];
|
||||
public FamilyGroupRequests: FamilyGroupManagerDisplayData[] = [];
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
|
||||
export class FamilyGroupEntity
|
||||
{
|
||||
public AddIndc: string = "";
|
||||
public CreatedBy: string = "";
|
||||
public CreatedDatetime: Date = new Date();
|
||||
public ExpireDatetime?: Date= null!;
|
||||
public FamilyLocalName: string = "";
|
||||
public FamilyName: string = "";
|
||||
public Notice: string = "";
|
||||
public OpenIndc: string = "";
|
||||
public UpdatedBy: string = "";
|
||||
public CreatedBy: string = "";
|
||||
public UpdatedDatetime: Date = new Date();
|
||||
public UpdatedBy: string = "";
|
||||
public FamilyName: string = "";
|
||||
public FamilyLocalName: string = "";
|
||||
public Notice: string = "";
|
||||
public ExpireDatetime?: Date= null!;
|
||||
public OpenIndc: string = "";
|
||||
public AddIndc: string = "";
|
||||
public ValidIndc: string = "";
|
||||
public OpenDirIndc: string = "";
|
||||
|
||||
static readonly ADD_INDC : string ='AddIndc';
|
||||
static readonly CREATED_BY : string ='CreatedBy';
|
||||
static readonly CREATED_DATETIME : string ='CreatedDatetime';
|
||||
static readonly EXPIRE_DATETIME : string ='ExpireDatetime';
|
||||
static readonly FAMILY_LOCAL_NAME : string ='FamilyLocalName';
|
||||
static readonly FAMILY_NAME : string ='FamilyName';
|
||||
static readonly NOTICE : string ='Notice';
|
||||
static readonly OPEN_INDC : string ='OpenIndc';
|
||||
static readonly UPDATED_BY : string ='UpdatedBy';
|
||||
static readonly CREATED_BY : string ='CreatedBy';
|
||||
static readonly UPDATED_DATETIME : string ='UpdatedDatetime';
|
||||
static readonly UPDATED_BY : string ='UpdatedBy';
|
||||
static readonly FAMILY_NAME : string ='FamilyName';
|
||||
static readonly FAMILY_LOCAL_NAME : string ='FamilyLocalName';
|
||||
static readonly NOTICE : string ='Notice';
|
||||
static readonly EXPIRE_DATETIME : string ='ExpireDatetime';
|
||||
static readonly OPEN_INDC : string ='OpenIndc';
|
||||
static readonly ADD_INDC : string ='AddIndc';
|
||||
static readonly VALID_INDC : string ='ValidIndc';
|
||||
static readonly OPEN_DIR_INDC : string ='OpenDirIndc';
|
||||
static readonly DB_NAME_FAMILY_GROUP : string ='family_group';
|
||||
static readonly DB_NAME_FIELDS : string ='add_indc,created_by,created_datetime,expire_datetime,family_local_name,family_name,notice,open_indc,updated_by,updated_datetime,valid_indc';
|
||||
static readonly DB_ADD_INDC : string ='add_indc';
|
||||
static readonly DB_CREATED_BY : string ='created_by';
|
||||
static readonly DB_NAME_FIELDS : string ='created_datetime,created_by,updated_datetime,updated_by,family_name,family_local_name,notice,expire_datetime,open_indc,add_indc,valid_indc,open_dir_indc';
|
||||
static readonly DB_CREATED_DATETIME : string ='created_datetime';
|
||||
static readonly DB_EXPIRE_DATETIME : string ='expire_datetime';
|
||||
static readonly DB_FAMILY_LOCAL_NAME : string ='family_local_name';
|
||||
static readonly DB_FAMILY_NAME : string ='family_name';
|
||||
static readonly DB_NOTICE : string ='notice';
|
||||
static readonly DB_OPEN_INDC : string ='open_indc';
|
||||
static readonly DB_UPDATED_BY : string ='updated_by';
|
||||
static readonly DB_CREATED_BY : string ='created_by';
|
||||
static readonly DB_UPDATED_DATETIME : string ='updated_datetime';
|
||||
static readonly DB_UPDATED_BY : string ='updated_by';
|
||||
static readonly DB_FAMILY_NAME : string ='family_name';
|
||||
static readonly DB_FAMILY_LOCAL_NAME : string ='family_local_name';
|
||||
static readonly DB_NOTICE : string ='notice';
|
||||
static readonly DB_EXPIRE_DATETIME : string ='expire_datetime';
|
||||
static readonly DB_OPEN_INDC : string ='open_indc';
|
||||
static readonly DB_ADD_INDC : string ='add_indc';
|
||||
static readonly DB_VALID_INDC : string ='valid_indc';
|
||||
static readonly DB_OPEN_DIR_INDC : string ='open_dir_indc';
|
||||
}
|
||||
@@ -3,9 +3,13 @@ import { FamilyGroupManagerEntity } from "./familyGroupManagerEntity";
|
||||
export class FamilyGroupManagerDisplayData extends FamilyGroupManagerEntity
|
||||
{
|
||||
public UserLocalName: string = "";
|
||||
public UserMobile: string = "";
|
||||
public AvatarUrl: string = "";
|
||||
public IsManager: boolean =false;
|
||||
|
||||
static readonly USER_LOCAL_NAME : string ='UserLocalName';
|
||||
static readonly DB_USER_LOCAL_NAME : string ='user_local_name';
|
||||
static readonly USER_MOBILE : string ='UserMobile';
|
||||
static readonly AVATAR_URL : string ='AvatarUrl';
|
||||
static readonly IS_MANAGER : string ='IsManager';
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"home": "Home",
|
||||
"file": "Cloud Drive",
|
||||
"media": "Media",
|
||||
"share": "Share",
|
||||
"share": "Groups",
|
||||
"mine": "Me"
|
||||
},
|
||||
"MinePage": {
|
||||
@@ -608,7 +608,7 @@
|
||||
"actionMove": "Move",
|
||||
"actionDownload": "Download",
|
||||
"actionDelete": "Delete",
|
||||
"actionShare": "Share to Family",
|
||||
"actionShare": "Share to Group Folder",
|
||||
"actionShareShort": "Share",
|
||||
"actionPublish": "Publish",
|
||||
"actionPublishAlbum": "Publish Album",
|
||||
@@ -644,7 +644,7 @@
|
||||
"moveSelected": "{count} item(s) selected",
|
||||
"moveHere": "Move here",
|
||||
"moveNoSub": "No subfolders",
|
||||
"shareTitle": "Choose a family folder",
|
||||
"shareTitle": "Choose a group folder",
|
||||
"shareHere": "Share here",
|
||||
"shareDone": "Shared",
|
||||
"shareNoFile": "No shareable files",
|
||||
@@ -704,7 +704,7 @@
|
||||
"emptyText": "This folder is empty"
|
||||
},
|
||||
"SharePage": {
|
||||
"title": "Family Groups",
|
||||
"title": "My Groups",
|
||||
"searchPlaceholder": "Search group name",
|
||||
"createTitle": "Create Group",
|
||||
"noAccess": "This group is private and you are not a member",
|
||||
@@ -712,6 +712,8 @@
|
||||
"chipPublic": "Public",
|
||||
"chipPrivate": "Private",
|
||||
"chipJoinable": "Joinable",
|
||||
"chipPending": "Applied, pending review",
|
||||
"chipAudit": "Pending",
|
||||
"chipManager": "Admin",
|
||||
"chipMember": "Member",
|
||||
"emptyGroups": "No groups to join yet",
|
||||
@@ -720,18 +722,32 @@
|
||||
"save": "Save",
|
||||
"amend": "Edit",
|
||||
"dissolve": "Disband",
|
||||
"dissolveHint": "Shared folders and memberships will be deleted together",
|
||||
"dissolveConfirm": "Disband \"{name}\"? Folders and memberships will be removed together",
|
||||
"dissolveDone": "Group disbanded",
|
||||
"saveDone": "Saved",
|
||||
"createDone": "Group created",
|
||||
"nameRequired": "Enter a group name",
|
||||
"tabOverview": "Overview",
|
||||
"tabMembers": "Members",
|
||||
"tabPerms": "Permissions",
|
||||
"statMembers": "members",
|
||||
"statManagers": "managers",
|
||||
"statPending": "pending",
|
||||
"groupInfo": "Group Info",
|
||||
"groupName": "Group Name",
|
||||
"groupDesc": "Description",
|
||||
"publicSetting": "Public group",
|
||||
"joinApply": "Allow join requests",
|
||||
"joinClosed": "Closed",
|
||||
"openDir": "Share all folders",
|
||||
"openDirPublic": "Shared",
|
||||
"openDirPrivate": "Restricted",
|
||||
"folders": "Shared Folders",
|
||||
"newFolder": "New Folder",
|
||||
"editFolder": "Edit folder",
|
||||
"folderName": "Folder name",
|
||||
"folderNote": "Note",
|
||||
"add": "Add",
|
||||
"folderPh": "Enter folder name",
|
||||
"notePh": "Note (optional)",
|
||||
@@ -750,9 +766,13 @@
|
||||
"rejectConfirm": "Reject this request?",
|
||||
"rejectDone": "Rejected",
|
||||
"members": "Members",
|
||||
"noMembers": "No members yet",
|
||||
"removeMember": "Remove",
|
||||
"removeMemberConfirm": "Remove this member from the group? Their folder permissions will be cleared",
|
||||
"permissions": "Folder Permissions",
|
||||
"revoke": "Revoke",
|
||||
"revokeConfirm": "Remove this member's access to the folder? They will no longer be able to open it",
|
||||
"revokeDone": "Folder access revoked",
|
||||
"applyJoin": "Join",
|
||||
"submitApply": "Submit",
|
||||
"joinPh": "Tell the admin why you want to join",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"home": "首页",
|
||||
"file": "网盘",
|
||||
"media": "多媒体",
|
||||
"share": "共享",
|
||||
"share": "群组",
|
||||
"mine": "我的"
|
||||
},
|
||||
"MinePage": {
|
||||
@@ -608,7 +608,7 @@
|
||||
"actionMove": "移动",
|
||||
"actionDownload": "下载",
|
||||
"actionDelete": "删除",
|
||||
"actionShare": "共享家人",
|
||||
"actionShare": "共享到群组目录",
|
||||
"actionShareShort": "共享",
|
||||
"actionPublish": "发布",
|
||||
"actionPublishAlbum": "发布专辑",
|
||||
@@ -644,7 +644,7 @@
|
||||
"moveSelected": "已选择 {count} 项",
|
||||
"moveHere": "移动到此",
|
||||
"moveNoSub": "没有子文件夹",
|
||||
"shareTitle": "选择家人目录",
|
||||
"shareTitle": "选择群组目录",
|
||||
"shareHere": "共享到此",
|
||||
"shareDone": "共享成功",
|
||||
"shareNoFile": "没有可共享的文件",
|
||||
@@ -704,7 +704,7 @@
|
||||
"emptyText": "这个目录是空的"
|
||||
},
|
||||
"SharePage": {
|
||||
"title": "家庭群组",
|
||||
"title": "我的群组",
|
||||
"searchPlaceholder": "搜索群组名称",
|
||||
"createTitle": "创建群组",
|
||||
"noAccess": "该群组为私密,您还不是成员",
|
||||
@@ -712,26 +712,42 @@
|
||||
"chipPublic": "公开",
|
||||
"chipPrivate": "私密",
|
||||
"chipJoinable": "可申请",
|
||||
"chipPending": "已申请,待审核",
|
||||
"chipAudit": "待审核",
|
||||
"chipManager": "管理员",
|
||||
"chipMember": "成员",
|
||||
"emptyGroups": "还没有可加入的群组",
|
||||
"noDesc": "暂无群组描述",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"amend": "修改",
|
||||
"amend": "编辑",
|
||||
"dissolve": "解散群",
|
||||
"dissolveHint": "会同时删除全部共享目录与成员关系",
|
||||
"dissolveConfirm": "确定解散「{name}」?解散后目录与成员关系一并清除",
|
||||
"dissolveDone": "已解散群组",
|
||||
"saveDone": "已保存",
|
||||
"createDone": "群组已创建",
|
||||
"nameRequired": "请输入群组名称",
|
||||
"tabOverview": "概览",
|
||||
"tabMembers": "成员",
|
||||
"tabPerms": "权限",
|
||||
"statMembers": "位成员",
|
||||
"statManagers": "管理员",
|
||||
"statPending": "待审核",
|
||||
"groupInfo": "群组信息",
|
||||
"groupName": "群组名称",
|
||||
"groupDesc": "群组描述",
|
||||
"publicSetting": "公开群组",
|
||||
"joinApply": "允许申请加入",
|
||||
"joinClosed": "不开放",
|
||||
"openDir": "公开成员目录权限",
|
||||
"openDirPublic": "公开",
|
||||
"openDirPrivate": "仅授权",
|
||||
"folders": "共享目录",
|
||||
"newFolder": "新建目录",
|
||||
"editFolder": "编辑目录",
|
||||
"folderName": "目录名称",
|
||||
"folderNote": "备注",
|
||||
"add": "添加",
|
||||
"folderPh": "请输入目录名称",
|
||||
"notePh": "备注(选填)",
|
||||
@@ -750,9 +766,13 @@
|
||||
"rejectConfirm": "确定拒绝该申请?",
|
||||
"rejectDone": "已拒绝",
|
||||
"members": "正式成员",
|
||||
"noMembers": "暂无正式成员",
|
||||
"removeMember": "移出",
|
||||
"removeMemberConfirm": "确定将该成员移出群组?其目录权限一并清除",
|
||||
"permissions": "目录权限",
|
||||
"revoke": "移除",
|
||||
"revokeConfirm": "确定移除该成员对此目录的权限?移除后其将无法再访问该目录",
|
||||
"revokeDone": "已移除目录权限",
|
||||
"applyJoin": "申请加入",
|
||||
"submitApply": "提交申请",
|
||||
"joinPh": "请填写申请原因,方便管理员审核",
|
||||
@@ -915,4 +935,4 @@
|
||||
"r404_3": "页面已下线或改名,收藏夹里还是旧地址",
|
||||
"r404_4": "服务器刚重启、菜单还没加载出来,刷新一次即可"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as Pinia from "pinia";
|
||||
import uViewPro, { httpPlugin } from "uview-pro";
|
||||
import { createSSRApp } from "vue";
|
||||
import { DEFAULT_LOCALE } from "@/common/locale-const";
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
UI_LOCALE_EN,
|
||||
currentUiLocale,
|
||||
} from "@/common/locale-const";
|
||||
import themes from "@/common/uview-pro.theme";
|
||||
import { enUS, mergeDbLocaleMessages, zhCN } from "@/locale";
|
||||
// #ifdef H5
|
||||
@@ -20,6 +24,21 @@ registerCleanUrls();
|
||||
setupUnknownPathFallback();
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* uni.showModal 按钮文案本地化:App 端走系统原生弹窗,按钮默认英文 OK/CANCEL
|
||||
* (H5 才是「确定/取消」);全项目确认弹窗几乎都不显式传 confirmText/cancelText,
|
||||
* 故全局包一层按当前界面语言补齐,显式传了的仍以调用方为准。
|
||||
*/
|
||||
const rawShowModal = uni.showModal.bind(uni);
|
||||
(uni as any).showModal = (options: any) => {
|
||||
const isEn = currentUiLocale() === UI_LOCALE_EN;
|
||||
return rawShowModal({
|
||||
confirmText: isEn ? "OK" : "确定",
|
||||
cancelText: isEn ? "Cancel" : "取消",
|
||||
...(options && typeof options === "object" ? options : {}),
|
||||
});
|
||||
};
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
app.use(uViewPro, {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/**
|
||||
* 音乐厅页(对齐旧 WebVue views/master/PreviewAudio.vue):
|
||||
* 黑胶唱机播放器外壳。入参经 query 传:mode=list&key=<目录路径> 播放当前目录音频;
|
||||
* mode=share&key=<群组系统名|共享目录路径> 播放共享目录音频(共享中心跳入);
|
||||
* 或 key=<专辑编号> 播放某音乐专辑;无 key 走默认专辑。取数逻辑见 composables/useMediaShow。
|
||||
* 文件页单击某首音频进来时额外带 start=<FileId>,定位到该曲并从它开始播。
|
||||
* 隐藏控制区时(v-model:controls)本壳顺带隐掉导航/系统栏做整页沉浸(H5 元素全屏在组件内处理)。
|
||||
@@ -69,14 +70,8 @@ onLoad(async (query) => {
|
||||
<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"
|
||||
:start-index="startIndex"
|
||||
:auto-play="autoStart"
|
||||
@close="closeMe"
|
||||
/>
|
||||
<vinyl-audio-player v-else v-model:controls="controls" :tracks="tracks" :start-index="startIndex"
|
||||
:auto-play="autoStart" @close="closeMe" />
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/**
|
||||
* 影视厅页(对齐旧 WebVue views/master/PreviewVideo.vue):
|
||||
* 自绘控制播放器外壳。入参经 query 传:mode=list&key=<目录路径> 播放当前目录视频;
|
||||
* mode=share&key=<群组系统名|共享目录路径> 播放共享目录视频(共享中心跳入);
|
||||
* 或 key=<专辑编号> 播放某影视专辑;无 key 走默认专辑。取数逻辑见 composables/useMediaShow。
|
||||
* 文件页单击某个视频进来时额外带 start=<FileId>,定位到该片源并从它开始播。
|
||||
* 隐藏控制区时(v-model:controls)本壳顺带隐掉导航/系统栏做整页沉浸。
|
||||
@@ -68,14 +69,8 @@ onLoad(async (query) => {
|
||||
<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"
|
||||
:start-index="startIndex"
|
||||
:auto-play="autoStart"
|
||||
@close="closeMe"
|
||||
/>
|
||||
<video-player v-else v-model:controls="controls" :videos="videos" :start-index="startIndex" :auto-play="autoStart"
|
||||
@close="closeMe" />
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,16 +5,20 @@
|
||||
* 点群组进组后才出现搜索/分类/排序/新建文件夹(旧系统 FamilyGroupShare 语义:
|
||||
* 大厅顶层不建目录,进组才挂 CreateFamilyDmsFolder)。
|
||||
* 组内条目动作按类型分流:目录下钻(无权限提示)、图片预览、视频/音频播放、
|
||||
* 下载、取消共享(DeleteFamilyDmsFolder 只摘映射不动原文件)。
|
||||
* 下载、取消共享(DeleteFamilyDmsFolder 只摘映射不动原文件;仅管理员或该条共享的创建者可见);
|
||||
* 「…」与单击非媒体文件统一弹自绘动作面板(item-action-sheet,视觉同网盘)。
|
||||
* 数据链在 composables/useShareHall;权限沿用 DmsFile(routes.ts),useAuthGuard 兜冷启动直达。
|
||||
*/
|
||||
import type { ImageViewerItem } from "@/components/image-viewer/image-viewer.vue";
|
||||
import type { ItemSheetAction } from "@/components/item-action-sheet/item-action-sheet.vue";
|
||||
import type { FileCategory, FileItem, FileTabItem } from "@/data/custom/fileBrowser";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
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";
|
||||
import { useLoginProfileStore } from "@/stores";
|
||||
import { fetchFileBytes, saveFileBytes, toImageUrl } from "@/utils/dmsBytes";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -28,9 +32,13 @@ const {
|
||||
loading,
|
||||
errorText,
|
||||
pathStack,
|
||||
currentFamily,
|
||||
inGroup,
|
||||
setMyUser,
|
||||
canUnshare,
|
||||
init,
|
||||
enterGroup,
|
||||
openAt,
|
||||
exitGroup,
|
||||
openFolder,
|
||||
back,
|
||||
@@ -40,8 +48,46 @@ const {
|
||||
unshare,
|
||||
} = useShareHall();
|
||||
|
||||
const profile = useLoginProfileStore();
|
||||
|
||||
/** 安全解码 query 参数(未编码/非法序列不抛 URIError) */
|
||||
function decodeParam(value: unknown): string {
|
||||
try {
|
||||
return decodeURIComponent(String(value ?? ""));
|
||||
}
|
||||
catch {
|
||||
return String(value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 群组详情「共享目录」行直达参数(family/label/folder/folderLabel)=====
|
||||
// onLoad 早于 useAuthGuard 的 onShow 回调,先把参数存起来,登录校验通过后消费
|
||||
const entryTarget = ref<{ family: string; label: string; folder: string; folderLabel: string } | null>(null);
|
||||
|
||||
onLoad((query) => {
|
||||
const family = decodeParam(query?.family);
|
||||
if (!family) {
|
||||
return;
|
||||
}
|
||||
entryTarget.value = {
|
||||
family,
|
||||
label: decodeParam(query?.label),
|
||||
folder: decodeParam(query?.folder),
|
||||
folderLabel: decodeParam(query?.folderLabel),
|
||||
};
|
||||
});
|
||||
|
||||
// 每次回到页面重拉:大厅群组与组内目录都可能被别的端改过
|
||||
useAuthGuard(async () => {
|
||||
// 取消共享的显隐要拿当前登录名比对记录创建者(管理员身份由进组时回填)
|
||||
setMyUser(profile.username);
|
||||
// 带直达参数首次显示:跳过大厅直接开到目标群组的指定目录(消费一次后清空,返回再显不重走)
|
||||
const target = entryTarget.value;
|
||||
if (target) {
|
||||
entryTarget.value = null;
|
||||
await openAt(target.family, target.label, target.folder, target.folderLabel);
|
||||
return;
|
||||
}
|
||||
if (inGroup.value) {
|
||||
await loadFolder(pathStack.value[pathStack.value.length - 1]?.value ?? "");
|
||||
}
|
||||
@@ -197,15 +243,36 @@ async function enterFolder(file: FileItem): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function doPreview(file: FileItem): Promise<void> {
|
||||
const record = records.value.get(file.id);
|
||||
if (!record) {
|
||||
// ----- 图片查看器(与文件页同款:缩略图秒开 + 按需原图 + 双指缩放/拖动)-----
|
||||
const viewerVisible = ref(false);
|
||||
const viewerItems = ref<ImageViewerItem[]>([]);
|
||||
/** 打开时定位的起始图(连览时从点击那张开始) */
|
||||
const viewerStartIndex = ref(0);
|
||||
|
||||
/** 图片预览:连览当前可见列表的全部图片(按列表排序),从点击那张开始,左右滑动切前/后一张 */
|
||||
function doPreview(file: FileItem): void {
|
||||
const images = visibleFiles.value.filter(f => f.kind === "image");
|
||||
if (!images.length) {
|
||||
return;
|
||||
}
|
||||
await withBusy(async () => {
|
||||
const urls = [toPlayableUrl(await fetchFileBytes(record))];
|
||||
uni.previewImage({ urls, current: urls[0] });
|
||||
viewerStartIndex.value = Math.max(0, images.findIndex(f => f.id === file.id));
|
||||
viewerItems.value = images.map((item) => {
|
||||
const preview: ImageViewerItem = {
|
||||
name: item.name,
|
||||
// 列表已加载的 Scale 缩略图秒开;老文件无缩略图时查看器会自动拉原图兜底
|
||||
thumb: item.thumbUrl || "",
|
||||
loadFull: async () => {
|
||||
const record = records.value.get(item.id);
|
||||
if (!record) {
|
||||
throw new Error(t("FilePage.actionFailed"));
|
||||
}
|
||||
// App 端 WebView 不吃 file:// 本地文件,高清图走 data: URI(toImageUrl 内部分流)
|
||||
return toImageUrl(await fetchFileBytes(record));
|
||||
},
|
||||
};
|
||||
return preview;
|
||||
});
|
||||
viewerVisible.value = true;
|
||||
}
|
||||
|
||||
async function doDownload(file: FileItem): Promise<void> {
|
||||
@@ -224,32 +291,15 @@ async function doDownload(file: FileItem): Promise<void> {
|
||||
}, 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 });
|
||||
/** 视频/音频播放:进音乐厅/影视厅(以当前共享目录为播放列表,从该条开始播,与文件页同款);
|
||||
* key 传「群组系统名|目录路径」,厅页 mode=share 走共享接口拉同目录媒体 */
|
||||
function doPlay(file: FileItem): void {
|
||||
const family = currentFamily.value;
|
||||
const path = pathStack.value[pathStack.value.length - 1]?.value ?? "";
|
||||
const hall = file.kind === "audio" ? "audioHall" : "videoHall";
|
||||
uni.navigateTo({
|
||||
url: `${pageUrl("mediaPlay")}?type=${kind}&name=${encodeURIComponent(name)}`,
|
||||
fail: (err: any) => {
|
||||
clearPlayback();
|
||||
toast(err?.errMsg || t("FilePage.actionFailed"));
|
||||
},
|
||||
url: `${pageUrl(hall)}?mode=share&key=${encodeURIComponent(`${family}|${path}`)}&start=${file.id}`,
|
||||
fail: (err: any) => toast(err?.errMsg || t("FilePage.actionFailed")),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,60 +317,62 @@ async function doUnshare(file: FileItem): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/** 按条目类型拼动作集(系统动作面板),取消共享任何类型都有 */
|
||||
function sheetActions(file: FileItem): Array<{ key: string; label: string }> {
|
||||
const actions: Array<{ key: string; label: string }> = [];
|
||||
/** 按条目类型拼动作集(与网盘动作面板同一视觉,图标均为 uview 已验证字形) */
|
||||
function sheetActions(file: FileItem): ItemSheetAction[] {
|
||||
const actions: ItemSheetAction[] = [];
|
||||
if (file.kind === "folder") {
|
||||
actions.push({ key: "enter", label: t("ShareHallPage.enter") });
|
||||
actions.push({ key: "enter", label: t("ShareHallPage.enter"), icon: "arrow-right" });
|
||||
}
|
||||
if (file.kind === "image") {
|
||||
actions.push({ key: "preview", label: t("ShareHallPage.preview") });
|
||||
actions.push({ key: "preview", label: t("ShareHallPage.preview"), icon: "eye" });
|
||||
}
|
||||
if (file.kind === "video" || file.kind === "audio") {
|
||||
actions.push({ key: "play", label: t("ShareHallPage.play") });
|
||||
actions.push({ key: "play", label: t("ShareHallPage.play"), icon: "play-circle-fill" });
|
||||
}
|
||||
if (file.kind !== "folder") {
|
||||
actions.push({ key: "download", label: t("ShareHallPage.download") });
|
||||
actions.push({ key: "download", label: t("ShareHallPage.download"), icon: "download" });
|
||||
}
|
||||
// 大厅顶层的群组卡没有共享记录,不给取消共享
|
||||
if (inGroup.value) {
|
||||
actions.push({ key: "unshare", label: t("ShareHallPage.unshare") });
|
||||
// 取消共享:大厅顶层(无记录)不显示;仅群管理员或该条共享的创建者可见(后端无操作人校验,前端收口);不可逆标红
|
||||
if (canUnshare(file)) {
|
||||
actions.push({ key: "unshare", label: t("ShareHallPage.unshare"), icon: "minus-circle", danger: true });
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
/** 当前动作面板目标;null 为关闭(「…」与单击非媒体文件共用同一入口) */
|
||||
const sheetFile = ref<FileItem | null>(null);
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
sheetFile.value = file;
|
||||
}
|
||||
|
||||
/** 单击条目:目录进组/下钻,图片预览,音视频播放,文档走动作面板(无预览能力) */
|
||||
/** 面板动作分发(键与 sheetActions 对应) */
|
||||
function onSheetAction(key: string, file: FileItem): void {
|
||||
sheetFile.value = null;
|
||||
if (key === "enter") {
|
||||
// 顶层群组卡的「打开」是进组,组内目录卡的「打开」才是下钻
|
||||
if (inGroup.value) {
|
||||
enterFolder(file);
|
||||
}
|
||||
else {
|
||||
enterTopGroup(file);
|
||||
}
|
||||
}
|
||||
else if (key === "preview") {
|
||||
doPreview(file);
|
||||
}
|
||||
else if (key === "play") {
|
||||
doPlay(file);
|
||||
}
|
||||
else if (key === "download") {
|
||||
doDownload(file);
|
||||
}
|
||||
else if (key === "unshare") {
|
||||
doUnshare(file);
|
||||
}
|
||||
}
|
||||
|
||||
/** 单击条目:目录进组/下钻,图片预览,音视频播放,文档与「…」同口径弹动作面板 */
|
||||
function onOpen(file: FileItem): void {
|
||||
if (!inGroup.value) {
|
||||
enterTopGroup(file);
|
||||
@@ -416,6 +468,12 @@ async function onCreateFolder(name: string): Promise<void> {
|
||||
<!-- 新建文件夹:复用文件页命名对话框(默认文案就是新建) -->
|
||||
<folder-name-dialog :visible="folderDialogVisible" :submitting="folderSubmitting"
|
||||
@close="folderDialogVisible = false" @confirm="onCreateFolder" />
|
||||
<!-- 条目动作面板:「…」与单击非媒体文件共用(自绘面板,视觉同网盘动作面板) -->
|
||||
<item-action-sheet :visible="!!sheetFile" :file="sheetFile"
|
||||
:actions="sheetFile ? sheetActions(sheetFile) : []" @close="sheetFile = null" @action="onSheetAction" />
|
||||
<!-- 图片查看器:与文件页同款(缩略图秒开连览 + 按需原图 + 手势) -->
|
||||
<image-viewer :visible="viewerVisible" :items="viewerItems" :start-index="viewerStartIndex"
|
||||
@close="viewerVisible = false" />
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
@@ -299,7 +299,7 @@ const pickedKind = computed<FileKind | null>(() => {
|
||||
return picked.every(f => f.kind === first.kind) ? first.kind : null;
|
||||
});
|
||||
|
||||
/** 旧系统 publishTooltip 简化口径:媒体类型统一「发布专辑」(按钮不超 4 字),文档仍「发布」 */
|
||||
/** 旧系统 publishTooltip 简化口径:发布只服务媒体类型,统一「发布专辑」(按钮不超 4 字) */
|
||||
const publishLabel = computed(() => {
|
||||
const kind = pickedKind.value;
|
||||
return kind === "image" || kind === "video" || kind === "audio"
|
||||
@@ -337,7 +337,7 @@ const previewKey = computed(() => {
|
||||
/** 选中含文件即可下载(旧系统 onDownloadSelected 就是滤掉目录逐个循环下载) */
|
||||
const hasFileSelected = computed(() => selectedFiles.value.some(f => f.kind !== "folder"));
|
||||
|
||||
/** 排1:下载(含文件即有)+ 共享/发布(同媒体类型且无目录)+ 删除恒在 */
|
||||
/** 排1:下载(含文件即有)+ 共享/发布(同媒体类型且无目录,文档无发布功能不显示)+ 删除恒在 */
|
||||
const multiActionsTop = computed<MultiAction[]>(() => {
|
||||
const actions: MultiAction[] = [];
|
||||
if (hasFileSelected.value) {
|
||||
@@ -345,7 +345,10 @@ const multiActionsTop = computed<MultiAction[]>(() => {
|
||||
}
|
||||
if (canSharePublish.value) {
|
||||
actions.push({ key: "share", label: t("FilePage.actionShareShort"), icon: "share-fill" });
|
||||
actions.push({ key: "publish", label: publishLabel.value, icon: "volume-fill" });
|
||||
// 发布仅图片/视频/音频(单选文档时共享仍在,发布不显示)
|
||||
if (pickedKind.value !== "document") {
|
||||
actions.push({ key: "publish", label: publishLabel.value, icon: "volume-fill" });
|
||||
}
|
||||
}
|
||||
actions.push({ key: "delete", label: t("FilePage.actionDelete"), icon: "trash", danger: true });
|
||||
return actions;
|
||||
@@ -389,14 +392,15 @@ function onToggleSelectItem(id: number): void {
|
||||
|
||||
// ===== 列表条目 / 悬浮按钮 =====
|
||||
async function onOpen(file: FileItem): Promise<void> {
|
||||
// 文件夹下钻走真实接口;文件点开即预览/播放(旧系统 onViewPic 同款分流)
|
||||
// 文件夹下钻走真实接口;图片连览、音视频进厅播放,文档与右侧「…」同口径弹出操作面板
|
||||
if (await openFolder(file)) {
|
||||
keyword.value = "";
|
||||
selectedIds.value = [];
|
||||
return;
|
||||
}
|
||||
if (file.kind === "image") {
|
||||
actionPreview([file]);
|
||||
// 连览当前可见列表的全部图片(按列表排序),从点击这张开始,左右滑动切前后一张
|
||||
actionPreview(visibleFiles.value.filter(f => f.kind === "image"), file);
|
||||
}
|
||||
else if (file.kind === "video" || file.kind === "audio") {
|
||||
// 单击即进音乐厅/影视厅(以当前目录为播放列表),从这一条开始播;
|
||||
@@ -404,8 +408,9 @@ async function onOpen(file: FileItem): Promise<void> {
|
||||
openInHall(file);
|
||||
}
|
||||
else {
|
||||
// 文档没有移动端预览器,点开按下载处理(比占位 toast 实用)
|
||||
actionDownload([file]);
|
||||
// 文档没有移动端预览器:与右侧「…」一致弹出操作面板(下载/重命名/移动/删除等由用户选),
|
||||
// 不再单击即静默下载
|
||||
onItemMore(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,17 +602,21 @@ async function actionDownload(targets: FileItem[]): Promise<void> {
|
||||
// ----- 图片查看器(缩略图秒开 + 按需原图,见 components/image-viewer)-----
|
||||
const viewerVisible = ref(false);
|
||||
const viewerItems = ref<ImageViewerItem[]>([]);
|
||||
/** 打开时定位的起始图(连览时从点击那张开始) */
|
||||
const viewerStartIndex = ref(0);
|
||||
|
||||
/** 图片预览:单选一张、多选多图相册连览。
|
||||
/** 图片预览:单选连览当前列表全部图片、多选只连览选中的图片。
|
||||
* 缩略图先行——thumbUrl 列表已加载过(甚至命中 HTTP 缓存),组装进查看器秒开;
|
||||
* 原图按需拉取(查看器底部「查看原图」触发 loadFull,走分片拼整 + 落盘旧通道)。
|
||||
* 替代旧 uni.previewImage 链路:它要先串行拉全部原图字节落盘才弹窗,慢的根因在此 */
|
||||
function actionPreview(targets: FileItem[]): void {
|
||||
* 替代旧 uni.previewImage 链路:它要先串行拉全部原图字节落盘才弹窗,慢的根因在此。
|
||||
* start 传了就从那张开始,否则从第一张开始 */
|
||||
function actionPreview(targets: FileItem[], start?: FileItem): void {
|
||||
const images = targets.filter(f => f.kind === "image");
|
||||
if (!images.length) {
|
||||
toast(t("FilePage.previewNoImage"));
|
||||
return;
|
||||
}
|
||||
viewerStartIndex.value = start ? Math.max(0, images.findIndex(f => f.id === start.id)) : 0;
|
||||
viewerItems.value = images.map((file) => {
|
||||
const item: ImageViewerItem = {
|
||||
name: file.name,
|
||||
@@ -656,7 +665,7 @@ function goPlay(type: "video" | "audio", name: string, url: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 共享家人:共享大厅式抽屉选目标目录(群组卡→逐级下钻→共享到此),
|
||||
// ----- 共享到群组目录:共享大厅式抽屉选目标目录(群组卡→逐级下钻→共享到此),
|
||||
// 多选文件一次批量建(CreateFamilyGroupFolder 数组入参,旧 handleSelectShare 同口径)-----
|
||||
const shareVisible = ref(false);
|
||||
const shareTargets = ref<FileItem[]>([]);
|
||||
@@ -1035,7 +1044,8 @@ const transferIconWhite = transferGlyph("#ffffff");
|
||||
<file-action-sheet :visible="!!actionFile" :file="actionFile" @close="actionFile = null"
|
||||
@action="onSheetAction" />
|
||||
<!-- 图片查看器:缩略图秒开连览,底部「查看原图」按需拉原图(全屏遮罩,打开时收起 tabbar) -->
|
||||
<image-viewer :visible="viewerVisible" :items="viewerItems" @close="viewerVisible = false" />
|
||||
<image-viewer :visible="viewerVisible" :items="viewerItems" :start-index="viewerStartIndex"
|
||||
@close="viewerVisible = false" />
|
||||
<folder-name-dialog :visible="folderDialogVisible" :submitting="folderSubmitting"
|
||||
@close="folderDialogVisible = false" @confirm="onCreateFolder" />
|
||||
<!-- 重命名:同一个命名对话框的第二个实例(预填当前名 + 重命名文案) -->
|
||||
@@ -1045,7 +1055,7 @@ const transferIconWhite = transferGlyph("#ffffff");
|
||||
<!-- 移动:网盘式逐级下钻选择器(进层浏览,「移动到此」提交当前层后直接调移动接口) -->
|
||||
<folder-move-picker :visible="moveVisible" :exclude-name="moveExcludeName" :source-path="moveSourcePath"
|
||||
:selected-count="moveTargets.length" @close="moveVisible = false" @confirm="onMovePick" />
|
||||
<!-- 共享家人:共享大厅式抽屉(群组卡→逐级下钻→「共享到此」),多选文件一次批量共享 -->
|
||||
<!-- 共享到群组目录:共享大厅式抽屉(群组卡→逐级下钻→「共享到此」),多选文件一次批量共享 -->
|
||||
<folder-share-picker :visible="shareVisible" :selected-count="shareTargets.length" @close="shareVisible = false"
|
||||
@confirm="onShareConfirm" />
|
||||
<!-- 发布:选专辑(确认键按类型叫「发布相册」等;附「新建相册」行跳媒体页创建态) -->
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -4,10 +4,12 @@
|
||||
* 骨架与文件页同套组件(导航/搜索/分类标签/列表/多选栏),差异——
|
||||
* 无上传、无新建文件夹、无快捷入口、不下钻目录;动作集按旧系统
|
||||
* DmsFileDeleteEnquiry 收敛为两个:还原 RestoreDmsFile、彻底删除 DeleteLog,
|
||||
* 单条点击弹系统动作面板,多选栏批量,彻底删除先过确认框。
|
||||
* 单条点击弹自绘动作面板(取消键跟页面语言,系统 showActionSheet 的取消键固定英文 Cancel),
|
||||
* 多选栏批量,彻底删除先过确认框。
|
||||
* 数据链在 composables/useTrashFiles(FindDeleteLogBySearchCriteria -> Logs),
|
||||
* 条目时间列展示的是删除时间。权限沿用 DmsFile(routes.ts),useAuthGuard 兜冷启动直达。
|
||||
*/
|
||||
import type { ItemSheetAction } from "@/components/item-action-sheet/item-action-sheet.vue";
|
||||
import type { FileCategory, FileItem, FileTabItem } from "@/data/custom/fileBrowser";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
@@ -160,19 +162,30 @@ function onClearSelect(): void {
|
||||
|
||||
// ===== 动作层(单条面板与多选栏共用) =====
|
||||
|
||||
/** 点条目/点⋯:系统动作面板二选一(回收站条目不做预览播放——记录已进删除态) */
|
||||
// ----- 单条动作面板:自绘面板(item-action-sheet,视觉同网盘/共享中心)-----
|
||||
const sheetFile = ref<FileItem | null>(null);
|
||||
|
||||
/** 面板动作集:回收站条目不做预览播放(记录已进删除态),只有还原/彻底删除 */
|
||||
function sheetActions(): ItemSheetAction[] {
|
||||
return [
|
||||
{ key: "restore", label: t("TrashPage.restore"), icon: "rewind-left" },
|
||||
{ key: "purge", label: t("TrashPage.purge"), icon: "trash", danger: true },
|
||||
];
|
||||
}
|
||||
|
||||
/** 点条目/点⋯:打开自绘动作面板二选一 */
|
||||
function openSheet(file: FileItem): void {
|
||||
uni.showActionSheet({
|
||||
itemList: [t("TrashPage.restore"), t("TrashPage.purge")],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
doRestore([file]);
|
||||
}
|
||||
else {
|
||||
doPurge([file]);
|
||||
}
|
||||
},
|
||||
});
|
||||
sheetFile.value = file;
|
||||
}
|
||||
|
||||
function onSheetAction(key: string, file: FileItem): void {
|
||||
sheetFile.value = null;
|
||||
if (key === "restore") {
|
||||
doRestore([file]);
|
||||
}
|
||||
else if (key === "purge") {
|
||||
doPurge([file]);
|
||||
}
|
||||
}
|
||||
|
||||
/** 动作完成后的收尾:清勾选、退多选态、按当前关键字重查 */
|
||||
@@ -306,6 +319,9 @@ function onMultiPurge(): void {
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 单条动作面板:自绘面板(取消键走页面语言,系统 showActionSheet 的 Cancel 固定英文) -->
|
||||
<item-action-sheet :visible="!!sheetFile" :file="sheetFile" :actions="sheetActions()"
|
||||
@close="sheetFile = null" @action="onSheetAction" />
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="#059669" d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2z"/>
|
||||
<circle fill="#059669" cx="9" cy="7" r="4"/>
|
||||
<path fill="none" stroke="#059669" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" d="M22 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path fill="none" stroke="#059669" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 458 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#606266" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 332 B |
@@ -44,8 +44,10 @@ export const useTabbarStore = defineStore(
|
||||
},
|
||||
{
|
||||
langKey: "TabBar.share",
|
||||
iconPath: "share",
|
||||
selectedIconPath: "share-fill",
|
||||
// 群组用自定义双人 SVG(与「我的」单人 account 区分;形状同 lucide users);
|
||||
// 图片图标不吃 color,故未选中/选中各烘一色(描边灰 #606266 / 实心绿 #059669)
|
||||
iconPath: "/static/tabbar/group.svg",
|
||||
selectedIconPath: "/static/tabbar/group-active.svg",
|
||||
},
|
||||
{
|
||||
langKey: "TabBar.mine",
|
||||
|
||||
Reference in new issue
Block a user