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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ 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 @@ 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