Files
Docker/uniapp/youjiala/src/pages/tab/media.vue
T
2026-10-16 11:57:29 +08:00

1095 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>