1095 lines
31 KiB
Vue
1095 lines
31 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 底部导航「多媒体」页:把旧 WebVue 三套专辑管理页
|
||
* PublishPhotoEnquiry/Operation(相册管理)、PublishAudioEnquiry/Operation(音乐专辑)、
|
||
* PublishVideoEnquiry/Operation(影视专辑)融合进一个单页三段式,交互部分参考「共享」tab。
|
||
* 顶部分段「相册 / 音乐 / 影视」切换 kind;列表态显示专辑卡片(带 tabbar),
|
||
* 点卡片进入编辑态(隐 tabbar):专辑信息 + 明细重排/删除 + 公开群组增删 + 保存/删除/展览。
|
||
* 数据链全在 composables/usePublishAlbums;本页只渲染 + 把点击转成调用。
|
||
* 「加新内容进专辑」仍走文件页发布链,本页不重复上传;「展览」按钮在 M4 接线到播放器页。
|
||
*/
|
||
import type { PublishAlbumDetail, PublishAlbumEntity, PublishKind } from "@/composables";
|
||
import { useLocale } from "uview-pro";
|
||
import { computed, ref } from "vue";
|
||
import {
|
||
useAuthGuard,
|
||
useFamilyGroups,
|
||
usePublishAlbums,
|
||
useThemeGradient,
|
||
useThemeTokens,
|
||
} from "@/composables";
|
||
import { pageUrl } from "@/router/routes";
|
||
import { useLoginProfileStore, useMediaIntentStore } from "@/stores";
|
||
|
||
const { t } = useLocale();
|
||
const { themeBackground } = useThemeGradient();
|
||
// 主题派生色经 CSS 变量下发:分段选中/头像/徽章/主按钮全部跟主题(不再硬编码蓝)
|
||
const { primary, accentGradient, accentSoft, accentShadow } = useThemeTokens();
|
||
const themeVars = computed(() => ({
|
||
"--fg-primary": primary.value,
|
||
"--fg-gradient": accentGradient.value,
|
||
"--fg-soft": accentSoft.value,
|
||
"--fg-shadow": accentShadow.value,
|
||
}));
|
||
const profile = useLoginProfileStore();
|
||
|
||
const {
|
||
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();
|
||
|
||
// 跨 tab 创建意图:文件页发布面板点「新建相册」reLaunch 过来时,自动进对应分段的创建态
|
||
const mediaIntent = useMediaIntentStore();
|
||
|
||
useAuthGuard(async () => {
|
||
setMyUser(profile.username);
|
||
await loadList();
|
||
const wantCreate = mediaIntent.takeCreate();
|
||
if (wantCreate) {
|
||
// setKind 内部会按新分段重拉列表,必须等它落定再进取态(否则 creating 被它同步复位)
|
||
await setKind(wantCreate);
|
||
startCreate();
|
||
}
|
||
});
|
||
|
||
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 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";
|
||
}
|
||
|
||
/** 三张分区卡(专辑信息/明细/群组)折叠态:默认全展开,点标题行切换 */
|
||
const folded = ref<Record<"info" | "details" | "groups", boolean>>({
|
||
info: false,
|
||
details: false,
|
||
groups: false,
|
||
});
|
||
|
||
function toggleFold(key: "info" | "details" | "groups"): void {
|
||
folded.value[key] = !folded.value[key];
|
||
}
|
||
|
||
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<Record<number, boolean>>({});
|
||
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<boolean> {
|
||
return new Promise((resolve) => {
|
||
uni.showModal({
|
||
title,
|
||
content,
|
||
success: res => resolve(!!res.confirm),
|
||
fail: () => resolve(false),
|
||
});
|
||
});
|
||
}
|
||
|
||
async function withBusy(task: () => Promise<void>, tip?: string): Promise<void> {
|
||
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<void> {
|
||
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<void> {
|
||
await loadList(keyword.value);
|
||
}
|
||
|
||
async function onOpenAlbum(entity: PublishAlbumEntity): Promise<void> {
|
||
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<void> {
|
||
if (!inDetail.value) {
|
||
if (getCurrentPages().length > 1) {
|
||
uni.navigateBack();
|
||
}
|
||
return;
|
||
}
|
||
await exitDetail();
|
||
}
|
||
|
||
async function onRetry(): Promise<void> {
|
||
await loadList(keyword.value);
|
||
}
|
||
|
||
function onAmend(): void {
|
||
startEdit();
|
||
}
|
||
|
||
async function onCancelEdit(): Promise<void> {
|
||
await cancelEdit();
|
||
}
|
||
|
||
async function onSave(): Promise<void> {
|
||
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<void> {
|
||
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<void> {
|
||
const ok = await confirmDialog(t("MediaPage.delete"), t("MediaPage.deleteDetailConfirm"));
|
||
if (!ok) {
|
||
return;
|
||
}
|
||
removeDetail(detailId);
|
||
}
|
||
|
||
// ===== 公开群组:选择器 / 删除 =====
|
||
async function onAddGroup(): Promise<void> {
|
||
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);
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 列表态显示 tabbar,进详情收起 -->
|
||
<app-page :hide-nav="true" :show-tabbar="!inDetail">
|
||
<view class="media-page" :style="[{ background: themeBackground }, themeVars]">
|
||
<!-- ===== 列表态:分段 + 专辑卡片 ===== -->
|
||
<view v-if="!inDetail" class="media-page__body">
|
||
<view class="media-page__head">
|
||
<page-navbar :title="t('MediaPage.title')" :show-back="false" :search-on="showSearch" :show-transfer="false"
|
||
:show-quick-toggle="false" @back="onBack" @toggle-search="onToggleSearch" />
|
||
<!-- 分段:相册 / 音乐 / 影视 -->
|
||
<view class="seg-bar">
|
||
<view v-for="seg in SEGMENTS" :key="seg.key" class="seg-bar__item"
|
||
:class="{ 'is-active': kind === seg.key }" @tap="onKindChange(seg.key)">
|
||
<u-icon :name="seg.icon" size="30" :color="kind === seg.key ? primary : '#8f959e'" />
|
||
<text class="seg-bar__text">{{ t(seg.labelKey) }}</text>
|
||
</view>
|
||
</view>
|
||
<search-input v-if="showSearch" v-model="keyword" :placeholder="t('FilePage.searchPlaceholder')"
|
||
@search="onSearch" />
|
||
</view>
|
||
<view class="media-page__scroll-wrap">
|
||
<scroll-view scroll-y class="media-page__scroll">
|
||
<view v-if="loading" class="media-page__state">
|
||
<text class="media-page__state-text">{{ t("FilePage.loading") }}</text>
|
||
</view>
|
||
<view v-else-if="errorText" class="media-page__state">
|
||
<text class="media-page__state-text media-page__state-text--error">
|
||
{{ errorText }}
|
||
</text>
|
||
<text class="media-page__retry" @click="onRetry">
|
||
{{ t("FilePage.retryBut") }}
|
||
</text>
|
||
</view>
|
||
<template v-else>
|
||
<view v-if="!list.length" class="media-page__empty">
|
||
<text class="media-page__state-text">{{ t("MediaPage.emptyAlbums") }}</text>
|
||
</view>
|
||
<view v-for="entity in list" :key="entity.PublishName" class="album-card" @tap="onOpenAlbum(entity)">
|
||
<view class="album-card__avatar">
|
||
<u-icon :name="kindIcon(kind)" size="48" color="#ffffff" />
|
||
</view>
|
||
<view class="album-card__main">
|
||
<text class="album-card__name">
|
||
{{ entity.PublishLocalName || t("MediaPage.unnamed") }}
|
||
</text>
|
||
<text class="album-card__desc">{{ albumDesc(entity) }}</text>
|
||
<view class="album-card__meta">
|
||
<text class="album-card__chip album-card__chip--code">
|
||
{{ entity.PublishName }}
|
||
</text>
|
||
<text v-if="entity.OpenIndc === 'Y'" class="album-card__chip">
|
||
{{ t("MediaPage.chipPublic") }}
|
||
</text>
|
||
<text v-if="entity.DefaultIndc === 'Y'" class="album-card__chip album-card__chip--default">
|
||
{{ t("MediaPage.chipDefault") }}
|
||
</text>
|
||
<text class="album-card__date">
|
||
{{ formatDate(entity.CreatedDatetime) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<u-icon name="arrow-right" size="30" color="#c0c4cc" />
|
||
</view>
|
||
</template>
|
||
<view class="media-page__list-space" />
|
||
</scroll-view>
|
||
</view>
|
||
<!-- 新建专辑浮动按钮:复用文件页同款可拖动组件 -->
|
||
<float-add-btn @click="onCreateAlbum" />
|
||
</view>
|
||
|
||
<!-- ===== 编辑态:专辑管理 ===== -->
|
||
<view v-else class="media-page__body">
|
||
<view class="media-page__head">
|
||
<hero-navbar :title="navTitle" @back="onBack" />
|
||
</view>
|
||
<view class="media-page__scroll-wrap">
|
||
<scroll-view scroll-y class="media-page__scroll">
|
||
<!-- 专辑信息头卡(仅查看态):白底卡 + 主题浅底图标 + 浅色动作钮(深底渐变白字对比不足,弃用) -->
|
||
<view v-if="!isEdit" class="detail-hero">
|
||
<view class="detail-hero__avatar">
|
||
<u-icon :name="kindIcon(kind)" size="64" :color="primary" />
|
||
</view>
|
||
<view class="detail-hero__main">
|
||
<text class="detail-hero__name">
|
||
{{ album?.PublishLocalName || t("MediaPage.unnamed") }}
|
||
</text>
|
||
<text class="detail-hero__code">
|
||
{{ album?.PublishName }}
|
||
</text>
|
||
<view class="detail-hero__chips">
|
||
<text v-if="album?.OpenIndc === 'Y'" class="detail-hero__chip">
|
||
{{ t("MediaPage.chipPublic") }}
|
||
</text>
|
||
<text v-if="album?.DefaultIndc === 'Y'" class="detail-hero__chip detail-hero__chip--default">
|
||
{{ t("MediaPage.chipDefault") }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<!-- 展览 / 修改 / 删除:右上角圆形图标动作 -->
|
||
<view class="detail-hero__acts">
|
||
<view v-if="canAmend" class="detail-hero__act" @tap="onExhibit">
|
||
<u-icon name="eye-fill" size="34" :color="primary" />
|
||
</view>
|
||
<view v-if="canAmend" class="detail-hero__act" @tap="onAmend">
|
||
<u-icon name="edit-pen" size="34" :color="primary" />
|
||
</view>
|
||
<view v-if="canDelete" class="detail-hero__act detail-hero__act--danger" @tap="onDelete">
|
||
<u-icon name="trash" size="34" color="#f0413e" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 动作栏(仅编辑态:取消 / 保存) -->
|
||
<view v-if="isEdit" class="detail-actions">
|
||
<view class="detail-actions__btn" @tap="onCancelEdit">
|
||
<text>{{ t("MediaPage.cancel") }}</text>
|
||
</view>
|
||
<view class="detail-actions__btn detail-actions__btn--primary" @tap="onSave">
|
||
<text>{{ t("MediaPage.save") }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 专辑信息卡(编辑态对齐设计图:编号灰字说明 + 描边输入 + 主题色开关);点标题行折叠/展开 -->
|
||
<view class="detail-card">
|
||
<view class="detail-card__head" @tap="toggleFold('info')">
|
||
<text class="detail-card__title">{{ t("MediaPage.albumInfo") }}</text>
|
||
<u-icon :name="folded.info ? 'arrow-right' : 'arrow-down'" size="26" color="#8f959e" />
|
||
</view>
|
||
<view v-show="!folded.info">
|
||
<view class="detail-field">
|
||
<text class="detail-field__label">{{ t("MediaPage.albumNo") }}</text>
|
||
<text class="detail-field__static">
|
||
{{
|
||
album?.PublishName
|
||
}}
|
||
</text>
|
||
</view>
|
||
<view class="detail-field">
|
||
<text class="detail-field__label">{{ t("MediaPage.albumName") }}</text>
|
||
<input v-model="album!.PublishLocalName" class="detail-field__input" :disabled="!isEdit"
|
||
:maxlength="50" :placeholder="t('MediaPage.nameRequired')">
|
||
</view>
|
||
<view class="detail-field">
|
||
<text class="detail-field__label">{{ t("MediaPage.albumDesc") }}</text>
|
||
<textarea v-model="album!.Notice" class="detail-field__textarea" :disabled="!isEdit" :maxlength="500"
|
||
:placeholder="t('MediaPage.noDesc')" />
|
||
</view>
|
||
<view class="detail-field detail-field--row">
|
||
<text class="detail-field__label">{{ t("MediaPage.publicLabel") }}</text>
|
||
<u-switch :model-value="album?.OpenIndc === 'Y'" :disabled="!isEdit" :active-color="primary"
|
||
@change="(v: boolean) => album && (album.OpenIndc = v ? 'Y' : 'N')" />
|
||
</view>
|
||
<view class="detail-field detail-field--row">
|
||
<text class="detail-field__label">{{ t("MediaPage.defaultLabel") }}</text>
|
||
<u-switch :model-value="album?.DefaultIndc === 'Y'" :disabled="!isEdit" :active-color="primary"
|
||
@change="(v: boolean) => album && (album.DefaultIndc = v ? 'Y' : 'N')" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 明细卡:点标题行折叠/展开 -->
|
||
<view class="detail-card">
|
||
<view class="detail-card__head" @tap="toggleFold('details')">
|
||
<text class="detail-card__title">{{ t("MediaPage.details") }}</text>
|
||
<u-icon :name="folded.details ? 'arrow-right' : 'arrow-down'" size="26" color="#8f959e" />
|
||
</view>
|
||
<view v-show="!folded.details">
|
||
<view v-if="!details.length" class="detail-empty">
|
||
<text>{{ t("MediaPage.noDetails") }}</text>
|
||
</view>
|
||
<view v-for="(item, index) in details" :key="item.DetailId || index" class="detail-item">
|
||
<view class="detail-item__row">
|
||
<text class="detail-item__index">{{ index + 1 }}</text>
|
||
<image v-if="coverOf(item) && !coverFailed[item.DetailId]" class="detail-item__cover"
|
||
:src="coverOf(item)" mode="aspectFill" @error="onCoverError(item.DetailId)" />
|
||
<view v-else class="detail-item__cover detail-item__cover--empty">
|
||
<u-icon :name="kindIcon(kind)" size="28" color="#c0c4cc" />
|
||
</view>
|
||
<!-- 查看态:描述灰字放封面右侧;编辑态输入框在封面下方整行(大框挤在行内会撑爆布局) -->
|
||
<view class="detail-item__desc">
|
||
<text v-if="!isEdit && item.Notice" class="detail-item__sub detail-item__sub--text">
|
||
{{ item.Notice }}
|
||
</text>
|
||
</view>
|
||
<view class="detail-item__acts">
|
||
<text v-if="isEdit && index > 0" class="detail-item__link" @tap="onMoveDetail(index, -1)">
|
||
{{ t("MediaPage.moveUp") }}
|
||
</text>
|
||
<text v-if="isEdit && index < details.length - 1" class="detail-item__link"
|
||
@tap="onMoveDetail(index, 1)">
|
||
{{ t("MediaPage.moveDown") }}
|
||
</text>
|
||
<text v-if="isEdit" class="detail-item__del" @tap="onRemoveDetail(item.DetailId)">
|
||
{{ t("MediaPage.delete") }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<textarea v-if="isEdit" v-model="item.Notice" class="detail-item__sub" :maxlength="500"
|
||
:placeholder="t('MediaPage.noDesc')" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 公开群组卡(仅本人可编辑时呈现,照旧 canAmend 显隐);点标题行折叠/展开 -->
|
||
<view v-if="canAmend" class="detail-card">
|
||
<view class="detail-card__head" @tap="toggleFold('groups')">
|
||
<text class="detail-card__title">{{ t("MediaPage.groups") }}</text>
|
||
<view class="detail-card__head-acts">
|
||
<text v-if="isEdit" class="detail-card__add" @tap.stop="onAddGroup">
|
||
+ {{ t("MediaPage.addGroup") }}
|
||
</text>
|
||
<u-icon :name="folded.groups ? 'arrow-right' : 'arrow-down'" size="26" color="#8f959e" />
|
||
</view>
|
||
</view>
|
||
<view v-show="!folded.groups">
|
||
<view v-if="!groups.length" class="detail-empty">
|
||
<text>{{ t("MediaPage.noGroups") }}</text>
|
||
</view>
|
||
<view v-for="(group, index) in groups" :key="group.FamilyName || index" class="detail-item">
|
||
<view class="detail-item__row">
|
||
<text class="detail-item__index">{{ index + 1 }}</text>
|
||
<text class="detail-item__name">
|
||
{{ group.FamilyName || t("MediaPage.unnamed") }}
|
||
</text>
|
||
<view class="detail-item__acts">
|
||
<text v-if="isEdit" class="detail-item__del" @tap="onRemoveGroup(group.FamilyName)">
|
||
{{ t("MediaPage.delete") }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="group-field">
|
||
<text class="group-field__label">{{ t("MediaPage.groupName") }}</text>
|
||
<text class="group-field__text">{{ group.FamilyLocalName || "-" }}</text>
|
||
</view>
|
||
<view class="group-field">
|
||
<text class="group-field__label">{{ t("MediaPage.groupDesc") }}</text>
|
||
<text class="group-field__text">{{ group.Notice || "-" }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="media-page__detail-space" />
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</app-page>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 骨架与文件页/共享页同套(fixed 全屏 + head/scroll-wrap 卡片分区) */
|
||
.media-page {
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.media-page__body {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.media-page__head {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.media-page__scroll-wrap {
|
||
flex: 1;
|
||
min-height: 0;
|
||
margin: 20rpx 20rpx 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.media-page__scroll {
|
||
height: 100%;
|
||
}
|
||
|
||
.media-page__list-space {
|
||
height: 160rpx;
|
||
}
|
||
|
||
.media-page__detail-space {
|
||
height: 48rpx;
|
||
}
|
||
|
||
.media-page__state,
|
||
.media-page__empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 24rpx;
|
||
padding: 64rpx 32rpx;
|
||
}
|
||
|
||
.media-page__state-text {
|
||
font-size: 26rpx;
|
||
color: #9aa0a6;
|
||
}
|
||
|
||
.media-page__state-text--error {
|
||
color: #f0413e;
|
||
}
|
||
|
||
.media-page__retry {
|
||
padding: 8rpx 28rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 26rpx;
|
||
color: var(--fg-primary, #2b8fff);
|
||
background-color: var(--fg-soft, #eaf4ff);
|
||
}
|
||
|
||
/* ===== 分段控件 ===== */
|
||
.seg-bar {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
margin: 12rpx 24rpx 4rpx;
|
||
}
|
||
|
||
.seg-bar__item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
flex: 1;
|
||
height: 72rpx;
|
||
border-radius: 999rpx;
|
||
background-color: #ffffff;
|
||
opacity: 0.7;
|
||
|
||
&.is-active {
|
||
color: var(--fg-primary, #2b8fff);
|
||
opacity: 1;
|
||
box-shadow: 0 6rpx 16rpx var(--fg-shadow, rgba(43, 143, 255, 0.18));
|
||
background: linear-gradient(135deg, var(--fg-soft, #eaf4ff), #ffffff);
|
||
}
|
||
}
|
||
|
||
.seg-bar__text {
|
||
font-size: 28rpx;
|
||
color: #8f959e;
|
||
}
|
||
|
||
.seg-bar__item.is-active .seg-bar__text {
|
||
color: var(--fg-primary, #2b8fff);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* ===== 列表卡片 ===== */
|
||
.album-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
margin: 0 4rpx 20rpx;
|
||
padding: 28rpx;
|
||
border-radius: 24rpx;
|
||
background-color: #ffffff;
|
||
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
|
||
}
|
||
|
||
.album-card__avatar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 20rpx;
|
||
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.album-card__main {
|
||
display: flex;
|
||
flex: 1;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.album-card__name {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1f2329;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.album-card__desc {
|
||
font-size: 24rpx;
|
||
color: #8f959e;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.album-card__meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.album-card__chip {
|
||
padding: 2rpx 14rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 20rpx;
|
||
color: #5c626b;
|
||
background-color: #f2f3f5;
|
||
}
|
||
|
||
.album-card__chip--code {
|
||
color: var(--fg-primary, #2b8fff);
|
||
background-color: var(--fg-soft, #eaf4ff);
|
||
}
|
||
|
||
.album-card__chip--default {
|
||
color: #ff9500;
|
||
background-color: #fff4e0;
|
||
}
|
||
|
||
.album-card__date {
|
||
margin-left: auto;
|
||
font-size: 20rpx;
|
||
color: #bfc3c9;
|
||
}
|
||
|
||
/* ===== 详情头卡 + 动作栏 ===== */
|
||
/* 查看态头卡:白底卡(同分区卡片风格),主题色只做点缀 */
|
||
.detail-hero {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 28rpx;
|
||
margin: 0 4rpx 20rpx;
|
||
padding: 40rpx 32rpx;
|
||
border-radius: 32rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.detail-hero__acts {
|
||
position: absolute;
|
||
top: 28rpx;
|
||
right: 28rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
/* 圆形动作钮:浅灰底 + 主题色图标,删除用浅红底红图标 */
|
||
.detail-hero__act {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 76rpx;
|
||
height: 76rpx;
|
||
border-radius: 50%;
|
||
background-color: #f2f3f5;
|
||
}
|
||
|
||
.detail-hero__act--danger {
|
||
background-color: #ffeceb;
|
||
}
|
||
|
||
.detail-hero__avatar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: 32rpx;
|
||
background-color: var(--fg-soft, #eaf4ff);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.detail-hero__main {
|
||
display: flex;
|
||
flex: 1;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 10rpx;
|
||
/* 右上角三枚圆钮横排,名称/编号行留避让空间 */
|
||
padding-right: 250rpx;
|
||
}
|
||
|
||
.detail-hero__name {
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
color: #1f2329;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* 编号独立成行:灰字说明 */
|
||
.detail-hero__code {
|
||
font-size: 26rpx;
|
||
color: #8f959e;
|
||
}
|
||
|
||
.detail-hero__chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
/* 头卡胶囊:公开=主题浅底主题字,默认=黄底橙字(同列表卡口径) */
|
||
.detail-hero__chip {
|
||
padding: 6rpx 22rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 24rpx;
|
||
color: var(--fg-primary, #2b8fff);
|
||
background-color: var(--fg-soft, #eaf4ff);
|
||
}
|
||
|
||
.detail-hero__chip--default {
|
||
color: #ff9500;
|
||
background-color: #fff4e0;
|
||
}
|
||
|
||
.detail-actions {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin: 0 4rpx 20rpx;
|
||
}
|
||
|
||
/* 动作钮统一规格(同 option-picker):等高圆角矩形 + 细描边,主钮纯色跟主题 */
|
||
.detail-actions__btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 1;
|
||
height: 96rpx;
|
||
border: 2rpx solid #e5e6eb;
|
||
border-radius: 24rpx;
|
||
font-size: 32rpx;
|
||
color: #5c626b;
|
||
background-color: #ffffff;
|
||
|
||
&--primary {
|
||
border-color: transparent;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
background-color: var(--fg-primary, #2b8fff);
|
||
}
|
||
|
||
&--exhibit {
|
||
border-color: transparent;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
background-color: #21b169;
|
||
}
|
||
|
||
&--danger {
|
||
border-color: transparent;
|
||
font-weight: 600;
|
||
color: #f0413e;
|
||
background-color: #ffeceb;
|
||
}
|
||
}
|
||
|
||
/* ===== 分区卡片 ===== */
|
||
.detail-card {
|
||
margin: 0 4rpx 20rpx;
|
||
padding: 28rpx;
|
||
border-radius: 24rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.detail-card__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
/* 标题行右侧一组:添加钮 + 折叠箭头 */
|
||
.detail-card__head-acts {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 28rpx;
|
||
}
|
||
|
||
.detail-card__title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #1f2329;
|
||
}
|
||
|
||
.detail-card__add {
|
||
font-size: 26rpx;
|
||
color: var(--fg-primary, #2b8fff);
|
||
}
|
||
|
||
.detail-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
.detail-field--row {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.detail-field__label {
|
||
font-size: 26rpx;
|
||
color: #5c626b;
|
||
}
|
||
|
||
/* 编号等不可编辑说明:纯灰字,不再做成禁用输入框(对齐设计图2) */
|
||
.detail-field__static {
|
||
font-size: 28rpx;
|
||
line-height: 1.6;
|
||
color: #8f959e;
|
||
}
|
||
|
||
.detail-field__input,
|
||
.detail-item__input {
|
||
box-sizing: border-box;
|
||
padding: 0 24rpx;
|
||
height: 88rpx;
|
||
border: 2rpx solid #e6e7ea;
|
||
border-radius: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #1f2329;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.detail-field__textarea {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
min-height: 200rpx;
|
||
padding: 20rpx 24rpx;
|
||
border: 2rpx solid #e6e7ea;
|
||
border-radius: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #1f2329;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.detail-empty {
|
||
padding: 40rpx 0;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #bfc3c9;
|
||
}
|
||
|
||
.detail-item {
|
||
padding: 20rpx 0;
|
||
border-bottom: 2rpx solid #f2f3f5;
|
||
}
|
||
|
||
.detail-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.detail-item__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.detail-item__index {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border-radius: 50%;
|
||
font-size: 22rpx;
|
||
color: #8f959e;
|
||
background-color: #f2f3f5;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.detail-item__cover {
|
||
width: 320rpx;
|
||
height: 320rpx;
|
||
border-radius: 16rpx;
|
||
background-color: #f2f3f5;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.detail-item__cover--empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.detail-item__name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 28rpx;
|
||
color: #1f2329;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.detail-item__acts {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.detail-item__link {
|
||
font-size: 24rpx;
|
||
color: var(--fg-primary, #2b8fff);
|
||
}
|
||
|
||
.detail-item__del {
|
||
font-size: 24rpx;
|
||
color: #f0413e;
|
||
}
|
||
|
||
/* 描述列:吃掉封面与动作钮之间的中段宽度 */
|
||
.detail-item__desc {
|
||
display: flex;
|
||
flex: 1;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.detail-item__sub {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
height: 88rpx;
|
||
margin-top: 12rpx;
|
||
padding: 12rpx 20rpx;
|
||
border: 2rpx solid #e6e7ea;
|
||
border-radius: 16rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.5;
|
||
color: #1f2329;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.detail-item__sub--text {
|
||
display: block;
|
||
height: auto;
|
||
border: none;
|
||
background-color: transparent;
|
||
margin-top: 0;
|
||
padding: 0;
|
||
line-height: 1.6;
|
||
color: #8f959e;
|
||
}
|
||
|
||
/* 公开群组明细行:编号作标题,下方两行带标签的群组名称/描述(照旧 ft-item-field) */
|
||
.group-field {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16rpx;
|
||
margin-top: 12rpx;
|
||
padding-left: 56rpx;
|
||
}
|
||
|
||
.group-field__label {
|
||
flex-shrink: 0;
|
||
font-size: 24rpx;
|
||
color: #8f959e;
|
||
}
|
||
|
||
.group-field__text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 24rpx;
|
||
color: #1f2329;
|
||
word-break: break-all;
|
||
}
|
||
</style>
|