554 lines
17 KiB
Vue
554 lines
17 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|||
|
|
import { useI18n } from 'vue-i18n';
|
|||
|
|
import { useRouter } from 'vue-router';
|
|||
|
|
import type { PublicAlbum, SortMode } from '../../api/client';
|
|||
|
|
|
|||
|
|
// UI-02 V02 活动信息区(从 ActivityPage 提取):宣传图 + 标题/分享 + 说明 +
|
|||
|
|
// 元信息行(浏览/日期/地点)+ 排序/分类工具栏。数据与筛选决策仍在父级
|
|||
|
|
// (ActivityPage 持有序列):本组件只做展示与事件上抛(select-sort /
|
|||
|
|
// select-category),语言切换由父级重取内容,选中分类以 id 跨语言保留。
|
|||
|
|
//
|
|||
|
|
// 数据诚实(方案 §4.1):
|
|||
|
|
// · viewCount 未知(null)显示「—」而非 0;已知值经 Intl.NumberFormat 分组;
|
|||
|
|
// · 日期按相册 IANA 时区格式化(紧凑日期;完整起止+时区放 title 提示),
|
|||
|
|
// 不悄悄换成设备时区;非法时区名回退本地时区(沿用原策略);
|
|||
|
|
// · 说明/场地/地址/宣传图缺失时自然省略节点,不渲染 null/undefined。
|
|||
|
|
const props = defineProps<{
|
|||
|
|
album: PublicAlbum;
|
|||
|
|
/** 浏览数权威值(album 初值 + /visits 与 stats.updated 校准),null=未知。 */
|
|||
|
|
viewCount: number | null;
|
|||
|
|
sortMode: SortMode;
|
|||
|
|
selectedCategory: string | null;
|
|||
|
|
/** 大图打开期间锁定排序(父级序列不变序,§4.2)。 */
|
|||
|
|
sortLocked?: boolean;
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
selectSort: [mode: SortMode];
|
|||
|
|
selectCategory: [categoryId: string | null];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const { t, locale } = useI18n();
|
|||
|
|
const router = useRouter();
|
|||
|
|
|
|||
|
|
// —— 宣传图:签名 URL 加载失败后收起(布局自适应,活动信息仍可用) ——
|
|||
|
|
const bannerFailed = ref(false);
|
|||
|
|
|
|||
|
|
const bannerUrl = computed(() => (props.album.banner !== null && !bannerFailed.value ? props.album.banner.url : null));
|
|||
|
|
|
|||
|
|
// —— 标题/说明的行数钳制与展开(§4.1:默认 2/3 行,超出可展开收起) ——
|
|||
|
|
const titleEl = ref<HTMLElement | null>(null);
|
|||
|
|
const descEl = ref<HTMLElement | null>(null);
|
|||
|
|
const titleClamped = ref(false);
|
|||
|
|
const descClamped = ref(false);
|
|||
|
|
const titleExpanded = ref(false);
|
|||
|
|
const descExpanded = ref(false);
|
|||
|
|
|
|||
|
|
function isClamped(el: HTMLElement | null): boolean {
|
|||
|
|
// -webkit-line-clamp 不改 scrollHeight:溢出时 scrollHeight > clientHeight。
|
|||
|
|
return el !== null && el.scrollHeight - el.clientHeight > 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function measureOverflow(): void {
|
|||
|
|
// 展开态保持按钮可见(供「收起」);收起态按实际溢出决定是否显示「展开」。
|
|||
|
|
titleClamped.value = titleExpanded.value || isClamped(titleEl.value);
|
|||
|
|
descClamped.value = descExpanded.value || isClamped(descEl.value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 相册/语言变化(文案长度随之变化)后:复位宣传图失败与展开态,重测溢出。
|
|||
|
|
watch(
|
|||
|
|
[() => props.album, locale],
|
|||
|
|
() => {
|
|||
|
|
bannerFailed.value = false;
|
|||
|
|
titleExpanded.value = false;
|
|||
|
|
descExpanded.value = false;
|
|||
|
|
measureOverflow();
|
|||
|
|
},
|
|||
|
|
{ flush: 'post' },
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 元素挂载与展开/收起切换后重测(收起后按内容恢复钳制检测)。
|
|||
|
|
watch([titleEl, descEl, titleExpanded, descExpanded], () => measureOverflow(), { flush: 'post' });
|
|||
|
|
|
|||
|
|
// —— 元信息行 ——
|
|||
|
|
const viewsText = computed(() => {
|
|||
|
|
if (props.viewCount === null) return t('viewer.viewsUnknown');
|
|||
|
|
return t('viewer.views', { count: new Intl.NumberFormat(locale.value).format(props.viewCount) });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/** 紧凑日期(无时分):首页元信息行用;窄屏也不爆宽。 */
|
|||
|
|
const dateText = computed(() => formatDate({ year: 'numeric', month: 'long', day: 'numeric' }));
|
|||
|
|
|
|||
|
|
/** 完整起止 + 时区:title 提示承载(§4.1「详情可查看完整起止与时区」)。 */
|
|||
|
|
const dateFullText = computed(() =>
|
|||
|
|
formatDate({
|
|||
|
|
year: 'numeric',
|
|||
|
|
month: 'long',
|
|||
|
|
day: 'numeric',
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit',
|
|||
|
|
timeZoneName: 'short',
|
|||
|
|
}),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
function formatDate(extra: Intl.DateTimeFormatOptions): string {
|
|||
|
|
const { timezone, startAt, endAt } = props.album;
|
|||
|
|
const format = (value: string, withZone: boolean): string => {
|
|||
|
|
const formatter = new Intl.DateTimeFormat(
|
|||
|
|
locale.value,
|
|||
|
|
withZone ? { ...extra, timeZone: timezone } : extra,
|
|||
|
|
);
|
|||
|
|
return formatter.format(new Date(value));
|
|||
|
|
};
|
|||
|
|
try {
|
|||
|
|
const start = format(startAt, true);
|
|||
|
|
return endAt === null ? start : `${start} – ${format(endAt, true)}`;
|
|||
|
|
} catch {
|
|||
|
|
// 非法时区名(RangeError):去掉 timeZone 按本地时区展示,优于整块失败。
|
|||
|
|
const start = format(startAt, false);
|
|||
|
|
return endAt === null ? start : `${start} – ${format(endAt, false)}`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 场地 + 地址拼接(任一缺失时只展示存在的部分;全缺 → 整项省略)。 */
|
|||
|
|
const venueText = computed(() => {
|
|||
|
|
const { venue, address } = props.album.translation;
|
|||
|
|
return [venue, address]
|
|||
|
|
.filter((part): part is string => part !== null)
|
|||
|
|
.join(' · ');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const descriptionText = computed(() => props.album.translation.description);
|
|||
|
|
|
|||
|
|
// —— 分享(V02 最小实现:复制相册链接;navigator.share 完整编排属 Task 5) ——
|
|||
|
|
const shareState = ref<'idle' | 'copied' | 'failed'>('idle');
|
|||
|
|
let shareResetTimer: ReturnType<typeof setTimeout> | undefined;
|
|||
|
|
|
|||
|
|
const albumUrl = computed(() => {
|
|||
|
|
const path = router.resolve({ name: 'activity', params: { token: props.album.publicToken } }).href;
|
|||
|
|
return new URL(path, window.location.origin).href;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
async function copyText(text: string): Promise<boolean> {
|
|||
|
|
try {
|
|||
|
|
if (navigator.clipboard?.writeText !== undefined) {
|
|||
|
|
await navigator.clipboard.writeText(text);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
// 走传统复制兜底
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
const area = document.createElement('textarea');
|
|||
|
|
area.value = text;
|
|||
|
|
area.setAttribute('readonly', '');
|
|||
|
|
area.style.position = 'fixed';
|
|||
|
|
area.style.opacity = '0';
|
|||
|
|
document.body.appendChild(area);
|
|||
|
|
area.select();
|
|||
|
|
const copied = document.execCommand('copy');
|
|||
|
|
area.remove();
|
|||
|
|
return copied;
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function share(): Promise<void> {
|
|||
|
|
const copied = await copyText(albumUrl.value);
|
|||
|
|
shareState.value = copied ? 'copied' : 'failed';
|
|||
|
|
if (shareResetTimer !== undefined) clearTimeout(shareResetTimer);
|
|||
|
|
// 失败时保留可选中文本(不自动消失),成功反馈数秒后收起。
|
|||
|
|
if (copied) {
|
|||
|
|
shareResetTimer = setTimeout(() => {
|
|||
|
|
shareState.value = 'idle';
|
|||
|
|
}, 3000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// —— 分类横向滚动的溢出提示(右侧渐隐,仅在确有未滚出内容时显示) ——
|
|||
|
|
const chipsEl = ref<HTMLElement | null>(null);
|
|||
|
|
const chipsOverflow = ref(false);
|
|||
|
|
|
|||
|
|
function updateChipsOverflow(): void {
|
|||
|
|
const el = chipsEl.value;
|
|||
|
|
if (el === null) {
|
|||
|
|
chipsOverflow.value = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
chipsOverflow.value = el.scrollWidth - el.clientWidth > 1 && el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
[chipsEl, () => props.album.categories, () => props.selectedCategory],
|
|||
|
|
() => updateChipsOverflow(),
|
|||
|
|
{ flush: 'post' },
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
if (shareResetTimer !== undefined) clearTimeout(shareResetTimer);
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="album-header">
|
|||
|
|
<!-- 宣传图:全宽 contain(不裁海报文字);无图/加载失败整块收起。 -->
|
|||
|
|
<div v-if="bannerUrl !== null" class="banner-wrap">
|
|||
|
|
<img
|
|||
|
|
class="banner"
|
|||
|
|
:src="bannerUrl"
|
|||
|
|
:alt="album.translation.title"
|
|||
|
|
@error="bannerFailed = true"
|
|||
|
|
>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<header class="album-info">
|
|||
|
|
<div class="title-row">
|
|||
|
|
<h1 ref="titleEl" class="title" :class="{ clamped: !titleExpanded }">{{ album.translation.title }}</h1>
|
|||
|
|
<button type="button" class="share-btn" @click="share">
|
|||
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|||
|
|
<path d="M12 3v12" />
|
|||
|
|
<path d="m7 8 5-5 5 5" />
|
|||
|
|
<path d="M5 13v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6" />
|
|||
|
|
</svg>
|
|||
|
|
<span>{{ t('viewer.share') }}</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<button v-if="titleClamped" type="button" class="text-expand" @click="titleExpanded = !titleExpanded">
|
|||
|
|
{{ titleExpanded ? t('viewer.readLess') : t('viewer.readMore') }}
|
|||
|
|
</button>
|
|||
|
|
<span v-if="shareState !== 'idle'" class="share-status" role="status">
|
|||
|
|
{{ shareState === 'copied' ? t('viewer.shareCopied') : t('viewer.shareCopyFailed') }}
|
|||
|
|
<span v-if="shareState === 'failed'" class="share-url">{{ albumUrl }}</span>
|
|||
|
|
</span>
|
|||
|
|
|
|||
|
|
<p
|
|||
|
|
v-if="descriptionText !== null"
|
|||
|
|
ref="descEl"
|
|||
|
|
class="description"
|
|||
|
|
:class="{ clamped: !descExpanded }"
|
|||
|
|
>
|
|||
|
|
{{ descriptionText }}
|
|||
|
|
</p>
|
|||
|
|
<button v-if="descClamped" type="button" class="text-expand" @click="descExpanded = !descExpanded">
|
|||
|
|
{{ descExpanded ? t('viewer.readLess') : t('viewer.readMore') }}
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<div class="meta">
|
|||
|
|
<span class="meta-item view-count">
|
|||
|
|
<svg class="meta-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|||
|
|
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
|
|||
|
|
<circle cx="12" cy="12" r="3" />
|
|||
|
|
</svg>
|
|||
|
|
<span>{{ viewsText }}</span>
|
|||
|
|
</span>
|
|||
|
|
<span v-if="dateText !== ''" class="meta-item" :title="dateFullText">
|
|||
|
|
<svg class="meta-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|||
|
|
<rect x="3" y="5" width="18" height="16" rx="2" />
|
|||
|
|
<path d="M8 3v4M16 3v4M3 10h18" />
|
|||
|
|
</svg>
|
|||
|
|
<span>{{ dateText }}</span>
|
|||
|
|
</span>
|
|||
|
|
<span v-if="venueText !== ''" class="meta-item">
|
|||
|
|
<svg class="meta-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|||
|
|
<path d="M12 21s-7-5.1-7-11a7 7 0 0 1 14 0c0 5.9-7 11-7 11Z" />
|
|||
|
|
<circle cx="12" cy="10" r="2.5" />
|
|||
|
|
</svg>
|
|||
|
|
<span>{{ venueText }}</span>
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<!-- 排序 tab(§4.2):高 44px;选中蓝字 + 底部 2px 线;大图打开期间锁定。 -->
|
|||
|
|
<nav class="sort-tabs" :aria-label="t('album.sortLabel')">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
class="sort-tab"
|
|||
|
|
:class="{ active: props.sortMode === 'latest' }"
|
|||
|
|
:aria-pressed="props.sortMode === 'latest'"
|
|||
|
|
:disabled="props.sortLocked"
|
|||
|
|
@click="emit('selectSort', 'latest')"
|
|||
|
|
>
|
|||
|
|
{{ t('viewer.latest') }}
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
class="sort-tab"
|
|||
|
|
:class="{ active: props.sortMode === 'hot' }"
|
|||
|
|
:aria-pressed="props.sortMode === 'hot'"
|
|||
|
|
:disabled="props.sortLocked"
|
|||
|
|
@click="emit('selectSort', 'hot')"
|
|||
|
|
>
|
|||
|
|
{{ t('viewer.popular') }}
|
|||
|
|
</button>
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
<!-- 分类胶囊:来自接口;无分类省略整条;All = 不传分类筛选。 -->
|
|||
|
|
<nav v-if="album.categories.length > 0" class="categories" :aria-label="t('album.categories')">
|
|||
|
|
<div ref="chipsEl" class="chips-track" @scroll.passive="updateChipsOverflow">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
class="chip"
|
|||
|
|
:class="{ active: props.selectedCategory === null }"
|
|||
|
|
:aria-pressed="props.selectedCategory === null"
|
|||
|
|
@click="emit('selectCategory', null)"
|
|||
|
|
>
|
|||
|
|
{{ t('viewer.allCategories') }}
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
v-for="category in album.categories"
|
|||
|
|
:key="category.id"
|
|||
|
|
type="button"
|
|||
|
|
class="chip"
|
|||
|
|
:class="{ active: props.selectedCategory === category.id }"
|
|||
|
|
:aria-pressed="props.selectedCategory === category.id"
|
|||
|
|
@click="emit('selectCategory', category.id)"
|
|||
|
|
>
|
|||
|
|
{{ category.name }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<span v-if="chipsOverflow" class="chips-fade" aria-hidden="true" />
|
|||
|
|
</nav>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
/* token 经祖先 .viewer-shell 提供;独立挂载(无 shell)时回退同值。 */
|
|||
|
|
.album-header {
|
|||
|
|
background: var(--viewer-surface, #fff);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* —— 宣传图:手机约 2.4:1;桌面限高 320px;contain 保海报文字(§3.3) —— */
|
|||
|
|
.banner-wrap {
|
|||
|
|
width: 100%;
|
|||
|
|
aspect-ratio: 2.4 / 1;
|
|||
|
|
max-height: 320px;
|
|||
|
|
background: var(--viewer-surface, #fff);
|
|||
|
|
}
|
|||
|
|
.banner {
|
|||
|
|
display: block;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
object-fit: contain;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* —— 信息区:16px 内边距由本区负责(页面无全局包围,§3.2) —— */
|
|||
|
|
.album-info {
|
|||
|
|
padding: 12px var(--viewer-pad-info, 16px) 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title-row {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
.title {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 1.375rem;
|
|||
|
|
line-height: 1.3;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: var(--viewer-text, #172033);
|
|||
|
|
overflow-wrap: break-word;
|
|||
|
|
}
|
|||
|
|
.title.clamped {
|
|||
|
|
display: -webkit-box;
|
|||
|
|
-webkit-box-orient: vertical;
|
|||
|
|
-webkit-line-clamp: 2;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.share-btn {
|
|||
|
|
flex: none;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
min-height: var(--viewer-touch-target, 44px);
|
|||
|
|
margin: -6px calc(var(--viewer-pad-info, 16px) * -1) -6px 0;
|
|||
|
|
border: none;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--viewer-primary, #1a73e8);
|
|||
|
|
padding: 0 6px;
|
|||
|
|
align-self: flex-start;
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.share-btn svg {
|
|||
|
|
width: 18px;
|
|||
|
|
height: 18px;
|
|||
|
|
fill: none;
|
|||
|
|
stroke: currentColor;
|
|||
|
|
stroke-width: 1.8;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-linejoin: round;
|
|||
|
|
}
|
|||
|
|
.share-btn:focus-visible {
|
|||
|
|
outline: 2px solid var(--viewer-primary, #1a73e8);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
}
|
|||
|
|
.share-status {
|
|||
|
|
display: block;
|
|||
|
|
margin: 2px 0 0;
|
|||
|
|
color: var(--viewer-text-secondary, #647084);
|
|||
|
|
font-size: 0.8125rem;
|
|||
|
|
}
|
|||
|
|
.share-url {
|
|||
|
|
user-select: text;
|
|||
|
|
color: var(--viewer-primary, #1a73e8);
|
|||
|
|
word-break: break-all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.text-expand {
|
|||
|
|
border: none;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--viewer-primary, #1a73e8);
|
|||
|
|
padding: 2px 0 0;
|
|||
|
|
margin: 2px 0 0;
|
|||
|
|
min-height: 32px;
|
|||
|
|
font-size: 0.8125rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.text-expand:focus-visible {
|
|||
|
|
outline: 2px solid var(--viewer-primary, #1a73e8);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* —— 说明:≤3 行 + 展开/收起;保留换行(pre-line),文本插值非 HTML 注入 —— */
|
|||
|
|
.description {
|
|||
|
|
margin: 8px 0 0;
|
|||
|
|
color: var(--viewer-text-secondary, #647084);
|
|||
|
|
font-size: 0.9375rem;
|
|||
|
|
line-height: 1.55;
|
|||
|
|
white-space: pre-line;
|
|||
|
|
overflow-wrap: break-word;
|
|||
|
|
}
|
|||
|
|
.description.clamped {
|
|||
|
|
display: -webkit-box;
|
|||
|
|
-webkit-box-orient: vertical;
|
|||
|
|
-webkit-line-clamp: 3;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* —— 元信息行:窄屏自然换行(§4.1);数字 tabular-nums —— */
|
|||
|
|
.meta {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 6px 20px;
|
|||
|
|
margin: 12px 0 0;
|
|||
|
|
color: var(--viewer-text-secondary, #647084);
|
|||
|
|
font-size: 0.8125rem;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
}
|
|||
|
|
.meta-item {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
min-width: 0;
|
|||
|
|
max-width: 100%;
|
|||
|
|
}
|
|||
|
|
.meta-item span {
|
|||
|
|
overflow-wrap: break-word;
|
|||
|
|
}
|
|||
|
|
.meta-icon {
|
|||
|
|
flex: none;
|
|||
|
|
width: 16px;
|
|||
|
|
height: 16px;
|
|||
|
|
fill: none;
|
|||
|
|
stroke: currentColor;
|
|||
|
|
stroke-width: 1.8;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-linejoin: round;
|
|||
|
|
}
|
|||
|
|
.view-count {
|
|||
|
|
font-variant-numeric: tabular-nums;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* —— 排序 tab —— */
|
|||
|
|
.sort-tabs {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 24px;
|
|||
|
|
margin-top: 8px;
|
|||
|
|
border-bottom: 1px solid var(--viewer-divider, #e5eaf1);
|
|||
|
|
}
|
|||
|
|
.sort-tab {
|
|||
|
|
position: relative;
|
|||
|
|
height: var(--viewer-touch-target, 44px);
|
|||
|
|
border: none;
|
|||
|
|
background: transparent;
|
|||
|
|
padding: 0 2px;
|
|||
|
|
color: var(--viewer-text-secondary, #647084);
|
|||
|
|
font-size: 0.9375rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.sort-tab.active {
|
|||
|
|
color: var(--viewer-primary, #1a73e8);
|
|||
|
|
}
|
|||
|
|
/* 选中底部 2px 线(§4.2) */
|
|||
|
|
.sort-tab.active::after {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: -1px;
|
|||
|
|
height: 2px;
|
|||
|
|
background: var(--viewer-primary, #1a73e8);
|
|||
|
|
}
|
|||
|
|
.sort-tab:focus-visible {
|
|||
|
|
outline: 2px solid var(--viewer-primary, #1a73e8);
|
|||
|
|
outline-offset: -2px;
|
|||
|
|
}
|
|||
|
|
.sort-tab:disabled {
|
|||
|
|
opacity: 0.55;
|
|||
|
|
cursor: default;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* —— 分类胶囊:横向滚动 + 右缘溢出提示 —— */
|
|||
|
|
.categories {
|
|||
|
|
position: relative;
|
|||
|
|
margin-top: 10px;
|
|||
|
|
}
|
|||
|
|
.chips-track {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
padding: 2px var(--viewer-pad-info, 16px) 6px;
|
|||
|
|
margin: -2px calc(var(--viewer-pad-info, 16px) * -1) 0;
|
|||
|
|
scrollbar-width: thin;
|
|||
|
|
}
|
|||
|
|
.chip {
|
|||
|
|
flex: none;
|
|||
|
|
min-height: 36px;
|
|||
|
|
border: 1px solid var(--viewer-divider, #e5eaf1);
|
|||
|
|
border-radius: var(--viewer-radius-pill, 20px);
|
|||
|
|
background: var(--viewer-surface, #fff);
|
|||
|
|
color: var(--viewer-text, #172033);
|
|||
|
|
padding: 0 16px;
|
|||
|
|
font-size: 0.8125rem;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.chip.active {
|
|||
|
|
border-color: var(--viewer-primary, #1a73e8);
|
|||
|
|
background: var(--viewer-primary, #1a73e8);
|
|||
|
|
color: var(--viewer-surface, #fff);
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
.chip:focus-visible {
|
|||
|
|
outline: 2px solid var(--viewer-primary, #1a73e8);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
/* 右缘渐隐提示仍有未滚出的分类(仅确有溢出且未滚到底时出现) */
|
|||
|
|
.chips-fade {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
width: 36px;
|
|||
|
|
pointer-events: none;
|
|||
|
|
background: linear-gradient(to right, transparent, var(--viewer-surface, #fff) 78%);
|
|||
|
|
}
|
|||
|
|
</style>
|