ow
This commit is contained in:
1 parent
fd67fa8f63
commit
211d2cd253
31 files changed
+1830
-454
No files matched your search
@@ -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。
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user