ow
This commit is contained in:
1 parent
e740ec77ec
commit
ceb3e7beec
20 files changed
+2280
-138
No files matched your search
@@ -31,6 +31,11 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 新建文件夹按钮开关:回收站等无建目录场景的页面传 false */
|
||||
showNewFolder: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -60,8 +65,8 @@ const sortIconDark = sortGlyph("#1f2329");
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="file-tab-filter__tools">
|
||||
<!-- 新建文件夹:直接弹命名对话框(上传已从悬浮「+」拆分,这里只管建目录) -->
|
||||
<view class="file-tab-filter__tool" @tap="emit('new-folder')">
|
||||
<!-- 新建文件夹:直接弹命名对话框(上传已从悬浮「+」拆分,这里只管建目录);回收站页关掉 -->
|
||||
<view v-if="showNewFolder" class="file-tab-filter__tool" @tap="emit('new-folder')">
|
||||
<image class="file-tab-filter__tool-img--folder" :src="folderIconDark" mode="aspectFit" />
|
||||
</view>
|
||||
<!-- 排序(从页面导航栏迁来):自绘三横线+下箭头,比原 level 更有降序感 -->
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 移动目标目录选择器(底部抽屉,网盘式逐级下钻):替代旧的整树展开——目录多时树又长又难扫,
|
||||
* 改成「进一层看一层、看中了点『移动到此』」的浏览式交互。
|
||||
* 数据仍走 FindDmsFolderData 一次拿全(文件夹 + 父子映射),本地按层取子级,下钻不打接口。
|
||||
* 规则对齐旧系统:根 = CreatedBy 等于 FolderLocalName 的那条;excludeName(被移动文件夹的
|
||||
* 系统名)连同子树整体不可进——不能把文件夹挪进自己怀里;sourcePath(条目现居目录)下
|
||||
* 「移动到此」置灰——原地移动无意义。
|
||||
* 组件不发移动请求:点「移动到此」把当前所在层 confirm 给页面层调 MoveDmsFolder。
|
||||
*/
|
||||
import type { PathNode } from "@/data/custom/fileBrowser";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import dmsFileService from "@/service/master/dmsFileService";
|
||||
|
||||
/** 一个目录层:系统名(提交用)+ 显示名 */
|
||||
export interface FolderRow {
|
||||
folderName: string;
|
||||
localName: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
/** 抽屉是否展开 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 剔除的目录系统名(被移动文件夹自身,其子树随之不可见) */
|
||||
excludeName: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 被移动条目现居目录的系统名:该层下「移动到此」置灰 */
|
||||
sourcePath: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 已选条目数(顶部「已选择 N 项」提示) */
|
||||
selectedCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
/** 点「移动到此」,带上当前所在目录 */
|
||||
(e: "confirm", row: FolderRow): void;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const loading = ref(false);
|
||||
/** 浏览路径栈:[0] 恒为根,末项 = 当前所在目录 */
|
||||
const trail = ref<FolderRow[]>([]);
|
||||
/** 全量索引:folderName → 实体 / 父子映射(每次重拉重建) */
|
||||
let byName = new Map<string, { FolderName: string; FolderLocalName: string; CreatedDatetime: Date | string }>();
|
||||
let maps: Array<{ FolderNameParent: string; FolderNameSon: string }> = [];
|
||||
|
||||
const current = computed<FolderRow | null>(() => trail.value[trail.value.length - 1] ?? null);
|
||||
|
||||
/** 当前层的子目录行(滤掉 excludeName 子树根,按显示名排序) */
|
||||
const rows = computed<FolderRow[]>(() => {
|
||||
if (!current.value) {
|
||||
return [];
|
||||
}
|
||||
return maps
|
||||
.filter(m => m.FolderNameParent === current.value!.folderName && m.FolderNameSon !== props.excludeName)
|
||||
.map(m => byName.get(m.FolderNameSon))
|
||||
.filter((f): f is NonNullable<typeof f> => !!f)
|
||||
.sort((a, b) => a.FolderLocalName.localeCompare(b.FolderLocalName, "zh-CN"))
|
||||
.map(f => ({ folderName: f.FolderName, localName: f.FolderLocalName }));
|
||||
});
|
||||
|
||||
/** 「移动到此」置灰:没有可提交层,或正站在条目现居目录 */
|
||||
const canMoveHere = computed(() => !!current.value && current.value.folderName !== props.sourcePath);
|
||||
|
||||
// 每次展开重拉并回到根:目录可能被别的操作改过,缓存会钻进了不存在的坑
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (on) => {
|
||||
if (on) {
|
||||
await load();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await dmsFileService.FindDmsFolderData();
|
||||
const folders = data?.DmsFolders ?? [];
|
||||
maps = data?.DmsFoldersMaps ?? [];
|
||||
byName = new Map(folders.map(f => [f.FolderName, f]));
|
||||
const root = folders.find(f => f.CreatedBy === f.FolderLocalName);
|
||||
trail.value = root ? [{ folderName: root.FolderName, localName: root.FolderLocalName }] : [];
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 目录创建时间展示(后端回 ISO 串/Date 均可能),取不到就不显示 */
|
||||
function fmtDate(folderName: string): string {
|
||||
const raw = byName.get(folderName)?.CreatedDatetime;
|
||||
const date = raw instanceof Date ? raw : new Date(String(raw ?? ""));
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return "";
|
||||
}
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
function onEnter(row: FolderRow): void {
|
||||
trail.value = [...trail.value, row];
|
||||
}
|
||||
|
||||
/** 头部面包屑(复用 file-breadcrumb):根段显示「我的网盘」,单击任意段回该层 */
|
||||
const crumbItems = computed<PathNode[]>(() => trail.value.map((row, index) => ({
|
||||
value: row.folderName,
|
||||
displayName: index === 0 ? t("FilePage.moveRoot") : row.localName,
|
||||
})));
|
||||
|
||||
/** 面包屑点某段:截断到该层(当前段组件内自行不响应) */
|
||||
function onJump(index: number): void {
|
||||
trail.value = trail.value.slice(0, index + 1);
|
||||
}
|
||||
|
||||
function onMoveHere(): void {
|
||||
if (current.value && canMoveHere.value) {
|
||||
emit("confirm", current.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<u-popup :model-value="props.visible" mode="bottom" round="24" @close="emit('close')">
|
||||
<view class="folder-move-picker">
|
||||
<!-- 头部:面包屑路径(复用 file-breadcrumb,单击回层、超长自动折叠)+ 关闭 -->
|
||||
<view class="folder-move-picker__head">
|
||||
<view class="folder-move-picker__crumb">
|
||||
<file-breadcrumb :items="crumbItems" :show-single="true" @nav="onJump" />
|
||||
</view>
|
||||
<view class="folder-move-picker__close" @tap="emit('close')">
|
||||
<u-icon name="close" size="36" color="#8f959e" />
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="props.selectedCount" class="folder-move-picker__picked">
|
||||
{{ t("FilePage.moveSelected").replace("{count}", String(props.selectedCount)) }}
|
||||
</text>
|
||||
<scroll-view scroll-y class="folder-move-picker__list">
|
||||
<view v-if="loading" class="folder-move-picker__empty">
|
||||
<u-loading mode="circle" />
|
||||
</view>
|
||||
<template v-else>
|
||||
<view v-if="!rows.length" class="folder-move-picker__empty">
|
||||
<text class="folder-move-picker__empty-text">
|
||||
{{ t("FilePage.moveNoSub") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="row in rows" :key="row.folderName" class="folder-move-picker__row" @tap="onEnter(row)">
|
||||
<!-- 图标库无 folder 字形,沿用项目 CSS 画文件夹惯例(同 file-action-sheet) -->
|
||||
<view class="folder-move-picker__folder" />
|
||||
<view class="folder-move-picker__row-main">
|
||||
<text class="folder-move-picker__row-text">
|
||||
{{ row.localName }}
|
||||
</text>
|
||||
<text v-if="fmtDate(row.folderName)" class="folder-move-picker__row-sub">
|
||||
{{ fmtDate(row.folderName) }}
|
||||
</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" size="26" color="#c0c4cc" />
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
<!-- 底部:通栏「移动到此」提交当前层(站在原地目录时置灰) -->
|
||||
<view class="folder-move-picker__footer">
|
||||
<view class="folder-move-picker__btn" :class="{ 'folder-move-picker__btn--muted': !canMoveHere }"
|
||||
@tap="onMoveHere">
|
||||
<text class="folder-move-picker__btn-text">
|
||||
{{ t("FilePage.moveHere") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="folder-move-picker__safe" />
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folder-move-picker {
|
||||
padding: 8rpx 32rpx 0;
|
||||
background-color: #ffffff;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 108rpx;
|
||||
}
|
||||
|
||||
/* 面包屑占满剩余宽度(自身可收缩,超长时组件内折叠出 …) */
|
||||
&__crumb {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__close {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 12rpx;
|
||||
}
|
||||
|
||||
&__picked {
|
||||
display: block;
|
||||
padding: 0 8rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
|
||||
&__list {
|
||||
max-height: 640rpx;
|
||||
min-height: 240rpx;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
&__empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__folder {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 56rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(180deg, #8ecbff, #4aa3ff);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
left: 0;
|
||||
width: 24rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 6rpx 6rpx 0 0;
|
||||
background: linear-gradient(180deg, #8ecbff, #4aa3ff);
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
&__row-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__row-text {
|
||||
overflow: hidden;
|
||||
font-size: 30rpx;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__row-sub {
|
||||
font-size: 22rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid #f0f1f3;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 76rpx;
|
||||
border-radius: 999rpx;
|
||||
background-color: #2b8fff;
|
||||
}
|
||||
|
||||
&__btn--muted {
|
||||
background-color: #d4d8de;
|
||||
}
|
||||
|
||||
&__btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&__safe {
|
||||
height: calc(24rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 新建文件夹命名对话框:居中卡片(标题 + 输入 + 取消/创建),
|
||||
* 只负责收集名称——非法字符校验、CreateDmsFolder 接口与结果提示由页面分流。
|
||||
* 命名对话框(新建文件夹 / 重命名共用):居中卡片(标题 + 输入 + 取消/确认)。
|
||||
* 只负责收集名称——非法字符校验、接口与结果提示由页面分流。
|
||||
* 新建态不传 title/confirmLabel 走默认「新建文件夹/创建」;重命名态由页面传入并预填 initialValue。
|
||||
* 提交成功后由父级收起 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,
|
||||
},
|
||||
/** 对话框是否展开 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 创建/重命名请求在途:置灰两个按钮 */
|
||||
submitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 标题文案,默认「新建文件夹」 */
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 确认按钮文案,默认「创建」 */
|
||||
confirmLabel: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 展开时预填的名称(重命名带当前名),空则清空输入 */
|
||||
initialValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 取消/遮罩关闭 */
|
||||
(e: "close"): void;
|
||||
/** 点了创建且名称非空 */
|
||||
(e: "confirm", name: string): void;
|
||||
/** 取消/遮罩关闭 */
|
||||
(e: "close"): void;
|
||||
/** 点了创建且名称非空 */
|
||||
(e: "confirm", name: string): void;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
@@ -34,109 +50,109 @@ const name = ref("");
|
||||
/** 空白名不算可提交 */
|
||||
const canSubmit = computed(() => name.value.trim().length > 0);
|
||||
|
||||
// 每次展开清掉上次的残留输入(对话框实例常驻 tab 页)
|
||||
// 每次展开按 initialValue 预填(重命名带当前名、新建为空),聚焦即全选方便改(对话框实例常驻 tab 页)
|
||||
watch(
|
||||
() => props.visible,
|
||||
(on) => {
|
||||
if (on) {
|
||||
name.value = "";
|
||||
}
|
||||
},
|
||||
() => props.visible,
|
||||
(on) => {
|
||||
if (on) {
|
||||
name.value = props.initialValue || "";
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function onConfirm(): void {
|
||||
if (!props.submitting && canSubmit.value) {
|
||||
emit("confirm", name.value.trim());
|
||||
}
|
||||
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>
|
||||
<!-- 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">
|
||||
{{ props.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>
|
||||
</u-popup>
|
||||
<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">
|
||||
{{ props.confirmLabel || 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;
|
||||
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;
|
||||
&__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;
|
||||
}
|
||||
|
||||
&__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;
|
||||
/* 名称为空或提交中:主按钮置灰(仍可点,由 onConfirm 拦截) */
|
||||
&--muted {
|
||||
background-color: #b9d6ff;
|
||||
}
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
&__btn-text {
|
||||
font-size: 28rpx;
|
||||
color: #5c626b;
|
||||
|
||||
&--primary {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 通用单选列表弹层(底部抽屉):标题 + 可滚动选项行 + 确认按钮。
|
||||
* 「共享家人」选目标群组目录、「发布」选专辑都吃它——数据由页面拉好喂进来,组件不发请求。
|
||||
* 行为:展开时重置为「未选中」(防上次残留误提交);点行仅选中,点确认才 emit confirm(key);
|
||||
* loading 显示转圈占位,items 空且不在 loading 时显示 emptyText。
|
||||
*/
|
||||
import { useLocale } from "uview-pro";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
export interface PickerOption {
|
||||
key: string;
|
||||
/** 主文案(如群组目录名 / 专辑名) */
|
||||
label: string;
|
||||
/** 次要说明(如共享路径 / 专辑描述),缺省不占行 */
|
||||
sub?: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
/** 弹层是否展开 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 候选项;key 必须非空且唯一 */
|
||||
items: {
|
||||
type: Array as () => PickerOption[],
|
||||
default: () => [],
|
||||
},
|
||||
/** 数据在途:列表区显示加载占位、确认按钮禁用 */
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 空列表文案 */
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 遮罩/取消关闭 */
|
||||
(e: "close"): void;
|
||||
/** 确认并带上选中项 key */
|
||||
(e: "confirm", key: string): void;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const picked = ref("");
|
||||
|
||||
// 每次展开清掉上次选择
|
||||
watch(
|
||||
() => props.visible,
|
||||
(on) => {
|
||||
if (on) {
|
||||
picked.value = "";
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function onConfirm(): void {
|
||||
if (picked.value && !props.loading) {
|
||||
emit("confirm", picked.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- u-popup 只认 modelValue(v-model);loading 中禁点遮罩防误关 -->
|
||||
<u-popup :model-value="props.visible" mode="bottom" round="24" :close-on-click-overlay="!props.loading"
|
||||
@close="emit('close')">
|
||||
<view class="option-picker">
|
||||
<text class="option-picker__title">
|
||||
{{ props.title }}
|
||||
</text>
|
||||
<scroll-view scroll-y class="option-picker__list">
|
||||
<view v-if="props.loading" class="option-picker__empty">
|
||||
<u-loading mode="circle" />
|
||||
</view>
|
||||
<template v-else>
|
||||
<view v-if="!props.items.length" class="option-picker__empty">
|
||||
<text class="option-picker__empty-text">
|
||||
{{ props.emptyText || t("FilePage.pickerEmpty") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="item in props.items" :key="item.key" class="option-picker__row"
|
||||
:class="{ 'option-picker__row--picked': picked === item.key }" @tap="picked = item.key">
|
||||
<view class="option-picker__row-main">
|
||||
<text class="option-picker__row-label">
|
||||
{{ item.label }}
|
||||
</text>
|
||||
<text v-if="item.sub" class="option-picker__row-sub">
|
||||
{{ item.sub }}
|
||||
</text>
|
||||
</view>
|
||||
<u-icon v-if="picked === item.key" name="checkmark" size="36" color="#2b8fff" />
|
||||
<view v-else class="option-picker__radio" />
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
<view class="option-picker__footer">
|
||||
<view class="option-picker__btn" @tap="emit('close')">
|
||||
<text class="option-picker__btn-text">
|
||||
{{ t("FilePage.folderCancel") }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="option-picker__btn option-picker__btn--primary"
|
||||
:class="{ 'option-picker__btn--muted': !picked || props.loading }" @tap="onConfirm">
|
||||
<text class="option-picker__btn-text option-picker__btn-text--primary">
|
||||
{{ t("FilePage.folderConfirm") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="option-picker__safe" />
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.option-picker {
|
||||
padding: 40rpx 32rpx 0;
|
||||
background-color: #ffffff;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
padding-bottom: 24rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__list {
|
||||
max-height: 560rpx;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
&__empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
|
||||
&--picked {
|
||||
background-color: #f0f6ff;
|
||||
}
|
||||
}
|
||||
|
||||
&__row-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
&__row-label {
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__row-sub {
|
||||
overflow: hidden;
|
||||
font-size: 22rpx;
|
||||
color: #8f959e;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 未选中态的空心圆点 */
|
||||
&__radio {
|
||||
flex-shrink: 0;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 3rpx solid #d4d7dd;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
|
||||
&__safe {
|
||||
height: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -37,6 +37,16 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
/** 传输列表入口开关:回收站等无上传场景的页面传 false */
|
||||
showTransfer: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/** 快捷入口区开关:无快捷入口的页面传 false(连带隐藏切换按钮) */
|
||||
showQuickToggle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -61,8 +71,8 @@ const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_
|
||||
{{ props.title }}
|
||||
</text>
|
||||
<view class="page-navbar__side page-navbar__side--right">
|
||||
<!-- 传输列表入口(常显):有进行中任务时点亮 + 数字角标 -->
|
||||
<view class="page-navbar__btn page-navbar__btn--transfer" @tap="emit('transfer')">
|
||||
<!-- 传输列表入口(常显):有进行中任务时点亮 + 数字角标;回收站页隐藏 -->
|
||||
<view v-if="props.showTransfer" 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 }}
|
||||
@@ -72,8 +82,8 @@ const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_
|
||||
<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')">
|
||||
<!-- 切换快捷入口区显隐;无快捷入口的页面隐藏 -->
|
||||
<view v-if="props.showQuickToggle" class="page-navbar__btn" @tap="emit('toggle-quick')">
|
||||
<u-icon name="grid" size="38" :color="props.quickOn ? '#2b8fff' : '#8f959e'" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
Reference in new issue
Block a user