This commit is contained in:
owenchen committed 2026-10-14 18:50:17 +08:00
1 parent 3194f98ac0
commit e740ec77ec
24 files changed
+1561 -65

No files matched your search

@@ -20,6 +20,11 @@ defineProps({
type: Boolean,
default: false,
},
/** 自定义返回动作(u-navbar customBack 透传):页面做无栈兜底跳转时用 */
customBack: {
type: Function as PropType<() => void>,
default: undefined,
},
customStyle: {
type: [String, Object] as PropType<string | Record<string, any>>,
default: "",
@@ -40,11 +45,9 @@ const background = reactive({
<template>
<view class="app-page" :class="{ 'has-tabbar': showTabbar }" :style="$u.toStyle(customStyle)">
<!-- #ifndef MP-ALIPAY -->
<u-navbar
v-if="!hideNav" :is-back="showNavBack && !showTabbar" :title="navTitle" :background="background" :is-fixed="true"
:immersive="false" back-icon-name="arrow-leftward" title-width="350" title-color="#ffffff"
back-icon-color="#ffffff"
/>
<u-navbar v-if="!hideNav" :is-back="showNavBack && !showTabbar" :title="navTitle" :background="background"
:is-fixed="true" :immersive="false" :custom-back="customBack" back-icon-name="arrow-leftward" title-width="350"
title-color="#ffffff" back-icon-color="#ffffff" />
<!-- #endif -->
<u-transition name="slide-left" :appear="true">
<slot />
@@ -55,21 +58,21 @@ const background = reactive({
<style lang="scss" scoped>
.app-page {
width: 100%;
min-height: 100vh;
// padding-bottom: 30rpx;
overflow-y: auto;
background-color: $u-bg-white;
-webkit-font-smoothing: antialiased;
color: $u-main-color;
transition: background 0.3s ease;
width: 100%;
min-height: 100vh;
// padding-bottom: 30rpx;
overflow-y: auto;
background-color: $u-bg-white;
-webkit-font-smoothing: antialiased;
color: $u-main-color;
transition: background 0.3s ease;
&.has-tabbar {
background-image: linear-gradient(135deg,
rgba(var(--u-type-primary-rgb, 41, 121, 255), 0.04) 0%,
rgba(var(--u-type-success-rgb, 25, 190, 107), 0.04) 40%,
rgba(var(--u-type-warning-rgb, 255, 153, 0), 0.04) 100%);
}
&.has-tabbar {
background-image: linear-gradient(135deg,
rgba(var(--u-type-primary-rgb, 41, 121, 255), 0.04) 0%,
rgba(var(--u-type-success-rgb, 25, 190, 107), 0.04) 40%,
rgba(var(--u-type-warning-rgb, 255, 153, 0), 0.04) 100%);
}
}
</style>
@@ -0,0 +1,90 @@
<script setup lang="ts">
/**
* 新建操作面板:点悬浮「+」弹出的底部抽屉,只两项——上传文件 / 新建文件夹
* (对应旧系统 OperatorStation 的 UploadOutlined + FolderAddOutlined 能力)。
* 组件只渲染与派发 action,真实上传/建目录由页面接 DMS 接口。
*/
import { useLocale } from "uview-pro";
import { computed } from "vue";
interface AddAction {
key: string;
label: string;
icon: string;
}
defineProps({
/** 面板是否展开 */
visible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
/** 抽屉关闭(遮罩/下滑) */
(e: "close"): void;
/** 点了某项新建动作 */
(e: "action", key: string): void;
}>();
const { t } = useLocale();
// computed:切换语言后文案跟随重算(tab 页组件实例常驻,setup 只跑一次)
const actions = computed<AddAction[]>(() => [
{ key: "upload", label: t("FilePage.addUpload"), icon: "arrow-up-fill" },
{ key: "folder", label: t("FilePage.addFolder"), icon: "plus-circle-fill" },
]);
</script>
<template>
<!-- u-popup 只认 modelValue(v-model),绑 :show 会静默弹不出 -->
<u-popup :model-value="visible" mode="bottom" round="24" @close="emit('close')">
<view class="file-add-sheet">
<text class="file-add-sheet__title">
{{ t("FilePage.addTitle") }}
</text>
<view v-for="action in actions" :key="action.key" class="file-add-sheet__row" @tap="emit('action', action.key)">
<u-icon :name="action.icon" size="40" color="#2b8fff" />
<text class="file-add-sheet__row-text">
{{ action.label }}
</text>
</view>
<view class="file-add-sheet__safe" />
</view>
</u-popup>
</template>
<style lang="scss" scoped>
.file-add-sheet {
padding: 40rpx 32rpx 0;
background-color: #ffffff;
&__title {
display: block;
padding: 0 8rpx 24rpx;
font-size: 26rpx;
color: #8f959e;
}
&__row {
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 16rpx;
padding: 30rpx 32rpx;
border-radius: 20rpx;
background-color: #f5f6f8;
}
&__row-text {
font-size: 30rpx;
color: #1f2329;
}
/* 底部留白:贴屏安全区(H5 无 env 时也有 32rpx 呼吸) */
&__safe {
height: calc(32rpx + env(safe-area-inset-bottom));
}
}
</style>
@@ -16,6 +16,11 @@ const props = defineProps({
type: Array as PropType<PathNode[]>,
default: () => [],
},
/** 只有根一段时也渲染(传输页目标目录用):文件页惯例是根目录不显示面包屑,默认关 */
showSingle: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
@@ -112,7 +117,7 @@ onUnmounted(() => {
</script>
<template>
<view v-if="items.length > 1" class="file-breadcrumb">
<view v-if="items.length > 1 || (showSingle && items.length === 1)" class="file-breadcrumb">
<text class="file-breadcrumb__root" @tap="onSegmentTap({ node: items[0], index: 0, ellipsis: false })">
{{ items[0].displayName }}
</text>
@@ -1,11 +1,14 @@
<script setup lang="ts">
/**
* 分类标签筛选:横向标签(全部/图片/视频/文档/音频)+ 右侧「列表视图切换」「多选」按钮。
* 分类标签筛选:横向标签(全部/图片/视频/文档/音频)+ 右侧「新建文件夹」「列表视图切换」「多选」按钮。
* 标签内容由页面传入(翻译在页面做);当前选中、视图模式、多选态都是外部状态,
* 组件只上报用户意图(change / toggle-view / toggle-select),自身不改任何东西。
*/
import type { PropType } from "vue";
import type { FileTabItem } from "@/data/custom/fileBrowser";
import { folderGlyph } from "@/utils/folderGlyph";
import { selectGlyph } from "@/utils/selectGlyph";
import { sortGlyph } from "@/utils/sortGlyph";
defineProps({
/** 分类标签列表 */
@@ -35,7 +38,15 @@ const emit = defineEmits<{
(e: "sort"): void;
(e: "toggle-view"): void;
(e: "toggle-select"): void;
(e: "new-folder"): void;
}>();
/* 多选开关的自绘字形(用户在 iconfont 挑选的方框+箭头):颜色固定,模块级生成一次 */
const selectIconDark = selectGlyph("#1f2329");
/* 新建文件夹的自绘字形(用户挑选的文件夹+加号):与「+」直传拆分后的专属入口 */
const folderIconDark = folderGlyph("#1f2329");
/* 排序的自绘字形(用户挑选的三横线+下箭头):替换原 level 字体图标 */
const sortIconDark = sortGlyph("#1f2329");
</script>
<template>
@@ -49,18 +60,22 @@ const emit = defineEmits<{
</view>
</scroll-view>
<view class="file-tab-filter__tools">
<!-- 排序(从页面导航栏迁来,倒数第3位):level 阶梯横线,比原 order 更有排序感 -->
<!-- 新建文件夹:直接弹命名对话框(上传已从悬浮「+」拆分,这里只管建目录) -->
<view class="file-tab-filter__tool" @tap="emit('new-folder')">
<image class="file-tab-filter__tool-img--folder" :src="folderIconDark" mode="aspectFit" />
</view>
<!-- 排序(从页面导航栏迁来):自绘三横线+下箭头,比原 level 更有降序感 -->
<view class="file-tab-filter__tool" @tap="emit('sort')">
<u-icon name="level" size="36" color="#1f2329" />
<image class="file-tab-filter__tool-img--sort" :src="sortIconDark" mode="aspectFit" />
</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="selectMode ? 'close-circle' : 'checkmark-circle'" size="36"
:color="selectMode ? '#2b8fff' : '#1f2329'" />
<image v-if="!selectMode" class="file-tab-filter__tool-img" :src="selectIconDark" mode="aspectFit" />
<u-icon v-else name="close-circle" size="36" color="#2b8fff" />
</view>
</view>
</view>
@@ -87,8 +102,8 @@ const emit = defineEmits<{
.file-tab-filter__tabs {
display: inline-flex;
align-items: center;
gap: 16rpx;
padding-left: 20rpx;
gap: 8rpx;
padding-left: 4rpx;
}
.file-tab-filter__tab {
@@ -120,4 +135,22 @@ const emit = defineEmits<{
width: 60rpx;
height: 60rpx;
}
/* 自绘多选字形:与同行 u-icon size=36 对齐 */
.file-tab-filter__tool-img {
width: 36rpx;
height: 36rpx;
}
/* 排序字形:原图 viewBox 1178×1024 非正方形,按 1.15:1 给宽高保持视觉重量 */
.file-tab-filter__tool-img--sort {
width: 38rpx;
height: 33rpx;
}
/* 新建文件夹字形:原图内容只占 viewBox 约 78%(四周留白多),放大补偿到与旁边同视觉大小 */
.file-tab-filter__tool-img--folder {
width: 46rpx;
height: 46rpx;
}
</style>
@@ -3,7 +3,9 @@
* 悬浮操作按钮:蓝色渐变「+」,可在屏幕内自由拖动,松手后位置写入 Storage 下次启动恢复。
* 只做外观与点击上报;新建/上传等动作语义由页面处理。
* 拖动实现:touch 事件(真机/App)+ mouse 事件(H5 桌面调试)双轨;
* 位移小于阈值视为轻点(emit click),超过阈值才算拖动(touch 后屏蔽 mouse 兼容事件防双触发)。
* 位移小于阈值视为轻点、超过才算拖动。点击上报走原生 click(onTap)而非 touchend/mouseup
* 手动 emit:保留浏览器用户激活链,否则轻点里同步拉起文件选择器等 gesture API 会被静默拦截;
* 拖动收尾置 suppressClick 吞掉随后的原生 click,防拖完误触。
*/
import { computed, onMounted, ref } from "vue";
@@ -90,6 +92,9 @@ function onDragMove(x: number, y: number): void {
pos.value = clampPos(originPos.x + dx, originPos.y + dy);
}
/** 拖动收尾后浏览器还会补发一次原生 click,置位吞掉它(轻点不置位,由 onTap 正常上报) */
let suppressClick = false;
function onDragEnd(): void {
if (!dragging) {
return;
@@ -97,10 +102,17 @@ function onDragEnd(): void {
dragging = false;
if (moved) {
uni.setStorageSync(STORAGE_KEY, pos.value);
suppressClick = true;
}
else {
emit("click");
}
/** 唯一点击出口:原生 click(鼠标轻点/触屏补发)保留用户手势,页面才能在里面拉起文件选择器 */
function onTap(): void {
if (suppressClick) {
suppressClick = false;
return;
}
emit("click");
}
// ===== touch(真机 / App)=====
@@ -128,7 +140,7 @@ function onMouseDown(e: MouseEvent): void {
if (touchHandled) {
return;
}
e.preventDefault();
// 注意不能 preventDefault:它会取消后续原生 click 及其用户激活,onTap 就再也收不到点击
onDragStart(e.clientX, e.clientY);
const move = (ev: MouseEvent) => onDragMove(ev.clientX, ev.clientY);
const up = () => {
@@ -143,7 +155,7 @@ function onMouseDown(e: MouseEvent): void {
<template>
<view class="float-add-btn" :style="posStyle" @touchstart="onTouchStart" @touchmove.stop.prevent="onTouchMove"
@touchend="onTouchEnd" @mousedown="onMouseDown">
@touchend="onTouchEnd" @mousedown="onMouseDown" @click="onTap">
<u-icon name="plus" size="52" color="#ffffff" />
</view>
</template>
@@ -0,0 +1,142 @@
<script setup lang="ts">
/**
* 新建文件夹命名对话框:居中卡片(标题 + 输入 + 取消/创建),
* 只负责收集名称——非法字符校验、CreateDmsFolder 接口与结果提示由页面分流。
* 提交成功后由父级收起 visible;submitting 期间禁用按钮防连点。
*/
import { useLocale } from "uview-pro";
import { computed, ref, watch } from "vue";
const props = defineProps({
/** 对话框是否展开 */
visible: {
type: Boolean,
default: false,
},
/** 创建请求在途:置灰两个按钮 */
submitting: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
/** 取消/遮罩关闭 */
(e: "close"): void;
/** 点了创建且名称非空 */
(e: "confirm", name: string): void;
}>();
const { t } = useLocale();
const name = ref("");
/** 空白名不算可提交 */
const canSubmit = computed(() => name.value.trim().length > 0);
// 每次展开清掉上次的残留输入(对话框实例常驻 tab 页)
watch(
() => props.visible,
(on) => {
if (on) {
name.value = "";
}
},
);
function onConfirm(): void {
if (!props.submitting && canSubmit.value) {
emit("confirm", name.value.trim());
}
}
</script>
<template>
<!-- u-popup 只认 modelValue(v-model);提交中禁点遮罩防误关 -->
<u-popup :model-value="props.visible" mode="center" round="24" :close-on-click-overlay="!props.submitting"
@close="emit('close')">
<view class="folder-name-dialog">
<text class="folder-name-dialog__title">
{{ t("FilePage.addFolder") }}
</text>
<input v-model="name" class="folder-name-dialog__input" :placeholder="t('FilePage.folderPlaceholder')"
:maxlength="50" :disabled="props.submitting" confirm-type="done" @confirm="onConfirm">
<view class="folder-name-dialog__actions">
<view class="folder-name-dialog__btn" @click="emit('close')">
<text class="folder-name-dialog__btn-text">
{{ t("FilePage.folderCancel") }}
</text>
</view>
<view class="folder-name-dialog__btn folder-name-dialog__btn--primary"
:class="{ 'folder-name-dialog__btn--muted': !canSubmit || props.submitting }" @click="onConfirm">
<text class="folder-name-dialog__btn-text folder-name-dialog__btn-text--primary">
{{ t("FilePage.folderConfirm") }}
</text>
</view>
</view>
</view>
</u-popup>
</template>
<style lang="scss" scoped>
.folder-name-dialog {
width: 560rpx;
padding: 40rpx 32rpx 32rpx;
background-color: #ffffff;
&__title {
display: block;
padding-bottom: 28rpx;
font-size: 32rpx;
font-weight: 600;
color: #1f2329;
text-align: center;
}
&__input {
box-sizing: border-box;
width: 100%;
height: 80rpx;
padding: 0 24rpx;
border: 2rpx solid #e6e7ea;
border-radius: 16rpx;
font-size: 28rpx;
color: #1f2329;
background-color: #f7f8fa;
}
&__actions {
display: flex;
gap: 20rpx;
margin-top: 32rpx;
}
&__btn {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 76rpx;
border-radius: 999rpx;
background-color: #f5f6f8;
&--primary {
background-color: #2b8fff;
}
/* 名称为空或提交中:主按钮置灰(仍可点,由 onConfirm 拦截) */
&--muted {
background-color: #b9d6ff;
}
}
&__btn-text {
font-size: 28rpx;
color: #5c626b;
&--primary {
color: #ffffff;
}
}
}
</style>
@@ -1,12 +1,16 @@
<script setup lang="ts">
/**
* 文件页顶部导航栏:「返回箭头 + 居中标题 + 右侧两个显隐开关」。
* 文件页顶部导航栏:「返回箭头 + 居中标题 + 传输入口 + 右侧两个显隐开关」。
* 根目录不显示返回(showBack=false);标题两侧等宽占位保证真居中。
* 右侧 search/grid 两个图标分别切换页面里搜索框与快捷入口区的显隐,
* 点亮(蓝)= 对应区域显示,置灰 = 隐藏;排序按钮在 file-tab-filter 工具区。
* 传输图标是跳转入口(带进行中计数角标),不参与显隐切换;字形用自绘 SVG(transferGlyph)。
* 纯展示组件:不自己做导航与显隐逻辑,按钮语义一律 emit 给页面决定。
* 顶栏配色跟随截图(白底黑字),不并入 app-page 的绿色 u-navbar,故页面用 hide-nav 让位。
*/
import { computed } from "vue";
import { TRANSFER_ACTIVE_COLOR, transferGlyph } from "@/utils/transferGlyph";
const props = defineProps({
/** 页面标题(页面用 t() 翻译好后传进来) */
title: {
@@ -28,13 +32,22 @@ const props = defineProps({
type: Boolean,
default: true,
},
/** 进行中上传任务数:>0 时传输图标点亮并挂数字角标 */
transferring: {
type: Number,
default: 0,
},
});
const emit = defineEmits<{
(e: "back"): void;
(e: "toggle-search"): void;
(e: "toggle-quick"): void;
(e: "transfer"): void;
}>();
/** 传输字形:有进行中任务点亮(iconfont 主色蓝),空闲置灰 */
const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_ACTIVE_COLOR : "#8f959e"));
</script>
<template>
@@ -48,6 +61,13 @@ const emit = defineEmits<{
{{ props.title }}
</text>
<view class="page-navbar__side page-navbar__side--right">
<!-- 传输列表入口(常显):有进行中任务时点亮 + 数字角标 -->
<view class="page-navbar__btn page-navbar__btn--transfer" @tap="emit('transfer')">
<image class="page-navbar__transfer-img" :src="transferIcon" mode="aspectFit" />
<text v-if="props.transferring" class="page-navbar__badge">
{{ props.transferring > 99 ? "99+" : props.transferring }}
</text>
</view>
<!-- 切换搜索框显隐 -->
<view class="page-navbar__btn" @tap="emit('toggle-search')">
<u-icon name="search" size="38" :color="props.searchOn ? '#2b8fff' : '#8f959e'" />
@@ -72,12 +92,12 @@ const emit = defineEmits<{
background-color: transparent;
}
/* 左右等宽占位(容纳右侧两个按钮):任何状态下标题都严格居中 */
/* 左右等宽占位(容纳右侧三个按钮):任何状态下标题都严格居中 */
.page-navbar__side {
display: flex;
align-items: center;
gap: 6rpx;
width: 148rpx;
width: 208rpx;
min-height: 64rpx;
flex-shrink: 0;
}
@@ -107,6 +127,31 @@ const emit = defineEmits<{
height: 60rpx;
}
/* 传输入口:右上角挂进行中计数角标 */
.page-navbar__btn--transfer {
position: relative;
}
/* 自绘 SVG 传输字形:与同行 u-icon size=38 视觉对齐 */
.page-navbar__transfer-img {
width: 40rpx;
height: 40rpx;
}
.page-navbar__badge {
position: absolute;
top: 2rpx;
right: 0;
min-width: 26rpx;
padding: 0 6rpx;
border-radius: 999rpx;
font-size: 18rpx;
line-height: 26rpx;
text-align: center;
color: #ffffff;
background-color: #f0413e;
}
.page-navbar__title {
flex: 1;
text-align: center;