ow
This commit is contained in:
1 parent
8a2adb0a92
commit
bd678b0112
22 files changed
+1968
-81
No files matched your search
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 目录面包屑(仅子目录显示,页面自行 v-if):样式对齐网盘 App 路径条——白底描边圆角、细 chevron 分隔。
|
||||
* 显示方式:放得下就全量展示;放不下时保留「根 + … + 尾部若干段」,被折叠的中间段收进「…」。
|
||||
* 折叠不是 CSS 裁切:H5 渲染后实测内部宽度,逐段把折叠点向后推,直到不再溢出(非 H5 端按段数近似折叠)。
|
||||
* 点「…」跳到被折叠段里最深的一层;点其它段跳到该层;当前目录(最后一段)点击无操作。
|
||||
* 纯展示组件:只 emit('nav', 段下标),截断与重查由页面/composable 决定。
|
||||
*/
|
||||
import type { PropType } from "vue";
|
||||
import type { PathNode } from "@/data/custom/fileBrowser";
|
||||
import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
/** 从根到当前目录的完整路径栈 */
|
||||
items: {
|
||||
type: Array as PropType<PathNode[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "nav", index: number): void;
|
||||
}>();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
/** 折叠点:下标 1..collapseFrom-1 的段被收进「…」;-1 表示全量展示 */
|
||||
const collapseFrom = ref(-1);
|
||||
|
||||
/** 折到底(根+…+末段)仍放不下:允许末段压缩出省略号。
|
||||
* 平时必须为 false——末段一旦可压缩,scrollWidth 就测不出真实溢出,折叠永远不会触发 */
|
||||
const tailClamped = ref(false);
|
||||
|
||||
interface Segment {
|
||||
node: PathNode;
|
||||
index: number;
|
||||
ellipsis: boolean;
|
||||
}
|
||||
|
||||
/** 实际渲染的段序列:根 +(可选 …)+ 尾部 */
|
||||
const shownSegments = computed<Segment[]>(() => {
|
||||
const items = props.items;
|
||||
if (collapseFrom.value < 1) {
|
||||
return items.map((node, index) => ({ node, index, ellipsis: false }));
|
||||
}
|
||||
const segments: Segment[] = [{ node: items[0], index: 0, ellipsis: false }];
|
||||
segments.push({
|
||||
node: { value: "", displayName: "…" },
|
||||
index: collapseFrom.value - 1,
|
||||
ellipsis: true,
|
||||
});
|
||||
items.slice(collapseFrom.value).forEach((node, i) => {
|
||||
segments.push({ node, index: collapseFrom.value + i, ellipsis: false });
|
||||
});
|
||||
return segments;
|
||||
});
|
||||
|
||||
function onSegmentTap(seg: Segment): void {
|
||||
// 当前目录段不用跳;「…」跳到被折叠段里最深的一层(其下标即 seg.index)
|
||||
if (seg.index >= props.items.length - 1) {
|
||||
return;
|
||||
}
|
||||
emit("nav", seg.index);
|
||||
}
|
||||
|
||||
/** 是否仍在溢出:H5 实测滚动宽;其它端拿段数近似(超过 4 段继续折) */
|
||||
function isOverflowing(): boolean {
|
||||
// #ifdef H5
|
||||
const root = instance?.proxy?.$el as HTMLElement | undefined;
|
||||
const inner = root?.querySelector?.(".file-breadcrumb__rest-inner");
|
||||
if (!inner) {
|
||||
return false;
|
||||
}
|
||||
return inner.scrollWidth > inner.clientWidth + 1;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return props.items.length - Math.max(collapseFrom.value, 1) > 3;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/** 重测折叠:先全展开,放不下就把折叠点一段一段往后推;折到底仍溢出才夹末段 */
|
||||
async function remeasure(): Promise<void> {
|
||||
collapseFrom.value = -1;
|
||||
tailClamped.value = false;
|
||||
if (props.items.length <= 1) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
let guard = props.items.length + 2;
|
||||
while (isOverflowing() && guard-- > 0) {
|
||||
// 折叠下限:根 + … + 末段(collapseFrom = length-1);两段路径无中间段可折
|
||||
if (props.items.length > 2 && collapseFrom.value < props.items.length - 1) {
|
||||
collapseFrom.value = collapseFrom.value < 2 ? 2 : collapseFrom.value + 1;
|
||||
await nextTick();
|
||||
continue;
|
||||
}
|
||||
tailClamped.value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 路径内容(非引用)变化都要重测:下钻/返回/面包屑跳转/重命名都会改变段数或段宽
|
||||
watch(() => props.items.map(node => `${node.value}|${node.displayName}`).join(">"), remeasure);
|
||||
// 窗口尺寸变化(横竖屏切换、H5 拉窗口)也要重测:折叠是按实测宽度算的
|
||||
onMounted(() => {
|
||||
remeasure();
|
||||
uni.onWindowResize(remeasure);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
uni.offWindowResize(remeasure);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view v-if="items.length > 1" class="file-breadcrumb">
|
||||
<text class="file-breadcrumb__root" @tap="onSegmentTap({ node: items[0], index: 0, ellipsis: false })">
|
||||
{{ items[0].displayName }}
|
||||
</text>
|
||||
<view class="file-breadcrumb__rest">
|
||||
<view class="file-breadcrumb__rest-inner">
|
||||
<template v-for="seg in shownSegments.slice(1)" :key="`${seg.index}-${seg.node.value}`">
|
||||
<u-icon class="file-breadcrumb__sep" name="arrow-right" size="22" color="#8f959e" />
|
||||
<text class="file-breadcrumb__item"
|
||||
:class="{ 'file-breadcrumb__item--ellipsis': seg.ellipsis, 'file-breadcrumb__item--clamped': tailClamped && seg.index === items.length - 1 }"
|
||||
@tap="onSegmentTap(seg)">
|
||||
{{ seg.node.displayName }}
|
||||
</text>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 面包屑:描边圈 + 浅浅的半透明白底(透出渐变底不发死白),分隔为细 chevron */
|
||||
.file-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 20rpx 28rpx;
|
||||
border: 2rpx solid rgba(31, 35, 41, 0.08);
|
||||
border-radius: 20rpx;
|
||||
background-color: rgba(255, 255, 255, 0.45);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-breadcrumb__root {
|
||||
flex-shrink: 0;
|
||||
font-size: 28rpx;
|
||||
color: #2b8fff;
|
||||
}
|
||||
|
||||
.file-breadcrumb__rest {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 左起排列(对齐截图:短路径贴左,放不下才折叠出 …);测量溢出靠 scrollWidth 对比 */
|
||||
.file-breadcrumb__rest-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-breadcrumb__item {
|
||||
flex-shrink: 0;
|
||||
font-size: 28rpx;
|
||||
color: #2b8fff;
|
||||
white-space: nowrap;
|
||||
|
||||
/* uni-text 内部 span 默认 pre-line,会让 nowrap/ellipsis 失效,须穿透覆盖 */
|
||||
:deep(span) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&--ellipsis {
|
||||
padding: 0 4rpx;
|
||||
color: #2b8fff;
|
||||
}
|
||||
|
||||
/* 折到底仍溢出时的末段兜底:flex-shrink+min-width:0 让末段收缩进可视框,
|
||||
* 内部 span 转 block 吃满收缩后的宽度出 CSS 省略号 */
|
||||
&--clamped {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
|
||||
:deep(span) {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-breadcrumb__sep {
|
||||
flex-shrink: 0;
|
||||
margin: 0 6rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,269 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件网格列表:卡片展示文件夹图标 / 文件类型图标、名称、元数据(文件大小 + 修改时间,
|
||||
* 文件夹只有时间、不显示用户),右上角三点「更多」。viewMode 支持 grid(三列卡片)与 list(单行列表)。
|
||||
* 多选模式(selectMode)下点击卡片改为勾选/取消;数据、选中集均由页面持有,这里只渲染与上报。
|
||||
*/
|
||||
import type { PropType } from "vue";
|
||||
import type { FileItem, FileKind } from "@/data/custom/fileBrowser";
|
||||
|
||||
const props = defineProps({
|
||||
/** 已过滤排序好的条目,组件按原顺序渲染 */
|
||||
files: {
|
||||
type: Array as PropType<FileItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/** 排布模式:grid 九宫格 / list 列表 */
|
||||
viewMode: {
|
||||
type: String as PropType<"grid" | "list">,
|
||||
default: "grid",
|
||||
},
|
||||
/** 是否处于多选模式 */
|
||||
selectMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 多选模式下已勾选的 id 集合 */
|
||||
selectedIds: {
|
||||
type: Array as PropType<number[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/** 空列表提示文案(页面翻译好传入) */
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 点开条目(非多选模式的单击) */
|
||||
(e: "open", file: FileItem): void;
|
||||
/** 卡片右上角「更多」 */
|
||||
(e: "more", file: FileItem): void;
|
||||
/** 多选模式下勾选状态变化 */
|
||||
(e: "toggle-select", id: number): void;
|
||||
}>();
|
||||
|
||||
// 文件类型 -> 图标与配色(文件夹走纯 CSS 造型,不用字体图标)
|
||||
const kindVisual: Record<Exclude<FileKind, "folder">, { icon: string; color: string }> = {
|
||||
image: { icon: "photo-fill", color: "#0ea5e9" },
|
||||
video: { icon: "play-right-fill", color: "#8b5cf6" },
|
||||
document: { icon: "file-text-fill", color: "#3b82f6" },
|
||||
audio: { icon: "volume-up", color: "#f59e0b" },
|
||||
};
|
||||
|
||||
function visualOf(kind: FileKind): { icon: string; color: string } | null {
|
||||
return kind === "folder" ? null : kindVisual[kind];
|
||||
}
|
||||
|
||||
function isSelected(file: FileItem): boolean {
|
||||
return props.selectedIds.includes(file.id);
|
||||
}
|
||||
|
||||
function onCardTap(file: FileItem): void {
|
||||
if (props.selectMode) {
|
||||
emit("toggle-select", file.id);
|
||||
return;
|
||||
}
|
||||
emit("open", file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="file-grid-list" :class="{ 'file-grid-list--list': viewMode === 'list' }">
|
||||
<view v-if="!files.length" class="file-grid-list__empty">
|
||||
<text class="file-grid-list__empty-text">
|
||||
{{ emptyText }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="file in files" :key="file.id" class="fgl-card"
|
||||
:class="{ 'fgl-card--selected': selectMode && isSelected(file) }">
|
||||
<view class="fgl-card__more" @tap.stop="emit('more', file)">
|
||||
<u-icon name="more-dot-fill" size="30" color="#b8bcc4" />
|
||||
</view>
|
||||
|
||||
<view class="fgl-card__body" @tap="onCardTap(file)">
|
||||
<!-- 文件夹:纯 CSS 造型(图标库没有 folder),主体 + 左上页签 -->
|
||||
<view v-if="file.kind === 'folder'" class="fgl-card__folder">
|
||||
<view class="fgl-card__folder-tab" />
|
||||
</view>
|
||||
<u-icon v-else :name="visualOf(file.kind)!.icon" size="88" :color="visualOf(file.kind)!.color" />
|
||||
|
||||
<view class="fgl-card__info">
|
||||
<text class="fgl-card__name">
|
||||
{{ file.name }}
|
||||
</text>
|
||||
<view class="fgl-card__meta">
|
||||
<text v-if="file.sizeText" class="fgl-card__meta-text">
|
||||
{{ file.sizeText }}
|
||||
</text>
|
||||
<text class="fgl-card__meta-text">
|
||||
{{ file.modifiedAt }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="selectMode" class="fgl-card__check">
|
||||
<u-icon :name="isSelected(file) ? 'checkmark-circle-fill' : 'checkmark-circle'" size="40"
|
||||
:color="isSelected(file) ? '#2b8fff' : '#c0c4cc'" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-grid-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16rpx 20rpx 40rpx;
|
||||
}
|
||||
|
||||
.file-grid-list__empty {
|
||||
width: 100%;
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-grid-list__empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
|
||||
.fgl-card {
|
||||
position: relative;
|
||||
width: calc(100% / 3 - 16rpx);
|
||||
margin: 8rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #f7f8fa;
|
||||
|
||||
&--selected {
|
||||
background-color: #eaf4ff;
|
||||
}
|
||||
}
|
||||
|
||||
.fgl-card__more {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 12rpx;
|
||||
background-color: rgba(31, 35, 41, 0.05);
|
||||
}
|
||||
|
||||
.fgl-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 36rpx 12rpx 0;
|
||||
}
|
||||
|
||||
/* ---- 纯 CSS 文件夹:蓝色渐变圆角主体 + 左上页签 ---- */
|
||||
.fgl-card__folder {
|
||||
position: relative;
|
||||
width: 132rpx;
|
||||
height: 104rpx;
|
||||
margin: 14rpx 0;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, #6ec2ff 0%, #2b8fff 100%);
|
||||
box-shadow: 0 10rpx 20rpx rgba(43, 143, 255, 0.25);
|
||||
}
|
||||
|
||||
.fgl-card__folder-tab {
|
||||
position: absolute;
|
||||
top: -16rpx;
|
||||
left: 14rpx;
|
||||
width: 56rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 10rpx 10rpx 0 0;
|
||||
background: linear-gradient(135deg, #6ec2ff 0%, #4aa6ff 100%);
|
||||
}
|
||||
|
||||
.fgl-card__info {
|
||||
width: 100%;
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
|
||||
.fgl-card__name {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fgl-card__meta {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
margin-top: 6rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fgl-card__meta-text {
|
||||
flex-shrink: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 22rpx;
|
||||
color: #9aa0a6;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fgl-card__check {
|
||||
position: absolute;
|
||||
right: 12rpx;
|
||||
bottom: 16rpx;
|
||||
}
|
||||
|
||||
/* ---- 列表模式:单行排布,覆盖网格默认样式 ---- */
|
||||
.file-grid-list--list {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
padding: 8rpx 24rpx 40rpx;
|
||||
}
|
||||
|
||||
.file-grid-list--list .fgl-card {
|
||||
width: 100%;
|
||||
margin: 0 0 16rpx;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.file-grid-list--list .fgl-card__more {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.file-grid-list--list .fgl-card__body {
|
||||
flex-direction: row;
|
||||
padding: 24rpx 64rpx 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.file-grid-list--list .fgl-card__folder {
|
||||
width: 72rpx;
|
||||
height: 56rpx;
|
||||
margin: 0 24rpx 0 0;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.file-grid-list--list .fgl-card__folder-tab {
|
||||
top: -10rpx;
|
||||
left: 8rpx;
|
||||
width: 30rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 6rpx 6rpx 0 0;
|
||||
}
|
||||
|
||||
.file-grid-list--list .fgl-card__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 分类标签筛选:横向标签(全部/图片/视频/文档/音频)+ 右侧「列表视图切换」「多选」按钮。
|
||||
* 标签内容由页面传入(翻译在页面做);当前选中、视图模式、多选态都是外部状态,
|
||||
* 组件只上报用户意图(change / toggle-view / toggle-select),自身不改任何东西。
|
||||
*/
|
||||
import type { PropType } from "vue";
|
||||
import type { FileTabItem } from "@/data/custom/fileBrowser";
|
||||
|
||||
defineProps({
|
||||
/** 分类标签列表 */
|
||||
tabs: {
|
||||
type: Array as PropType<FileTabItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/** 当前选中的分类 key */
|
||||
active: {
|
||||
type: String,
|
||||
default: "all",
|
||||
},
|
||||
/** 当前视图:grid 九宫格 / list 列表 */
|
||||
viewMode: {
|
||||
type: String as PropType<"grid" | "list">,
|
||||
default: "list",
|
||||
},
|
||||
/** 是否处于多选模式 */
|
||||
selectMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change", key: FileTabItem["key"]): void;
|
||||
(e: "sort"): void;
|
||||
(e: "toggle-view"): void;
|
||||
(e: "toggle-select"): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="file-tab-filter">
|
||||
<scroll-view class="file-tab-filter__scroll" scroll-x :show-scrollbar="false">
|
||||
<view class="file-tab-filter__tabs">
|
||||
<text v-for="tab in tabs" :key="tab.key" class="file-tab-filter__tab"
|
||||
:class="{ 'file-tab-filter__tab--active': tab.key === active }" @tap="emit('change', tab.key)">
|
||||
{{ tab.label }}
|
||||
</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="file-tab-filter__tools">
|
||||
<!-- 排序(从页面导航栏迁来,倒数第3位):level 阶梯横线,比原 order 更有排序感 -->
|
||||
<view class="file-tab-filter__tool" @tap="emit('sort')">
|
||||
<u-icon name="level" size="36" color="#1f2329" />
|
||||
</view>
|
||||
<!-- 显示的是「点后会切到什么」:网格态给列表图标,列表态给网格图标 -->
|
||||
<view class="file-tab-filter__tool" @tap="emit('toggle-view')">
|
||||
<u-icon :name="viewMode === 'grid' ? 'list' : 'grid-fill'" size="36" color="#1f2329" />
|
||||
</view>
|
||||
<view class="file-tab-filter__tool" @tap="emit('toggle-select')">
|
||||
<u-icon name="checkmark-circle" size="36" :color="selectMode ? '#2b8fff' : '#1f2329'" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 分类条卡片:与面包屑/搜索框同风格——描边圈 + 浅半透明白底,透出渐变底 */
|
||||
.file-tab-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20rpx 24rpx 0;
|
||||
padding: 8rpx 16rpx;
|
||||
border: 2rpx solid rgba(31, 35, 41, 0.08);
|
||||
border-radius: 24rpx;
|
||||
background-color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.file-tab-filter__scroll {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-tab-filter__tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.file-tab-filter__tab {
|
||||
padding: 8rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background-color: #f2f3f5;
|
||||
font-size: 24rpx;
|
||||
color: #5c626b;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--active {
|
||||
background-color: #1f2329;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.file-tab-filter__tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-tab-filter__tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 悬浮操作按钮:右下角圆形蓝色渐变「+」,固定在底部导航之上。
|
||||
* 只做外观与点击上报;新建/上传等动作语义由页面处理。
|
||||
*/
|
||||
defineProps({
|
||||
/** 额外底部偏移(rpx 数值),供不同页面避开各自的底部元素 */
|
||||
bottomOffset: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="float-add-btn" :style="{ bottom: `${180 + bottomOffset}rpx` }" @tap="emit('click')">
|
||||
<u-icon name="plus" size="52" color="#ffffff" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.float-add-btn {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
z-index: 8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6ec2ff 0%, #2b8fff 100%);
|
||||
box-shadow: 0 12rpx 32rpx rgba(43, 143, 255, 0.4);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件页顶部导航栏:「返回箭头 + 居中标题 + 右侧两个显隐开关」。
|
||||
* 根目录不显示返回(showBack=false);标题两侧等宽占位保证真居中。
|
||||
* 右侧 search/grid 两个图标分别切换页面里搜索框与快捷入口区的显隐,
|
||||
* 点亮(蓝)= 对应区域显示,置灰 = 隐藏;排序按钮在 file-tab-filter 工具区。
|
||||
* 纯展示组件:不自己做导航与显隐逻辑,按钮语义一律 emit 给页面决定。
|
||||
* 顶栏配色跟随截图(白底黑字),不并入 app-page 的绿色 u-navbar,故页面用 hide-nav 让位。
|
||||
*/
|
||||
const props = defineProps({
|
||||
/** 页面标题(页面用 t() 翻译好后传进来) */
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/** 是否显示返回箭头:仅进入子目录后显示 */
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 搜索框当前是否可见(决定开关图标高亮态) */
|
||||
searchOn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/** 快捷入口区当前是否可见(决定开关图标高亮态) */
|
||||
quickOn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "back"): void;
|
||||
(e: "toggle-search"): void;
|
||||
(e: "toggle-quick"): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-navbar">
|
||||
<view class="page-navbar__side page-navbar__side--left">
|
||||
<view v-if="props.showBack" class="page-navbar__back" @tap="emit('back')">
|
||||
<u-icon name="arrow-left" size="42" color="#1f2329" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="page-navbar__title">
|
||||
{{ props.title }}
|
||||
</text>
|
||||
<view class="page-navbar__side page-navbar__side--right">
|
||||
<!-- 切换搜索框显隐 -->
|
||||
<view class="page-navbar__btn" @tap="emit('toggle-search')">
|
||||
<u-icon name="search" size="38" :color="props.searchOn ? '#2b8fff' : '#8f959e'" />
|
||||
</view>
|
||||
<!-- 切换快捷入口区显隐 -->
|
||||
<view class="page-navbar__btn" @tap="emit('toggle-quick')">
|
||||
<u-icon name="grid" size="38" :color="props.quickOn ? '#2b8fff' : '#8f959e'" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
/* 透明顶栏:透出页面的浅蓝→白渐变底 */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 左右等宽占位(容纳右侧两个按钮):任何状态下标题都严格居中 */
|
||||
.page-navbar__side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
width: 148rpx;
|
||||
min-height: 64rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-navbar__side--left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.page-navbar__side--right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.page-navbar__back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
/* 显隐开关按钮:与 file-tab-filter 工具图标同尺寸,保持一行工具观感 */
|
||||
.page-navbar__btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
.page-navbar__title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 38rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 快捷功能入口:横向滚动的图标按钮组(最近访问 / 收藏 / 已下载 / 团队文件 / 共享文件)。
|
||||
* 单项内容(图标名 + 渐变色 + 文案)全部由页面通过 items 传入,组件只渲染与派发点击。
|
||||
*/
|
||||
import type { PropType } from "vue";
|
||||
import type { QuickEntryItem } from "@/data/custom/fileBrowser";
|
||||
|
||||
defineProps({
|
||||
/** 入口列表,顺序即展示顺序 */
|
||||
items: {
|
||||
type: Array as PropType<QuickEntryItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click", key: string): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view class="quick-entry" scroll-x :show-scrollbar="false">
|
||||
<view class="quick-entry__row">
|
||||
<view v-for="item in items" :key="item.key" class="quick-entry__item" @tap="emit('click', item.key)">
|
||||
<view class="quick-entry__tile"
|
||||
:style="{ backgroundImage: `linear-gradient(135deg, ${item.bgFrom}, ${item.bgTo})` }">
|
||||
<u-icon :name="item.icon" size="44" color="#ffffff" />
|
||||
</view>
|
||||
<text class="quick-entry__label">
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 透明快捷入口:透出页面渐变底;左右留边仍由页面外层包装负责
|
||||
(scroll-view 根节点固定 100% 宽,自身加 margin 会右侧溢出被裁,实测踩过) */
|
||||
.quick-entry {
|
||||
padding: 28rpx 0 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background-color: transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.quick-entry__row {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.quick-entry__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 128rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-entry__tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 28rpx;
|
||||
box-shadow: 0 8rpx 16rpx rgba(31, 35, 41, 0.08);
|
||||
}
|
||||
|
||||
.quick-entry__label {
|
||||
margin-top: 14rpx;
|
||||
font-size: 26rpx;
|
||||
color: #1f2329;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 搜索框:uView Pro 的 u-input + 放大镜前缀图标,圆角浅灰底(对齐截图样式)。
|
||||
* 输入即回传(配合 v-model 语义),键盘确认派发 search 事件;
|
||||
* 注意 uview-pro 的 u-input 只派发 input/update:modelValue,没有 change 事件,
|
||||
* 绑错 @change 会导致关键字永不回传(实测踩过)。组件不碰接口,过滤时机由页面决定。
|
||||
*/
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
/** 关键字(v-model 语义) */
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 占位文案(页面翻译好传入) */
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void;
|
||||
(e: "search", value: string): void;
|
||||
}>();
|
||||
|
||||
// u-input 默认白底带边框,这里压成透明让它融进外层浅灰圆角容器
|
||||
const inputStyle = computed(() => ({
|
||||
backgroundColor: "transparent",
|
||||
}));
|
||||
|
||||
function onInput(value: string): void {
|
||||
emit("update:modelValue", value);
|
||||
}
|
||||
|
||||
function onConfirm(value: string): void {
|
||||
emit("search", value ?? props.modelValue);
|
||||
}
|
||||
|
||||
function onClear(): void {
|
||||
emit("update:modelValue", "");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="search-input">
|
||||
<view class="search-input__box">
|
||||
<u-icon name="search" size="34" color="#9aa0a6" />
|
||||
<view class="search-input__field">
|
||||
<u-input :model-value="modelValue" :placeholder="placeholder" :border="false" clearable
|
||||
:custom-style="inputStyle" placeholder-style="color: #9aa0a6" confirm-type="search" @input="onInput"
|
||||
@confirm="onConfirm" @clear="onClear" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 分区卡片风格:外层透明露渐变底,胶囊内部浅白色、留描边定形 */
|
||||
.search-input {
|
||||
padding: 2rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.search-input__box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
border: 2rpx solid rgba(31, 35, 41, 0.08);
|
||||
border-radius: 399rpx;
|
||||
background-color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.search-input__field {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user