diff --git a/uniapp/youjiala/src/common/http.interceptor.ts b/uniapp/youjiala/src/common/http.interceptor.ts
index 273bfa2..943a61b 100644
--- a/uniapp/youjiala/src/common/http.interceptor.ts
+++ b/uniapp/youjiala/src/common/http.interceptor.ts
@@ -69,15 +69,6 @@ export function clearLoginState() {
uni.removeStorageSync(JWT_STORAGE_KEY);
}
-/**
- * 是否已有登录会话:只认请求层的 AccessToken(令牌唯一来源,与 nav-guard 口径一致)。
- * 不在此判过期——AccessToken 过期时请求层会拿 RefreshToken 自动续期,续期失败再 goLogin 退回登录页,
- * 所以"登录过"就足以决定冷启动是否直接进 dashboard。
- */
-export function hasLoginSession(): boolean {
- return !!getJwtInfo()?.AccessToken;
-}
-
function getToken(): string {
return getJwtInfo()?.AccessToken ?? "";
}
diff --git a/uniapp/youjiala/src/components/file-breadcrumb/file-breadcrumb.vue b/uniapp/youjiala/src/components/file-breadcrumb/file-breadcrumb.vue
new file mode 100644
index 0000000..165f5c2
--- /dev/null
+++ b/uniapp/youjiala/src/components/file-breadcrumb/file-breadcrumb.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+ {{ items[0].displayName }}
+
+
+
+
+
+
+ {{ seg.node.displayName }}
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/components/file-grid-list/file-grid-list.vue b/uniapp/youjiala/src/components/file-grid-list/file-grid-list.vue
new file mode 100644
index 0000000..4a33618
--- /dev/null
+++ b/uniapp/youjiala/src/components/file-grid-list/file-grid-list.vue
@@ -0,0 +1,269 @@
+
+
+
+
+
+
+ {{ emptyText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ file.name }}
+
+
+
+ {{ file.sizeText }}
+
+
+ {{ file.modifiedAt }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/components/file-tab-filter/file-tab-filter.vue b/uniapp/youjiala/src/components/file-tab-filter/file-tab-filter.vue
new file mode 100644
index 0000000..b40d97a
--- /dev/null
+++ b/uniapp/youjiala/src/components/file-tab-filter/file-tab-filter.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/components/float-add-btn/float-add-btn.vue b/uniapp/youjiala/src/components/float-add-btn/float-add-btn.vue
new file mode 100644
index 0000000..24e4f81
--- /dev/null
+++ b/uniapp/youjiala/src/components/float-add-btn/float-add-btn.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/components/page-navbar/page-navbar.vue b/uniapp/youjiala/src/components/page-navbar/page-navbar.vue
new file mode 100644
index 0000000..972ad29
--- /dev/null
+++ b/uniapp/youjiala/src/components/page-navbar/page-navbar.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/components/quick-entry/quick-entry.vue b/uniapp/youjiala/src/components/quick-entry/quick-entry.vue
new file mode 100644
index 0000000..5682de4
--- /dev/null
+++ b/uniapp/youjiala/src/components/quick-entry/quick-entry.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/components/search-input/search-input.vue b/uniapp/youjiala/src/components/search-input/search-input.vue
new file mode 100644
index 0000000..72a1d91
--- /dev/null
+++ b/uniapp/youjiala/src/components/search-input/search-input.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/composables/index.ts b/uniapp/youjiala/src/composables/index.ts
index cfbeaa6..497993d 100644
--- a/uniapp/youjiala/src/composables/index.ts
+++ b/uniapp/youjiala/src/composables/index.ts
@@ -2,4 +2,6 @@ export * from "./useAdminList";
export * from "./useAdminMenu";
export * from "./useAdminPick";
export * from "./useAuthGuard";
+export * from "./useDmsFiles";
export * from "./useLang";
+export * from "./useThemeGradient";
diff --git a/uniapp/youjiala/src/composables/useDmsFiles.ts b/uniapp/youjiala/src/composables/useDmsFiles.ts
new file mode 100644
index 0000000..63c5a82
--- /dev/null
+++ b/uniapp/youjiala/src/composables/useDmsFiles.ts
@@ -0,0 +1,286 @@
+/**
+ * 文件页数据源:复刻旧 WebVue DmsFileEnquiry.vue 的调用链(D:\Docker\WebVue\src\views\master)
+ *
+ * 1. FindDmsFolders() -> SelectOption[]:rps[0] 是空占位项,rps[1] 才是根目录「我的网盘」,
+ * 旧页面 rootPath = rps[1].BindValue 的取值习惯原样保留;
+ * 2. FindBySearchCriteria(criteria):Filter = [FilePath single 当前目录],
+ * 路径为空则不挂 FilePath 过滤查全量(旧 onSearchByFilePath 对空 rootPath 的分支);
+ * 有关键字再追加 [OrgFileName text contain](旧 onSearchByName 的两段过滤);
+ * limit 给 999999 一次拉全(旧页面同值,目录场景不分页);
+ * 3. 结果文件夹置顶(旧页面 orderBy IsDirectoryIndc desc)。
+ *
+ * 目录导航:点文件夹把该记录的 FileName 当作新的 FilePath 重查(旧 onOpenFolder ->
+ * onChangeFolder),pathStack 记录面包屑供返回;类型判定优先看后端 FileCategory
+ * (P/V/A/D/L,见 FileTypePolicy 常量),缺省时按扩展名兜底(fileTypePolicy)。
+ */
+import type { DmsFileUploadViewDisplayData } from "@/data/auto/dmsFileUploadViewDisplayData";
+import type { FileItem, FileKind, PathNode } from "@/data/custom/fileBrowser";
+import { useLocale } from "uview-pro";
+import { computed, ref } from "vue";
+import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria";
+import { DmsFileUploadViewDisplayData as UploadView } from "@/data/auto/dmsFileUploadViewDisplayData";
+import { SearchFilter } from "@/data/auto/searchFilter";
+import fileTypePolicy, { FileTypePolicy } from "@/policy/custom/fileTypePolicy";
+import dmsFileService from "@/service/master/dmsFileService";
+
+/** 目录场景一次拉全,与旧页面 pagingParams.pageSize 同值 */
+const FIND_LIMIT = 999999;
+
+/** FileCategory 编码 -> 页面分类;缺省或未知编码走扩展名兜底 */
+function kindOf(record: DmsFileUploadViewDisplayData): FileKind {
+ if (record.IsDirectoryIndc === "Y") {
+ return "folder";
+ }
+ switch (record.FileCategory) {
+ case FileTypePolicy.PHOTO:
+ return "image";
+ case FileTypePolicy.VIDEO:
+ return "video";
+ case FileTypePolicy.AUDIO:
+ return "audio";
+ case FileTypePolicy.DOCUMENT:
+ case FileTypePolicy.LYRIC:
+ return "document";
+ }
+ const key = record.OrderKey || record.OrgFileName || "";
+ if (fileTypePolicy.isImage(key)) {
+ return "image";
+ }
+ if (fileTypePolicy.isVideo(key)) {
+ return "video";
+ }
+ if (fileTypePolicy.isAudio(key)) {
+ return "audio";
+ }
+ return "document";
+}
+
+/** 兼容 "2026-05-13 08:17:00" 这类 iOS Date 解析失败的格式 */
+function parseTime(value: Date | string | undefined): number {
+ if (!value) {
+ return 0;
+ }
+ if (value instanceof Date) {
+ return value.getTime();
+ }
+ const normalized = value
+ .replace(/-/g, "/")
+ .replace(/\.\d+Z?$/, "")
+ .replace(/T/, " ")
+ .replace(/Z$/, "");
+ const time = new Date(normalized).getTime();
+ return Number.isNaN(time) ? 0 : time;
+}
+
+/** 字节数 -> 可读大小文案;0/缺省返回空串(文件夹不显示大小) */
+function formatSize(bytes: number | undefined): string {
+ if (!bytes || bytes <= 0) {
+ return "";
+ }
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ let value = bytes;
+ let i = 0;
+ while (value >= 1024 && i < units.length - 1) {
+ value /= 1024;
+ i += 1;
+ }
+ return `${i === 0 ? value : value.toFixed(1)} ${units[i]}`;
+}
+
+export function useDmsFiles() {
+ const { t } = useLocale();
+
+ const files = ref([]);
+ /** FileId -> 原始记录:预览/下载/删除等后续动作要拿全字段 */
+ const records = ref(new Map());
+ const loading = ref(false);
+ const errorText = ref("");
+ const rootPath = ref("");
+ const currPath = ref("");
+ const pathStack = ref([]);
+
+ /** 是否在根目录:决定导航栏返回箭头是「退目录」还是「退页面」 */
+ const atRoot = computed(() => pathStack.value.length <= 1);
+
+ function toFileItem(record: DmsFileUploadViewDisplayData): FileItem {
+ const time = parseTime(record.CreatedDatetime);
+ const date = time ? new Date(time) : null;
+ const now = new Date();
+ const pad = (n: number) => String(n).padStart(2, "0");
+ let modifiedAt = "";
+ if (date) {
+ // 今天的条目显示「今天 HH:mm」,其余显示日期(网盘习惯);if/else 写法防编辑器折行
+ if (date.toDateString() === now.toDateString()) {
+ modifiedAt = `${t("FilePage.todayLabel")} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
+ } else {
+ modifiedAt = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
+ }
+ }
+ return {
+ id: record.FileId,
+ name: record.OrgFileName || record.FileName,
+ kind: kindOf(record),
+ // 卡片元数据不显示用户:文件夹 sizeText 为空只剩时间,文件显示大小+时间
+ // FileSize 单位是 KB(旧系统 DmsFileEnquiry 下载缓冲 (FileSize+1)*1024 字节佐证),先换算成字节
+ sizeText: formatSize(
+ record.FileSize ? record.FileSize * 1024 : undefined,
+ ),
+ modifiedAt,
+ sortTime: time,
+ path: record.FileName,
+ };
+ }
+
+ function buildCriteria(path: string, keyword: string): DmsFileSearchCriteria {
+ const criteria = new DmsFileSearchCriteria();
+ const filters: SearchFilter[] = [];
+
+ // 旧 onSearchByFilePath 语义:路径为空(本地库 FindDmsFolders 只回一条空 BindValue 时)
+ // 不挂 FilePath 过滤、照常查询全量,而不是当错误处理
+ if (path) {
+ const pathFilter = new SearchFilter();
+ pathFilter.Column = UploadView.DB_FILE_PATH;
+ pathFilter.Type = "SingleSelection";
+ pathFilter.Operator = "single";
+ pathFilter.Value = path;
+ filters.push(pathFilter);
+ }
+
+ if (keyword.trim()) {
+ const nameFilter = new SearchFilter();
+ nameFilter.Column = UploadView.DB_ORG_FILE_NAME;
+ nameFilter.Type = "text";
+ nameFilter.Operator = "contain";
+ nameFilter.Value = keyword.trim();
+ filters.push(nameFilter);
+ }
+
+ criteria.Filter = filters;
+ criteria.limit = FIND_LIMIT;
+ criteria.page = 1;
+ criteria.GridColumns = [];
+ return criteria;
+ }
+
+ /** 查一个目录;path 为空查全量(旧 onSearchByFilePath 空路径语义),keyword 非空时服务端按名称 contain 过滤 */
+ async function loadFolder(path: string, keyword = ""): Promise {
+ loading.value = true;
+ errorText.value = "";
+ try {
+ const data = await dmsFileService.FindBySearchCriteria(
+ buildCriteria(path, keyword),
+ );
+ const rows = data?.Datas ?? [];
+ // 文件夹置顶(旧页面 orderBy IsDirectoryIndc desc),组内保持后端顺序;
+ // for-of 而非单参数箭头:编辑器格式化会反复加回 (r) => 括号,与 arrow-parens 对撞
+ const dirs: DmsFileUploadViewDisplayData[] = [];
+ const others: DmsFileUploadViewDisplayData[] = [];
+ for (const row of rows) {
+ if (row.IsDirectoryIndc === "Y") {
+ dirs.push(row);
+ } else {
+ others.push(row);
+ }
+ }
+ const sorted = [...dirs, ...others];
+ files.value = sorted.map(toFileItem);
+ const nextRecords = new Map();
+ for (const row of sorted) {
+ nextRecords.set(row.FileId, row);
+ }
+ records.value = nextRecords;
+ currPath.value = path;
+ } catch (error: any) {
+ // 错误提示已由 http.interceptor 统一 toast,这里只留页面内可见的失败态
+ files.value = [];
+ records.value = new Map();
+ errorText.value = error?.message || t("FilePage.loadFailed");
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ /** 初始化:拉目录树拿根路径,再查根目录(旧 onMounted -> initBindOption -> onSearchByFilePath) */
+ async function init(): Promise {
+ loading.value = true;
+ errorText.value = "";
+ try {
+ const options = (await dmsFileService.FindDmsFolders()) ?? [];
+ // rps[0] 是空占位项,rps[1] 才是根目录「我的网盘」,与旧页面取值一致;
+ // 本地库实测只回一条 [{DisplayName:"我的网盘", BindValue:""}]——旧系统对此不视为错误:
+ // 根面包屑无条件放入(value 可为空),空根路径走 loadFolder("") 查全量
+ const root = options.length > 1 ? options[1] : options[0];
+ rootPath.value = root?.BindValue ?? "";
+ pathStack.value = [
+ {
+ value: rootPath.value,
+ displayName: root?.DisplayName || t("FilePage.myCloud"),
+ },
+ ];
+ await loadFolder(rootPath.value);
+ } catch (error: any) {
+ errorText.value = error?.message || t("FilePage.loadFailed");
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ /** 点文件夹下钻;非文件夹返回 false 交给页面处理打开动作 */
+ async function openFolder(file: FileItem): Promise {
+ if (file.kind !== "folder" || !file.path) {
+ return false;
+ }
+ pathStack.value.push({ value: file.path, displayName: file.name });
+ await loadFolder(file.path);
+ return true;
+ }
+
+ /** 返回上一级目录;已在根目录返回 false 交给页面处理退出 */
+ async function back(): Promise {
+ if (pathStack.value.length <= 1) {
+ return false;
+ }
+ pathStack.value.pop();
+ const prev = pathStack.value[pathStack.value.length - 1];
+ await loadFolder(prev.value);
+ return true;
+ }
+
+ /** 服务端按名称搜索当前目录(关键字清空即重查全目录) */
+ async function search(keyword: string): Promise {
+ await loadFolder(currPath.value, keyword);
+ }
+
+ /** 面包屑跳转:截断到第 index 层并查该层;已在该层则忽略 */
+ async function goTo(index: number): Promise {
+ if (index < 0 || index >= pathStack.value.length - 1) {
+ return;
+ }
+ pathStack.value = pathStack.value.slice(0, index + 1);
+ await loadFolder(pathStack.value[index].value);
+ }
+
+ /** 失败重试:还停在当前目录就重查目录,初始化失败则整个链路重来 */
+ async function retry(): Promise {
+ if (currPath.value) {
+ await loadFolder(currPath.value);
+ return;
+ }
+ await init();
+ }
+
+ return {
+ files,
+ records,
+ loading,
+ errorText,
+ pathStack,
+ atRoot,
+ init,
+ openFolder,
+ back,
+ goTo,
+ search,
+ retry,
+ };
+}
diff --git a/uniapp/youjiala/src/composables/useThemeGradient.ts b/uniapp/youjiala/src/composables/useThemeGradient.ts
new file mode 100644
index 0000000..8d9d28b
--- /dev/null
+++ b/uniapp/youjiala/src/composables/useThemeGradient.ts
@@ -0,0 +1,48 @@
+import { useTheme } from "uview-pro";
+import { computed } from "vue";
+
+/**
+ * 主题渐变 composable:把 uView Pro 当前主题的 primary 派生成页面底渐变。
+ * 背景:文件页等页面头部各块已透明化,页面底用「主题色混白」的三段纵向渐变
+ * 承接主题切换(mine 页 setTheme 后无需刷新即联动);混白而非固定色值,
+ * 保证任意主题下都是同色系浅底,白卡与文字对比天然保持。
+ * 用法:const { themeBackground } = useThemeGradient();
+ *
+ * 浓度档可经 options 覆盖(如弹窗想更淡:{ top: 0.15, ... }),默认 25%/14%/5%。
+ */
+
+/** 渐变三段的主题色浓度(0-1,越大越浓),按 0% / 40% / 100% 三个停靠点铺 */
+export interface ThemeGradientOptions {
+ /** 顶部浓度,默认 0.25 */
+ top?: number;
+ /** 40% 高度处浓度,默认 0.14 */
+ middle?: number;
+ /** 底部浓度,默认 0.05 */
+ bottom?: number;
+}
+
+/** uView 出厂蓝,仅作 currentTheme 尚未就绪时的兜底(正常初始化后不会被读到) */
+const FALLBACK_PRIMARY = "#2979ff";
+
+/** 主题 primary 按比例混入白色(ratio 为主题色占比),得到同色系浅色 rgb() */
+export function mixWithWhite(hex: string, ratio: number): string {
+ const n = hex.replace("#", "");
+ const channel = (start: number) =>
+ Number.parseInt(n.slice(start, start + 2), 16);
+ const mix = (c: number) => Math.round(c * ratio + 255 * (1 - ratio));
+ return `rgb(${mix(channel(0))}, ${mix(channel(2))}, ${mix(channel(4))})`;
+}
+
+export function useThemeGradient(options: ThemeGradientOptions = {}) {
+ const { top = 0.25, middle = 0.14, bottom = 0.05 } = options;
+ // currentTheme 是包级单例 ref:任意页 setTheme,这里即时重算
+ const { currentTheme } = useTheme();
+
+ /** 页面底渐变(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 { currentTheme, themeBackground };
+}
diff --git a/uniapp/youjiala/src/data/custom/fileBrowser.ts b/uniapp/youjiala/src/data/custom/fileBrowser.ts
new file mode 100644
index 0000000..8a5d144
--- /dev/null
+++ b/uniapp/youjiala/src/data/custom/fileBrowser.ts
@@ -0,0 +1,47 @@
+/**
+ * 文件浏览页的数据契约:页面持有数据与状态,六个展示组件只吃 props。
+ * 展示字段(sizeText / modifiedAt)由页面格式化成最终文案,组件不再二次加工。
+ */
+
+/** 分类标签的取值:全部 / 图片 / 视频 / 文档 / 音频 */
+export type FileCategory = "all" | "image" | "video" | "document" | "audio";
+
+/** 单个条目的类型:四种文件 + 文件夹 */
+export type FileKind = Exclude | "folder";
+
+export interface FileItem {
+ id: number;
+ name: string;
+ kind: FileKind;
+ /** 文件大小文案(如「2.9 MB」);文件夹为空串——卡片不显示大小也不显示用户 */
+ sizeText: string;
+ /** 修改时间展示文案(今天 / 具体日期),仅用于显示 */
+ modifiedAt: string;
+ /** 可排序的时间戳(毫秒):展示文案没法比大小,排序用它 */
+ sortTime: number;
+ /** DMS 记录的 FileName:文件夹点它下钻(当作下次查询的 FilePath),文件留作下载/预览键 */
+ path: string;
+}
+
+/** 快捷功能入口的单项:图标 + 渐变底色,页面给什么画什么 */
+export interface QuickEntryItem {
+ key: string;
+ label: string;
+ icon: string;
+ bgFrom: string;
+ bgTo: string;
+}
+
+/** 分类标签单项 */
+export interface FileTabItem {
+ key: FileCategory;
+ label: string;
+}
+
+/** 面包屑/目录栈单项:从根到当前目录 */
+export interface PathNode {
+ /** 查询用的 FilePath 值 */
+ value: string;
+ /** 展示名(根为「我的网盘」,其余为文件夹 OrgFileName) */
+ displayName: string;
+}
diff --git a/uniapp/youjiala/src/locale/lang/en-US.json b/uniapp/youjiala/src/locale/lang/en-US.json
index a1354e4..2ba676e 100644
--- a/uniapp/youjiala/src/locale/lang/en-US.json
+++ b/uniapp/youjiala/src/locale/lang/en-US.json
@@ -14,6 +14,41 @@
"share": "Share",
"mine": "Me"
},
+ "MinePage": {
+ "title": "Me",
+ "themeTitle": "Theme Color",
+ "themeHint": "Component colors update instantly on switch and the choice is remembered",
+ "darkTitle": "Dark Mode",
+ "darkAuto": "Follow System",
+ "darkLight": "Light",
+ "darkDark": "Dark",
+ "currentTheme": "Current Theme"
+ },
+ "FilePage": {
+ "title": "Files",
+ "searchPlaceholder": "Search",
+ "recent": "Recent",
+ "favorite": "Favorites",
+ "downloaded": "Downloaded",
+ "team": "Team Files",
+ "shared": "Shared",
+ "catAll": "All",
+ "catImage": "Images",
+ "catVideo": "Videos",
+ "catDocument": "Docs",
+ "catAudio": "Audio",
+ "emptyText": "No matching files",
+ "sortTimeDesc": "Newest first",
+ "sortTimeAsc": "Oldest first",
+ "sortName": "Sort by name",
+ "selectedCount": "{count} selected",
+ "todoTip": "Coming soon",
+ "todayLabel": "Today",
+ "myCloud": "My Cloud",
+ "loading": "Loading…",
+ "loadFailed": "Failed to load, please retry",
+ "retryBut": "Retry"
+ },
"AdminPage": {
"filterBut": "Filter",
"filterTitle": "Filter Conditions",
diff --git a/uniapp/youjiala/src/locale/lang/zh-CN.json b/uniapp/youjiala/src/locale/lang/zh-CN.json
index 4f1cecd..65321a1 100644
--- a/uniapp/youjiala/src/locale/lang/zh-CN.json
+++ b/uniapp/youjiala/src/locale/lang/zh-CN.json
@@ -14,6 +14,41 @@
"share": "共享",
"mine": "我的"
},
+ "MinePage": {
+ "title": "我的",
+ "themeTitle": "主题颜色",
+ "themeHint": "切换后组件配色即时更新,选择会被记住",
+ "darkTitle": "深色模式",
+ "darkAuto": "跟随系统",
+ "darkLight": "浅色",
+ "darkDark": "深色",
+ "currentTheme": "当前主题"
+ },
+ "FilePage": {
+ "title": "文件",
+ "searchPlaceholder": "搜索",
+ "recent": "最近访问",
+ "favorite": "收藏",
+ "downloaded": "已下载",
+ "team": "团队文件",
+ "shared": "共享文件",
+ "catAll": "全部",
+ "catImage": "图片",
+ "catVideo": "视频",
+ "catDocument": "文档",
+ "catAudio": "音频",
+ "emptyText": "没有匹配的文件",
+ "sortTimeDesc": "按修改时间降序",
+ "sortTimeAsc": "按修改时间升序",
+ "sortName": "按名称排序",
+ "selectedCount": "已选 {count} 项",
+ "todoTip": "功能开发中,敬请期待",
+ "todayLabel": "今天",
+ "myCloud": "我的云盘",
+ "loading": "加载中…",
+ "loadFailed": "加载失败,请重试",
+ "retryBut": "重试"
+ },
"AdminPage": {
"filterBut": "筛选",
"filterTitle": "筛选条件",
diff --git a/uniapp/youjiala/src/md/theme.md b/uniapp/youjiala/src/md/theme.md
new file mode 100644
index 0000000..669e13c
--- /dev/null
+++ b/uniapp/youjiala/src/md/theme.md
@@ -0,0 +1,82 @@
+# uView Pro 主题系统与渐变底接线
+
+适用范围:`src/common/uview-pro.theme.ts`(主题定义)、`src/main.ts`(注册)、`src/composables/useThemeGradient.ts`(渐变派生)、`src/pages/tab/mine.vue`(切换入口)、文件页等使用主题渐变底的页面。
+路由与权限见 [page-routing.md](./page-routing.md)、[permission.md](./permission.md);环境变量见 [environment.md](./environment.md)。
+
+---
+
+## 1. 主题从哪来:定义 → 注册 → 使用
+
+- **定义**:`src/common/uview-pro.theme.ts` 导出主题数组,每项 `{ name, label, color }`;`color` 必含 `primary/error/warning/success/info`,`*Light/*Dark/*Disabled` 可缺(库有 `applyColorFallbacks` 自动派生)。当前四套:`purple 霞光紫 / green 清翠绿 / orange 暖阳橙 / dark 午夜蓝`。
+- **注册**:`main.ts` 装插件时一次性传入,改默认主题或增删主题都只动这两个文件:
+
+```text
+app.use(uViewPro, {
+ theme: {
+ themes, // 来自 common/uview-pro.theme.ts
+ defaultTheme: "green", // 冷启动且 Storage 无记录时用它
+ defaultDarkMode: "light",
+ },
+ ...
+});
+```
+
+- **使用**:组件配色走 CSS 变量 `--u-type-primary`(注入在 `:root`),或 JS 里读 `useTheme().currentTheme`。
+
+## 2. 切换与持久化:不要自建 store
+
+```text
+const { themes, currentTheme, setTheme, darkMode, setDarkMode, isInDarkMode } = useTheme();
+setTheme("orange"); // 切换 + 写 Storage('uview-pro-theme')
+setDarkMode("auto"); // 'auto' | 'light' | 'dark',写 Storage('uview-pro-dark-mode')
+```
+
+要点(均为库行为,实测确认):
+
+- `useTheme()` 的 `currentTheme/darkMode` 是**包级单例 ref**:任何页面调用拿到的都是同一份,mine 页切主题、文件页渐变即时联动,**无需刷新、无需事件总线**;
+- 持久化由库自动完成(Storage 键 `uview-pro-theme`、`uview-pro-dark-mode`),冷启动 `initTheme` 自动恢复,**页面层不要再包一层 Pinia store**;
+- `themes` 列表 = **内置 defaultThemes 与自定义主题合并**(`config-provider.normalizeThemes` 的 `mergeThemes`),所以 mine 页会多渲染一个出厂「默认蓝 #2979ff」色点——这是库设计,不是 bug;只想要自定义 4 套的话按 `name` 过滤即可。
+
+## 3. 深色模式的生效层级
+
+`setDarkMode("dark")` 实测:`html` 类切到 `u-theme-dark`、`body` 背景变暗、`--u-type-primary` 重算为暗色降饱和变体。
+
+已知小瑕疵:`.u-config-provider` 元素自身的 `u-theme-light` 类**不跟随**切换(uView Pro 内部响应性缺口,仅该类,不影响变量与 html/body 层生效)。
+
+注意:本项目自定义样式大量写死色值(文件页白卡、`#2b8fff` 强调色等),**页面不会自动变暗**;要让整页跟随暗黑,需要把关键色逐个改走 `--u-type-*` 变量,属独立改造任务。
+
+## 4. 渐变底:useThemeGradient(多页共用)
+
+文件页头部各块(navbar/面包屑/搜索/快捷入口/分类条)已透明化,页面底用「主题色混白」的纵向渐变承接主题色。派生逻辑在 `src/composables/useThemeGradient.ts`,规则:
+
+- `mixWithWhite(primary, ratio)`:主题 primary 按占比混白,`c*ratio + 255*(1-ratio)`,输出 `rgb()`;
+- 三段停靠:顶部 25% 浓度 → 40% 高度 14% → 底部 5%(`linear-gradient(180deg, ...)`),保证任意主题都是同色系浅底,白卡与文字对比天然保持;
+- 浓度可覆盖:`useThemeGradient({ top: 0.15, middle: 0.08, bottom: 0.03 })`。
+
+新页面接入只要两行:
+
+```text
+const { themeBackground } = useThemeGradient();
+
+
+```
+
+实测对照(顶部 = primary 混白 25%):green `#059669` → `rgb(193,229,218)`;purple `#7c3aed` → `rgb(222,206,251)`;dark `#0b3d91` → `rgb(194,207,228)`。
+
+历史备注:文件页渐变曾经是写死的 `#b8e5fd → #eaf7fe`(蓝色系),不随主题;现已全部由本 composable 接管,**不要再在页面里加固定渐变覆盖**。
+
+## 5. mine 页设置入口
+
+`src/pages/tab/mine.vue` 两张卡片:
+
+- **主题颜色**:遍历 `useTheme().themes` 渲染色点(背景取 `theme.color.primary`,选中打勾),点击 `setTheme(theme.name)`;
+- **深色模式**:跟随系统/浅色/深色三段胶囊,点击 `setDarkMode(mode)`;`DarkMode` 类型顶层未导出,页面内以同构字面量联合 `"auto" | "light" | "dark"` 声明。
+
+词条在语言包 `MinePage` 块(`src/locale/lang/zh-CN.json` / `en-US.json`);主题 `label`(霞光紫等)来自 `uview-pro.theme.ts`,是中文写死的——若要随语言切换需给主题加双 label 字段,暂未做。
+
+## 6. 新增/修改主题的固定步骤
+
+1. 在 `common/uview-pro.theme.ts` 数组增删一项(`name` 必须全局唯一,是 Storage 持久化的键值);
+2. 如需换默认主题,改 `main.ts` 的 `defaultTheme`;
+3. 无需动 mine 页——色点列表由 `themes` 响应式驱动;
+4. 验证:`npx vue-tsc --noEmit -p tsconfig.json` 退出码 0 + 浏览器实测 mine 切换 → 文件页渐变联动 → F5 持久化。
diff --git a/uniapp/youjiala/src/pages.json b/uniapp/youjiala/src/pages.json
index f0ac1a4..f4fb31a 100644
--- a/uniapp/youjiala/src/pages.json
+++ b/uniapp/youjiala/src/pages.json
@@ -1,13 +1,15 @@
{
"pages": [
- // 入口页在 H5 渲染在根路径;/login、/dashboard 等短地址由 router/h5-clean-url 运行时追加路由记录
+ // 入口页在 H5 渲染在根路径(当前入口=dashboard 首页,未登录由页面级守卫 useAuthGuard 送去登录页);
+ // /login、/dashboard 等短地址由 router/h5-clean-url 运行时追加路由记录
// 新增页面须同步登记 router/routes.ts 的 PAGE_PATHS,否则只有真实路径可访问
- {
- "path": "pages/login/login"
- },
+ // 注意:换入口页须同步 routes.ts 里 "/" 的页面归属(resolvePageName)
{
"path": "pages/dashboard/dashboard"
},
+ {
+ "path": "pages/login/login"
+ },
// 错误页:H5 短地址由 CLEAN_PATH_OVERRIDES 指定为 /error/page40x,与旧 WebVue 地址一致
{
"path": "pages/error/page401"
diff --git a/uniapp/youjiala/src/pages/login/login.vue b/uniapp/youjiala/src/pages/login/login.vue
index 0b438d7..407db62 100644
--- a/uniapp/youjiala/src/pages/login/login.vue
+++ b/uniapp/youjiala/src/pages/login/login.vue
@@ -4,10 +4,10 @@
// 加密已抽到 @/common/pwd-crypto(wxmp-rsa 纯 JS 实现,H5/小程序/App 全端可用)
import type { LoginData } from "@/data/auto/loginData";
import type { LoginProfile } from "@/data/auto/loginProfile";
-import { onLoad, onShow } from "@dcloudio/uni-app";
+import { onLoad } from "@dcloudio/uni-app";
import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
-import { backendLocale, baseUrl, hasLoginSession } from "@/common/http.interceptor";
+import { backendLocale, baseUrl } from "@/common/http.interceptor";
import { encryptLoginPassword } from "@/common/pwd-crypto";
import { LoginData as LoginDataClass } from "@/data/auto/loginData";
import { reloadLocales } from "@/locale";
@@ -36,17 +36,10 @@ onLoad((options) => {
});
/**
- * 冷启动直接落在登录入口且本地已有登录会话 → 自动进 dashboard,
- * 省掉"明明登录过还闪一下登录页"。令牌若已过期,dashboard 首个请求会触发请求层用
- * RefreshToken 自动续期;续期失败再 goLogin 退回本页,不会卡在受保护页里。
- * 被"有意送回登录页"的场景(401 去登录 / 403 重新登录)不必再设豁免:
- * goLoginWithRedirect 已先清登录态,此刻 hasLoginSession 必为假,表单正常展示。
+ * 已登录访问 /login 不再自动放行进 dashboard——本页是"切换账号"的唯一入口,
+ * 自动跳走会让用户够不到登录表单。旧令牌在 Login 成功后被 updateLoginProfile 覆盖;
+ * 由守卫送回本页的场景 goLoginWithRedirect 已先 clearLoginState,会话本就是干净的。
*/
-onShow(() => {
- if (hasLoginSession()) {
- uni.reLaunch({ url: redirectUrl.value });
- }
-});
// 登录页中英文切换:走 uView Pro 语言包(t / setLocale),语言包 name 为 zh-CN / en-US
const { t, setLocale, currentLocale } = useLocale();
diff --git a/uniapp/youjiala/src/pages/tab/file.vue b/uniapp/youjiala/src/pages/tab/file.vue
index dfbb9bf..214e3ae 100644
--- a/uniapp/youjiala/src/pages/tab/file.vue
+++ b/uniapp/youjiala/src/pages/tab/file.vue
@@ -1,44 +1,360 @@
-
-
-
- 文件
-
-
- 测试页,功能待接入
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("FilePage.selectedCount", { count: selectedIds.length }) }}
+
+
+
+
+
+
+
+
+
+
+ {{ t("FilePage.loading") }}
+
+
+
+
+ {{ errorText }}
+
+
+ {{ t("FilePage.retryBut") }}
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/pages/tab/mine.vue b/uniapp/youjiala/src/pages/tab/mine.vue
index 308d4e6..1b3a7cf 100644
--- a/uniapp/youjiala/src/pages/tab/mine.vue
+++ b/uniapp/youjiala/src/pages/tab/mine.vue
@@ -1,43 +1,172 @@
-
-
-
- 我的
-
-
- 测试页,功能待接入
-
+
+
+
+
+
+ {{ t("MinePage.themeTitle") }}
+
+
+
+
+
+
+
+ {{ theme.label }}
+
+
+
+
+ {{ t("MinePage.themeHint") }}
+
+
+
+
+
+ {{ t("MinePage.darkTitle") }}
+
+
+
+
+ {{ opt.label }}
+
+
+
+
diff --git a/uniapp/youjiala/src/router/nav-guard.ts b/uniapp/youjiala/src/router/nav-guard.ts
index b3958e5..a37be95 100644
--- a/uniapp/youjiala/src/router/nav-guard.ts
+++ b/uniapp/youjiala/src/router/nav-guard.ts
@@ -34,6 +34,7 @@ function isLoggedIn(): boolean {
*
* 顺序即语义:地址不存在(404) -> 没登录(401) -> 没权限(403)
* 403 必须排在 401 之后:未登录时权限档案多半也是空的,先给 401 才谈得上"去登录"
+ * 注意 error401 只是"没令牌"的判定结果,落点由 dispatchBlocked 决定(直接去登录页,不经过 401 页)
*/
function evaluate(url: string): ErrorPageName | null {
const name: PageName | null = resolvePageName(url);
@@ -52,6 +53,20 @@ function evaluate(url: string): ErrorPageName | null {
return null;
}
+/**
+ * 被拦地址的统一分派:
+ * - 没令牌(401) → 直接去登录表单(goLoginWithRedirect 带 redirect,登录成功回原页),
+ * 不再多一跳 401 错误页——首页场景下"未登录=去登录",401 页保留给手动访问 /error/page401 等场景;
+ * - 其余(403 等)→ 对应错误页
+ */
+function dispatchBlocked(blocked: ErrorPageName, from: string): void {
+ if (blocked === "error401") {
+ goLoginWithRedirect(from);
+ return;
+ }
+ jumpToError(blocked, from);
+}
+
/** 跳错误页;redirectTo 保住返回栈("返回上一页"还有内容),页面栈为空时退化成 reLaunch */
function jumpToError(blocked: ErrorPageName, from: string): void {
const url = errorPageUrl(blocked, from);
@@ -81,7 +96,7 @@ export function ensureCurrentPageAuthorized(): boolean {
}
// getCurrentPages 只会给出真实路径,而 ?from= 是给用户看的:换成当前端的地址形态
const name = resolvePageName(path);
- jumpToError(blocked, name ? pageUrl(name) : path);
+ dispatchBlocked(blocked, name ? pageUrl(name) : path);
return false;
}
@@ -95,7 +110,7 @@ export function setupNavGuard(): void {
if (!blocked) {
return true;
}
- jumpToError(blocked, url);
+ dispatchBlocked(blocked, url);
// 取消本次导航
return false;
},
@@ -128,8 +143,7 @@ export function setupUnknownPathFallback(): void {
* redirect 与 from 一样不做预编码,理由见 routes.ts 的 errorPageUrl
*
* 进登录页前先清登录态:走到这里都是"有意去登录/换号"(401 去登录、403 重新登录),
- * 旧会话整体作废——否则登录页的冷启动自动放行(hasLoginSession)会把这个还带着令牌的
- * 用户直接弹回原页,403"重新登录"根本够不到登录表单。
+ * 旧会话整体作废,登录页始终是干净的换号入口(登录页已不再自动放行跳转,清态是进一步兜底)。
*/
export function goLoginWithRedirect(from?: string): void {
clearLoginState();
diff --git a/uniapp/youjiala/src/router/routes.ts b/uniapp/youjiala/src/router/routes.ts
index 17f0f62..490b457 100644
--- a/uniapp/youjiala/src/router/routes.ts
+++ b/uniapp/youjiala/src/router/routes.ts
@@ -71,11 +71,11 @@ export const PAGE_PERMISSIONS: Partial> = {
// 底部导航 5 个 tab:真实权限尚未定义,按当前要求统一用 UserOperation 占位。
// 注意 dashboard 也被登记后,登录落地页(首页)会开始按 UserOperation 做路由级校验;
// 路由级是 fail-open(档案缺失/为空时放行),真正的边界仍在后端。
- dashboard: "UserOperation",
- fileTab: "UserOperation",
- mediaTab: "UserOperation",
- shareTab: "UserOperation",
- mineTab: "UserOperation",
+ dashboard: "DmsFile",
+ fileTab: "DmsFile",
+ mediaTab: "DmsFile",
+ shareTab: "DmsFile",
+ mineTab: "DmsFile",
};
/**
@@ -147,12 +147,13 @@ export function resolvePageName(url: string): PageName | null {
}
const path = normalizePath(url);
const names = Object.keys(PAGE_PATHS) as PageName[];
- // "/" 是 H5 入口页(登录页)的路由 path,不对应任何字面配置
+ // "/" 是 H5 入口页的路由 path,不对应任何字面配置——指向 pages.json 的第一个页面(当前为 dashboard)。
+ // 换入口页必须两处同步改:pages.json 的 pages 顺序 + 这里,否则未登录判断/redirect 校验会认错页面
if (path === "/") {
- return "login";
+ return "dashboard";
}
const hit = names.find(
- name => path === toRealPath(name) || path === toCleanPath(name),
+ (name) => path === toRealPath(name) || path === toCleanPath(name),
);
return hit ?? null;
}
@@ -190,8 +191,7 @@ export function decodeQueryPath(value: unknown): string {
}
try {
return decodeURIComponent(raw);
- }
- catch {
+ } catch {
return raw;
}
}
diff --git a/uniapp/youjiala/src/static/logo.png b/uniapp/youjiala/src/static/logo.png
index ffeb00e..a4a1d10 100644
Binary files a/uniapp/youjiala/src/static/logo.png and b/uniapp/youjiala/src/static/logo.png differ