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 index cc57fac..aa7d843 100644 --- a/uniapp/youjiala/src/components/file-grid-list/file-grid-list.vue +++ b/uniapp/youjiala/src/components/file-grid-list/file-grid-list.vue @@ -84,11 +84,24 @@ function onCardTap(file: FileItem): void { - - - + + + + + + + + + + + + - @@ -165,6 +178,23 @@ function onCardTap(file: FileItem): void { padding: 36rpx 12rpx 0; } +/* 图标区包装:角标 absolute 定位的参照系 */ +.fgl-card__icon-wrap { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +/* 共享角标组:压在图标右上角(旧 MyComputerFamily badge 同位) */ +.fgl-card__badges { + position: absolute; + top: -8rpx; + right: -44rpx; + display: flex; + gap: 4rpx; +} + /* ---- 纯 CSS 文件夹:蓝色渐变圆角主体 + 左上页签 ---- */ .fgl-card__folder { position: relative; @@ -263,6 +293,12 @@ function onCardTap(file: FileItem): void { border-radius: 6rpx 6rpx 0 0; } +/* 列表态图标小(72rpx),角标组贴紧一点不出格 */ +.file-grid-list--list .fgl-card__badges { + top: -12rpx; + right: -8rpx; +} + .file-grid-list--list .fgl-card__info { flex: 1; min-width: 0; 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 index 9962b1a..4c8cc56 100644 --- a/uniapp/youjiala/src/components/file-tab-filter/file-tab-filter.vue +++ b/uniapp/youjiala/src/components/file-tab-filter/file-tab-filter.vue @@ -36,6 +36,11 @@ defineProps({ type: Boolean, default: true, }, + /** 多选开关按钮:无批量动作的页面(如共享大厅)传 false */ + showSelect: { + type: Boolean, + default: true, + }, }); const emit = defineEmits<{ @@ -77,8 +82,8 @@ const sortIconDark = sortGlyph("#1f2329"); - - + + diff --git a/uniapp/youjiala/src/components/folder-name-dialog/folder-name-dialog.vue b/uniapp/youjiala/src/components/folder-name-dialog/folder-name-dialog.vue index 787a9b1..1dbaae5 100644 --- a/uniapp/youjiala/src/components/folder-name-dialog/folder-name-dialog.vue +++ b/uniapp/youjiala/src/components/folder-name-dialog/folder-name-dialog.vue @@ -34,6 +34,11 @@ const props = defineProps({ type: String, default: "", }, + /** 输入框占位文案:缺省沿用「新建文件夹」语义,群描述/申请原因等场景由页面覆写 */ + placeholder: { + type: String, + default: "", + }, }); const emit = defineEmits<{ @@ -75,8 +80,9 @@ function onConfirm(): void { {{ props.title || t("FilePage.addFolder") }} - + diff --git a/uniapp/youjiala/src/components/folder-share-picker/folder-share-picker.vue b/uniapp/youjiala/src/components/folder-share-picker/folder-share-picker.vue new file mode 100644 index 0000000..d27aa34 --- /dev/null +++ b/uniapp/youjiala/src/components/folder-share-picker/folder-share-picker.vue @@ -0,0 +1,392 @@ + + + + + diff --git a/uniapp/youjiala/src/components/page-navbar/page-navbar.vue b/uniapp/youjiala/src/components/page-navbar/page-navbar.vue index f6f130a..c52961d 100644 --- a/uniapp/youjiala/src/components/page-navbar/page-navbar.vue +++ b/uniapp/youjiala/src/components/page-navbar/page-navbar.vue @@ -47,6 +47,11 @@ const props = defineProps({ type: Boolean, default: true, }, + /** 搜索图标开关:无搜索场景的页面/层级传 false(如共享大厅顶层群组卡列表) */ + showSearchToggle: { + type: Boolean, + default: true, + }, }); const emit = defineEmits<{ @@ -78,8 +83,8 @@ const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_ {{ props.transferring > 99 ? "99+" : props.transferring }} - - + + diff --git a/uniapp/youjiala/src/components/photo-slide-player/photo-slide-player.vue b/uniapp/youjiala/src/components/photo-slide-player/photo-slide-player.vue new file mode 100644 index 0000000..2a03da3 --- /dev/null +++ b/uniapp/youjiala/src/components/photo-slide-player/photo-slide-player.vue @@ -0,0 +1,1358 @@ + + + + + diff --git a/uniapp/youjiala/src/components/video-player/video-player.vue b/uniapp/youjiala/src/components/video-player/video-player.vue new file mode 100644 index 0000000..56ed232 --- /dev/null +++ b/uniapp/youjiala/src/components/video-player/video-player.vue @@ -0,0 +1,1107 @@ + + + + + diff --git a/uniapp/youjiala/src/components/vinyl-audio-player/vinyl-audio-player.vue b/uniapp/youjiala/src/components/vinyl-audio-player/vinyl-audio-player.vue new file mode 100644 index 0000000..a52ae0d --- /dev/null +++ b/uniapp/youjiala/src/components/vinyl-audio-player/vinyl-audio-player.vue @@ -0,0 +1,1245 @@ + + + + + diff --git a/uniapp/youjiala/src/composables/index.ts b/uniapp/youjiala/src/composables/index.ts index 4e06709..4145d72 100644 --- a/uniapp/youjiala/src/composables/index.ts +++ b/uniapp/youjiala/src/composables/index.ts @@ -3,6 +3,10 @@ export * from "./useAdminMenu"; export * from "./useAdminPick"; export * from "./useAuthGuard"; export * from "./useDmsFiles"; +export * from "./useFamilyGroups"; export * from "./useLang"; +export * from "./useMediaShow"; +export * from "./usePublishAlbums"; +export * from "./useShareHall"; export * from "./useThemeGradient"; export * from "./useTrashFiles"; diff --git a/uniapp/youjiala/src/composables/useDmsFiles.ts b/uniapp/youjiala/src/composables/useDmsFiles.ts index 126fa78..8efe320 100644 --- a/uniapp/youjiala/src/composables/useDmsFiles.ts +++ b/uniapp/youjiala/src/composables/useDmsFiles.ts @@ -95,6 +95,21 @@ export function formatSize(bytes: number | undefined): string { return `${i === 0 ? value : value.toFixed(1)} ${units[i]}`; } +/** 卡片时间文案(文件页/回收站/共享大厅共用):今天「{todayLabel} HH:mm」,其余 YYYY-MM-DD */ +export function formatModifiedLabel(time: number, todayLabel: string): string { + if (!time) { + return ""; + } + const date = new Date(time); + const now = new Date(); + const pad = (n: number) => String(n).padStart(2, "0"); + // if/else 写法防编辑器折行 + if (date.toDateString() === now.toDateString()) { + return `${todayLabel} ${pad(date.getHours())}:${pad(date.getMinutes())}`; + } + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; +} + export function useDmsFiles() { const { t } = useLocale(); @@ -112,18 +127,6 @@ export function useDmsFiles() { 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, @@ -133,7 +136,7 @@ export function useDmsFiles() { sizeText: formatSize( record.FileSize ? record.FileSize * 1024 : undefined, ), - modifiedAt, + modifiedAt: formatModifiedLabel(time, t("FilePage.todayLabel")), sortTime: time, path: record.FileName, }; diff --git a/uniapp/youjiala/src/composables/useFamilyGroups.ts b/uniapp/youjiala/src/composables/useFamilyGroups.ts new file mode 100644 index 0000000..349f19c --- /dev/null +++ b/uniapp/youjiala/src/composables/useFamilyGroups.ts @@ -0,0 +1,353 @@ +/** + * 家人群组数据源:把旧 WebVue 的 FamilyGroupEnquiry.vue(列表 + 申请加入)与 + * FamilyGroupOperation.vue(建群/改群 + 共享目录 + 待审成员 + 正式成员 + 目录权限) + * 两套页面的调用链合并到一个 composable,供「共享」tab 页两态复用。 + * + * 后端契约(CloudBuilder.Master.Service/FamilyGroupService.auto.cs 实测): + * 1. 列表 FindBySearchCriteria -> data.Datas:后端自动追加「有效 且(公开 或 我是成员)」, + * 所以列表天然= 公开群 + 我的群,不用前端再筛;关键字挂 [family_local_name text contain]。 + * 2. 详情 FindByKey(familyName) -> FamilyGroupData: + * - 非管理员时 FamilyGroupPermissions=null、部分 Managers=null、Folders 的 IsSharePermission 标记有无权限; + * - 管理员才有 FamilyGroupRequests(待审)与完整 Managers/Permissions; + * - 隐私群非成员返回空 FamilyGroupData(FamilyGroup.FamilyName 为空串当作没数据)。 + * 3. 建群 Insert / 改群 Update:整个 FamilyGroupData 回传,目录靠 FolderId 匹配(新项 FolderId 递增、 + * CreatedBy 空串=后端插入)、成员靠 IsManager 布尔改 ManagerIndc、权限在 Update 里不处理(走即时接口)。 + * 4. 即时动作接口: + * - 申请加入 InsertFamilyGroupManager({FamilyName, Notice})(用户名后端取当前登录者,忽略入参); + * - 同意 UpdateFamilyGroupManager({FamilyName, UserName, ValidIndc:"Y"}); + * - 拒绝/移除成员 DeleteFamilyGroupManager({FamilyName, UserName}); + * - 分配目录权限 InsertFamilyGroupPermission([{FamilyName, FolderName, UserName}]); + * - 解散群 Delete(familyName)(后端校验必须是管理员)。 + * 5. 删已入库的共享目录前先 FindDmsFolderMapByFolderName(folderName) 查有无内容,有则拦(旧系统同逻辑)。 + */ +import type { FamilyGroupData } from "@/data/auto/familyGroupData"; +import type { FamilyGroupEntity } from "@/data/auto/familyGroupEntity"; +import type { FamilyGroupFolderDisplayData } from "@/data/auto/familyGroupFolderDisplayData"; +import type { FamilyGroupManagerDisplayData } from "@/data/auto/familyGroupManagerDisplayData"; +import { useLocale } from "uview-pro"; +import { computed, ref } from "vue"; +import { FamilyGroupData as FamilyGroupDataModel } from "@/data/auto/familyGroupData"; +import { FamilyGroupFolderDisplayData as FolderRow } from "@/data/auto/familyGroupFolderDisplayData"; +import { FamilyGroupManagerEntity } from "@/data/auto/familyGroupManagerEntity"; +import { FamilyGroupPermissionEntity } from "@/data/auto/familyGroupPermissionEntity"; +import { FamilyGroupSearchCriteria } from "@/data/auto/familyGroupSearchCriteria"; +import { SearchFilter } from "@/data/auto/searchFilter"; +import dmsFileService from "@/service/master/dmsFileService"; +import familyGroupService from "@/service/master/familyGroupService"; + +/** 列表一次拉全(旧页面分页在移动端不需要,划到底再考虑) */ +const FIND_LIMIT = 999999; + +/** 创建日期只取年月日:卡片展示用,不需要 formatModifiedLabel 的「今天」语义 */ +function formatDate(value: Date | string | undefined): string { + if (!value) { + return ""; + } + const time = + typeof value === "string" ? new Date(value).getTime() : value.getTime(); + if (!time) { + return ""; + } + const d = new Date(time); + const pad = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + +export function useFamilyGroups() { + const { t } = useLocale(); + + // ===== 列表态 ===== + const groups = ref([]); + const loading = ref(false); + const errorText = ref(""); + + // ===== 详情态(FamilyGroupOperation)===== + const detail = ref(new FamilyGroupDataModel()); + /** 正在新建(还没落库的空白群):详情按编辑态渲染,保存走 Insert */ + const creating = ref(false); + /** 编辑态:字段可改、目录/成员/权限可增删,出现保存/取消 */ + const isEdit = ref(false); + // 当前登录用户名:判断「我是不是某成员」「我还没加入」用 + const myName = ref(""); + /** 是否停在群管理详情(false=群组列表) */ + const inDetail = computed( + () => !!detail.value.FamilyGroup?.FamilyName || creating.value, + ); + + /** 当前用户是否群管理员(后端 FindByKey 权威回填,比前端猜可靠) */ + const isManager = computed(() => !!detail.value.IsManager); + /** 是否已是成员:正式成员名单里有我(非管理员时后端只回我自己的那条) */ + const isMember = computed(() => + (detail.value.FamilyGroupManagers ?? []).some( + (m) => m.UserName === myName.value, + ), + ); + + function setMyUser(name: string): void { + myName.value = name; + } + + // ===== 列表 ===== + async function loadList(keyword = ""): Promise { + loading.value = true; + errorText.value = ""; + try { + const criteria = new FamilyGroupSearchCriteria(); + const filters: SearchFilter[] = []; + if (keyword.trim()) { + const kw = new SearchFilter(); + kw.Column = "family_local_name"; + kw.Type = "text"; + kw.Operator = "contain"; + kw.Value = keyword.trim(); + filters.push(kw); + } + criteria.Filter = filters; + criteria.limit = FIND_LIMIT; + criteria.page = 1; + criteria.GridColumns = []; + const data = await familyGroupService.FindBySearchCriteria(criteria); + groups.value = data?.Datas ?? []; + } catch (error: any) { + groups.value = []; + errorText.value = error?.message || t("FilePage.loadFailed"); + } finally { + loading.value = false; + } + } + + // ===== 进详情 ===== + async function openGroup(familyName: string): Promise { + loading.value = true; + errorText.value = ""; + try { + const data = await familyGroupService.FindByKey(familyName); + // 隐私群非成员 / 记录不存在:后端回空壳(FamilyName 空串),页面按「无权限查看」处理 + if (!data?.FamilyGroup?.FamilyName) { + detail.value = new FamilyGroupDataModel(); + errorText.value = t("SharePage.noAccess"); + return; + } + detail.value = normalize(data); + } catch (error: any) { + errorText.value = error?.message || t("FilePage.loadFailed"); + } finally { + loading.value = false; + } + } + + /** 后端可能把非管理员的 Managers/Permissions/Requests 置 null,统一补空数组供模板遍历 */ + function normalize(data: FamilyGroupData): FamilyGroupData { + data.FamilyGroupFolders = data.FamilyGroupFolders ?? []; + data.FamilyGroupManagers = data.FamilyGroupManagers ?? []; + data.FamilyGroupRequests = data.FamilyGroupRequests ?? []; + data.FamilyGroupPermissions = data.FamilyGroupPermissions ?? []; + return data; + } + + /** 退出详情回列表:可能刚改过群,重拉列表 */ + async function exitDetail(): Promise { + detail.value = new FamilyGroupDataModel(); + creating.value = false; + isEdit.value = false; + await loadList(); + } + + // ===== 新建群 ===== + function startCreate(): void { + const blank = new FamilyGroupDataModel(); + blank.IsOpen = true; + blank.IsAdd = true; + detail.value = blank; + creating.value = true; + isEdit.value = true; + } + + /** 进入编辑(仅管理员):快照原值便于取消还原 */ + function startEdit(): void { + isEdit.value = true; + } + + /** 取消编辑:新建态直接退出详情,改群态重新拉详情丢弃本地改动 */ + async function cancelEdit(): Promise { + if (creating.value) { + await exitDetail(); + return; + } + isEdit.value = false; + await openGroup(detail.value.FamilyGroup.FamilyName); + } + + // ===== 共享目录(挂在 FamilyGroupData.FamilyGroupFolders,保存时后端落库)===== + function addFolder(localName: string, notice = ""): void { + const folders = detail.value.FamilyGroupFolders ?? []; + const maxId = folders.reduce((max, f) => Math.max(max, f.FolderId || 0), 0); + const row = new FolderRow(); + row.FolderId = maxId + 1; + row.FolderLocalName = localName; + row.Notice = notice; + folders.push(row); + } + + /** + * 删目录:已入库的(有 FolderName)先查有无文件内容,有则拦; + * 连带把该目录的本地权限记录一起摘掉(旧 deleteFamilyGroupFolder 同逻辑),保存生效。 + */ + async function removeFolder( + folderId: number, + folderName: string, + ): Promise { + if (folderName) { + const count = + await dmsFileService.FindDmsFolderMapByFolderName(folderName); + if (count && count > 0) { + throw new Error(t("SharePage.folderNotEmpty")); + } + } + detail.value.FamilyGroupFolders = ( + detail.value.FamilyGroupFolders ?? [] + ).filter((f) => f.FolderId !== folderId); + if (folderName) { + detail.value.FamilyGroupPermissions = ( + detail.value.FamilyGroupPermissions ?? [] + ).filter((p) => p.FolderName !== folderName); + } + } + + // ===== 成员 / 审核 / 权限 ===== + /** 同意申请:即时接口,成功后重拉详情 */ + async function approveRequest(userName: string): Promise { + const entity = new FamilyGroupManagerEntity(); + entity.FamilyName = familyNameOf(); + entity.UserName = userName; + entity.ValidIndc = "Y"; + await familyGroupService.UpdateFamilyGroupManager(entity); + await openGroup(familyNameOf()); + } + + /** 拒绝申请 / 移除成员:都走 DeleteFamilyGroupManager(拒绝的是待审那条,移除的是正式那条,同一接口) */ + async function removeMember(userName: string): Promise { + const entity = new FamilyGroupManagerEntity(); + entity.FamilyName = familyNameOf(); + entity.UserName = userName; + await familyGroupService.DeleteFamilyGroupManager(entity); + await openGroup(familyNameOf()); + } + + /** 设/取消管理员:编辑态改本地 IsManager,保存群时后端 SaveFamilyGroupManagers 落 ManagerIndc */ + function toggleManager(userName: string): void { + const target = (detail.value.FamilyGroupManagers ?? []).find( + (m) => m.UserName === userName, + ); + if (target) { + target.IsManager = !target.IsManager; + } + } + + /** 分配目录权限给某成员:即时接口,成功后重拉详情 */ + async function assignPermission( + folderName: string, + userName: string, + ): Promise { + const entity = new FamilyGroupPermissionEntity(); + entity.FamilyName = familyNameOf(); + entity.FolderName = folderName; + entity.UserName = userName; + await familyGroupService.InsertFamilyGroupPermission([entity]); + await openGroup(familyNameOf()); + } + + /** 申请加入当前群(非成员,公开且可申请的群才会露这个入口) */ + async function applyJoin(notice: string): Promise { + const entity = new FamilyGroupManagerEntity(); + entity.FamilyName = familyNameOf(); + entity.Notice = notice; + await familyGroupService.InsertFamilyGroupManager(entity); + } + + // ===== 保存 / 解散 ===== + async function save(): Promise { + const submitted = buildSubmitData(); + const result = creating.value + ? await familyGroupService.Insert(submitted) + : await familyGroupService.Update(submitted); + detail.value = normalize(result); + creating.value = false; + isEdit.value = false; + } + + /** 群名去空白;提交与本地判定都以此为准 */ + function familyNameOf(): string { + return detail.value.FamilyGroup?.FamilyName?.trim() ?? ""; + } + + /** 组装提交体:OpenIndc/AddIndc 后端从 IsOpen/IsAdd 派生,这里只保证布尔与名称就绪、数组归一 */ + function buildSubmitData(): FamilyGroupData { + const data = detail.value; + data.FamilyGroup.FamilyLocalName = ( + data.FamilyGroup.FamilyLocalName || "" + ).trim(); + data.FamilyGroupFolders = data.FamilyGroupFolders ?? []; + data.FamilyGroupManagers = data.FamilyGroupManagers ?? []; + data.FamilyGroupRequests = data.FamilyGroupRequests ?? []; + data.FamilyGroupPermissions = data.FamilyGroupPermissions ?? []; + return data; + } + + async function dissolve(): Promise { + await familyGroupService.Delete(familyNameOf()); + await exitDetail(); + } + + /** 某目录已授权的成员名(分配权限时从候选里排掉,避免重复授权) */ + function authorizedUsers(folderName: string): string[] { + return (detail.value.FamilyGroupPermissions ?? []) + .filter((p) => p.FolderName === folderName) + .map((p) => p.UserName); + } + + return { + // state + groups, + loading, + errorText, + detail, + inDetail, + creating, + isEdit, + isManager, + isMember, + setMyUser, + // list + loadList, + openGroup, + exitDetail, + // create/edit + startCreate, + startEdit, + cancelEdit, + // folders + addFolder, + removeFolder, + // members / requests / permissions + approveRequest, + removeMember, + toggleManager, + assignPermission, + applyJoin, + // save / dissolve + save, + dissolve, + // helpers + authorizedUsers, + formatDate, + }; +} + +/** 详情遍历常用的展示类型转出,页面 import type 用 */ +export type { + FamilyGroupData, + FamilyGroupEntity, + FamilyGroupFolderDisplayData, + FamilyGroupManagerDisplayData, +}; diff --git a/uniapp/youjiala/src/composables/useMediaShow.ts b/uniapp/youjiala/src/composables/useMediaShow.ts new file mode 100644 index 0000000..12f087f --- /dev/null +++ b/uniapp/youjiala/src/composables/useMediaShow.ts @@ -0,0 +1,165 @@ +/** + * 展览播放器数据装载:把旧 WebVue 的 Preview.vue / PreviewAudio.vue / PreviewVideo.vue + * 三页 onMounted 的取数逻辑收敛成一个 kind 参数化 composable,供三个展示页复用。 + * + * 三态取数(照抄旧页语义): + * 1. mode=list:key 当作「目录路径」,走 DMS 查询 [FilePath single=key] + [FileType single=P/A/V] + * -> 文件页 album/musicHall/videoHall 快捷入口用,展览当前目录里的全部该类文件; + * 2. 否则 key 非空:按专辑编号取明细(photo 用 FindPublishPhotoDetailByPublishName, + * audio/video 无该方法 -> FindByKey(key).Details); + * 3. 否则(无 key):取默认专辑(photo FindPublishPhotoDetail 全量,audio/video FindPublishXxxDefault)。 + * + * 后端把同一素材给两个地址:缩略图/转码给 Formatted*,原图/原码给 Url/FileName。 + * 幻灯轮播与列表用缩略图(加载快),精看/下载/播放用原图原码。视频原码 Url 对专辑明细 + * 存的是 OrderKey,需优先取 FormattedUrl/FormattedM3u8。 + */ +import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria"; +import { DmsFileUploadViewDisplayData as UploadView } from "@/data/auto/dmsFileUploadViewDisplayData"; +import { SearchFilter } from "@/data/auto/searchFilter"; +import { FileTypePolicy } from "@/policy/custom/fileTypePolicy"; +import dmsFileService from "@/service/master/dmsFileService"; +import publishAudioService from "@/service/master/publishAudioService"; +import publishPhotoService from "@/service/master/publishPhotoService"; +import publishVideoService from "@/service/master/publishVideoService"; + +const FIND_LIMIT = 999999; + +/** 幻灯片项:舞台/列表用 thumb,精看/下载用 full */ +export interface SlideItem { + url: string; + thumb: string; + full: string; + name?: string; +} + +/** 音轨项 */ +export interface TrackItem { + url: string; + pic?: string; + title?: string; + lrc?: string; + name?: string; +} + +/** 视频项:url=原码,m3u8=转码 HLS */ +export interface VideoItem { + url: string; + m3u8?: string; + pic?: string; + title?: string; + name?: string; +} + +/** 去扩展名得展示名 */ +function baseName(path: string): string { + return String(path || "").replace(/\.[^./\\]+$/, ""); +} + +/** 构造 DMS 目录+类型过滤条件(mode=list) */ +function listCriteria(path: string, fileType: string): DmsFileSearchCriteria { + const criteria = new DmsFileSearchCriteria(); + const filters: SearchFilter[] = []; + const pathMatch = new SearchFilter(); + pathMatch.Column = UploadView.DB_FILE_PATH; + pathMatch.Type = "SingleSelection"; + pathMatch.Operator = "single"; + pathMatch.Value = path; + filters.push(pathMatch); + const typeMatch = new SearchFilter(); + typeMatch.Column = UploadView.DB_FILE_TYPE; + typeMatch.Type = "SingleSelection"; + typeMatch.Operator = "single"; + typeMatch.Value = fileType; + filters.push(typeMatch); + criteria.Filter = filters; + criteria.limit = FIND_LIMIT; + criteria.page = 1; + criteria.GridColumns = []; + return criteria; +} + +async function listRows(fileType: string, path: string): Promise { + const data = await dmsFileService.FindBySearchCriteria( + listCriteria(path, fileType), + ); + return data?.Datas ?? []; +} + +export function useMediaShow() { + /** 相册展览条目 */ + async function loadPhotos(mode: string, key: string): Promise { + if (mode === "list") { + const rows = await listRows(FileTypePolicy.PHOTO, key); + return rows.map((r: any) => ({ + url: r.FormattedPic || r.Url, + thumb: r.FormattedPic || r.Url, + full: r.Url || r.FormattedPic, + name: baseName(r.OrgFileName || r.Url), + })); + } + let details: any[] = []; + if (key) { + details = + await publishPhotoService.FindPublishPhotoDetailByPublishName(key); + } else { + details = await publishPhotoService.FindPublishPhotoDetail(); + } + return (details ?? []).map((d: any) => ({ + url: d.FormattedPic || d.FileName, + thumb: d.FormattedPic || d.FileName, + full: d.FileName || d.FormattedPic, + name: baseName(d.FileName) || d.Notice || undefined, + })); + } + + /** 音乐厅曲目 */ + async function loadTracks(mode: string, key: string): Promise { + if (mode === "list") { + const rows = await listRows(FileTypePolicy.AUDIO, key); + return rows.map((r: any) => ({ + url: r.Url, + pic: r.FormattedPic || "", + title: r.OrgFileName || baseName(r.Url), + name: baseName(r.OrgFileName || r.Url), + })); + } + const data = key + ? await publishAudioService.FindByKey(key) + : await publishAudioService.FindPublishAudioDefault(); + const details = (data?.Details ?? []) as any[]; + return details.map((d: any) => ({ + url: d.FormattedUrl || d.Url, + pic: d.FormattedPic || d.Pic || "", + title: d.Title || baseName(d.Url), + lrc: d.FormattedLrc || d.Lrc || "", + name: d.Title || baseName(d.Url), + })); + } + + /** 影视厅片源 */ + async function loadVideos(mode: string, key: string): Promise { + if (mode === "list") { + const rows = await listRows(FileTypePolicy.VIDEO, key); + return rows.map((r: any) => ({ + url: r.Url, + m3u8: r.FormattedM3u8 || "", + pic: r.FormattedPic || "", + title: r.OrgFileName || baseName(r.Url), + name: baseName(r.OrgFileName || r.Url), + })); + } + const data = key + ? await publishVideoService.FindByKey(key) + : await publishVideoService.FindPublishVideoDefault(); + const details = (data?.Details ?? []) as any[]; + return details.map((d: any) => ({ + url: d.FormattedUrl || d.Url, + m3u8: d.FormattedM3u8 || "", + pic: d.FormattedPic || d.Pic || "", + title: d.Title || baseName(d.Url), + name: d.Title || baseName(d.Url), + })); + } + + return { loadPhotos, loadTracks, loadVideos }; +} diff --git a/uniapp/youjiala/src/composables/usePublishAlbums.ts b/uniapp/youjiala/src/composables/usePublishAlbums.ts new file mode 100644 index 0000000..2754b59 --- /dev/null +++ b/uniapp/youjiala/src/composables/usePublishAlbums.ts @@ -0,0 +1,407 @@ +/** + * 多媒体「专辑」数据源:把旧 WebVue 的三套专辑管理页 + * PublishPhotoEnquiry + PublishPhotoOperation(相册管理) + * PublishAudioEnquiry + PublishAudioOperation(音乐专辑) + * PublishVideoEnquiry + PublishVideoOperation(影视专辑) + * 融合成一个 kind 参数化 composable,供底部「多媒体」tab 单页三段式两态复用。 + * + * 关键契约(照抄旧 Operation,勿自作主张): + * 1. 列表 FindBySearchCriteria -> photo 用 data.Publishs、audio/video 用 data.Datas; + * 后端已按「有效 且(公开 或 我是成员)」过滤,前端不再筛。 + * 2. 详情 FindByKey(publishName) -> PublishXxxData { PublishXxx, Details[], FamilyGroups[] }, + * 明细随 FindByKey 一并回,不单独查明细(展览页才用 FindPublishPhotoDetailByPublishName)。 + * 3. 编辑:明细上移/下移/删除、公开群组增删——全部是本地改 data,最后整包 Insert/Update 落库 + * (旧 upSon/downSon/deleteSon/addFamilyGroup/deleteFamilyGroup 均无独立后端接口)。 + * 4. 保存判定:PublishXxx.CreatedBy 有值走 Update,否则 Insert(新专辑编号由后端生成)。 + * 5. 归属:OwnerName !== 当前登录名 -> 只能查看,不能改/删(旧系统同逻辑)。 + * 6. 专辑「加入新内容」不在本页——沿用文件页发布链(file.vue onPublishConfirm)。 + * + * 三套实体(PublishPhoto/Audio/VideoEntity)公共字段命名一致,故用统一视图访问; + * 差异只在顶层属性名(PublishPhoto/PublishAudio/PublishVideo)与列表键(Publishs/Datas)。 + */ +import { useLocale } from "uview-pro"; +import { computed, ref } from "vue"; +import { PublishAudioData } from "@/data/auto/publishAudioData"; +import { PublishAudioSearchCriteria } from "@/data/auto/publishAudioSearchCriteria"; +import { PublishPhotoData } from "@/data/auto/publishPhotoData"; +import { PublishPhotoSearchCriteria } from "@/data/auto/publishPhotoSearchCriteria"; +import { PublishVideoData } from "@/data/auto/publishVideoData"; +import { PublishVideoSearchCriteria } from "@/data/auto/publishVideoSearchCriteria"; +import { SearchFilter } from "@/data/auto/searchFilter"; +import publishAudioService from "@/service/master/publishAudioService"; +import publishPhotoService from "@/service/master/publishPhotoService"; +import publishVideoService from "@/service/master/publishVideoService"; + +/** 三种专辑类型 */ +export type PublishKind = "photo" | "audio" | "video"; + +/** 三套实体的公共可编辑字段(命名逐字一致) */ +export interface PublishAlbumEntity { + PublishName: string; + PublishLocalName: string; + Notice: string; + OpenIndc: string; + DefaultIndc: string; + OwnerName: string; + CreatedBy: string; + OrderBy: number; + [key: string]: any; +} + +/** 明细行:photo 用 FileName(原图)+ FormattedPic(缩略图);audio/video 用 Url/Title/Pic/Lrc + Formatted* */ +export interface PublishAlbumDetail { + DetailId: number; + OrderBy: number; + Notice: string; + [key: string]: any; +} + +/** 公开群组行 */ +export interface PublishAlbumGroup { + FamilyName: string; + FamilyLocalName?: string; + Notice?: string; + [key: string]: any; +} + +/** 列表一次拉全(旧页面分页在移动端不需要) */ +const FIND_LIMIT = 999999; + +/** 五个增删改查方法在三服务里签名一致,抽成最小接口供适配器持有 */ +interface PublishCrudService { + FindByKey: (key: string) => Promise; + FindBySearchCriteria: (criteria: any) => Promise; + Insert: (data: any) => Promise; + Update: (data: any) => Promise; + Delete: (key: string) => Promise; +} + +interface KindAdapter { + /** 顶层实体属性名 */ + entityKey: "PublishPhoto" | "PublishAudio" | "PublishVideo"; + /** 列表结果键:photo=Publishs,audio/video=Datas */ + listKey: "Publishs" | "Datas"; + /** 查询标题的列名(三表 DB 列均为 publish_local_name) */ + titleColumn: string; + service: PublishCrudService; + /** 空白 Data 工厂(新建时用,带默认实体 + 空数组) */ + newBlank: () => Record; + /** 空白查询条件工厂 */ + newCriteria: () => any; +} + +const ADAPTERS: Record = { + photo: { + entityKey: "PublishPhoto", + listKey: "Publishs", + titleColumn: "publish_local_name", + service: publishPhotoService, + newBlank: () => new PublishPhotoData(), + newCriteria: () => new PublishPhotoSearchCriteria(), + }, + audio: { + entityKey: "PublishAudio", + listKey: "Datas", + titleColumn: "publish_local_name", + service: publishAudioService, + newBlank: () => new PublishAudioData(), + newCriteria: () => new PublishAudioSearchCriteria(), + }, + video: { + entityKey: "PublishVideo", + listKey: "Datas", + titleColumn: "publish_local_name", + service: publishVideoService, + newBlank: () => new PublishVideoData(), + newCriteria: () => new PublishVideoSearchCriteria(), + }, +}; + +/** 创建日期只取年月日:卡片展示用 */ +export function formatAlbumDate(value: Date | string | undefined): string { + if (!value) { + return ""; + } + const time = + typeof value === "string" ? new Date(value).getTime() : value.getTime(); + if (!time) { + return ""; + } + const d = new Date(time); + const pad = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + +export function usePublishAlbums() { + const { t } = useLocale(); + + // ===== 当前分段(相册/音乐/影视)===== + const kind = ref("photo"); + const adapter = () => ADAPTERS[kind.value]; + + // ===== 列表态 ===== + const list = ref([]); + const loading = ref(false); + const errorText = ref(""); + + // ===== 编辑态(Operation)===== + /** 当前详情:整包 PublishXxxData(含实体 + Details + FamilyGroups),保存原样回传 */ + const detail = ref | null>(null); + const creating = ref(false); + const isEdit = ref(false); + /** 是否非本人专辑:进详情按后端 OwnerName 判定,决定能否改/删 */ + const notOwner = ref(false); + const myName = ref(""); + + function setMyUser(name: string): void { + myName.value = name; + } + + /** 顶层实体(三 kind 字段命名一致,统一视图) */ + const album = computed( + () => (detail.value ? detail.value[adapter().entityKey] : null) ?? null, + ); + const details = computed( + () => detail.value?.Details ?? [], + ); + const groups = computed( + () => detail.value?.FamilyGroups ?? [], + ); + + /** 是否停在专辑详情(false=专辑列表) */ + const inDetail = computed(() => !!album.value?.PublishName || creating.value); + /** 归属判定:新建态恒可改;已入库需 OwnerName 命中当前登录名 */ + const isOwner = computed( + () => creating.value || (!notOwner.value && !!album.value), + ); + const canAmend = computed(() => inDetail.value && isOwner.value); + const canDelete = computed(() => inDetail.value && isOwner.value); + + // ===== 分段切换 ===== + async function setKind(next: PublishKind): Promise { + if (kind.value === next) { + return; + } + kind.value = next; + // 切分段:退出可能打开的详情,并按新 kind 重新拉列表(否则下方内容不刷新) + detail.value = null; + creating.value = false; + isEdit.value = false; + notOwner.value = false; + await loadList(); + } + + // ===== 列表 ===== + async function loadList(keyword = ""): Promise { + loading.value = true; + errorText.value = ""; + try { + const a = adapter(); + const criteria = a.newCriteria(); + const filters: SearchFilter[] = []; + if (keyword.trim()) { + const kw = new SearchFilter(); + kw.Column = a.titleColumn; + kw.Type = "text"; + kw.Operator = "contain"; + kw.Value = keyword.trim(); + filters.push(kw); + } + criteria.Filter = filters; + criteria.limit = FIND_LIMIT; + criteria.page = 1; + criteria.GridColumns = []; + const data = await a.service.FindBySearchCriteria(criteria); + list.value = (data?.[a.listKey] ?? []) as PublishAlbumEntity[]; + } catch (error: any) { + list.value = []; + errorText.value = error?.message || t("FilePage.loadFailed"); + } finally { + loading.value = false; + } + } + + // ===== 进详情 ===== + async function openAlbum(publishName: string): Promise { + loading.value = true; + errorText.value = ""; + try { + const data = await adapter().service.FindByKey(publishName); + const entity = data?.[adapter().entityKey]; + if (!entity?.PublishName) { + detail.value = null; + errorText.value = t("MediaPage.noAccess"); + return; + } + data.Details = data.Details ?? []; + data.FamilyGroups = data.FamilyGroups ?? []; + notOwner.value = entity.OwnerName !== myName.value; + detail.value = data; + } catch (error: any) { + errorText.value = error?.message || t("FilePage.loadFailed"); + } finally { + loading.value = false; + } + } + + /** 退出详情回列表;keepKind=true 时不清分段(切分段内部调用) */ + async function exitDetail(keepKind = false): Promise { + detail.value = null; + creating.value = false; + isEdit.value = false; + notOwner.value = false; + if (!keepKind) { + await loadList(); + } + } + + // ===== 新建 / 编辑 ===== + function startCreate(): void { + const blank = adapter().newBlank(); + blank.Details = []; + blank.FamilyGroups = []; + detail.value = blank; + creating.value = true; + isEdit.value = true; + notOwner.value = false; + } + + function startEdit(): void { + isEdit.value = true; + } + + async function cancelEdit(): Promise { + if (creating.value) { + await exitDetail(); + return; + } + isEdit.value = false; + await openAlbum(album.value!.PublishName); + } + + // ===== 明细本地增删改(保存整包落库)===== + /** 上移(-1)/下移(+1):与相邻行交换位置,越界不动(旧 upSon/downSon) */ + function moveDetail(index: number, dir: -1 | 1): void { + const rows = detail.value?.Details as PublishAlbumDetail[] | undefined; + if (!rows) { + return; + } + const target = index + dir; + if (target < 0 || target >= rows.length) { + return; + } + const [item] = rows.splice(index, 1); + rows.splice(target, 0, item); + } + + /** 删除明细(旧 deleteSon):按 DetailId 过滤 */ + function removeDetail(detailId: number): void { + if (!detail.value) { + return; + } + detail.value.Details = ( + detail.value.Details as PublishAlbumDetail[] + ).filter((d) => d.DetailId !== detailId); + } + + // ===== 公开群组本地增删(保存整包落库)===== + /** 加入公开群组(旧 addFamilyGroup -> handleSelectFamilyGroup);已存在则忽略 */ + function addGroup( + familyName: string, + familyLocalName = "", + notice = "", + ): void { + if (!detail.value) { + return; + } + const rows = detail.value.FamilyGroups as PublishAlbumGroup[]; + if (rows.some((g) => g.FamilyName === familyName)) { + return; + } + rows.push({ + FamilyName: familyName, + FamilyLocalName: familyLocalName, + Notice: notice, + }); + } + + function removeGroup(familyName: string): void { + if (!detail.value) { + return; + } + detail.value.FamilyGroups = ( + detail.value.FamilyGroups as PublishAlbumGroup[] + ).filter((g) => g.FamilyName !== familyName); + } + + // ===== 保存 / 删除 ===== + async function save(): Promise { + const a = adapter(); + const submitted = detail.value!; + const entity = submitted[a.entityKey] as PublishAlbumEntity; + entity.PublishLocalName = (entity.PublishLocalName || "").trim(); + submitted.Details = submitted.Details ?? []; + submitted.FamilyGroups = submitted.FamilyGroups ?? []; + const result = entity.CreatedBy + ? await a.service.Update(submitted) + : await a.service.Insert(submitted); + result.Details = result.Details ?? []; + result.FamilyGroups = result.FamilyGroups ?? []; + detail.value = result; + notOwner.value = + (result[a.entityKey] as PublishAlbumEntity).OwnerName !== myName.value; + creating.value = false; + isEdit.value = false; + } + + async function dissolve(): Promise { + await adapter().service.Delete(album.value!.PublishName); + await exitDetail(); + } + + /** 专辑当前编号(供展览跳转拼参数) */ + function publishNameOf(): string { + return album.value?.PublishName?.trim() ?? ""; + } + + return { + // kind + kind, + setKind, + // state + list, + loading, + errorText, + detail, + album, + details, + groups, + inDetail, + creating, + isEdit, + isOwner, + canAmend, + canDelete, + setMyUser, + // list / detail + loadList, + openAlbum, + exitDetail, + // create / edit + startCreate, + startEdit, + cancelEdit, + // details + moveDetail, + removeDetail, + // groups + addGroup, + removeGroup, + // save / dissolve + save, + dissolve, + // helpers + publishNameOf, + formatDate: formatAlbumDate, + }; +} diff --git a/uniapp/youjiala/src/composables/useShareHall.ts b/uniapp/youjiala/src/composables/useShareHall.ts new file mode 100644 index 0000000..8a5c72a --- /dev/null +++ b/uniapp/youjiala/src/composables/useShareHall.ts @@ -0,0 +1,258 @@ +/** + * 共享大厅数据源:复刻旧 WebVue FamilyGroupShare.vue 的调用链(D:\Docker\WebVue\src\views\master) + * + * 1. 顶层(大厅):FindFamilyDmsFolders() -> SelectOption[],每个家人群组就是一张「共享目录」卡片, + * 顶层没有任何写操作(不建目录/不上传); + * 2. 进组:FindFamilyBySearchCriteria -> data.Families,Filter 恒挂 + * [FamilyName single 群组] + [FilePath single 当前目录];组记录的 file_path 存的是 + * 父级标识(组根直属 = 群组编号,子目录 = 父目录 FileName),组根传空串会被 + * 后端忽略空值条件把全组所有层级返出(实测子目录被提到组根),故空串转群组编号; + * 关键字追加 [OrgFileName text contain]; + * 3. 下钻:旧 onOpenFolder 双条件 IsDirectoryIndc=Y && PermissionIndc=Y, + * 无权限的目录点进去要提示; + * 4. 组内新建目录:CreateFamilyDmsFolder({ FilePath, FolderLocalName, FamilyName, Notice }) + * (旧 DmsFolderModal 的 isFamilyGroup 分支,不是文件页的 CreateDmsFolder); + * 5. 取消共享:DeleteFamilyDmsFolder(record)(旧 onDelete,摘掉的是共享映射不动原文件)。 + */ +import type { FamilyDmsFileViewDisplayData } from "@/data/auto/familyDmsFileViewDisplayData"; +import type { FileItem, PathNode } from "@/data/custom/fileBrowser"; +import { useLocale } from "uview-pro"; +import { computed, ref } from "vue"; +import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria"; +import { FamilyDmsFileViewDisplayData as FamilyView } from "@/data/auto/familyDmsFileViewDisplayData"; +import { FamilyDmsFileViewEntity } from "@/data/auto/familyDmsFileViewEntity"; +import { SearchFilter } from "@/data/auto/searchFilter"; +import dmsFileService from "@/service/master/dmsFileService"; +import { + formatModifiedLabel, + formatSize, + kindOf, + parseTime, +} from "./useDmsFiles"; + +/** 目录场景一次拉全,与旧页面 pagingParams.pageSize 同值 */ +const FIND_LIMIT = 999999; + +export function useShareHall() { + const { t } = useLocale(); + + /** 大厅顶层:群组卡片(path = FamilyName,id 用序号——群组本身没有 FileId) */ + const groups = ref([]); + /** 当前进入的群组系统名;空串 = 停在大厅顶层 */ + const currentFamily = ref(""); + const files = ref([]); + /** FileId -> 原始共享记录:下钻/取消共享/下载要拿全字段 */ + const records = ref(new Map()); + const loading = ref(false); + const errorText = ref(""); + const currPath = ref(""); + const pathStack = ref([]); + + /** 是否在组内(组根也算):决定导航返回、面包屑与「新建文件夹」按钮显隐 */ + const inGroup = computed(() => !!currentFamily.value); + + async function init(): Promise { + loading.value = true; + errorText.value = ""; + try { + const options = (await dmsFileService.FindFamilyDmsFolders()) ?? []; + groups.value = options.map((opt, index) => ({ + id: index + 1, + name: opt.DisplayName || opt.BindValue, + kind: "folder" as const, + sizeText: "", + modifiedAt: "", + sortTime: 0, + path: opt.BindValue, + })); + } catch (error: any) { + // 错误提示已由 http.interceptor 统一 toast,这里只留页面内可见的失败态 + groups.value = []; + errorText.value = error?.message || t("FilePage.loadFailed"); + } finally { + loading.value = false; + } + } + + function toFileItem(record: FamilyDmsFileViewDisplayData): FileItem { + const time = parseTime(record.CreatedDatetime); + return { + id: record.FileId, + name: record.OrgFileName || record.FileName, + kind: kindOf(record), + // FileSize 单位是 KB(旧系统下载缓冲 (FileSize+1)*1024 字节佐证),先换算成字节 + sizeText: formatSize( + record.FileSize ? record.FileSize * 1024 : undefined, + ), + modifiedAt: formatModifiedLabel(time, t("FilePage.todayLabel")), + sortTime: time, + path: record.FileName, + // 锁/共享角标照旧 MyComputerFamily:锁仅目录渲染(组件层控制), + // 共享角标 = PermissionIndc=Y 且 FavoriteIndc=Y(收藏过共享) + permission: record.PermissionIndc === "Y" ? "Y" : "N", + shared: record.PermissionIndc === "Y" && record.FavoriteIndc === "Y", + }; + } + + /** 查当前群组的一个目录;path 空串 = 组根,转群组编号(组直属记录的 file_path 存的就是它) */ + async function loadFolder(path: string, keyword = ""): Promise { + const queryPath = path || currentFamily.value; + loading.value = true; + errorText.value = ""; + try { + const criteria = new DmsFileSearchCriteria(); + const filters: SearchFilter[] = []; + + const familyFilter = new SearchFilter(); + familyFilter.Column = FamilyView.DB_FAMILY_NAME; + familyFilter.Type = "SingleSelection"; + familyFilter.Operator = "single"; + familyFilter.Value = currentFamily.value; + filters.push(familyFilter); + + const pathFilter = new SearchFilter(); + pathFilter.Column = FamilyView.DB_FILE_PATH; + pathFilter.Type = "SingleSelection"; + pathFilter.Operator = "single"; + pathFilter.Value = queryPath; + filters.push(pathFilter); + + if (keyword.trim()) { + const nameFilter = new SearchFilter(); + nameFilter.Column = FamilyView.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 = []; + const data = await dmsFileService.FindFamilyBySearchCriteria(criteria); + const rows = data?.Families ?? []; + // 文件夹置顶(旧页面 orderBy IsDirectoryIndc desc),组内保持后端顺序 + const dirs: FamilyDmsFileViewDisplayData[] = []; + const others: FamilyDmsFileViewDisplayData[] = []; + 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 = queryPath; + } catch (error: any) { + files.value = []; + records.value = new Map(); + errorText.value = error?.message || t("FilePage.loadFailed"); + } finally { + loading.value = false; + } + } + + /** 进组:路径栈重置为组根(旧系统 rootPath="" 的组根语义),value 空串供面包屑回组根 */ + async function enterGroup(group: FileItem): Promise { + currentFamily.value = group.path; + pathStack.value = [{ value: "", displayName: group.name }]; + await loadFolder(""); + } + + /** 退出到大厅:清组与列表,重拉群组(组内可能刚建过目录) */ + async function exitGroup(): Promise { + currentFamily.value = ""; + files.value = []; + records.value = new Map(); + pathStack.value = []; + currPath.value = ""; + await init(); + } + + /** + * 点文件夹下钻;非文件夹返回 false 交给页面处理打开动作。 + * 无权限的目录(旧 onOpenFolder 的 PermissionIndc 检查)抛提示文案由页面 toast + */ + async function openFolder(file: FileItem): Promise { + if (file.kind !== "folder" || !file.path) { + return false; + } + const record = records.value.get(file.id); + if (record?.IsDirectoryIndc !== "Y") { + return false; + } + if (record.PermissionIndc !== "Y") { + throw new Error(t("ShareHallPage.noPermission")); + } + pathStack.value.push({ value: file.path, displayName: file.name }); + await loadFolder(file.path); + return true; + } + + /** 返回上一级:组根再退交回大厅(页面调 exitGroup) */ + 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; + } + + /** 面包屑跳转:截断到第 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); + } + + /** 组内新建目录(旧 DmsFolderModal isFamilyGroup 分支):挂在当前目录下 */ + async function createFolder(localName: string): Promise { + const entity = new FamilyDmsFileViewEntity(); + entity.FilePath = currPath.value; + entity.FolderLocalName = localName; + entity.FamilyName = currentFamily.value; + entity.Notice = ""; + await dmsFileService.CreateFamilyDmsFolder(entity); + await loadFolder(currPath.value); + } + + /** 取消共享(旧 onDelete):摘掉群组里的目录映射,原文件仍在所有者网盘 */ + async function unshare(file: FileItem): Promise { + const record = records.value.get(file.id); + if (record) { + await dmsFileService.DeleteFamilyDmsFolder(record); + await loadFolder(currPath.value); + } + } + + return { + groups, + files, + records, + loading, + errorText, + pathStack, + currentFamily, + inGroup, + init, + enterGroup, + exitGroup, + openFolder, + back, + goTo, + loadFolder, + createFolder, + unshare, + }; +} diff --git a/uniapp/youjiala/src/composables/useTrashFiles.ts b/uniapp/youjiala/src/composables/useTrashFiles.ts index 9bf2782..2b21ad7 100644 --- a/uniapp/youjiala/src/composables/useTrashFiles.ts +++ b/uniapp/youjiala/src/composables/useTrashFiles.ts @@ -17,7 +17,12 @@ import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria"; import { DmsFileUploadDeleteLogViewDisplayData as DeleteLogView } from "@/data/auto/dmsFileUploadDeleteLogViewDisplayData"; import { SearchFilter } from "@/data/auto/searchFilter"; import dmsFileService from "@/service/master/dmsFileService"; -import { formatSize, kindOf, parseTime } from "./useDmsFiles"; +import { + formatModifiedLabel, + formatSize, + kindOf, + parseTime, +} from "./useDmsFiles"; /** 目录场景一次拉全,与旧页面 pagingParams.pageSize 同值 */ const FIND_LIMIT = 999999; @@ -35,18 +40,6 @@ export function useTrashFiles() { /** 删除日志 -> 卡片条目:时间列展示的是删除时间(记录 CreatedDatetime),path 留原目录备查 */ function toFileItem(record: DmsFileUploadDeleteLogViewDisplayData): 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 (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, @@ -55,7 +48,7 @@ export function useTrashFiles() { sizeText: formatSize( record.FileSize ? record.FileSize * 1024 : undefined, ), - modifiedAt, + modifiedAt: formatModifiedLabel(time, t("FilePage.todayLabel")), sortTime: time, path: record.FilePath, }; diff --git a/uniapp/youjiala/src/data/custom/fileBrowser.ts b/uniapp/youjiala/src/data/custom/fileBrowser.ts index 8a5d144..e67450a 100644 --- a/uniapp/youjiala/src/data/custom/fileBrowser.ts +++ b/uniapp/youjiala/src/data/custom/fileBrowser.ts @@ -21,6 +21,13 @@ export interface FileItem { sortTime: number; /** DMS 记录的 FileName:文件夹点它下钻(当作下次查询的 FilePath),文件留作下载/预览键 */ path: string; + /** + * 共享域角标(旧 MyComputerFamily 同义,仅目录显示锁): + * "Y" 有权限(开锁绿)/ "N" 无权限(闭锁灰);缺省不显示——文件页/回收站/顶层群组卡没有这个概念 + */ + permission?: "Y" | "N"; + /** 已被收藏共享(PermissionIndc=Y 且 FavoriteIndc=Y):显示蓝色共享角标 */ + shared?: boolean; } /** 快捷功能入口的单项:图标 + 渐变底色,页面给什么画什么 */ diff --git a/uniapp/youjiala/src/locale/lang/en-US.json b/uniapp/youjiala/src/locale/lang/en-US.json index 86fda41..c33e78d 100644 --- a/uniapp/youjiala/src/locale/lang/en-US.json +++ b/uniapp/youjiala/src/locale/lang/en-US.json @@ -30,7 +30,7 @@ "album": "Family Album", "musicHall": "Music Hall", "videoHall": "Video Hall", - "shareCenter": "Share Center", + "shareCenter": "Share Hall", "trash": "Trash", "catAll": "All", "catImage": "Images", @@ -85,6 +85,7 @@ "moveHere": "Move here", "moveNoSub": "No subfolders", "shareTitle": "Choose a family folder", + "shareHere": "Share here", "shareDone": "Shared", "shareNoFile": "No shareable files", "publishNoFile": "No publishable files", @@ -117,6 +118,115 @@ "pickFirst": "Select items first", "emptyText": "Trash is empty" }, + "ShareHallPage": { + "title": "Share Hall", + "noPermission": "No permission for this folder", + "enter": "Open", + "preview": "Preview", + "play": "Play", + "download": "Download", + "unshare": "Unshare", + "unshareConfirmText": "Unshare \"{name}\"? The original file stays in its owner's drive", + "unshareDone": "Unshared", + "createDone": "Folder created", + "emptyGroups": "No shared groups yet", + "emptyText": "This folder is empty" + }, + "SharePage": { + "title": "Family Groups", + "createTitle": "Create Group", + "noAccess": "This group is private and you are not a member", + "unnamed": "Unnamed", + "chipPublic": "Public", + "chipPrivate": "Private", + "chipJoinable": "Joinable", + "chipManager": "Admin", + "chipMember": "Member", + "emptyGroups": "No groups to join yet", + "noDesc": "No description", + "cancel": "Cancel", + "save": "Save", + "amend": "Edit", + "dissolve": "Disband", + "dissolveConfirm": "Disband \"{name}\"? Folders and memberships will be removed together", + "dissolveDone": "Group disbanded", + "saveDone": "Saved", + "createDone": "Group created", + "nameRequired": "Enter a group name", + "groupInfo": "Group Info", + "groupName": "Group Name", + "groupDesc": "Description", + "publicSetting": "Public group", + "joinApply": "Allow join requests", + "folders": "Shared Folders", + "newFolder": "New Folder", + "add": "Add", + "folderPh": "Enter folder name", + "notePh": "Note (optional)", + "noFolders": "No shared folders", + "deleteFolder": "Delete Folder", + "deleteFolderConfirm": "Delete this shared folder? It cannot be deleted if it still contains items", + "folderNotEmpty": "The folder still contains items and cannot be deleted", + "delete": "Delete", + "assign": "Assign", + "assignDone": "Permission assigned", + "noCandidate": "All members already have this folder", + "requests": "Pending Requests", + "approve": "Approve", + "approveDone": "Approved", + "reject": "Reject", + "rejectConfirm": "Reject this request?", + "rejectDone": "Rejected", + "members": "Members", + "removeMember": "Remove", + "removeMemberConfirm": "Remove this member from the group? Their folder permissions will be cleared", + "permissions": "Folder Permissions", + "applyJoin": "Join", + "submitApply": "Submit", + "joinPh": "Tell the admin why you want to join", + "applyDone": "Request submitted, awaiting review" + }, + "MediaPage": { + "title": "Media", + "createTitle": "New Album", + "segPhoto": "Photos", + "segAudio": "Music", + "segVideo": "Video", + "emptyAlbums": "No albums yet", + "unnamed": "Untitled", + "noDesc": "No description", + "noAccess": "You don't have access to this album", + "chipPublic": "Public", + "chipDefault": "Default", + "albumInfo": "Album Info", + "albumNo": "Album No.", + "albumName": "Album Name", + "albumDesc": "Album Description", + "autoNo": "Generated on save", + "publicLabel": "Public to everyone", + "defaultLabel": "Set as default", + "nameRequired": "Please enter an album name", + "details": "Album Items", + "noDetails": "No items yet. Publish content to this album from the Files page", + "moveUp": "Up", + "moveDown": "Down", + "delete": "Delete", + "deleteDetailConfirm": "Remove this item from the album?", + "groups": "Shared Groups", + "groupName": "Group Name", + "groupDesc": "Group Description", + "addGroup": "Add Group", + "noGroups": "Not published to any group yet", + "noGroupsAvailable": "No groups available, or all already published", + "exhibit": "Exhibit", + "amend": "Edit", + "save": "Save", + "cancel": "Cancel", + "deleteConfirm": "Delete album \"{name}\"? Items and group publishing will be removed", + "deleteDone": "Album deleted", + "saveDone": "Saved", + "createDone": "Album created" + }, "TransportPage": { "title": "Transfers", "active": "Active", diff --git a/uniapp/youjiala/src/locale/lang/zh-CN.json b/uniapp/youjiala/src/locale/lang/zh-CN.json index e2d9ee6..9cea14c 100644 --- a/uniapp/youjiala/src/locale/lang/zh-CN.json +++ b/uniapp/youjiala/src/locale/lang/zh-CN.json @@ -30,7 +30,7 @@ "album": "有家相册", "musicHall": "音乐厅", "videoHall": "影视厅", - "shareCenter": "共享中心", + "shareCenter": "共享大厅", "trash": "回收站", "catAll": "全部", "catImage": "图片", @@ -85,6 +85,7 @@ "moveHere": "移动到此", "moveNoSub": "没有子文件夹", "shareTitle": "选择家人目录", + "shareHere": "共享到此", "shareDone": "共享成功", "shareNoFile": "没有可共享的文件", "publishNoFile": "没有可发布的文件", @@ -117,6 +118,115 @@ "pickFirst": "请先勾选条目", "emptyText": "回收站是空的" }, + "ShareHallPage": { + "title": "共享大厅", + "noPermission": "您还没有该目录的权限", + "enter": "打开", + "preview": "预览", + "play": "播放", + "download": "下载", + "unshare": "取消共享", + "unshareConfirmText": "确定取消共享「{name}」?原文件仍在其所有者网盘中", + "unshareDone": "已取消共享", + "createDone": "已创建文件夹", + "emptyGroups": "还没有共享的群组", + "emptyText": "这个目录是空的" + }, + "SharePage": { + "title": "家庭群组", + "createTitle": "创建群组", + "noAccess": "该群组为私密,您还不是成员", + "unnamed": "未命名", + "chipPublic": "公开", + "chipPrivate": "私密", + "chipJoinable": "可申请", + "chipManager": "管理员", + "chipMember": "成员", + "emptyGroups": "还没有可加入的群组", + "noDesc": "暂无群组描述", + "cancel": "取消", + "save": "保存", + "amend": "修改", + "dissolve": "解散群", + "dissolveConfirm": "确定解散「{name}」?解散后目录与成员关系一并清除", + "dissolveDone": "已解散群组", + "saveDone": "已保存", + "createDone": "群组已创建", + "nameRequired": "请输入群组名称", + "groupInfo": "群组信息", + "groupName": "群组名称", + "groupDesc": "群组描述", + "publicSetting": "公开群组", + "joinApply": "允许申请加入", + "folders": "共享目录", + "newFolder": "新建目录", + "add": "添加", + "folderPh": "请输入目录名称", + "notePh": "备注(选填)", + "noFolders": "暂无共享目录", + "deleteFolder": "删除目录", + "deleteFolderConfirm": "确定删除该共享目录?目录内若仍有内容将无法删除", + "folderNotEmpty": "目录内仍有内容,无法删除", + "delete": "删除", + "assign": "分配权限", + "assignDone": "已分配权限", + "noCandidate": "成员都已获得该目录权限", + "requests": "待审核成员", + "approve": "同意", + "approveDone": "已通过", + "reject": "拒绝", + "rejectConfirm": "确定拒绝该申请?", + "rejectDone": "已拒绝", + "members": "正式成员", + "removeMember": "移出", + "removeMemberConfirm": "确定将该成员移出群组?其目录权限一并清除", + "permissions": "目录权限", + "applyJoin": "申请加入", + "submitApply": "提交申请", + "joinPh": "请填写申请原因,方便管理员审核", + "applyDone": "申请已提交,请等待审核" + }, + "MediaPage": { + "title": "多媒体", + "createTitle": "新建专辑", + "segPhoto": "相册", + "segAudio": "音乐", + "segVideo": "影视", + "emptyAlbums": "还没有专辑", + "unnamed": "未命名", + "noDesc": "暂无描述", + "noAccess": "该专辑无权限查看", + "chipPublic": "公开", + "chipDefault": "默认", + "albumInfo": "专辑信息", + "albumNo": "专辑编号", + "albumName": "专辑名称", + "albumDesc": "专辑描述", + "autoNo": "保存后由系统生成", + "publicLabel": "对所有人公开", + "defaultLabel": "设为默认专辑", + "nameRequired": "请输入专辑名称", + "details": "专辑明细", + "noDetails": "暂无明细,请到「文件」页发布内容到本专辑", + "moveUp": "上移", + "moveDown": "下移", + "delete": "删除", + "deleteDetailConfirm": "确定从专辑中移除该明细?", + "groups": "公开群组", + "groupName": "群组名称", + "groupDesc": "群组描述", + "addGroup": "添加群组", + "noGroups": "尚未发布到任何群组", + "noGroupsAvailable": "没有可添加的群组,或都已发布过", + "exhibit": "展览", + "amend": "修改", + "save": "保存", + "cancel": "取消", + "deleteConfirm": "确定删除专辑「{name}」?明细与群组发布关系一并清除", + "deleteDone": "已删除专辑", + "saveDone": "已保存", + "createDone": "专辑已创建" + }, "TransportPage": { "title": "传输列表", "active": "进行中", diff --git a/uniapp/youjiala/src/pages.json b/uniapp/youjiala/src/pages.json index 71b7151..a5f4b1a 100644 --- a/uniapp/youjiala/src/pages.json +++ b/uniapp/youjiala/src/pages.json @@ -52,9 +52,25 @@ { "path": "pages/media/play/play" }, + // 相册展览(短地址 /photo-show):多媒体专辑「展览」+ 文件页相册快捷入口,全屏幻灯 + { + "path": "pages/media/photo-show/photo-show" + }, + // 音乐厅(短地址 /audio-hall):多媒体音乐专辑「展览」+ 文件页音乐快捷入口,黑胶播放器 + { + "path": "pages/media/audio-hall/audio-hall" + }, + // 影视厅(短地址 /video-hall):多媒体影视专辑「展览」+ 文件页影视快捷入口,自绘播放器 + { + "path": "pages/media/video-hall/video-hall" + }, // 回收站(短地址 /trash):文件页快捷入口跳入,还原/彻底删除 { "path": "pages/trash/trash" + }, + // 共享大厅(短地址 /share-hall):文件页快捷入口跳入,家人共享目录浏览与组内建目录 + { + "path": "pages/share-hall/share-hall" } ], // 全局配置 diff --git a/uniapp/youjiala/src/pages/media/audio-hall/audio-hall.vue b/uniapp/youjiala/src/pages/media/audio-hall/audio-hall.vue new file mode 100644 index 0000000..02ee8d8 --- /dev/null +++ b/uniapp/youjiala/src/pages/media/audio-hall/audio-hall.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/uniapp/youjiala/src/pages/media/photo-show/photo-show.vue b/uniapp/youjiala/src/pages/media/photo-show/photo-show.vue new file mode 100644 index 0000000..1bfebf6 --- /dev/null +++ b/uniapp/youjiala/src/pages/media/photo-show/photo-show.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/uniapp/youjiala/src/pages/media/video-hall/video-hall.vue b/uniapp/youjiala/src/pages/media/video-hall/video-hall.vue new file mode 100644 index 0000000..b0069e2 --- /dev/null +++ b/uniapp/youjiala/src/pages/media/video-hall/video-hall.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/uniapp/youjiala/src/pages/share-hall/share-hall.vue b/uniapp/youjiala/src/pages/share-hall/share-hall.vue new file mode 100644 index 0000000..761f2b5 --- /dev/null +++ b/uniapp/youjiala/src/pages/share-hall/share-hall.vue @@ -0,0 +1,485 @@ + + + + + diff --git a/uniapp/youjiala/src/pages/tab/file.vue b/uniapp/youjiala/src/pages/tab/file.vue index 842a080..41faa52 100644 --- a/uniapp/youjiala/src/pages/tab/file.vue +++ b/uniapp/youjiala/src/pages/tab/file.vue @@ -15,6 +15,7 @@ * 权限暂用 UserOperation(见 routes.ts 的 PAGE_PERMISSIONS);useAuthGuard 兜冷启动直达。 */ import type { FolderRow } from "@/components/folder-move-picker/folder-move-picker.vue"; +import type { ShareTarget } from "@/components/folder-share-picker/folder-share-picker.vue"; import type { PickerOption } from "@/components/option-picker/option-picker.vue"; import type { FileCategory, FileItem, FileKind, FileTabItem, QuickEntryItem } from "@/data/custom/fileBrowser"; import { useLocale } from "uview-pro"; @@ -22,14 +23,12 @@ import { computed, ref, watch } from "vue"; import { useAuthGuard, useDmsFiles, useThemeGradient } from "@/composables"; import { DmsFileEntity } from "@/data/auto/dmsFileEntity"; import { FamilyGroupFolderEntity } from "@/data/auto/familyGroupFolderEntity"; -import { FamilyGroupManagerUserSearchCriteria } from "@/data/auto/familyGroupManagerUserSearchCriteria"; import { PublishAudioSearchCriteria } from "@/data/auto/publishAudioSearchCriteria"; import { PublishData } from "@/data/auto/publishData"; import { PublishPhotoSearchCriteria } from "@/data/auto/publishPhotoSearchCriteria"; import { PublishVideoSearchCriteria } from "@/data/auto/publishVideoSearchCriteria"; import { pageUrl } from "@/router/routes"; import dmsFileService from "@/service/master/dmsFileService"; -import familyGroupService from "@/service/master/familyGroupService"; import publishAudioService from "@/service/master/publishAudioService"; import publishPhotoService from "@/service/master/publishPhotoService"; import publishVideoService from "@/service/master/publishVideoService"; @@ -180,12 +179,37 @@ function onSearch(value: string): void { search(value); } +/** 展览/播放三入口:以当前目录为上下文,mode=list + key=目录路径跳对应播放器页 */ +function goShowWithFolder(name: "photoShow" | "audioHall" | "videoHall"): void { + const folder = pathStack.value[pathStack.value.length - 1]?.value ?? ""; + uni.navigateTo({ + url: `${pageUrl(name)}?mode=list&key=${encodeURIComponent(folder)}`, + fail: (err: any) => toast(err?.errMsg || t("FilePage.actionFailed")), + }); +} + function onQuickClick(key: string): void { - // 回收站已落地(pages/trash);其余入口仍是占位提示 + // 回收站/共享大厅(pages/trash、pages/share-hall);相册/音乐厅/影视厅展览当前目录 if (key === "trash") { uni.navigateTo({ url: pageUrl("trash") }); return; } + if (key === "shareCenter") { + uni.navigateTo({ url: pageUrl("shareHall") }); + return; + } + if (key === "album") { + goShowWithFolder("photoShow"); + return; + } + if (key === "musicHall") { + goShowWithFolder("audioHall"); + return; + } + if (key === "videoHall") { + goShowWithFolder("videoHall"); + return; + } const entry = quickEntries.value.find(e => e.key === key); toast(entry ? `${entry.label} · ${t("FilePage.todoTip")}` : t("FilePage.todoTip")); } @@ -600,15 +624,12 @@ function goPlay(type: "video" | "audio", name: string, url: string): void { }); } -// ----- 共享家人:选收藏夹里的家人目录,组装 FamilyGroupFolderEntity 批量建(旧 handleSelectShare)----- +// ----- 共享家人:共享大厅式抽屉选目标目录(群组卡→逐级下钻→共享到此), +// 多选文件一次批量建(CreateFamilyGroupFolder 数组入参,旧 handleSelectShare 同口径)----- const shareVisible = ref(false); -const shareLoading = ref(false); -const shareItems = ref([]); -/** 展示用 items 之外的组装字段(FamilyName/TopFolderName 提交才需要) */ -const shareFolders = ref>([]); const shareTargets = ref([]); -async function actionShare(targets: FileItem[]): Promise { +function actionShare(targets: FileItem[]): void { const picked = targets.filter(f => f.kind !== "folder"); if (!picked.length) { toast(t("FilePage.shareNoFile")); @@ -616,41 +637,11 @@ async function actionShare(targets: FileItem[]): Promise { } shareTargets.value = picked; shareVisible.value = true; - shareLoading.value = true; - try { - const criteria = new FamilyGroupManagerUserSearchCriteria(); - criteria.Filter = []; - criteria.limit = 999; - criteria.page = 1; - criteria.GridColumns = []; - const data = await familyGroupService.FamilyFolderFavoriteBySearchCriteria(criteria); - const favorites = data?.Favorites ?? []; - shareFolders.value = favorites.map(f => ({ - key: f.FolderName, - familyName: f.FamilyName, - topFolderName: f.TopFolderName, - })); - shareItems.value = favorites.map(f => ({ - key: f.FolderName, - label: `${f.FamilyLocalName} / ${f.FolderLocalName}`, - sub: f.Notice, - })); - } - catch (error: any) { - toast(error?.message || t("FilePage.actionFailed")); - } - finally { - shareLoading.value = false; - } } -async function onShareConfirm(key: string): Promise { - const target = shareFolders.value.find(f => f.key === key); - shareVisible.value = false; - if (!target) { - return; - } +async function onShareConfirm(target: ShareTarget): Promise { const filesToShare = shareTargets.value; + shareVisible.value = false; await withBusy(async () => { const rows = filesToShare.map((file) => { const record = recordOf(file); @@ -660,7 +651,7 @@ async function onShareConfirm(key: string): Promise { // 旧系统组装:FolderName 文件系统名、FolderLocalName 原始名、FilePath 目标共享目录 fgp.FolderName = record?.FileName ?? file.path; fgp.FolderLocalName = record?.OrgFileName ?? file.name; - fgp.FilePath = target.key; + fgp.FilePath = target.filePath; return fgp; }); await dmsFileService.CreateFamilyGroupFolder(rows); @@ -701,7 +692,8 @@ async function actionPublish(targets: FileItem[]): Promise { publishVisible.value = true; publishLoading.value = true; try { - let names: string[] = []; + // 拉到的专辑行含名称(PublishLocalName)/描述(Notice)/编号(PublishName),不能只留编号 + let rows: Array<{ PublishName?: string; PublishLocalName?: string; Notice?: string }> = []; if (first.kind === "image") { const criteria = new PublishPhotoSearchCriteria(); criteria.Filter = []; @@ -709,7 +701,7 @@ async function actionPublish(targets: FileItem[]): Promise { criteria.page = 1; criteria.GridColumns = []; const data = await publishPhotoService.FindBySearchCriteria(criteria); - names = (data?.Publishs ?? []).map(p => p.PublishName); + rows = data?.Publishs ?? []; } else if (first.kind === "video") { const criteria = new PublishVideoSearchCriteria(); @@ -718,7 +710,7 @@ async function actionPublish(targets: FileItem[]): Promise { criteria.page = 1; criteria.GridColumns = []; const data = await publishVideoService.FindBySearchCriteria(criteria); - names = (data?.Datas ?? []).map(p => p.PublishName); + rows = data?.Datas ?? []; } else { const criteria = new PublishAudioSearchCriteria(); @@ -727,9 +719,24 @@ async function actionPublish(targets: FileItem[]): Promise { criteria.page = 1; criteria.GridColumns = []; const data = await publishAudioService.FindBySearchCriteria(criteria); - names = (data?.Datas ?? []).map(p => p.PublishName); + rows = data?.Datas ?? []; } - publishItems.value = names.filter(Boolean).map(n => ({ key: n, label: n })); + // 主行显示专辑名称(无名称回落编号),副行显示描述 + 编号;key 仍用编号供 onPublishConfirm 发布 + publishItems.value = rows + .filter(p => p?.PublishName) + .map((p) => { + const code = p.PublishName || ""; + const hasName = !!p.PublishLocalName; + const label = p.PublishLocalName || code; + const parts: string[] = []; + if (p.Notice) { + parts.push(p.Notice); + } + if (hasName) { + parts.push(code); + } + return { key: code, label, sub: parts.join(" · ") }; + }); } catch (error: any) { toast(error?.message || t("FilePage.actionFailed")); @@ -966,9 +973,8 @@ const transferIconWhite = transferGlyph("#ffffff"); - - + /** - * 底部导航「多媒体」测试页:先占位,后续接照片/视频/音频。 - * 权限暂用 UserOperation(见 routes.ts 的 PAGE_PERMISSIONS);useAuthGuard 兜冷启动直达。 + * 底部导航「多媒体」页:把旧 WebVue 三套专辑管理页 + * PublishPhotoEnquiry/Operation(相册管理)、PublishAudioEnquiry/Operation(音乐专辑)、 + * PublishVideoEnquiry/Operation(影视专辑)融合进一个单页三段式,交互部分参考「共享」tab。 + * 顶部分段「相册 / 音乐 / 影视」切换 kind;列表态显示专辑卡片(带 tabbar), + * 点卡片进入编辑态(隐 tabbar):专辑信息 + 明细重排/删除 + 公开群组增删 + 保存/删除/展览。 + * 数据链全在 composables/usePublishAlbums;本页只渲染 + 把点击转成调用。 + * 「加新内容进专辑」仍走文件页发布链,本页不重复上传;「展览」按钮在 M4 接线到播放器页。 */ -import { useAuthGuard } from "@/composables"; +import type { PublishAlbumDetail, PublishAlbumEntity, PublishKind } from "@/composables"; +import { useLocale } from "uview-pro"; +import { computed, ref } from "vue"; +import { + useAuthGuard, + useFamilyGroups, + usePublishAlbums, + useThemeGradient, +} from "@/composables"; +import { pageUrl } from "@/router/routes"; +import { useLoginProfileStore } from "@/stores"; -useAuthGuard(); +const { t } = useLocale(); +const { themeBackground } = useThemeGradient(); +const profile = useLoginProfileStore(); + +const { + kind, + setKind, + list, + loading, + errorText, + album, + details, + groups, + inDetail, + creating, + isEdit, + canAmend, + canDelete, + setMyUser, + loadList, + openAlbum, + exitDetail, + startCreate, + startEdit, + cancelEdit, + moveDetail, + removeDetail, + addGroup, + removeGroup, + save, + dissolve, + formatDate, +} = usePublishAlbums(); + +// 公开群组选择器复用「共享」数据源:拉我可见的群做候选 +const familyGroups = useFamilyGroups(); + +useAuthGuard(async () => { + setMyUser(profile.username); + await loadList(); +}); + +const keyword = ref(""); +const showSearch = ref(false); +const busy = ref(false); + +/** 分段配置:kind -> 文案 key + 图标 */ +const SEGMENTS: { key: PublishKind; labelKey: string; icon: string }[] = [ + { key: "photo", labelKey: "MediaPage.segPhoto", icon: "photo" }, + { key: "audio", labelKey: "MediaPage.segAudio", icon: "volume" }, + { key: "video", labelKey: "MediaPage.segVideo", icon: "play-circle-fill" }, +]; + +const navTitle = computed(() => { + if (creating.value) { + return t("MediaPage.createTitle"); + } + if (inDetail.value) { + return album.value?.PublishLocalName || segmentLabel(kind.value); + } + return t("MediaPage.title"); +}); + +function segmentLabel(k: PublishKind): string { + return t(SEGMENTS.find(s => s.key === k)?.labelKey ?? "MediaPage.title"); +} + +function kindIcon(k: PublishKind): string { + return SEGMENTS.find(s => s.key === k)?.icon ?? "photo"; +} + +function albumDesc(entity: PublishAlbumEntity): string { + return entity.Notice || t("MediaPage.noDesc"); +} + +/** 明细封面地址:photo 用缩略图/原图,audio/video 用 Pic */ +function coverOf(detail: PublishAlbumDetail): string { + return detail.FormattedPic || detail.Pic || detail.FileName || ""; +} + +/** 加载失败的封面(按 DetailId 记录):图片地址取不到时回落成占位图标,避免留白 */ +const coverFailed = ref>({}); +function onCoverError(detailId: number): void { + if (detailId == null) { + return; + } + coverFailed.value = { ...coverFailed.value, [detailId]: true }; +} + +function toast(title: string): void { + uni.showToast({ title, icon: "none" }); +} + +function confirmDialog(title: string, content: string): Promise { + return new Promise((resolve) => { + uni.showModal({ + title, + content, + success: res => resolve(!!res.confirm), + fail: () => resolve(false), + }); + }); +} + +async function withBusy(task: () => Promise, tip?: string): Promise { + if (busy.value) { + return; + } + busy.value = true; + uni.showLoading({ title: tip || t("FilePage.pleaseWait"), mask: true }); + try { + await task(); + } + catch (error: any) { + toast(error?.message || t("FilePage.actionFailed")); + } + finally { + uni.hideLoading(); + busy.value = false; + } +} + +// ===== 分段切换 ===== +async function onKindChange(next: PublishKind): Promise { + if (kind.value === next) { + return; + } + keyword.value = ""; + showSearch.value = false; + await withBusy(async () => { + await setKind(next); + }); +} + +// ===== 列表态 ===== +function onToggleSearch(): void { + showSearch.value = !showSearch.value; +} + +async function onSearch(): Promise { + await loadList(keyword.value); +} + +async function onOpenAlbum(entity: PublishAlbumEntity): Promise { + keyword.value = ""; + coverFailed.value = {}; + await withBusy(async () => { + await openAlbum(entity.PublishName); + }); + if (errorText.value && !inDetail.value) { + toast(errorText.value); + } +} + +function onCreateAlbum(): void { + startCreate(); +} + +// ===== 详情:返回 / 编辑 / 保存 / 删除 / 展览 ===== +async function onBack(): Promise { + if (!inDetail.value) { + if (getCurrentPages().length > 1) { + uni.navigateBack(); + } + return; + } + await exitDetail(); +} + +async function onRetry(): Promise { + await loadList(keyword.value); +} + +function onAmend(): void { + startEdit(); +} + +async function onCancelEdit(): Promise { + await cancelEdit(); +} + +async function onSave(): Promise { + if (!(album.value?.PublishLocalName || "").trim()) { + toast(t("MediaPage.nameRequired")); + return; + } + const wasCreating = creating.value; + await withBusy(async () => { + await save(); + toast(wasCreating ? t("MediaPage.createDone") : t("MediaPage.saveDone")); + }); +} + +async function onDelete(): Promise { + const ok = await confirmDialog( + t("MediaPage.delete"), + t("MediaPage.deleteConfirm").replace("{name}", album.value?.PublishLocalName || ""), + ); + if (!ok) { + return; + } + await withBusy(async () => { + await dissolve(); + toast(t("MediaPage.deleteDone")); + }); +} + +/** 展览:把当前 kind + 专辑编号映射到对应播放器页(mode=view,非 list) */ +function onExhibit(): void { + const pageName = kind.value === "photo" + ? "photoShow" + : kind.value === "audio" ? "audioHall" : "videoHall"; + const name = album.value?.PublishName || ""; + uni.navigateTo({ + url: `${pageUrl(pageName)}?mode=view&key=${encodeURIComponent(name)}`, + fail: (err: any) => toast(err?.errMsg || t("FilePage.actionFailed")), + }); +} + +// ===== 明细:上移 / 下移 / 删除 ===== +function onMoveDetail(index: number, dir: -1 | 1): void { + moveDetail(index, dir); +} + +async function onRemoveDetail(detailId: number): Promise { + const ok = await confirmDialog(t("MediaPage.delete"), t("MediaPage.deleteDetailConfirm")); + if (!ok) { + return; + } + removeDetail(detailId); +} + +// ===== 公开群组:选择器 / 删除 ===== +async function onAddGroup(): Promise { + await withBusy(async () => { + await familyGroups.loadList(); + }); + const candidates = familyGroups.groups.value.filter( + g => !groups.value.some(row => row.FamilyName === g.FamilyName), + ); + if (!candidates.length) { + toast(t("MediaPage.noGroupsAvailable")); + return; + } + uni.showActionSheet({ + itemList: candidates.map(g => g.FamilyLocalName || g.FamilyName), + success: (res) => { + const pick = candidates[res.tapIndex]; + addGroup(pick.FamilyName, pick.FamilyLocalName, pick.Notice || ""); + }, + }); +} + +function onRemoveGroup(familyName: string): void { + removeGroup(familyName); +}