ow
This commit is contained in:
1 parent
b858386e86
commit
3b2ee8523b
28 files changed
+2285
-1496
No files matched your search
@@ -3,7 +3,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { $u, useLocale } from "uview-pro";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { PAGE_PATHS, pageUrl, TAB_PAGES } from "@/router/routes";
|
||||
import { useTabbarStore } from "@/stores";
|
||||
|
||||
@@ -60,11 +60,32 @@ function indexOfCurrentRoute(): number {
|
||||
return TAB_PAGES.findIndex(name => PAGE_PATHS[name] === route);
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面栈就绪门闸:u-tabbar 的 onMounted 会直接读 getCurrentPages 栈顶的 .route,
|
||||
* 而 reLaunch 重建页面的瞬间栈顶 route 可能还没赋值(读到 undefined 抛
|
||||
* "Cannot read properties of undefined (reading 'route')")。等栈顶拿到 route 再挂载,
|
||||
* 规避这个组件内部时序(node_modules 不宜直接改)。
|
||||
*/
|
||||
const ready = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
const index = indexOfCurrentRoute();
|
||||
if (index >= 0) {
|
||||
store.setActiveIndex(index);
|
||||
}
|
||||
// 上限兜底:route 理论上必在若干 tick 内赋值;万一等不到也放行挂载,
|
||||
// 宁可让 u-tabbar 报一次可捕获的错误,也不能让 tabbar 永远不出现
|
||||
let attempts = 0;
|
||||
const settle = (): void => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages[pages.length - 1]?.route || ++attempts > 50) {
|
||||
ready.value = true;
|
||||
const index = indexOfCurrentRoute();
|
||||
if (index >= 0) {
|
||||
store.setActiveIndex(index);
|
||||
}
|
||||
}
|
||||
else {
|
||||
nextTick(settle);
|
||||
}
|
||||
};
|
||||
settle();
|
||||
});
|
||||
|
||||
/** item 未配 pagePath,u-tabbar 只会 emit change 而不自行 switchTab;跳转在这里做 */
|
||||
@@ -79,7 +100,8 @@ function onChange(index: number): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<u-tabbar v-bind="tabbarProps" @change="onChange" />
|
||||
<!-- ready 门闸:页面栈就绪后才挂载,避开 u-tabbar onMounted 空栈读 route 的崩溃窗口 -->
|
||||
<u-tabbar v-if="ready" v-bind="tabbarProps" @change="onChange" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -457,7 +457,12 @@ function close(): void {
|
||||
<text class="ftc__title">
|
||||
{{ props.title || t('FamilyTreePage.pedigreeChart') }}
|
||||
</text>
|
||||
<u-icon name="close" size="40" color="#8f959e" @click="close" />
|
||||
<!-- 退出钮统一为「取消」文字胶囊(底部抽屉风格语言,不再用 X) -->
|
||||
<view class="ftc__cancel" @tap="close">
|
||||
<text class="ftc__cancel-text">
|
||||
{{ t("FamilyTreePage.cancel") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 悬浮控制条:放大/缩小/百分比复位/横纵布局切换(对齐旧图 G6 工具钮) -->
|
||||
<view class="ftc__ctrl">
|
||||
@@ -513,6 +518,17 @@ function close(): void {
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.ftc__cancel {
|
||||
padding: 12rpx 40rpx;
|
||||
border-radius: 999rpx;
|
||||
background-color: #f2f3f5;
|
||||
}
|
||||
|
||||
.ftc__cancel-text {
|
||||
font-size: 26rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
|
||||
/* 悬浮控制条:右侧竖排,白底圆角轻阴影 */
|
||||
.ftc__ctrl {
|
||||
position: absolute;
|
||||
|
||||
@@ -10,11 +10,14 @@ import type { PropType } from "vue";
|
||||
import type { FileItem, FileKind } from "@/data/custom/fileBrowser";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed } from "vue";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
|
||||
interface ActionDef {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
/** 危险动作(删除):图标与文字标红 */
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
@@ -39,6 +42,9 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
// 文件夹图形随主题(与 file-grid-list / move / share picker 同一强调渐变)
|
||||
const { accentGradient, accentSoft } = useThemeTokens();
|
||||
|
||||
// 类型 -> 头部图标与配色,与 file-grid-list 的 kindVisual 同一套
|
||||
const kindVisual: Record<Exclude<FileKind, "folder">, { icon: string; color: string }> = {
|
||||
image: { icon: "photo-fill", color: "#0ea5e9" },
|
||||
@@ -60,7 +66,7 @@ const gridActions = computed<ActionDef[]>(() => {
|
||||
if (file.kind !== "folder") {
|
||||
actions.unshift({ key: "download", label: t("FilePage.actionDownload"), icon: "download" });
|
||||
}
|
||||
actions.push({ key: "delete", label: t("FilePage.actionDelete"), icon: "trash" });
|
||||
actions.push({ key: "delete", label: t("FilePage.actionDelete"), icon: "trash", danger: true });
|
||||
return actions;
|
||||
});
|
||||
|
||||
@@ -101,11 +107,11 @@ function onAction(action: ActionDef): void {
|
||||
<!-- u-popup 只认 modelValue(v-model),绑 :show 会静默弹不出 -->
|
||||
<u-popup :model-value="visible" mode="bottom" round="24" @close="emit('close')">
|
||||
<view class="file-action-sheet">
|
||||
<!-- 头部:类型图标 + 名称/元信息 + 详情 -->
|
||||
<!-- 头部白卡:类型图标 + 名称/元信息(「i」详情钮已移除,边界由取消/遮罩承担) -->
|
||||
<view class="file-action-sheet__head">
|
||||
<view v-if="file && file.kind === 'folder'" class="file-action-sheet__folder">
|
||||
<view class="file-action-sheet__folder-tab" />
|
||||
<view class="file-action-sheet__folder-body" />
|
||||
<view class="file-action-sheet__folder-tab" :style="{ background: accentSoft }" />
|
||||
<view class="file-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" />
|
||||
@@ -117,29 +123,37 @@ function onAction(action: ActionDef): void {
|
||||
{{ file && file.kind !== 'folder' ? `${file.sizeText} · ` : '' }}{{ file?.modifiedAt }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="file-action-sheet__info" @tap="onAction({ key: 'detail', label: '', icon: '' })">
|
||||
<u-icon name="info-circle" size="40" color="#8f959e" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 宫格动作(图1 第一排大按钮) -->
|
||||
<!-- 宫格动作:白卡上图标下文字,删除标红 -->
|
||||
<view class="file-action-sheet__grid">
|
||||
<view v-for="action in gridActions" :key="action.key" class="file-action-sheet__grid-btn"
|
||||
@tap="onAction(action)">
|
||||
<u-icon :name="action.icon" size="44" color="#1f2329" />
|
||||
<text class="file-action-sheet__grid-text">
|
||||
<u-icon :name="action.icon" size="44" :color="action.danger ? '#f0413e' : '#1f2329'" />
|
||||
<text class="file-action-sheet__grid-text"
|
||||
:style="action.danger ? { color: '#f0413e' } : undefined">
|
||||
{{ action.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 行式动作(图1 下方整行菜单) -->
|
||||
<!-- 行式动作:左图标 + 文字,右尖角 -->
|
||||
<view v-for="action in listActions" :key="action.key" class="file-action-sheet__row"
|
||||
@tap="onAction(action)">
|
||||
<text class="file-action-sheet__row-text">
|
||||
{{ action.label }}
|
||||
<view class="file-action-sheet__row-main">
|
||||
<u-icon :name="action.icon" size="38" color="#1f2329" />
|
||||
<text class="file-action-sheet__row-text">
|
||||
{{ action.label }}
|
||||
</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" size="26" color="#c0c4cc" />
|
||||
</view>
|
||||
|
||||
<!-- 取消通栏:替代原「i」的退出锚点 -->
|
||||
<view class="file-action-sheet__cancel" @tap="emit('close')">
|
||||
<text class="file-action-sheet__cancel-text">
|
||||
{{ t("FilePage.folderCancel") }}
|
||||
</text>
|
||||
<u-icon :name="action.icon" size="38" color="#1f2329" />
|
||||
</view>
|
||||
|
||||
<view class="file-action-sheet__safe" />
|
||||
@@ -148,15 +162,19 @@ function onAction(action: ActionDef): void {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格(对齐设计图):浅灰面板底 + 白色圆角卡分块 */
|
||||
.file-action-sheet {
|
||||
padding: 40rpx 32rpx 0;
|
||||
background-color: #ffffff;
|
||||
padding: 32rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 0 8rpx 32rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 文件夹造型与 file-grid-list 同源:主体 + 左上页签 */
|
||||
@@ -174,7 +192,6 @@ function onAction(action: ActionDef): void {
|
||||
width: 36rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 8rpx 8rpx 0 0;
|
||||
background-color: #6cb7ff;
|
||||
}
|
||||
|
||||
&__folder-body {
|
||||
@@ -184,7 +201,6 @@ function onAction(action: ActionDef): void {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(135deg, #7cc0ff, #3d8bfd);
|
||||
}
|
||||
|
||||
&__meta {
|
||||
@@ -209,24 +225,21 @@ function onAction(action: ActionDef): void {
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* 宫格钮:白卡、上图标下文字居中 */
|
||||
&__grid-btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 28rpx 0;
|
||||
border-radius: 20rpx;
|
||||
background-color: #f5f6f8;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__grid-text {
|
||||
@@ -239,10 +252,17 @@ function onAction(action: ActionDef): void {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #f5f6f8;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__row-text {
|
||||
@@ -250,6 +270,22 @@ function onAction(action: ActionDef): void {
|
||||
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));
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed } from "vue";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
|
||||
interface AddAction {
|
||||
key: string;
|
||||
@@ -29,6 +30,8 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
// 行图标随主题(与文件夹图形同一 primary 派生)
|
||||
const { primary } = useThemeTokens();
|
||||
|
||||
// computed:切换语言后文案跟随重算(tab 页组件实例常驻,setup 只跑一次)
|
||||
const actions = computed<AddAction[]>(() => [
|
||||
@@ -45,9 +48,18 @@ const actions = computed<AddAction[]>(() => [
|
||||
{{ 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 }}
|
||||
<view class="file-add-sheet__row-main">
|
||||
<u-icon :name="action.icon" size="40" :color="primary" />
|
||||
<text class="file-add-sheet__row-text">
|
||||
{{ action.label }}
|
||||
</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" size="26" color="#c0c4cc" />
|
||||
</view>
|
||||
<!-- 取消通栏(底部抽屉统一风格) -->
|
||||
<view class="file-add-sheet__cancel" @tap="emit('close')">
|
||||
<text class="file-add-sheet__cancel-text">
|
||||
{{ t("FilePage.folderCancel") }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="file-add-sheet__safe" />
|
||||
@@ -56,13 +68,14 @@ const actions = computed<AddAction[]>(() => [
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡行 */
|
||||
.file-add-sheet {
|
||||
padding: 40rpx 32rpx 0;
|
||||
background-color: #ffffff;
|
||||
padding: 32rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
padding: 0 8rpx 24rpx;
|
||||
padding: 8rpx 8rpx 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
@@ -70,11 +83,18 @@ const actions = computed<AddAction[]>(() => [
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #f5f6f8;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__row-text {
|
||||
@@ -82,6 +102,21 @@ const actions = computed<AddAction[]>(() => [
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 8rpx;
|
||||
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));
|
||||
|
||||
@@ -137,15 +137,16 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 面包屑:描边圈 + 浅浅的半透明白底(透出渐变底不发死白),分隔为细 chevron */
|
||||
/* 卡片表面统一风格:白底 + 细边框 + 柔和投影,与搜索框/分类条同一层级;分隔为细 chevron */
|
||||
.file-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 20rpx 28rpx;
|
||||
border: 2rpx solid rgba(31, 35, 41, 0.08);
|
||||
border: 2rpx solid rgba(31, 35, 41, 0.06);
|
||||
border-radius: 20rpx;
|
||||
background-color: rgba(255, 255, 255, 0.45);
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
*/
|
||||
import type { PropType } from "vue";
|
||||
import type { FileItem, FileKind } from "@/data/custom/fileBrowser";
|
||||
import { computed } from "vue";
|
||||
import { useThemeTokens } from "@/composables/useThemeGradient";
|
||||
|
||||
const props = defineProps({
|
||||
/** 已过滤排序好的条目,组件按原顺序渲染 */
|
||||
@@ -60,6 +62,21 @@ function isSelected(file: FileItem): boolean {
|
||||
return props.selectedIds.includes(file.id);
|
||||
}
|
||||
|
||||
// 主题强调色派生(useThemeGradient):文件夹图形/勾选/选中底统一跟随 primary,不再硬编码蓝
|
||||
const { primary, accentGradient, accentSoft, accentShadow } = useThemeTokens();
|
||||
|
||||
/** 文件夹主体:主题渐变 + 同色柔和投影 */
|
||||
const folderStyle = computed(() => ({
|
||||
background: accentGradient.value,
|
||||
boxShadow: `0 10rpx 20rpx ${accentShadow.value}`,
|
||||
}));
|
||||
|
||||
/** 文件夹左上页签:同渐变 */
|
||||
const folderTabStyle = computed(() => ({ background: accentGradient.value }));
|
||||
|
||||
/** 多选选中卡:主题浅色底 */
|
||||
const cardSelectedStyle = computed(() => ({ backgroundColor: accentSoft.value }));
|
||||
|
||||
function onCardTap(file: FileItem): void {
|
||||
if (props.selectMode) {
|
||||
emit("toggle-select", file.id);
|
||||
@@ -77,18 +94,19 @@ function onCardTap(file: FileItem): void {
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="file in files" :key="file.id" class="fgl-card"
|
||||
:class="{ 'fgl-card--selected': selectMode && isSelected(file) }">
|
||||
:class="{ 'fgl-card--selected': selectMode && isSelected(file) }"
|
||||
:style="selectMode && isSelected(file) ? cardSelectedStyle : undefined">
|
||||
<!-- 「更多」与多选勾选圈互斥:多选模式下只留 check 圆,不再同屏两个操作图标 -->
|
||||
<view v-if="!selectMode" class="fgl-card__more" @tap.stop="emit('more', file)">
|
||||
<u-icon name="more-dot-fill" size="30" color="#b8bcc4" />
|
||||
<u-icon class="fgl-card__more-icon" name="more-dot-fill" size="30" color="#b8bcc4" />
|
||||
</view>
|
||||
|
||||
<view class="fgl-card__body" @tap="onCardTap(file)">
|
||||
<!-- 图标区包装:共享域的权限/收藏角标叠加在图标右上角(旧 MyComputerFamily 同款) -->
|
||||
<view class="fgl-card__icon-wrap">
|
||||
<!-- 文件夹:纯 CSS 造型(图标库没有 folder),主体 + 左上页签 -->
|
||||
<view v-if="file.kind === 'folder'" class="fgl-card__folder">
|
||||
<view class="fgl-card__folder-tab" />
|
||||
<view v-if="file.kind === 'folder'" class="fgl-card__folder" :style="folderStyle">
|
||||
<view class="fgl-card__folder-tab" :style="folderTabStyle" />
|
||||
</view>
|
||||
<u-icon v-else :name="visualOf(file.kind)!.icon" size="88" :color="visualOf(file.kind)!.color" />
|
||||
<!-- 锁仅目录显示(旧系统:文件不挂锁):绿开锁=有权限,灰闭锁=无权限;
|
||||
@@ -119,7 +137,7 @@ function onCardTap(file: FileItem): void {
|
||||
|
||||
<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'" />
|
||||
:color="isSelected(file) ? primary : '#c0c4cc'" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -144,17 +162,21 @@ function onCardTap(file: FileItem): void {
|
||||
color: #9aa0a6;
|
||||
}
|
||||
|
||||
/* 卡片表面统一风格:白底 + 细边框 + 柔和投影(替代灰底 #f7f8fa 与页面底糊在一起的问题) */
|
||||
.fgl-card {
|
||||
position: relative;
|
||||
width: calc(100% / 3 - 16rpx);
|
||||
margin: 8rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border: 2rpx solid rgba(31, 35, 41, 0.06);
|
||||
border-radius: 20rpx;
|
||||
background-color: #f7f8fa;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background-color: #eaf4ff;
|
||||
}
|
||||
/* 横三点旋转 90° 成竖排(图标库无竖版 more),绕中心转不影响外层定位 */
|
||||
.fgl-card__more-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.fgl-card__more {
|
||||
@@ -195,15 +217,13 @@ function onCardTap(file: FileItem): void {
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
/* ---- 纯 CSS 文件夹:蓝色渐变圆角主体 + 左上页签 ---- */
|
||||
/* ---- 纯 CSS 文件夹:主题渐变圆角主体 + 左上页签(配色由 script 的 folderStyle/folderTabStyle 内联下发) ---- */
|
||||
.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 {
|
||||
@@ -213,7 +233,6 @@ function onCardTap(file: FileItem): void {
|
||||
width: 56rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 10rpx 10rpx 0 0;
|
||||
background: linear-gradient(135deg, #6ec2ff 0%, #4aa6ff 100%);
|
||||
}
|
||||
|
||||
.fgl-card__info {
|
||||
|
||||
@@ -92,15 +92,16 @@ const sortIconDark = sortGlyph("#1f2329");
|
||||
</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: 2rpx solid rgba(31, 35, 41, 0.06);
|
||||
border-radius: 24rpx;
|
||||
background-color: rgba(255, 255, 255, 0.45);
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
|
||||
}
|
||||
|
||||
.file-tab-filter__scroll {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* 拖动收尾置 suppressClick 吞掉随后的原生 click,防拖完误触。
|
||||
*/
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useThemeTokens } from "@/composables/useThemeGradient";
|
||||
|
||||
const props = defineProps({
|
||||
/** 初始底部偏移(rpx 数值,仅首次无存储记录时生效),供不同页面避开各自的底部元素 */
|
||||
@@ -72,6 +73,13 @@ const posStyle = computed(() => ({
|
||||
visibility: ready.value ? "visible" : "hidden",
|
||||
}));
|
||||
|
||||
/** 按钮面色跟随主题(与文件夹图形同派生,主题统一):渐变 + 同色柔光投影 */
|
||||
const { accentGradient, accentShadow } = useThemeTokens();
|
||||
const skinStyle = computed(() => ({
|
||||
background: accentGradient.value,
|
||||
boxShadow: `0 12rpx 32rpx ${accentShadow.value}`,
|
||||
}));
|
||||
|
||||
function onDragStart(x: number, y: number): void {
|
||||
dragging = true;
|
||||
moved = false;
|
||||
@@ -154,8 +162,8 @@ function onMouseDown(e: MouseEvent): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="float-add-btn" :style="posStyle" @touchstart="onTouchStart" @touchmove.stop.prevent="onTouchMove"
|
||||
@touchend="onTouchEnd" @mousedown="onMouseDown" @click="onTap">
|
||||
<view class="float-add-btn" :style="[posStyle, skinStyle]" @touchstart="onTouchStart"
|
||||
@touchmove.stop.prevent="onTouchMove" @touchend="onTouchEnd" @mousedown="onMouseDown" @click="onTap">
|
||||
<u-icon name="plus" size="52" color="#ffffff" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -172,8 +180,7 @@ function onMouseDown(e: MouseEvent): void {
|
||||
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);
|
||||
/* 面色与投影由 skinStyle 内联下发(主题派生),这里只留形态 */
|
||||
touch-action: none;
|
||||
/* 屏蔽浏览器默认滚动手势,touchmove 才不被吞 */
|
||||
user-select: none;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import type { PathNode } from "@/data/custom/fileBrowser";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
import dmsFileService from "@/service/master/dmsFileService";
|
||||
|
||||
/** 一个目录层:系统名(提交用)+ 显示名 */
|
||||
@@ -75,6 +76,11 @@ const rows = computed<FolderRow[]>(() => {
|
||||
/** 「移动到此」置灰:没有可提交层,或正站在条目现居目录 */
|
||||
const canMoveHere = computed(() => !!current.value && current.value.folderName !== props.sourcePath);
|
||||
|
||||
const { accentGradient, primary } = useThemeTokens();
|
||||
/** 文件夹图形用主题强调渐变(同 file-grid-list);提交钮用纯色(渐变浅档在白字下发糊),禁用态回落到置灰样式 */
|
||||
const folderStyle = computed(() => ({ "--fg-folder": accentGradient.value }));
|
||||
const submitStyle = computed(() => (canMoveHere.value ? { background: primary.value } : undefined));
|
||||
|
||||
// 每次展开重拉并回到根:目录可能被别的操作改过,缓存会钻进了不存在的坑
|
||||
watch(
|
||||
() => props.visible,
|
||||
@@ -136,47 +142,53 @@ function onMoveHere(): void {
|
||||
<template>
|
||||
<u-popup :model-value="props.visible" mode="bottom" round="24" @close="emit('close')">
|
||||
<view class="folder-move-picker">
|
||||
<!-- 头部:面包屑路径(复用 file-breadcrumb,单击回层、超长自动折叠)+ 关闭 -->
|
||||
<!-- 头部白卡:面包屑路径(复用 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 class="folder-move-picker__card">
|
||||
<scroll-view scroll-y class="folder-move-picker__list">
|
||||
<view v-if="loading" class="folder-move-picker__empty">
|
||||
<u-loading mode="circle" />
|
||||
</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) }}
|
||||
<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>
|
||||
<u-icon name="arrow-right" size="26" color="#c0c4cc" />
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
<!-- 底部:通栏「移动到此」提交当前层(站在原地目录时置灰) -->
|
||||
<view v-for="row in rows" :key="row.folderName" class="folder-move-picker__row"
|
||||
@tap="onEnter(row)">
|
||||
<!-- 图标库无 folder 字形,沿用项目 CSS 画文件夹惯例;颜色随主题(--fg-folder) -->
|
||||
<view class="folder-move-picker__folder" :style="folderStyle" />
|
||||
<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>
|
||||
<!-- 底部:取消 + 通栏「移动到此」提交当前层(站在原地目录时置灰) -->
|
||||
<view class="folder-move-picker__footer">
|
||||
<view class="folder-move-picker__btn folder-move-picker__btn--cancel" @tap="emit('close')">
|
||||
<text class="folder-move-picker__cancel-text">
|
||||
{{ t("FilePage.folderCancel") }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="folder-move-picker__btn" :class="{ 'folder-move-picker__btn--muted': !canMoveHere }"
|
||||
@tap="onMoveHere">
|
||||
:style="submitStyle" @tap="onMoveHere">
|
||||
<text class="folder-move-picker__btn-text">
|
||||
{{ t("FilePage.moveHere") }}
|
||||
</text>
|
||||
@@ -188,14 +200,18 @@ function onMoveHere(): void {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块 */
|
||||
.folder-move-picker {
|
||||
padding: 8rpx 32rpx 0;
|
||||
background-color: #ffffff;
|
||||
padding: 24rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 108rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 面包屑占满剩余宽度(自身可收缩,超长时组件内折叠出 …) */
|
||||
@@ -205,19 +221,48 @@ function onMoveHere(): void {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__close {
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 12rpx;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding-top: 28rpx;
|
||||
}
|
||||
|
||||
/* 底部按键统一规格(同 option-picker):等高圆角矩形,非胶囊 */
|
||||
&__btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
background-color: #2b8fff;
|
||||
|
||||
/* 取消:白底细描边灰字(底部抽屉统一语言) */
|
||||
&--cancel {
|
||||
border: 2rpx solid #e5e6eb;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&__cancel-text {
|
||||
font-size: 32rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
|
||||
&__picked {
|
||||
display: block;
|
||||
padding: 0 8rpx 16rpx;
|
||||
padding: 20rpx 8rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
|
||||
&__card {
|
||||
padding: 8rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__list {
|
||||
max-height: 640rpx;
|
||||
min-height: 240rpx;
|
||||
@@ -249,7 +294,7 @@ function onMoveHere(): void {
|
||||
width: 56rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(180deg, #8ecbff, #4aa3ff);
|
||||
background: var(--fg-folder, linear-gradient(180deg, #8ecbff, #4aa3ff));
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
@@ -258,7 +303,7 @@ function onMoveHere(): void {
|
||||
width: 24rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 6rpx 6rpx 0 0;
|
||||
background: linear-gradient(180deg, #8ecbff, #4aa3ff);
|
||||
background: var(--fg-folder, linear-gradient(180deg, #8ecbff, #4aa3ff));
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
@@ -284,29 +329,12 @@ function onMoveHere(): void {
|
||||
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-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
|
||||
const props = defineProps({
|
||||
/** 对话框是否展开 */
|
||||
@@ -49,6 +50,8 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
// 确认钮纯色跟主题(渐变浅档白字发糊)
|
||||
const { primary } = useThemeTokens();
|
||||
|
||||
const name = ref("");
|
||||
|
||||
@@ -90,7 +93,8 @@ function onConfirm(): void {
|
||||
</text>
|
||||
</view>
|
||||
<view class="folder-name-dialog__btn folder-name-dialog__btn--primary"
|
||||
:class="{ 'folder-name-dialog__btn--muted': !canSubmit || props.submitting }" @click="onConfirm">
|
||||
:class="{ 'folder-name-dialog__btn--muted': !canSubmit || props.submitting }" :style="{ background: primary }"
|
||||
@click="onConfirm">
|
||||
<text class="folder-name-dialog__btn-text folder-name-dialog__btn-text--primary">
|
||||
{{ props.confirmLabel || t("FilePage.folderConfirm") }}
|
||||
</text>
|
||||
@@ -133,30 +137,34 @@ function onConfirm(): void {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
/* 底部按键统一规格(同 option-picker):等高圆角矩形 + 细描边,非胶囊 */
|
||||
&__btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 76rpx;
|
||||
border-radius: 999rpx;
|
||||
background-color: #f5f6f8;
|
||||
height: 96rpx;
|
||||
border: 2rpx solid #e5e6eb;
|
||||
border-radius: 24rpx;
|
||||
background-color: #ffffff;
|
||||
|
||||
&--primary {
|
||||
border-color: transparent;
|
||||
background-color: #2b8fff;
|
||||
}
|
||||
|
||||
/* 名称为空或提交中:主按钮置灰(仍可点,由 onConfirm 拦截) */
|
||||
/* 名称为空或提交中:纯色 + 透明度置灰(色相不脱节,仍可点,由 onConfirm 拦截) */
|
||||
&--muted {
|
||||
background-color: #b9d6ff;
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-text {
|
||||
font-size: 28rpx;
|
||||
font-size: 32rpx;
|
||||
color: #5c626b;
|
||||
|
||||
&--primary {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import type { PathNode } from "@/data/custom/fileBrowser";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria";
|
||||
import { FamilyDmsFileViewDisplayData } from "@/data/auto/familyDmsFileViewDisplayData";
|
||||
import { SearchFilter } from "@/data/auto/searchFilter";
|
||||
@@ -19,34 +20,34 @@ import dmsFileService from "@/service/master/dmsFileService";
|
||||
|
||||
/** 共享目标层:组 + 顶层目录系统名(旧系统取消共享按它查权限)+ 当前目录标识 */
|
||||
export interface ShareTarget {
|
||||
familyName: string;
|
||||
topFolderName: string;
|
||||
filePath: string;
|
||||
familyName: string;
|
||||
topFolderName: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
/** 浏览路径的一段:value = 群组编号(组根)或目录系统名(子层) */
|
||||
interface CrumbRow {
|
||||
value: string;
|
||||
displayName: string;
|
||||
value: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
/** 抽屉是否展开 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 已选待共享文件数(顶部「已选择 N 项」提示) */
|
||||
selectedCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
/** 抽屉是否展开 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 已选待共享文件数(顶部「已选择 N 项」提示) */
|
||||
selectedCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
/** 点「共享到此」,带上当前所在层的目标三元组 */
|
||||
(e: "confirm", target: ShareTarget): void;
|
||||
(e: "close"): void;
|
||||
/** 点「共享到此」,带上当前所在层的目标三元组 */
|
||||
(e: "confirm", target: ShareTarget): void;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
@@ -66,327 +67,351 @@ const inGroup = computed(() => !!trail.value.length);
|
||||
/** 「共享到此」只在进入下一层目录后出现:群组名那一层(trail 只有组根一段)不允许直接共享 */
|
||||
const canShareHere = computed(() => trail.value.length >= 2);
|
||||
|
||||
const { accentGradient, primary } = useThemeTokens();
|
||||
/** 文件夹图形用主题强调渐变(同 file-grid-list);提交钮用纯色(渐变浅档在白字下发糊) */
|
||||
const folderStyle = computed(() => ({ "--fg-folder": accentGradient.value }));
|
||||
const submitStyle = computed(() => (canShareHere.value ? { background: primary.value } : undefined));
|
||||
|
||||
/** 「共享到此」的目标三元组:组根 TopFolderName = 群组编号,子层 = 路径首段目录 */
|
||||
const currentTarget = computed<ShareTarget | null>(() => {
|
||||
if (!inGroup.value) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
familyName: familyName.value,
|
||||
topFolderName: trail.value[0].value,
|
||||
filePath: trail.value[trail.value.length - 1].value,
|
||||
};
|
||||
if (!inGroup.value) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
familyName: familyName.value,
|
||||
topFolderName: trail.value[0].value,
|
||||
filePath: trail.value[trail.value.length - 1].value,
|
||||
};
|
||||
});
|
||||
|
||||
// 每次展开重拉群组并回大厅顶层:群组可能被别的端改过,缓存会弹进已消失的坑
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (on) => {
|
||||
if (on) {
|
||||
trail.value = [];
|
||||
familyName.value = "";
|
||||
await loadGroups();
|
||||
}
|
||||
},
|
||||
() => props.visible,
|
||||
async (on) => {
|
||||
if (on) {
|
||||
trail.value = [];
|
||||
familyName.value = "";
|
||||
await loadGroups();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function loadGroups(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const options = (await dmsFileService.FindFamilyDmsFolders()) ?? [];
|
||||
groups.value = options.map(opt => ({ value: opt.BindValue, label: opt.DisplayName || opt.BindValue }));
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const options = (await dmsFileService.FindFamilyDmsFolders()) ?? [];
|
||||
groups.value = options.map(opt => ({ value: opt.BindValue, label: opt.DisplayName || opt.BindValue }));
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查当前层子目录:两段过滤与共享大厅 loadFolder 同口径(组根标识=群组编号照挂) */
|
||||
async function loadDirs(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const criteria = new DmsFileSearchCriteria();
|
||||
const familyFilter = new SearchFilter();
|
||||
familyFilter.Column = FamilyDmsFileViewDisplayData.DB_FAMILY_NAME;
|
||||
familyFilter.Type = "SingleSelection";
|
||||
familyFilter.Operator = "single";
|
||||
familyFilter.Value = familyName.value;
|
||||
const pathFilter = new SearchFilter();
|
||||
pathFilter.Column = FamilyDmsFileViewDisplayData.DB_FILE_PATH;
|
||||
pathFilter.Type = "SingleSelection";
|
||||
pathFilter.Operator = "single";
|
||||
pathFilter.Value = trail.value[trail.value.length - 1].value;
|
||||
criteria.Filter = [familyFilter, pathFilter];
|
||||
criteria.limit = 999999;
|
||||
criteria.page = 1;
|
||||
criteria.GridColumns = [];
|
||||
const data = await dmsFileService.FindFamilyBySearchCriteria(criteria);
|
||||
dirs.value = (data?.Families ?? [])
|
||||
.filter(r => r.IsDirectoryIndc === "Y")
|
||||
.map(r => ({
|
||||
value: r.FileName,
|
||||
label: r.OrgFileName || r.FileName,
|
||||
permitted: r.PermissionIndc === "Y",
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN"));
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const criteria = new DmsFileSearchCriteria();
|
||||
const familyFilter = new SearchFilter();
|
||||
familyFilter.Column = FamilyDmsFileViewDisplayData.DB_FAMILY_NAME;
|
||||
familyFilter.Type = "SingleSelection";
|
||||
familyFilter.Operator = "single";
|
||||
familyFilter.Value = familyName.value;
|
||||
const pathFilter = new SearchFilter();
|
||||
pathFilter.Column = FamilyDmsFileViewDisplayData.DB_FILE_PATH;
|
||||
pathFilter.Type = "SingleSelection";
|
||||
pathFilter.Operator = "single";
|
||||
pathFilter.Value = trail.value[trail.value.length - 1].value;
|
||||
criteria.Filter = [familyFilter, pathFilter];
|
||||
criteria.limit = 999999;
|
||||
criteria.page = 1;
|
||||
criteria.GridColumns = [];
|
||||
const data = await dmsFileService.FindFamilyBySearchCriteria(criteria);
|
||||
dirs.value = (data?.Families ?? [])
|
||||
.filter(r => r.IsDirectoryIndc === "Y")
|
||||
.map(r => ({
|
||||
value: r.FileName,
|
||||
label: r.OrgFileName || r.FileName,
|
||||
permitted: r.PermissionIndc === "Y",
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN"));
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function enterGroup(group: { value: string; label: string }): void {
|
||||
familyName.value = group.value;
|
||||
trail.value = [{ value: group.value, displayName: group.label }];
|
||||
dirs.value = [];
|
||||
void loadDirs();
|
||||
familyName.value = group.value;
|
||||
trail.value = [{ value: group.value, displayName: group.label }];
|
||||
dirs.value = [];
|
||||
void loadDirs();
|
||||
}
|
||||
|
||||
/** 下钻子目录;无权限的目录进不去(与共享大厅 PermissionIndc 口径一致) */
|
||||
function enterDir(row: { value: string; label: string; permitted: boolean }): void {
|
||||
if (!row.permitted) {
|
||||
uni.showToast({ title: t("ShareHallPage.noPermission"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
trail.value = [...trail.value, { value: row.value, displayName: row.label }];
|
||||
void loadDirs();
|
||||
if (!row.permitted) {
|
||||
uni.showToast({ title: t("ShareHallPage.noPermission"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
trail.value = [...trail.value, { value: row.value, displayName: row.label }];
|
||||
void loadDirs();
|
||||
}
|
||||
|
||||
/** 组内面包屑:首段=群组名,单击回该层(当前段组件内自行不响应) */
|
||||
const crumbItems = computed<PathNode[]>(() => trail.value.map(row => ({
|
||||
value: row.value,
|
||||
displayName: row.displayName,
|
||||
value: row.value,
|
||||
displayName: row.displayName,
|
||||
})));
|
||||
|
||||
function onJump(index: number): void {
|
||||
trail.value = trail.value.slice(0, index + 1);
|
||||
void loadDirs();
|
||||
trail.value = trail.value.slice(0, index + 1);
|
||||
void loadDirs();
|
||||
}
|
||||
|
||||
function onShareHere(): void {
|
||||
const target = currentTarget.value;
|
||||
if (target) {
|
||||
emit("confirm", target);
|
||||
}
|
||||
const target = currentTarget.value;
|
||||
if (target) {
|
||||
emit("confirm", target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<u-popup :model-value="props.visible" mode="bottom" round="24" @close="emit('close')">
|
||||
<view class="folder-share-picker">
|
||||
<!-- 头部:大厅层显示标题,组内显示面包屑(单击回层、超长自动折叠)+ 关闭 -->
|
||||
<view class="folder-share-picker__head">
|
||||
<text v-if="!inGroup" class="folder-share-picker__title">
|
||||
{{ t("FilePage.shareTitle") }}
|
||||
</text>
|
||||
<view v-else class="folder-share-picker__crumb">
|
||||
<file-breadcrumb :items="crumbItems" :show-single="true" @nav="onJump" />
|
||||
</view>
|
||||
<view class="folder-share-picker__close" @tap="emit('close')">
|
||||
<u-icon name="close" size="36" color="#8f959e" />
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="props.selectedCount" class="folder-share-picker__picked">
|
||||
{{ t("FilePage.moveSelected").replace("{count}", String(props.selectedCount)) }}
|
||||
</text>
|
||||
<scroll-view scroll-y class="folder-share-picker__list">
|
||||
<view v-if="loading" class="folder-share-picker__empty">
|
||||
<u-loading mode="circle" />
|
||||
</view>
|
||||
<template v-else>
|
||||
<!-- 大厅顶层:群组卡片(共享大厅图2 同款:浅灰圆卡 + 蓝色文件夹) -->
|
||||
<template v-if="!inGroup">
|
||||
<view v-if="!groups.length" class="folder-share-picker__empty">
|
||||
<text class="folder-share-picker__empty-text">
|
||||
{{ t("FilePage.pickerEmpty") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="g in groups" :key="g.value" class="folder-share-picker__group"
|
||||
@tap="enterGroup(g)">
|
||||
<view class="folder-share-picker__folder" />
|
||||
<text class="folder-share-picker__group-text">
|
||||
{{ g.label }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 组内:子目录行(点行进下层,「共享到此」提交当前层) -->
|
||||
<template v-else>
|
||||
<view v-if="!dirs.length" class="folder-share-picker__empty">
|
||||
<text class="folder-share-picker__empty-text">
|
||||
{{ t("FilePage.moveNoSub") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="row in dirs" :key="row.value" class="folder-share-picker__row"
|
||||
@tap="enterDir(row)">
|
||||
<view class="folder-share-picker__folder" />
|
||||
<text class="folder-share-picker__row-text"
|
||||
:class="{ 'folder-share-picker__row-text--dim': !row.permitted }">
|
||||
{{ row.label }}
|
||||
</text>
|
||||
<u-icon v-if="!row.permitted" name="lock" size="26" color="#8c8c8c" />
|
||||
<u-icon v-else name="arrow-right" size="26" color="#c0c4cc" />
|
||||
</view>
|
||||
</template>
|
||||
</template>
|
||||
</scroll-view>
|
||||
<!-- 底部:通栏「共享到此」提交当前层(群组名那一层不显示——必须进到下一层目录才能共享) -->
|
||||
<view v-if="canShareHere" class="folder-share-picker__footer">
|
||||
<view class="folder-share-picker__btn" @tap="onShareHere">
|
||||
<text class="folder-share-picker__btn-text">
|
||||
{{ t("FilePage.shareHere") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="folder-share-picker__safe" />
|
||||
<u-popup :model-value="props.visible" mode="bottom" round="24" @close="emit('close')">
|
||||
<view class="folder-share-picker">
|
||||
<!-- 头部白卡:大厅层显示标题,组内显示面包屑(单击回层、超长自动折叠);退出走底部「取消」 -->
|
||||
<view class="folder-share-picker__head">
|
||||
<text v-if="!inGroup" class="folder-share-picker__title">
|
||||
{{ t("FilePage.shareTitle") }}
|
||||
</text>
|
||||
<view v-else class="folder-share-picker__crumb">
|
||||
<file-breadcrumb :items="crumbItems" :show-single="true" @nav="onJump" />
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
<text v-if="props.selectedCount" class="folder-share-picker__picked">
|
||||
{{ t("FilePage.moveSelected").replace("{count}", String(props.selectedCount)) }}
|
||||
</text>
|
||||
<!-- 列表白卡(底部抽屉统一风格) -->
|
||||
<view class="folder-share-picker__card">
|
||||
<scroll-view scroll-y class="folder-share-picker__list">
|
||||
<view v-if="loading" class="folder-share-picker__empty">
|
||||
<u-loading mode="circle" />
|
||||
</view>
|
||||
<template v-else>
|
||||
<!-- 大厅顶层:群组卡片(共享大厅图2 同款:浅灰圆卡 + 蓝色文件夹) -->
|
||||
<template v-if="!inGroup">
|
||||
<view v-if="!groups.length" class="folder-share-picker__empty">
|
||||
<text class="folder-share-picker__empty-text">
|
||||
{{ t("FilePage.pickerEmpty") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="g in groups" :key="g.value" class="folder-share-picker__group" @tap="enterGroup(g)">
|
||||
<view class="folder-share-picker__folder" :style="folderStyle" />
|
||||
<text class="folder-share-picker__group-text">
|
||||
{{ g.label }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 组内:子目录行(点行进下层,「共享到此」提交当前层) -->
|
||||
<template v-else>
|
||||
<view v-if="!dirs.length" class="folder-share-picker__empty">
|
||||
<text class="folder-share-picker__empty-text">
|
||||
{{ t("FilePage.moveNoSub") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="row in dirs" :key="row.value" class="folder-share-picker__row" @tap="enterDir(row)">
|
||||
<view class="folder-share-picker__folder" :style="folderStyle" />
|
||||
<text class="folder-share-picker__row-text"
|
||||
:class="{ 'folder-share-picker__row-text--dim': !row.permitted }">
|
||||
{{ row.label }}
|
||||
</text>
|
||||
<u-icon v-if="!row.permitted" name="lock" size="26" color="#8c8c8c" />
|
||||
<u-icon v-else name="arrow-right" size="26" color="#c0c4cc" />
|
||||
</view>
|
||||
</template>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 底部:取消常驻;「共享到此」只在可提交层出现(群组名那一层必须进到下一层目录才能共享) -->
|
||||
<view class="folder-share-picker__footer">
|
||||
<view class="folder-share-picker__btn folder-share-picker__btn--cancel" @tap="emit('close')">
|
||||
<text class="folder-share-picker__cancel-text">
|
||||
{{ t("FilePage.folderCancel") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="canShareHere" class="folder-share-picker__btn" :style="submitStyle" @tap="onShareHere">
|
||||
<text class="folder-share-picker__btn-text">
|
||||
{{ t("FilePage.shareHere") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="folder-share-picker__safe" />
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块 */
|
||||
.folder-share-picker {
|
||||
padding: 8rpx 32rpx 0;
|
||||
padding: 24rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 108rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 108rpx;
|
||||
&__title {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 面包屑占满剩余宽度(自身可收缩,超长时组件内折叠出 …) */
|
||||
&__crumb {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__picked {
|
||||
display: block;
|
||||
padding: 20rpx 8rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
|
||||
&__card {
|
||||
padding: 8rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
|
||||
/* 群组卡片:共享大厅顶层同款浅灰圆底行 */
|
||||
&__group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
&__group-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__row-text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 30rpx;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&--dim {
|
||||
color: #9aa0a6;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
text-align: center;
|
||||
/* CSS 画文件夹(图标库无 folder 字形,同 folder-move-picker);颜色随主题 --fg-folder */
|
||||
&__folder {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 56rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 10rpx;
|
||||
background: var(--fg-folder, linear-gradient(180deg, #8ecbff, #4aa3ff));
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
left: 0;
|
||||
width: 24rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 6rpx 6rpx 0 0;
|
||||
background: var(--fg-folder, linear-gradient(180deg, #8ecbff, #4aa3ff));
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
/* 面包屑占满剩余宽度(自身可收缩,超长时组件内折叠出 …) */
|
||||
&__crumb {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding-top: 28rpx;
|
||||
}
|
||||
|
||||
/* 底部按键统一规格(同 option-picker):等高圆角矩形,非胶囊 */
|
||||
&__btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
background-color: #2b8fff;
|
||||
|
||||
/* 取消:白底细描边灰字(底部抽屉统一语言) */
|
||||
&--cancel {
|
||||
border: 2rpx solid #e5e6eb;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 12rpx;
|
||||
}
|
||||
&__cancel-text {
|
||||
font-size: 32rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
|
||||
&__picked {
|
||||
display: block;
|
||||
padding: 0 8rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
&__btn-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
|
||||
/* 群组卡片:共享大厅顶层同款浅灰圆底行 */
|
||||
&__group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
&__group-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__row-text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 30rpx;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&--dim {
|
||||
color: #9aa0a6;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS 画文件夹(图标库无 folder 字形,同 folder-move-picker / 图2 蓝色系) */
|
||||
&__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: "";
|
||||
}
|
||||
}
|
||||
|
||||
&__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-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&__safe {
|
||||
height: calc(24rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
&__safe {
|
||||
height: calc(24rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -126,8 +126,7 @@ const hasContent = computed(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<u-popup :model-value="visible" mode="bottom" round="24" :closeable="true" close-icon-pos="top-right"
|
||||
:safe-area-inset-bottom="true" @close="emit('close')">
|
||||
<u-popup :model-value="visible" mode="bottom" round="24" :safe-area-inset-bottom="true" @close="emit('close')">
|
||||
<view class="help-preview">
|
||||
<scroll-view class="hp-body" scroll-y :show-scrollbar="false">
|
||||
<!-- 头部:圆形图标徽标 + 标题 -->
|
||||
@@ -223,24 +222,50 @@ const hasContent = computed(
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<!-- 取消通栏(底部抽屉统一风格:不再用右上角 X) -->
|
||||
<view class="hp-cancel" @tap="emit('close')">
|
||||
<text class="hp-cancel-text">
|
||||
{{ t("HelpPreview.cancel") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格:浅灰面板底,文档内容整体放白色圆角卡 */
|
||||
.help-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 82vh;
|
||||
padding: 8rpx 32rpx 24rpx;
|
||||
background-color: #ffffff;
|
||||
padding: 24rpx;
|
||||
background-color: #f2f3f5;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.hp-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: calc(82vh - 32rpx);
|
||||
max-height: calc(82vh - 176rpx);
|
||||
padding: 8rpx 28rpx 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
/* 取消通栏:白卡灰字,与文档卡同语言 */
|
||||
.hp-cancel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 24rpx;
|
||||
padding: 26rpx 0;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.hp-cancel-text {
|
||||
font-size: 28rpx;
|
||||
color: #5c626b;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 沉浸式主题头部(传输列表页同款风格):柔光主题渐变底 + 白胶囊返回 + 居中主题色标题。
|
||||
* 所有「进入后返回」的子页页头统一用它:回收站 / 共享大厅 / 相册详情 / 新建专辑 / 传输列表。
|
||||
* 标题真居中靠两侧等宽胶囊(返回 64rpx + gap 16rpx):右侧无搜索时放透明占位。
|
||||
* 渐变与 transport 页一致:primary 混白 35% 起、混白 10% 止(浓色带色觉冲击大的教训)。
|
||||
*/
|
||||
import { computed } from "vue";
|
||||
import { mixWithWhite, useThemeTokens } from "@/composables";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 右侧搜索胶囊(与返回同宽同形,保持标题居中) */
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 搜索展开态:图标高亮为主题色 */
|
||||
searchOn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "back"): void;
|
||||
(e: "toggle-search"): void;
|
||||
}>();
|
||||
|
||||
const { primary } = useThemeTokens();
|
||||
|
||||
const heroStyle = computed(() => ({
|
||||
background: `linear-gradient(180deg, ${mixWithWhite(primary.value, 0.35)} 0%, ${mixWithWhite(primary.value, 0.1)} 100%)`,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="hero-navbar" :style="heroStyle">
|
||||
<view class="hero-navbar__status-bar" />
|
||||
<view class="hero-navbar__nav">
|
||||
<view class="hero-navbar__pill" @tap="emit('back')">
|
||||
<u-icon name="arrow-left" size="32" :color="primary" />
|
||||
</view>
|
||||
<text class="hero-navbar__title" :style="{ color: primary }">
|
||||
{{ props.title }}
|
||||
</text>
|
||||
<!-- 右侧:搜索胶囊或等宽透明占位(占位保证标题仍几何居中) -->
|
||||
<view v-if="props.showSearch" class="hero-navbar__pill" @tap="emit('toggle-search')">
|
||||
<u-icon name="search" size="30" :color="props.searchOn ? primary : '#5c626b'" />
|
||||
</view>
|
||||
<view v-else class="hero-navbar__pill hero-navbar__pill--ghost" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 沉浸式头部:状态栏占位 + 导航行(传输列表页同款几何) */
|
||||
.hero-navbar {
|
||||
padding-bottom: 8rpx;
|
||||
|
||||
&__status-bar {
|
||||
height: var(--status-bar-height);
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
/* 白柔光胶囊钮(浅底上用白底 + 主题色图标) */
|
||||
&__pill {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
|
||||
&--ghost {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,8 @@
|
||||
* loading 显示转圈占位,items 空且不在 loading 时显示 emptyText。
|
||||
*/
|
||||
import { useLocale } from "uview-pro";
|
||||
import { ref, watch } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
|
||||
export interface PickerOption {
|
||||
key: string;
|
||||
@@ -14,6 +15,8 @@ export interface PickerOption {
|
||||
label: string;
|
||||
/** 次要说明(如共享路径 / 专辑描述),缺省不占行 */
|
||||
sub?: string;
|
||||
/** 行首图标(如相册=photo),缺省不占位 */
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
@@ -41,6 +44,16 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 确认键文案:发布场景传「发布相册」等,缺省仍用「创建」 */
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/** 附加「新建」行文案(如 新建相册):非空即渲染,点按 emit create */
|
||||
createText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -48,12 +61,22 @@ const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
/** 确认并带上选中项 key */
|
||||
(e: "confirm", key: string): void;
|
||||
/** 点了附加的「新建」行 */
|
||||
(e: "create"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
// 勾选色与确认钮随主题;确认钮用纯色(渐变浅档在白字下发糊),未选择/提交中用透明度做置灰态
|
||||
const { primary } = useThemeTokens();
|
||||
|
||||
const picked = ref("");
|
||||
|
||||
const confirmStyle = computed(() => ({
|
||||
background: primary.value,
|
||||
opacity: picked.value && !props.loading ? 1 : 0.45,
|
||||
}));
|
||||
|
||||
// 每次展开清掉上次选择
|
||||
watch(
|
||||
() => props.visible,
|
||||
@@ -79,41 +102,52 @@ function onConfirm(): void {
|
||||
<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 class="option-picker__card">
|
||||
<scroll-view scroll-y class="option-picker__list">
|
||||
<view v-if="props.loading" class="option-picker__empty">
|
||||
<u-loading mode="circle" />
|
||||
</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 }}
|
||||
<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>
|
||||
<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 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">
|
||||
<u-icon v-if="item.icon" :name="item.icon" size="40" :color="primary" />
|
||||
<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-circle-fill" size="40" :color="primary" />
|
||||
<view v-else class="option-picker__radio" />
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 附加「新建」行(如 新建相册):点按跳创建页 -->
|
||||
<view v-if="props.createText" class="option-picker__create" @tap="emit('create')">
|
||||
<u-icon name="plus-circle-fill" size="40" :color="primary" />
|
||||
<text class="option-picker__create-text">
|
||||
{{ props.createText }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="26" color="#c0c4cc" />
|
||||
</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">
|
||||
<view class="option-picker__btn option-picker__btn--primary" :style="confirmStyle" @tap="onConfirm">
|
||||
<text class="option-picker__btn-text option-picker__btn-text--primary">
|
||||
{{ t("FilePage.folderConfirm") }}
|
||||
{{ props.confirmText || t("FilePage.folderConfirm") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -123,17 +157,26 @@ function onConfirm(): void {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块 */
|
||||
.option-picker {
|
||||
padding: 40rpx 32rpx 0;
|
||||
background-color: #ffffff;
|
||||
padding: 32rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
padding-bottom: 24rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 8rpx 28rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
text-align: center;
|
||||
border-bottom: 2rpx solid #e8eaed;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__card {
|
||||
padding: 8rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__list {
|
||||
@@ -156,6 +199,7 @@ function onConfirm(): void {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
|
||||
@@ -170,7 +214,6 @@ function onConfirm(): void {
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
&__row-label {
|
||||
@@ -198,36 +241,52 @@ function onConfirm(): void {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 附加「新建」行:与列表卡同款白卡(图标 + 文案 + 右尖角) */
|
||||
&__create {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__create-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
&__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;
|
||||
height: 96rpx;
|
||||
border: 2rpx solid #e5e6eb;
|
||||
border-radius: 24rpx;
|
||||
background-color: #ffffff;
|
||||
|
||||
&--primary {
|
||||
border-color: transparent;
|
||||
background-color: #2b8fff;
|
||||
}
|
||||
|
||||
/* 未选择或提交中:主按钮置灰(仍可点,由 onConfirm 拦截) */
|
||||
&--muted {
|
||||
background-color: #b9d6ff;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-text {
|
||||
font-size: 28rpx;
|
||||
font-size: 32rpx;
|
||||
color: #5c626b;
|
||||
|
||||
&--primary {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 文件页顶部导航栏:「返回箭头 + 居中标题 + 传输入口 + 右侧两个显隐开关」。
|
||||
* 根目录不显示返回(showBack=false);标题两侧等宽占位保证真居中。
|
||||
* 文件页顶部导航栏:「返回箭头 + 左对齐标题 …… 传输入口 + 右侧两个显隐开关」。
|
||||
* 根目录不显示返回(showBack=false);标题左对齐、操作图标右对齐,稳定的两端对齐结构
|
||||
* (避免居中标题与右侧图标在图标增减时产生的不对称视觉负担)。
|
||||
* 右侧 search/grid 两个图标分别切换页面里搜索框与快捷入口区的显隐,
|
||||
* 点亮(蓝)= 对应区域显示,置灰 = 隐藏;排序按钮在 file-tab-filter 工具区。
|
||||
* 传输图标是跳转入口(带进行中计数角标),不参与显隐切换;字形用自绘 SVG(transferGlyph)。
|
||||
@@ -67,10 +68,8 @@ const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_
|
||||
|
||||
<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 v-if="props.showBack" class="page-navbar__back" @tap="emit('back')">
|
||||
<u-icon name="arrow-left" size="42" color="#1f2329" />
|
||||
</view>
|
||||
<text class="page-navbar__title">
|
||||
{{ props.title }}
|
||||
@@ -107,20 +106,15 @@ const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 左右等宽占位(容纳右侧三个按钮):任何状态下标题都严格居中 */
|
||||
/* 右侧操作图标组:宽度自适应内容、不参与标题伸缩,标题始终左对齐 */
|
||||
.page-navbar__side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
width: 208rpx;
|
||||
min-height: 64rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-navbar__side--left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.page-navbar__side--right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -169,7 +163,8 @@ const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_
|
||||
|
||||
.page-navbar__title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
font-size: 38rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayData";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
import { usePolicyOptions } from "@/composables/usePolicyOptions";
|
||||
import { FamilyPersonData } from "@/data/auto/familyPersonData";
|
||||
import { FamilyPersonDisplayData as FamilyPersonDisplayDataCls } from "@/data/auto/familyPersonDisplayData";
|
||||
@@ -27,6 +28,8 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
// 确认钮纯色跟主题(渐变浅档白字发糊)
|
||||
const { primary } = useThemeTokens();
|
||||
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
|
||||
|
||||
const genderItems = computed(() => getPolicyOptions("GenderPolicy"));
|
||||
@@ -104,31 +107,34 @@ async function onConfirm(): Promise<void> {
|
||||
{{ props.title || t('FamilyPersonPage.quickCreateTitle') }}
|
||||
</text>
|
||||
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.surname") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<u-input v-model="surname" :border="true" :maxlength="100"
|
||||
:placeholder="t('FamilyPersonPage.surnamePlaceholder')" />
|
||||
</view>
|
||||
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.genealogyName") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<u-input v-model="genealogyName" :border="true" :maxlength="100"
|
||||
:placeholder="t('FamilyPersonPage.genealogyNamePlaceholder')" />
|
||||
</view>
|
||||
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.gender") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<view class="pqc__picker" @tap="genderPickerVisible = true">
|
||||
<text :class="{ 'pqc__picker-empty': !genderLabel }">
|
||||
{{ genderLabel || t('FamilyPersonPage.genderPlaceholder') }}
|
||||
<!-- 表单白卡(底部抽屉统一风格:灰底 + 白色圆角分块) -->
|
||||
<view class="pqc__card">
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.surname") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="26" color="#c4c8cf" />
|
||||
<u-input v-model="surname" :border="true" :maxlength="100"
|
||||
:placeholder="t('FamilyPersonPage.surnamePlaceholder')" />
|
||||
</view>
|
||||
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.genealogyName") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<u-input v-model="genealogyName" :border="true" :maxlength="100"
|
||||
:placeholder="t('FamilyPersonPage.genealogyNamePlaceholder')" />
|
||||
</view>
|
||||
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.gender") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<view class="pqc__picker" @tap="genderPickerVisible = true">
|
||||
<text :class="{ 'pqc__picker-empty': !genderLabel }">
|
||||
{{ genderLabel || t('FamilyPersonPage.genderPlaceholder') }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="26" color="#c4c8cf" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -138,7 +144,8 @@ async function onConfirm(): Promise<void> {
|
||||
{{ t("AdminPage.cancelBut") }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="pqc__btn pqc__btn--primary" :class="{ 'pqc__btn--muted': saving }" @tap="onConfirm">
|
||||
<view class="pqc__btn pqc__btn--primary" :class="{ 'pqc__btn--muted': saving }"
|
||||
:style="{ background: primary }" @tap="onConfirm">
|
||||
<text class="pqc__btn-text pqc__btn-text--primary">
|
||||
{{ saving ? t("AdminPage.savingTip") : t("AdminPage.confirmBut") }}
|
||||
</text>
|
||||
@@ -154,9 +161,10 @@ async function onConfirm(): Promise<void> {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块 */
|
||||
.pqc {
|
||||
padding: 40rpx 32rpx 0;
|
||||
background-color: #ffffff;
|
||||
padding: 32rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
@@ -167,6 +175,12 @@ async function onConfirm(): Promise<void> {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__card {
|
||||
padding: 28rpx 28rpx 4rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__field {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
@@ -203,32 +217,37 @@ async function onConfirm(): Promise<void> {
|
||||
&__footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 8rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
/* 底部按键统一规格(同 option-picker):等高圆角矩形 + 细描边,非胶囊 */
|
||||
&__btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 76rpx;
|
||||
border-radius: 999rpx;
|
||||
background-color: #f5f6f8;
|
||||
height: 96rpx;
|
||||
border: 2rpx solid #e5e6eb;
|
||||
border-radius: 24rpx;
|
||||
background-color: #ffffff;
|
||||
|
||||
&--primary {
|
||||
border-color: transparent;
|
||||
background-color: #2b8fff;
|
||||
}
|
||||
|
||||
/* 提交中:纯色 + 透明度置灰(色相不脱节) */
|
||||
&--muted {
|
||||
background-color: #b9d6ff;
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-text {
|
||||
font-size: 28rpx;
|
||||
font-size: 32rpx;
|
||||
color: #5c626b;
|
||||
|
||||
&--primary {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ function onClear(): void {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 分区卡片风格:外层透明露渐变底,胶囊内部浅白色、留描边定形 */
|
||||
/* 卡片表面统一风格:白底 + 细边框 + 柔和投影(替代半透明白叠加的灰浊感),圆角矩形(对齐设计图,非全胶囊) */
|
||||
.search-input {
|
||||
padding: 20rpx 24rpx 0;
|
||||
}
|
||||
@@ -66,10 +66,11 @@ function onClear(): void {
|
||||
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);
|
||||
padding: 14rpx 28rpx;
|
||||
border: 2rpx solid rgba(31, 35, 41, 0.06);
|
||||
border-radius: 24rpx;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
|
||||
}
|
||||
|
||||
.search-input__field {
|
||||
|
||||
@@ -33,16 +33,65 @@ export function mixWithWhite(hex: string, ratio: number): string {
|
||||
return `rgb(${mix(channel(0))}, ${mix(channel(2))}, ${mix(channel(4))})`;
|
||||
}
|
||||
|
||||
/** 主题 primary 转 rgba()(光晕/投影等需要带透明度的场合) */
|
||||
export function rgbaOf(hex: string, alpha: number): string {
|
||||
const n = hex.replace("#", "");
|
||||
const channel = (start: number) =>
|
||||
Number.parseInt(n.slice(start, start + 2), 16);
|
||||
return `rgba(${channel(0)}, ${channel(2)}, ${channel(4)}, ${alpha})`;
|
||||
}
|
||||
|
||||
export function useThemeGradient(options: ThemeGradientOptions = {}) {
|
||||
const { top = 0.25, middle = 0.14, bottom = 0.05 } = options;
|
||||
// currentTheme 是包级单例 ref:任意页 setTheme,这里即时重算
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const primary = computed(
|
||||
() => currentTheme.value?.color?.primary ?? FALLBACK_PRIMARY,
|
||||
);
|
||||
|
||||
/** 页面底渐变(180deg 纵向),直接绑到内联 style 的 background */
|
||||
const themeBackground = computed(() => {
|
||||
const primary = currentTheme.value?.color?.primary ?? FALLBACK_PRIMARY;
|
||||
return `linear-gradient(180deg, ${mixWithWhite(primary, top)} 0%, ${mixWithWhite(primary, middle)} 40%, ${mixWithWhite(primary, bottom)} 100%)`;
|
||||
return `linear-gradient(180deg, ${mixWithWhite(primary.value, top)} 0%, ${mixWithWhite(primary.value, middle)} 40%, ${mixWithWhite(primary.value, bottom)} 100%)`;
|
||||
});
|
||||
|
||||
return { currentTheme, themeBackground };
|
||||
/**
|
||||
* 顶部光晕底(多层背景):主题色径向光晕从顶端洒下、自然过渡到近白内容区,
|
||||
* 替代旧三段线性渐变的「整页铺色」,视觉层级更清晰;随主题即时联动。
|
||||
*/
|
||||
const themeGlowBackground = computed(() => {
|
||||
const p = primary.value;
|
||||
return [
|
||||
`radial-gradient(140% 68% at 50% -12%, ${mixWithWhite(p, 0.34)} 0%, ${mixWithWhite(p, 0.14)} 46%, ${rgbaOf(p, 0)} 72%)`,
|
||||
`linear-gradient(180deg, ${mixWithWhite(p, 0.1)} 0%, ${mixWithWhite(p, 0.03)} 34%, #f6f7f9 62%)`,
|
||||
].join(", ");
|
||||
});
|
||||
|
||||
return { currentTheme, primary, themeBackground, themeGlowBackground };
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题强调色 token:把 primary 派生成渐变面/浅色底/透明投影三件套,
|
||||
* 供文件夹图形、悬浮「+」、卡片选中态等共用,保证「主题统一」而非各自硬编码蓝。
|
||||
* 用法:const { accentGradient, accentSoft, accentShadow } = useThemeTokens();
|
||||
*/
|
||||
export function useThemeTokens() {
|
||||
const { currentTheme } = useTheme();
|
||||
const primary = computed(
|
||||
() => currentTheme.value?.color?.primary ?? FALLBACK_PRIMARY,
|
||||
);
|
||||
|
||||
/** 主渐变面(135deg 浅→浓):文件夹主体、悬浮按钮等强调面 */
|
||||
const accentGradient = computed(
|
||||
() =>
|
||||
`linear-gradient(135deg, ${mixWithWhite(primary.value, 0.55)} 0%, ${primary.value} 100%)`,
|
||||
);
|
||||
|
||||
/** 强调色浅底:卡片选中态等弱强调面 */
|
||||
const accentSoft = computed(() => mixWithWhite(primary.value, 0.1));
|
||||
|
||||
/** 强调色透明投影:跟随强调色的柔和阴影 */
|
||||
const accentShadow = computed(() => rgbaOf(primary.value, 0.3));
|
||||
|
||||
return { currentTheme, primary, accentGradient, accentSoft, accentShadow };
|
||||
}
|
||||
@@ -84,6 +84,7 @@
|
||||
"personSearch": "Member Search",
|
||||
"pedigreeChart": "Pedigree Chart",
|
||||
"ancestryChart": "Ancestry Chart",
|
||||
"cancel": "Cancel",
|
||||
"chartEmpty": "No family tree data",
|
||||
"applyManage": "Apply for Manager",
|
||||
"requestSentTip": "Request submitted, waiting for approval",
|
||||
@@ -334,7 +335,8 @@
|
||||
"flow": "Operation Steps",
|
||||
"features": "Features & Icons",
|
||||
"tip": "Tip",
|
||||
"empty": "Nothing to preview yet. Fill in the introduction, steps, or reference a feature group first."
|
||||
"empty": "Nothing to preview yet. Fill in the introduction, steps, or reference a feature group first.",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"HelpFeaturePage": {
|
||||
"title": "Help Features",
|
||||
@@ -360,7 +362,7 @@
|
||||
},
|
||||
"FilePage": {
|
||||
"title": "Files",
|
||||
"searchPlaceholder": "Search",
|
||||
"searchPlaceholder": "Search files / folders",
|
||||
"album": "Family Album",
|
||||
"musicHall": "Music Hall",
|
||||
"videoHall": "Video Hall",
|
||||
@@ -378,6 +380,10 @@
|
||||
"selectedCount": "{count} selected",
|
||||
"selectAll": "Select All",
|
||||
"unselectAll": "Unselect All",
|
||||
"actionSelect": "Select",
|
||||
"visPrivate": "Private",
|
||||
"visGroup": "Group",
|
||||
"visPublic": "Public",
|
||||
"actionRename": "Rename",
|
||||
"actionMove": "Move",
|
||||
"actionDownload": "Download",
|
||||
@@ -427,6 +433,12 @@
|
||||
"publishPickPhoto": "Choose a photo album",
|
||||
"publishPickAudio": "Choose an audio album",
|
||||
"publishPickVideo": "Choose a video album",
|
||||
"publishConfirmPhoto": "Publish Album",
|
||||
"publishConfirmAudio": "Publish Music Album",
|
||||
"publishConfirmVideo": "Publish Video Album",
|
||||
"publishCreatePhoto": "New Album",
|
||||
"publishCreateAudio": "New Music Set",
|
||||
"publishCreateVideo": "New Video Set",
|
||||
"publishDone": "Published",
|
||||
"downloadNoFile": "No downloadable files",
|
||||
"downloadDone": "Downloaded",
|
||||
@@ -537,6 +549,7 @@
|
||||
"albumName": "Album Name",
|
||||
"albumDesc": "Album Description",
|
||||
"autoNo": "Generated on save",
|
||||
"autoNoHint": " (auto-generated, cannot be modified)",
|
||||
"publicLabel": "Public to everyone",
|
||||
"defaultLabel": "Set as default",
|
||||
"nameRequired": "Please enter an album name",
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"personSearch": "族员搜索",
|
||||
"pedigreeChart": "谱系图",
|
||||
"ancestryChart": "祖谱系图",
|
||||
"cancel": "取消",
|
||||
"chartEmpty": "暂无族谱数据",
|
||||
"applyManage": "申请管理",
|
||||
"requestSentTip": "申请已提交,等待管理员审批",
|
||||
@@ -334,7 +335,8 @@
|
||||
"flow": "操作流程",
|
||||
"features": "功能点与图标说明",
|
||||
"tip": "提示",
|
||||
"empty": "还没有可预览的帮助内容,请先填写功能说明、操作步骤或引用功能点组。"
|
||||
"empty": "还没有可预览的帮助内容,请先填写功能说明、操作步骤或引用功能点组。",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"HelpFeaturePage": {
|
||||
"title": "帮助功能点查询",
|
||||
@@ -360,7 +362,7 @@
|
||||
},
|
||||
"FilePage": {
|
||||
"title": "文件",
|
||||
"searchPlaceholder": "搜索",
|
||||
"searchPlaceholder": "搜索文件 / 文件夹",
|
||||
"album": "有家相册",
|
||||
"musicHall": "音乐厅",
|
||||
"videoHall": "影视厅",
|
||||
@@ -378,6 +380,10 @@
|
||||
"selectedCount": "已选 {count} 项",
|
||||
"selectAll": "全选",
|
||||
"unselectAll": "取消全选",
|
||||
"actionSelect": "多选",
|
||||
"visPrivate": "私有",
|
||||
"visGroup": "群组",
|
||||
"visPublic": "公开",
|
||||
"actionRename": "重命名",
|
||||
"actionMove": "移动",
|
||||
"actionDownload": "下载",
|
||||
@@ -427,6 +433,12 @@
|
||||
"publishPickPhoto": "选择相册",
|
||||
"publishPickAudio": "选择音乐集",
|
||||
"publishPickVideo": "选择影视集",
|
||||
"publishConfirmPhoto": "发布相册",
|
||||
"publishConfirmAudio": "发布音乐专辑",
|
||||
"publishConfirmVideo": "发布视频专辑",
|
||||
"publishCreatePhoto": "新建相册",
|
||||
"publishCreateAudio": "新建音乐集",
|
||||
"publishCreateVideo": "新建影视集",
|
||||
"publishDone": "发布成功",
|
||||
"downloadNoFile": "没有可下载的文件",
|
||||
"downloadDone": "下载完成",
|
||||
@@ -537,6 +549,7 @@
|
||||
"albumName": "专辑名称",
|
||||
"albumDesc": "专辑描述",
|
||||
"autoNo": "保存后由系统生成",
|
||||
"autoNoHint": "(系统自动生成,不可修改)",
|
||||
"publicLabel": "对所有人公开",
|
||||
"defaultLabel": "设为默认专辑",
|
||||
"nameRequired": "请输入专辑名称",
|
||||
|
||||
@@ -370,9 +370,8 @@ async function onCreateFolder(name: string): Promise<void> {
|
||||
<app-page :hide-nav="true">
|
||||
<view class="hall-page" :style="{ background: themeBackground }">
|
||||
<view class="hall-page__head">
|
||||
<!-- 导航:标题随层级变化;返回箭头常显(顶层退出回文件页,组内退层);搜索图标仅组内;传输/快捷入口开关关闭 -->
|
||||
<page-navbar :title="navTitle" :show-back="true" :search-on="showSearch" :show-transfer="false"
|
||||
:show-quick-toggle="false" :show-search-toggle="inGroup" @back="onBack"
|
||||
<!-- 导航:标题随层级变化;返回箭头常显(顶层退出回文件页,组内退层);搜索图标仅组内 -->
|
||||
<hero-navbar :title="navTitle" :show-search="inGroup" :search-on="showSearch" @back="onBack"
|
||||
@toggle-search="onToggleSearch" />
|
||||
<!-- 组内才有的路径条:两层以上显示(组根不显示,与文件页惯例一致) -->
|
||||
<view v-if="inGroup && pathStack.length > 1" class="hall-page__crumb">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* 底部导航「文件」页:按截图拆解为独立组件(components/ 下各自一个 vue 文件),
|
||||
* 页面只持有状态与数据,经 props 单向下发、事件(emit)上报,组件之间不互相引用:
|
||||
* page-navbar 顶部导航(居中标题;仅子目录态显示返回箭头)
|
||||
* page-navbar 顶部导航(左对齐标题 + 右对齐操作图标;仅子目录态显示返回箭头)
|
||||
* file-breadcrumb 目录面包屑(仅子目录态:左起排列,放不下折成 根+…+尾部段)
|
||||
* search-input 搜索框(v-model 即时过滤,确认键触发服务端搜索)
|
||||
* quick-entry 快捷功能入口(横向滚动图标组)
|
||||
@@ -32,6 +32,7 @@ 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 { useMediaIntentStore } from "@/stores/mediaIntent";
|
||||
import { useUploadQueueStore } from "@/stores/uploadQueue";
|
||||
import { chooseFiles } from "@/utils/chooseFiles";
|
||||
import { fetchFileBytes, saveFileBytes, toPlayableUrl } from "@/utils/dmsBytes";
|
||||
@@ -40,13 +41,16 @@ import { transferGlyph } from "@/utils/transferGlyph";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
// 页面底渐变跟随主题(派生逻辑在 composables/useThemeGradient,多页共用)
|
||||
const { themeBackground } = useThemeGradient();
|
||||
// 页面底跟随主题(派生逻辑在 composables/useThemeGradient,多页共用):
|
||||
// 顶部主题色光晕 → 自然过渡到近白内容区,替代整页铺色的灰浊感
|
||||
const { themeGlowBackground } = useThemeGradient();
|
||||
|
||||
const { files, records, loading, errorText, pathStack, atRoot, init, openTrail, retry, openFolder, back, goTo, search } = useDmsFiles();
|
||||
|
||||
// 上传队列(全局 store):入队在本页、胶囊读它的进行中计数、守卫回调消费它的面包屑导航请求
|
||||
const uploadQueue = useUploadQueueStore();
|
||||
// 跨 tab 意图通道:发布面板「新建相册」→ reLaunch 去媒体页自动进创建态
|
||||
const mediaIntent = useMediaIntentStore();
|
||||
|
||||
// 首次显示拉根目录,之后每次回到页面只刷新当前目录(重进 tab 不该丢位置重新拉全链);
|
||||
// 回页末尾消费传输页的面包屑导航请求(reLaunch 重建后恢复到目标目录)
|
||||
@@ -675,6 +679,41 @@ const publishTitle = computed(() => {
|
||||
return publishKind.value === "audio" ? t("FilePage.publishPickAudio") : t("FilePage.publishPickPhoto");
|
||||
});
|
||||
|
||||
/** 确认键按类型叫「发布相册/发布音乐专辑/发布视频专辑」,不再笼统叫「创建」 */
|
||||
const publishConfirmText = computed(() => {
|
||||
if (publishKind.value === "video") {
|
||||
return t("FilePage.publishConfirmVideo");
|
||||
}
|
||||
return publishKind.value === "audio" ? t("FilePage.publishConfirmAudio") : t("FilePage.publishConfirmPhoto");
|
||||
});
|
||||
|
||||
/** 附加「新建」行文案:与确认键同类型口径 */
|
||||
const publishCreateText = computed(() => {
|
||||
if (publishKind.value === "video") {
|
||||
return t("FilePage.publishCreateVideo");
|
||||
}
|
||||
return publishKind.value === "audio" ? t("FilePage.publishCreateAudio") : t("FilePage.publishCreatePhoto");
|
||||
});
|
||||
|
||||
/** 专辑行首图标:与媒体页分段图标同口径 */
|
||||
const publishRowIcon = computed(() =>
|
||||
publishKind.value === "video" ? "play-circle-fill" : publishKind.value === "audio" ? "volume" : "photo",
|
||||
);
|
||||
|
||||
/** 「新建相册」行:关弹层 → 登记创建意图 → 切到媒体 tab(onShow 消费后进创建态) */
|
||||
function goCreateAlbum(): void {
|
||||
publishVisible.value = false;
|
||||
const kindMap: Record<"image" | "video" | "audio", "photo" | "video" | "audio"> = {
|
||||
image: "photo",
|
||||
video: "video",
|
||||
audio: "audio",
|
||||
};
|
||||
mediaIntent.requestCreate(kindMap[publishKind.value]);
|
||||
// 项目无原生 tabBar(自定义 app-tabbar),switchTab 会报 can not switch to no-tabBar page,
|
||||
// 切 tab 与 app-tabbar.onChange 同套路用 reLaunch;意图由媒体页首显回调消费
|
||||
uni.reLaunch({ url: pageUrl("mediaTab") });
|
||||
}
|
||||
|
||||
async function actionPublish(targets: FileItem[]): Promise<void> {
|
||||
const picked = targets.filter(f => f.kind !== "folder");
|
||||
if (!picked.length) {
|
||||
@@ -735,7 +774,7 @@ async function actionPublish(targets: FileItem[]): Promise<void> {
|
||||
if (hasName) {
|
||||
parts.push(code);
|
||||
}
|
||||
return { key: code, label, sub: parts.join(" · ") };
|
||||
return { key: code, label, sub: parts.join(" · "), icon: publishRowIcon.value };
|
||||
});
|
||||
}
|
||||
catch (error: any) {
|
||||
@@ -886,7 +925,7 @@ const transferIconWhite = transferGlyph("#ffffff");
|
||||
弹层 z-index 再高也压不住它,直接隐藏最干净(主流云盘同款交互) -->
|
||||
<app-page :hide-nav="true"
|
||||
:show-tabbar="atRoot && !actionFile && !selectMode && !folderDialogVisible && !renameVisible && !moveVisible && !shareVisible && !publishVisible">
|
||||
<view class="file-page" :style="{ background: themeBackground }">
|
||||
<view class="file-page" :style="{ background: themeGlowBackground }">
|
||||
<!-- 上半屏整体固定,只有文件列表所在的 scroll-view 内部滚动(整页不滚) -->
|
||||
<view class="file-page__head">
|
||||
<page-navbar :title="navTitle" :show-back="!atRoot" :search-on="showSearch" :quick-on="showQuick"
|
||||
@@ -976,9 +1015,10 @@ const transferIconWhite = transferGlyph("#ffffff");
|
||||
<!-- 共享家人:共享大厅式抽屉(群组卡→逐级下钻→「共享到此」),多选文件一次批量共享 -->
|
||||
<folder-share-picker :visible="shareVisible" :selected-count="shareTargets.length" @close="shareVisible = false"
|
||||
@confirm="onShareConfirm" />
|
||||
<!-- 发布:选专辑 -->
|
||||
<!-- 发布:选专辑(确认键按类型叫「发布相册」等;附「新建相册」行跳媒体页创建态) -->
|
||||
<option-picker :visible="publishVisible" :title="publishTitle" :items="publishItems" :loading="publishLoading"
|
||||
:empty-text="t('FilePage.pickerEmpty')" @close="publishVisible = false" @confirm="onPublishConfirm" />
|
||||
:empty-text="t('FilePage.pickerEmpty')" :confirm-text="publishConfirmText" :create-text="publishCreateText"
|
||||
@close="publishVisible = false" @create="goCreateAlbum" @confirm="onPublishConfirm" />
|
||||
<!-- 多选底部操作栏:常驻非模态(无遮罩),勾选与动作可交替进行;
|
||||
动作集是旧系统多选子集——共享/发布按同媒体类型条件,重命名/移动仅单选 -->
|
||||
<view v-if="selectMode" class="file-page__actions">
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -12,11 +12,19 @@
|
||||
import type { FamilyGroupEntity } from "@/data/auto/familyGroupEntity";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { useAuthGuard, useFamilyGroups, useThemeGradient } from "@/composables";
|
||||
import { useAuthGuard, useFamilyGroups, useThemeGradient, useThemeTokens } from "@/composables";
|
||||
import { useLoginProfileStore } from "@/stores";
|
||||
|
||||
const { t } = useLocale();
|
||||
const { themeBackground } = useThemeGradient();
|
||||
// 主题派生色经 CSS 变量下发(与媒体页同套):头卡/徽章/主按钮/链接全部跟主题
|
||||
const { primary, accentGradient, accentSoft, accentShadow } = useThemeTokens();
|
||||
const themeVars = computed(() => ({
|
||||
"--fg-primary": primary.value,
|
||||
"--fg-gradient": accentGradient.value,
|
||||
"--fg-soft": accentSoft.value,
|
||||
"--fg-shadow": accentShadow.value,
|
||||
}));
|
||||
const profile = useLoginProfileStore();
|
||||
|
||||
const {
|
||||
@@ -74,6 +82,20 @@ const navTitle = computed(() => {
|
||||
return t("SharePage.title");
|
||||
});
|
||||
|
||||
/** 详情区分区卡折叠态(与媒体页同款交互):默认全展开,点标题行切换 */
|
||||
type FoldKey = "info" | "folders" | "requests" | "members" | "permissions";
|
||||
const folded = ref<Record<FoldKey, boolean>>({
|
||||
info: false,
|
||||
folders: false,
|
||||
requests: false,
|
||||
members: false,
|
||||
permissions: false,
|
||||
});
|
||||
|
||||
function toggleFold(key: FoldKey): void {
|
||||
folded.value[key] = !folded.value[key];
|
||||
}
|
||||
|
||||
/** 申请加入入口:非新建、非管理员、非成员,且群开启了「可申请」 */
|
||||
const canApply = computed(() =>
|
||||
!creating.value
|
||||
@@ -291,7 +313,7 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
<template>
|
||||
<!-- 列表态显示 tabbar,进详情(或弹层展开)收起 -->
|
||||
<app-page :hide-nav="true" :show-tabbar="!inDetail && !dialogVisible">
|
||||
<view class="share-page" :style="{ background: themeBackground }">
|
||||
<view class="share-page" :style="[{ background: themeBackground }, themeVars]">
|
||||
<!-- ===== 列表态:公开群 + 我的群 ===== -->
|
||||
<view v-if="!inDetail" class="share-page__body">
|
||||
<view class="share-page__head">
|
||||
@@ -365,42 +387,50 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
<!-- ===== 详情态:群管理 ===== -->
|
||||
<view v-else class="share-page__body">
|
||||
<view class="share-page__head">
|
||||
<page-navbar :title="navTitle" :show-back="true" :show-transfer="false" :show-quick-toggle="false"
|
||||
:show-search-toggle="false" @back="onBack" />
|
||||
<hero-navbar :title="navTitle" @back="onBack" />
|
||||
</view>
|
||||
<view class="share-page__scroll-wrap">
|
||||
<scroll-view scroll-y class="share-page__scroll">
|
||||
<!-- 群信息头卡 -->
|
||||
<!-- 群信息头卡(与媒体页同套):白底 + 主题浅底图标 + 编号灰字独立行 -->
|
||||
<view class="detail-hero">
|
||||
<view class="detail-hero__avatar">
|
||||
<u-icon name="account-fill" size="56" color="#ffffff" />
|
||||
<u-icon name="account-fill" size="64" :color="primary" />
|
||||
</view>
|
||||
<view class="detail-hero__main">
|
||||
<text class="detail-hero__name">
|
||||
{{ detail.FamilyGroup.FamilyLocalName || t("SharePage.unnamed") }}
|
||||
</text>
|
||||
<text class="detail-hero__code">
|
||||
{{ detail.FamilyGroup.FamilyName }}
|
||||
</text>
|
||||
<view class="detail-hero__chips">
|
||||
<text class="group-card__chip group-card__chip--code">
|
||||
{{ detail.FamilyGroup.FamilyName }}
|
||||
</text>
|
||||
<text v-if="detail.IsOpen" class="group-card__chip">
|
||||
<text v-if="detail.IsOpen" class="detail-hero__chip">
|
||||
{{ t("SharePage.chipPublic") }}
|
||||
</text>
|
||||
<text v-if="detail.IsAdd" class="group-card__chip">
|
||||
<text v-if="detail.IsAdd" class="detail-hero__chip">
|
||||
{{ t("SharePage.chipJoinable") }}
|
||||
</text>
|
||||
<text v-if="isManager" class="group-card__chip group-card__chip--admin">
|
||||
<text v-if="isManager" class="detail-hero__chip detail-hero__chip--admin">
|
||||
{{ t("SharePage.chipManager") }}
|
||||
</text>
|
||||
<text v-else-if="isMember" class="group-card__chip">
|
||||
<text v-else-if="isMember" class="detail-hero__chip detail-hero__chip--member">
|
||||
{{ t("SharePage.chipMember") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 修改 / 解散:右上角圆形图标动作(与媒体页同套) -->
|
||||
<view v-if="!isEdit && isManager" class="detail-hero__acts">
|
||||
<view class="detail-hero__act" @tap="onAmend">
|
||||
<u-icon name="edit-pen" size="34" :color="primary" />
|
||||
</view>
|
||||
<view class="detail-hero__act detail-hero__act--danger" @tap="onDissolve">
|
||||
<u-icon name="trash" size="34" color="#f0413e" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动作栏:按角色/态显示 -->
|
||||
<view class="detail-actions">
|
||||
<!-- 动作栏:编辑态取消/保存;非成员可申请加入(修改/解散已升为头卡图标钮) -->
|
||||
<view v-if="isEdit || canApply" class="detail-actions">
|
||||
<template v-if="isEdit">
|
||||
<view class="detail-actions__btn" @tap="onCancelEdit">
|
||||
<text>{{ t("SharePage.cancel") }}</text>
|
||||
@@ -409,178 +439,198 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
<text>{{ t("SharePage.save") }}</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view v-if="isManager" class="detail-actions__btn detail-actions__btn--primary"
|
||||
@tap="onAmend">
|
||||
<text>{{ t("SharePage.amend") }}</text>
|
||||
</view>
|
||||
<view v-if="canApply" class="detail-actions__btn detail-actions__btn--join"
|
||||
@tap="onApplyJoin">
|
||||
<text>{{ t("SharePage.applyJoin") }}</text>
|
||||
</view>
|
||||
<view v-if="isManager" class="detail-actions__btn detail-actions__btn--danger"
|
||||
@tap="onDissolve">
|
||||
<text>{{ t("SharePage.dissolve") }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 群组信息卡 -->
|
||||
<view class="detail-card">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.groupInfo") }}
|
||||
</text>
|
||||
<view class="detail-field">
|
||||
<text class="detail-field__label">
|
||||
{{ t("SharePage.groupName") }}
|
||||
</text>
|
||||
<input v-model="detail.FamilyGroup.FamilyLocalName" class="detail-field__input"
|
||||
:disabled="!isEdit" :maxlength="50" :placeholder="t('SharePage.nameRequired')">
|
||||
</view>
|
||||
<view class="detail-field">
|
||||
<text class="detail-field__label">
|
||||
{{ t("SharePage.groupDesc") }}
|
||||
</text>
|
||||
<textarea v-model="detail.FamilyGroup.Notice" class="detail-field__textarea"
|
||||
:disabled="!isEdit" :maxlength="256" :placeholder="t('SharePage.noDesc')" />
|
||||
</view>
|
||||
<view class="detail-field detail-field--row">
|
||||
<text class="detail-field__label">
|
||||
{{ t("SharePage.publicSetting") }}
|
||||
</text>
|
||||
<u-switch :model-value="detail.IsOpen" :disabled="!isEdit"
|
||||
@change="(v: boolean) => detail.IsOpen = v" />
|
||||
</view>
|
||||
<view class="detail-field detail-field--row">
|
||||
<text class="detail-field__label">
|
||||
{{ t("SharePage.joinApply") }}
|
||||
</text>
|
||||
<u-switch :model-value="detail.IsAdd" :disabled="!isEdit"
|
||||
@change="(v: boolean) => detail.IsAdd = v" />
|
||||
<view v-else-if="canApply" class="detail-actions__btn detail-actions__btn--join"
|
||||
@tap="onApplyJoin">
|
||||
<text>{{ t("SharePage.applyJoin") }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 共享目录卡 -->
|
||||
<!-- 群组信息卡:点标题行折叠/展开 -->
|
||||
<view class="detail-card">
|
||||
<view class="detail-card__head">
|
||||
<view class="detail-card__head" @tap="toggleFold('info')">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.groupInfo") }}
|
||||
</text>
|
||||
<u-icon :name="folded.info ? 'arrow-right' : 'arrow-down'" size="26" color="#8f959e" />
|
||||
</view>
|
||||
<view v-show="!folded.info">
|
||||
<view class="detail-field">
|
||||
<text class="detail-field__label">
|
||||
{{ t("SharePage.groupName") }}
|
||||
</text>
|
||||
<input v-model="detail.FamilyGroup.FamilyLocalName" class="detail-field__input"
|
||||
:disabled="!isEdit" :maxlength="50" :placeholder="t('SharePage.nameRequired')">
|
||||
</view>
|
||||
<view class="detail-field">
|
||||
<text class="detail-field__label">
|
||||
{{ t("SharePage.groupDesc") }}
|
||||
</text>
|
||||
<textarea v-model="detail.FamilyGroup.Notice" class="detail-field__textarea"
|
||||
:disabled="!isEdit" :maxlength="256" :placeholder="t('SharePage.noDesc')" />
|
||||
</view>
|
||||
<view class="detail-field detail-field--row">
|
||||
<text class="detail-field__label">
|
||||
{{ t("SharePage.publicSetting") }}
|
||||
</text>
|
||||
<u-switch :model-value="detail.IsOpen" :disabled="!isEdit" :active-color="primary"
|
||||
@change="(v: boolean) => detail.IsOpen = v" />
|
||||
</view>
|
||||
<view class="detail-field detail-field--row">
|
||||
<text class="detail-field__label">
|
||||
{{ t("SharePage.joinApply") }}
|
||||
</text>
|
||||
<u-switch :model-value="detail.IsAdd" :disabled="!isEdit" :active-color="primary"
|
||||
@change="(v: boolean) => detail.IsAdd = v" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 共享目录卡:点标题行折叠/展开,「新建」钮不触发折叠 -->
|
||||
<view class="detail-card">
|
||||
<view class="detail-card__head" @tap="toggleFold('folders')">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.folders") }}
|
||||
</text>
|
||||
<text v-if="isEdit" class="detail-card__add" @tap="onNewFolder">
|
||||
+ {{ t("SharePage.newFolder") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="!detail.FamilyGroupFolders.length" class="detail-empty">
|
||||
<text>{{ t("SharePage.noFolders") }}</text>
|
||||
</view>
|
||||
<view v-for="(folder, index) in detail.FamilyGroupFolders" :key="folder.FolderId || index"
|
||||
class="detail-item">
|
||||
<view class="detail-item__row">
|
||||
<text class="detail-item__index">
|
||||
{{ index + 1 }}
|
||||
<view class="detail-card__head-acts">
|
||||
<text v-if="isEdit" class="detail-card__add" @tap.stop="onNewFolder">
|
||||
+ {{ t("SharePage.newFolder") }}
|
||||
</text>
|
||||
<u-icon :name="folded.folders ? 'arrow-right' : 'arrow-down'" size="26"
|
||||
color="#8f959e" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-show="!folded.folders">
|
||||
<view v-if="!detail.FamilyGroupFolders.length" class="detail-empty">
|
||||
<text>{{ t("SharePage.noFolders") }}</text>
|
||||
</view>
|
||||
<view v-for="(folder, index) in detail.FamilyGroupFolders"
|
||||
:key="folder.FolderId || index" class="detail-item">
|
||||
<view class="detail-item__row">
|
||||
<text class="detail-item__index">
|
||||
{{ index + 1 }}
|
||||
</text>
|
||||
<input v-if="isEdit" v-model="folder.FolderLocalName" class="detail-item__input"
|
||||
:maxlength="50" :placeholder="t('SharePage.folderPh')">
|
||||
<text v-else class="detail-item__name">
|
||||
{{ folder.FolderLocalName || t("SharePage.unnamed") }}
|
||||
</text>
|
||||
<view class="detail-item__acts">
|
||||
<text v-if="!isEdit && folder.IsSharePermission" class="detail-item__link"
|
||||
@tap="onAssignPermission(folder.FolderName)">
|
||||
{{ t("SharePage.assign") }}
|
||||
</text>
|
||||
<text v-if="isEdit" class="detail-item__del" @tap="
|
||||
onRemoveFolder(folder.FolderId, folder.FolderName)
|
||||
">
|
||||
{{ t("SharePage.delete") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<input v-if="isEdit" v-model="folder.Notice" class="detail-item__sub"
|
||||
:maxlength="50" :placeholder="t('SharePage.notePh')">
|
||||
<text v-else-if="folder.Notice" class="detail-item__sub detail-item__sub--text">
|
||||
{{ folder.Notice }}
|
||||
</text>
|
||||
<input v-if="isEdit" v-model="folder.FolderLocalName" class="detail-item__input"
|
||||
:maxlength="50" :placeholder="t('SharePage.folderPh')">
|
||||
<text v-else class="detail-item__name">
|
||||
{{ folder.FolderLocalName || t("SharePage.unnamed") }}
|
||||
</text>
|
||||
<view class="detail-item__acts">
|
||||
<text v-if="!isEdit && folder.IsSharePermission" class="detail-item__link"
|
||||
@tap="onAssignPermission(folder.FolderName)">
|
||||
{{ t("SharePage.assign") }}
|
||||
</text>
|
||||
<text v-if="isEdit" class="detail-item__del" @tap="
|
||||
onRemoveFolder(folder.FolderId, folder.FolderName)
|
||||
">
|
||||
{{ t("SharePage.delete") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<input v-if="isEdit" v-model="folder.Notice" class="detail-item__sub" :maxlength="50"
|
||||
:placeholder="t('SharePage.notePh')">
|
||||
<text v-else-if="folder.Notice" class="detail-item__sub detail-item__sub--text">
|
||||
{{ folder.Notice }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待审成员(仅管理员)-->
|
||||
<!-- 待审成员(仅管理员):点标题行折叠/展开 -->
|
||||
<view v-if="isManager && detail.FamilyGroupRequests.length" class="detail-card">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.requests") }}
|
||||
</text>
|
||||
<view v-for="(req, index) in detail.FamilyGroupRequests" :key="req.UserName || index"
|
||||
class="detail-item">
|
||||
<view class="detail-item__row">
|
||||
<text class="detail-item__index">
|
||||
{{ index + 1 }}
|
||||
</text>
|
||||
<text class="detail-item__name">
|
||||
{{ req.UserLocalName || req.UserName }}
|
||||
</text>
|
||||
<view class="detail-item__acts">
|
||||
<text class="detail-item__link" @tap="onApprove(req.UserName)">
|
||||
{{ t("SharePage.approve") }}
|
||||
</text>
|
||||
<text class="detail-item__del" @tap="onReject(req.UserName)">
|
||||
{{ t("SharePage.reject") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="req.Notice" class="detail-item__sub detail-item__sub--text">
|
||||
{{ req.Notice }}
|
||||
<view class="detail-card__head" @tap="toggleFold('requests')">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.requests") }}
|
||||
</text>
|
||||
<u-icon :name="folded.requests ? 'arrow-right' : 'arrow-down'" size="26"
|
||||
color="#8f959e" />
|
||||
</view>
|
||||
<view v-show="!folded.requests">
|
||||
<view v-for="(req, index) in detail.FamilyGroupRequests" :key="req.UserName || index"
|
||||
class="detail-item">
|
||||
<view class="detail-item__row">
|
||||
<text class="detail-item__index">
|
||||
{{ index + 1 }}
|
||||
</text>
|
||||
<text class="detail-item__name">
|
||||
{{ req.UserLocalName || req.UserName }}
|
||||
</text>
|
||||
<view class="detail-item__acts">
|
||||
<text class="detail-item__link" @tap="onApprove(req.UserName)">
|
||||
{{ t("SharePage.approve") }}
|
||||
</text>
|
||||
<text class="detail-item__del" @tap="onReject(req.UserName)">
|
||||
{{ t("SharePage.reject") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="req.Notice" class="detail-item__sub detail-item__sub--text">
|
||||
{{ req.Notice }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 正式成员 -->
|
||||
<!-- 正式成员:点标题行折叠/展开 -->
|
||||
<view v-if="detail.FamilyGroupManagers.length" class="detail-card">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.members") }}
|
||||
</text>
|
||||
<view v-for="(mem, index) in detail.FamilyGroupManagers" :key="mem.UserName || index"
|
||||
class="detail-item">
|
||||
<view class="detail-item__row">
|
||||
<text class="detail-item__index">
|
||||
{{ index + 1 }}
|
||||
</text>
|
||||
<text class="detail-item__name">
|
||||
{{ mem.UserLocalName || mem.UserName }}
|
||||
</text>
|
||||
<view class="detail-item__acts">
|
||||
<u-switch v-if="isEdit" :model-value="mem.IsManager" size="40"
|
||||
:disabled="mem.UserName === detail.FamilyGroup.CreatedBy"
|
||||
@change="() => toggleManager(mem.UserName)" />
|
||||
<text v-else-if="mem.IsManager"
|
||||
class="group-card__chip group-card__chip--admin">
|
||||
{{ t("SharePage.chipManager") }}
|
||||
<view class="detail-card__head" @tap="toggleFold('members')">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.members") }}
|
||||
</text>
|
||||
<u-icon :name="folded.members ? 'arrow-right' : 'arrow-down'" size="26"
|
||||
color="#8f959e" />
|
||||
</view>
|
||||
<view v-show="!folded.members">
|
||||
<view v-for="(mem, index) in detail.FamilyGroupManagers" :key="mem.UserName || index"
|
||||
class="detail-item">
|
||||
<view class="detail-item__row">
|
||||
<text class="detail-item__index">
|
||||
{{ index + 1 }}
|
||||
</text>
|
||||
<text v-if="isEdit && mem.UserName !== detail.FamilyGroup.CreatedBy"
|
||||
class="detail-item__del" @tap="onRemoveMember(mem.UserName)">
|
||||
{{ t("SharePage.removeMember") }}
|
||||
<text class="detail-item__name">
|
||||
{{ mem.UserLocalName || mem.UserName }}
|
||||
</text>
|
||||
<view class="detail-item__acts">
|
||||
<u-switch v-if="isEdit" :model-value="mem.IsManager" size="40"
|
||||
:active-color="primary"
|
||||
:disabled="mem.UserName === detail.FamilyGroup.CreatedBy"
|
||||
@change="() => toggleManager(mem.UserName)" />
|
||||
<text v-else-if="mem.IsManager"
|
||||
class="detail-hero__chip detail-hero__chip--admin">
|
||||
{{ t("SharePage.chipManager") }}
|
||||
</text>
|
||||
<text v-if="isEdit && mem.UserName !== detail.FamilyGroup.CreatedBy"
|
||||
class="detail-item__del" @tap="onRemoveMember(mem.UserName)">
|
||||
{{ t("SharePage.removeMember") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 目录权限(仅管理员)-->
|
||||
<!-- 目录权限(仅管理员):点标题行折叠/展开 -->
|
||||
<view v-if="isManager && detail.FamilyGroupPermissions.length" class="detail-card">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.permissions") }}
|
||||
</text>
|
||||
<view v-for="(perm, index) in detail.FamilyGroupPermissions"
|
||||
:key="`${perm.FolderName}-${perm.UserName}-${index}`" class="detail-item">
|
||||
<view class="detail-item__row">
|
||||
<text class="detail-item__index">
|
||||
{{ index + 1 }}
|
||||
</text>
|
||||
<text class="detail-item__name">
|
||||
{{ perm.FolderLocalName || perm.FolderName }}
|
||||
</text>
|
||||
<text class="detail-item__sub detail-item__sub--inline">
|
||||
{{ perm.UserLocalName || perm.UserName }}
|
||||
</text>
|
||||
<view class="detail-card__head" @tap="toggleFold('permissions')">
|
||||
<text class="detail-card__title">
|
||||
{{ t("SharePage.permissions") }}
|
||||
</text>
|
||||
<u-icon :name="folded.permissions ? 'arrow-right' : 'arrow-down'" size="26"
|
||||
color="#8f959e" />
|
||||
</view>
|
||||
<view v-show="!folded.permissions">
|
||||
<view v-for="(perm, index) in detail.FamilyGroupPermissions"
|
||||
:key="`${perm.FolderName}-${perm.UserName}-${index}`" class="detail-item">
|
||||
<view class="detail-item__row">
|
||||
<text class="detail-item__index">
|
||||
{{ index + 1 }}
|
||||
</text>
|
||||
<text class="detail-item__name">
|
||||
{{ perm.FolderLocalName || perm.FolderName }}
|
||||
</text>
|
||||
<text class="detail-item__sub detail-item__sub--inline">
|
||||
{{ perm.UserLocalName || perm.UserName }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -663,8 +713,8 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
padding: 8rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
color: #2b8fff;
|
||||
background-color: #eaf4ff;
|
||||
color: var(--fg-primary, #2b8fff);
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
}
|
||||
|
||||
/* ===== 列表卡片 ===== */
|
||||
@@ -686,7 +736,7 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #4a9dff, #2b8fff);
|
||||
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -731,8 +781,8 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
}
|
||||
|
||||
.group-card__chip--code {
|
||||
color: #2b8fff;
|
||||
background-color: #eaf4ff;
|
||||
color: var(--fg-primary, #2b8fff);
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
}
|
||||
|
||||
.group-card__chip--admin {
|
||||
@@ -746,25 +796,50 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
color: #bfc3c9;
|
||||
}
|
||||
|
||||
/* ===== 详情头卡 + 动作栏 ===== */
|
||||
/* ===== 详情头卡 + 动作栏(与媒体页同套:白底卡 + 主题点缀) ===== */
|
||||
.detail-hero {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
gap: 28rpx;
|
||||
margin: 0 4rpx 20rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #4a9dff, #2b8fff);
|
||||
padding: 40rpx 32rpx;
|
||||
border-radius: 32rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 头卡右上角圆形动作钮(同媒体页):修改=浅灰底主题图标,解散=浅红底红图标 */
|
||||
.detail-hero__acts {
|
||||
position: absolute;
|
||||
top: 28rpx;
|
||||
right: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.detail-hero__act {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&--danger {
|
||||
background-color: #ffeceb;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-hero__avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
background-color: rgba(255, 255, 255, 0.24);
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 32rpx;
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -773,22 +848,48 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.detail-hero__name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 群编号独立成行:灰字说明(同媒体页专辑编号) */
|
||||
.detail-hero__code {
|
||||
font-size: 26rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
.detail-hero__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
gap: 12rpx;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
/* 头卡胶囊:公开/可申请=主题浅底主题字,管理员=黄底橙字,成员=浅灰 */
|
||||
.detail-hero__chip {
|
||||
padding: 6rpx 22rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--fg-primary, #2b8fff);
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
|
||||
&--admin {
|
||||
color: #ff9500;
|
||||
background-color: #fff4e0;
|
||||
}
|
||||
|
||||
&--member {
|
||||
color: #5c626b;
|
||||
background-color: #f2f3f5;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
@@ -797,31 +898,32 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
margin: 0 4rpx 20rpx;
|
||||
}
|
||||
|
||||
/* 动作钮统一规格(同媒体页/option-picker):等高圆角矩形 + 细描边,主钮纯色跟主题 */
|
||||
.detail-actions__btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
height: 96rpx;
|
||||
border: 2rpx solid #e5e6eb;
|
||||
border-radius: 24rpx;
|
||||
font-size: 32rpx;
|
||||
color: #5c626b;
|
||||
background-color: #ffffff;
|
||||
|
||||
&--primary {
|
||||
border-color: transparent;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background-color: #2b8fff;
|
||||
background-color: var(--fg-primary, #2b8fff);
|
||||
}
|
||||
|
||||
&--join {
|
||||
border-color: transparent;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background-color: #21b169;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
color: #f0413e;
|
||||
background-color: #ffeceb;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 分区卡片 ===== */
|
||||
@@ -838,15 +940,22 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* 标题行右侧一组:新建钮 + 折叠箭头 */
|
||||
.detail-card__head-acts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
}
|
||||
|
||||
.detail-card__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.detail-card__add {
|
||||
font-size: 26rpx;
|
||||
color: #2b8fff;
|
||||
color: var(--fg-primary, #2b8fff);
|
||||
}
|
||||
|
||||
.detail-field {
|
||||
@@ -871,25 +980,25 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
.detail-item__input,
|
||||
.detail-item__sub {
|
||||
box-sizing: border-box;
|
||||
padding: 0 20rpx;
|
||||
height: 68rpx;
|
||||
padding: 0 24rpx;
|
||||
height: 88rpx;
|
||||
border: 2rpx solid #e6e7ea;
|
||||
border-radius: 14rpx;
|
||||
font-size: 26rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
background-color: #f7f8fa;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.detail-field__textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
min-height: 200rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
border: 2rpx solid #e6e7ea;
|
||||
border-radius: 14rpx;
|
||||
font-size: 26rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
background-color: #f7f8fa;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.detail-empty {
|
||||
@@ -951,7 +1060,7 @@ function groupDesc(group: FamilyGroupEntity): string {
|
||||
|
||||
.detail-item__link {
|
||||
font-size: 24rpx;
|
||||
color: #2b8fff;
|
||||
color: var(--fg-primary, #2b8fff);
|
||||
}
|
||||
|
||||
.detail-item__del {
|
||||
|
||||
@@ -8,17 +8,23 @@
|
||||
*/
|
||||
import type { UploadItem } from "@/stores/uploadQueue";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { useAuthGuard, useThemeGradient } from "@/composables";
|
||||
import { computed } from "vue";
|
||||
import { mixWithWhite, useAuthGuard, useThemeTokens } from "@/composables";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import { useUploadQueueStore } from "@/stores/uploadQueue";
|
||||
|
||||
const { t } = useLocale();
|
||||
const { themeBackground } = useThemeGradient();
|
||||
const { primary } = useThemeTokens();
|
||||
const uploadQueue = useUploadQueueStore();
|
||||
|
||||
// 本页无后端数据要拉,守卫只做登录拦截(未登录直接送去登录页)
|
||||
useAuthGuard();
|
||||
|
||||
/** 内容底:从 hero 浅端继续淡出到白,替代整页平铺的灰浊渐变 */
|
||||
const pageStyle = computed(() => ({
|
||||
background: `linear-gradient(180deg, ${mixWithWhite(primary.value, 0.1)} 0%, #ffffff 46%)`,
|
||||
}));
|
||||
|
||||
/** 字节数可读化:0/拿不到显示 -- */
|
||||
function formatSize(bytes: number): string {
|
||||
if (!bytes || bytes < 0) {
|
||||
@@ -95,75 +101,89 @@ function onBack(): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :nav-title="t('TransportPage.title')" :custom-back="onBack">
|
||||
<view class="transport-page" :style="{ background: themeBackground }">
|
||||
<view v-if="!uploadQueue.items.length" class="transport-page__empty">
|
||||
<u-empty mode="list" :text="t('TransportPage.empty')" icon-size="120" />
|
||||
<app-page :hide-nav="true">
|
||||
<view class="transport-page" :style="pageStyle">
|
||||
<!-- 沉浸式主题头部(hero-navbar 组件即由此页风格抽出):白胶囊返回 + 居中主题色标题 -->
|
||||
<hero-navbar :title="t('TransportPage.title')" @back="onBack" />
|
||||
|
||||
<view class="transport-page__body">
|
||||
<view v-if="!uploadQueue.items.length" class="transport-page__empty">
|
||||
<u-empty mode="list" :text="t('TransportPage.empty')" icon-size="120" />
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<!-- 进行中队(排队 + 上传中) -->
|
||||
<view v-if="uploadQueue.activeItems.length" class="transport-page__group">
|
||||
<view class="transport-page__group-head">
|
||||
<view class="transport-page__group-title">
|
||||
<text class="transport-page__group-name">
|
||||
{{ t("TransportPage.active") }}
|
||||
</text>
|
||||
<text class="transport-page__group-count">({{ uploadQueue.activeItems.length }})</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="item in uploadQueue.activeItems" :key="item.id" class="transport-item">
|
||||
<view class="transport-item__row">
|
||||
<u-icon :name="kindIcon(item.name)" size="44" :color="primary" />
|
||||
<view class="transport-item__main">
|
||||
<text class="transport-item__name">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="transport-item__status">
|
||||
{{ statusLine(item) }} · {{ formatSize(item.size) }}
|
||||
</text>
|
||||
<u-line-progress v-if="item.status === 'uploading'" :percent="item.progress" height="8"
|
||||
:show-percent="false" :active-color="primary" />
|
||||
</view>
|
||||
<view class="transport-item__btn" @tap="uploadQueue.cancel(item.id)">
|
||||
{{ t("TransportPage.cancel") }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.targetTrail.length" class="transport-item__crumb" @tap="goTargetFolder(item)">
|
||||
<file-breadcrumb :items="item.targetTrail" show-single />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已结束队(成功 / 失败 / 取消) -->
|
||||
<view v-if="uploadQueue.doneItems.length" class="transport-page__group">
|
||||
<view class="transport-page__group-head">
|
||||
<view class="transport-page__group-title">
|
||||
<text class="transport-page__group-name">
|
||||
{{ t("TransportPage.done") }}
|
||||
</text>
|
||||
<text class="transport-page__group-count">({{ uploadQueue.doneItems.length }})</text>
|
||||
</view>
|
||||
<text class="transport-page__clear" :style="{ color: primary }" @tap="uploadQueue.clearDone()">
|
||||
{{ t("TransportPage.clearDone") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="item in uploadQueue.doneItems" :key="item.id" class="transport-item">
|
||||
<view class="transport-item__row">
|
||||
<u-icon :name="kindIcon(item.name)" size="44" :color="statusColor(item)" />
|
||||
<view class="transport-item__main">
|
||||
<text class="transport-item__name">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="transport-item__status" :style="{ color: statusColor(item) }">
|
||||
{{ statusLine(item) }} · {{ formatSize(item.size) }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="item.status !== 'success'" class="transport-item__btn" @tap="uploadQueue.retry(item.id)">
|
||||
{{ t("TransportPage.retry") }}
|
||||
</view>
|
||||
<!-- 删除降权:文字按钮改 30px 图标按钮(灰垃圾桶),少占位少干扰 -->
|
||||
<view class="transport-item__icon-btn" @tap="uploadQueue.remove(item.id)">
|
||||
<u-icon name="trash" size="32" color="#8f959e" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.targetTrail.length" class="transport-item__crumb" @tap="goTargetFolder(item)">
|
||||
<file-breadcrumb :items="item.targetTrail" show-single />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<!-- 进行中队(排队 + 上传中) -->
|
||||
<view v-if="uploadQueue.activeItems.length" class="transport-page__group">
|
||||
<text class="transport-page__group-title">
|
||||
{{ t("TransportPage.active") }} ({{ uploadQueue.activeItems.length }})
|
||||
</text>
|
||||
<view v-for="item in uploadQueue.activeItems" :key="item.id" class="transport-item">
|
||||
<view class="transport-item__row">
|
||||
<u-icon :name="kindIcon(item.name)" size="44" color="#2b8fff" />
|
||||
<view class="transport-item__main">
|
||||
<text class="transport-item__name">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="transport-item__status">
|
||||
{{ statusLine(item) }} · {{ formatSize(item.size) }}
|
||||
</text>
|
||||
<u-line-progress v-if="item.status === 'uploading'" :percent="item.progress" height="8"
|
||||
:show-percent="false" active-color="#2b8fff" />
|
||||
</view>
|
||||
<view class="transport-item__btn" @tap="uploadQueue.cancel(item.id)">
|
||||
{{ t("TransportPage.cancel") }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.targetTrail.length" class="transport-item__crumb" @tap="goTargetFolder(item)">
|
||||
<file-breadcrumb :items="item.targetTrail" show-single />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已结束队(成功 / 失败 / 取消) -->
|
||||
<view v-if="uploadQueue.doneItems.length" class="transport-page__group">
|
||||
<view class="transport-page__group-head">
|
||||
<text class="transport-page__group-title">
|
||||
{{ t("TransportPage.done") }} ({{ uploadQueue.doneItems.length }})
|
||||
</text>
|
||||
<text class="transport-page__clear" @tap="uploadQueue.clearDone()">
|
||||
{{ t("TransportPage.clearDone") }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="item in uploadQueue.doneItems" :key="item.id" class="transport-item">
|
||||
<view class="transport-item__row">
|
||||
<u-icon :name="kindIcon(item.name)" size="44" :color="statusColor(item)" />
|
||||
<view class="transport-item__main">
|
||||
<text class="transport-item__name">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="transport-item__status" :style="{ color: statusColor(item) }">
|
||||
{{ statusLine(item) }} · {{ formatSize(item.size) }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="item.status !== 'success'" class="transport-item__btn" @tap="uploadQueue.retry(item.id)">
|
||||
{{ t("TransportPage.retry") }}
|
||||
</view>
|
||||
<view class="transport-item__btn transport-item__btn--plain" @tap="uploadQueue.remove(item.id)">
|
||||
{{ t("TransportPage.remove") }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.targetTrail.length" class="transport-item__crumb" @tap="goTargetFolder(item)">
|
||||
<file-breadcrumb :items="item.targetTrail" show-single />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
@@ -171,9 +191,12 @@ function onBack(): void {
|
||||
<style lang="scss" scoped>
|
||||
.transport-page {
|
||||
min-height: calc(100vh - var(--window-top, 0px));
|
||||
padding: 24rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
|
||||
&__body {
|
||||
padding: 24rpx 24rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
&__empty {
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
@@ -189,16 +212,28 @@ function onBack(): void {
|
||||
}
|
||||
|
||||
&__group-title {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 8rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 分组名加重视觉锚点,计数退灰(对齐设计图「已结束 (5)」) */
|
||||
&__group-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
&__group-count {
|
||||
font-size: 24rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
/* 清空完成色由 script 内联下发(主题 primary),这里只留形态 */
|
||||
&__clear {
|
||||
padding: 8rpx 8rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #2979ff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +244,10 @@ function onBack(): void {
|
||||
margin-bottom: 16rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
/* 表面统一三件套:页面下半部已淡成白,纯白卡必须靠描边 + 投影拉开边界 */
|
||||
border: 2rpx solid rgba(31, 35, 41, 0.06);
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
|
||||
|
||||
/* 行主体:图标 + 文本区 + 动作按钮横向排列(面包屑另占一行) */
|
||||
&__row {
|
||||
@@ -218,9 +256,9 @@ function onBack(): void {
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 目标目录面包屑行:整行可点,点任意段都跳回文件页的目标目录(单段也走同款卡片) */
|
||||
/* 目标目录面包屑行:整行可点,点任意段都跳回文件页的目标目录;左缘与文件图标对齐 */
|
||||
&__crumb {
|
||||
padding-left: 64rpx;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&__main {
|
||||
@@ -251,11 +289,18 @@ function onBack(): void {
|
||||
font-size: 24rpx;
|
||||
color: #2979ff;
|
||||
background-color: #eaf4ff;
|
||||
}
|
||||
|
||||
&--plain {
|
||||
color: #8f959e;
|
||||
background-color: #f5f6f8;
|
||||
}
|
||||
/* 删除:30px(60rpx)图标按钮,浅灰圆底,视觉重量远低于文字胶囊 */
|
||||
&__icon-btn {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #f5f6f8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -234,9 +234,9 @@ function onMultiPurge(): void {
|
||||
<app-page :hide-nav="true">
|
||||
<view class="trash-page" :style="{ background: themeBackground }">
|
||||
<view class="trash-page__head">
|
||||
<!-- 与文件页同款导航:标题固定「回收站」,返回箭头常显,传输/快捷入口开关关闭 -->
|
||||
<page-navbar :title="t('TrashPage.title')" :show-back="true" :search-on="showSearch"
|
||||
:show-transfer="false" :show-quick-toggle="false" @back="onBack" @toggle-search="onToggleSearch" />
|
||||
<!-- 与传输页同款沉浸头部:标题固定「回收站」,返回箭头常显,右侧搜索胶囊 -->
|
||||
<hero-navbar :title="t('TrashPage.title')" :show-search="true" :search-on="showSearch" @back="onBack"
|
||||
@toggle-search="onToggleSearch" />
|
||||
<search-input v-if="showSearch" v-model="keyword" :placeholder="t('FilePage.searchPlaceholder')"
|
||||
@search="onSearch" />
|
||||
<!-- 分类栏复用文件页组件:新建文件夹开关关闭 -->
|
||||
|
||||
@@ -15,5 +15,6 @@ pinia.use(
|
||||
export default pinia;
|
||||
|
||||
export * from "./loginProfile";
|
||||
export * from "./mediaIntent";
|
||||
export * from "./tabbar";
|
||||
export * from "./uploadQueue";
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { PublishKind } from "@/composables/usePublishAlbums";
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
/**
|
||||
* 多媒体页跨 tab 意图通道:文件页发布面板点「新建相册」时 reLaunch 过去
|
||||
* (项目无原生 tabBar,切 tab 走 reLaunch 重建页面),落到 store 里等 media 页
|
||||
* 首显回调消费,自动进入对应 kind 的创建态。
|
||||
* 与 uploadQueue.requestNav 同一套路:登记 → 取件即清,防止下次切页重复触发。
|
||||
*/
|
||||
export const useMediaIntentStore = defineStore("mediaIntent", () => {
|
||||
const pendingCreate = ref<PublishKind | null>(null);
|
||||
|
||||
/** 发布面板点「新建相册/音乐集/影视集」时登记 */
|
||||
function requestCreate(kind: PublishKind): void {
|
||||
pendingCreate.value = kind;
|
||||
}
|
||||
|
||||
/** media 页 onShow 消费:取出即清 */
|
||||
function takeCreate(): PublishKind | null {
|
||||
const kind = pendingCreate.value;
|
||||
pendingCreate.value = null;
|
||||
return kind;
|
||||
}
|
||||
|
||||
return { requestCreate, takeCreate };
|
||||
});
|
||||
Reference in new issue
Block a user