feat(app,viewer): WeChat card share + 'Share to...' label + site og tags

- 按钮文字「分享到微信/其他应用」→「分享到...」(用户需求)
- 微信卡片分享(fluwx 6):WECHAT_APPID 经 --dart-define 注入;已配置且
  装微信 → WXWebpage 卡片(标题=相册名、描述=日期·地点、缩略图=封面压缩
  240px PNG ≤32KB、点击进观众链接);未配置/未装 → 回退系统分享面板
- viewer index.html 加站点级 og 标签(微信/QQ 抓取链接预览用;每相册动态
  og 需服务端注入,登记后续增强)
- 397/397 全绿、analyze 0

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-23 16:05:53 +08:00
parent 77b1519209
commit 5a207b395e
20 changed files with 288 additions and 22 deletions
+4 -1
View File
@@ -25,7 +25,10 @@ export type EmbedUpMessage =
| { photoliveEmbed: true; type: 'resize'; height: number }
| { photoliveEmbed: true; type: 'collapse' }
| { photoliveEmbed: true; type: 'config'; configVersion: number; settings: EmbedSlotSettings }
| { photoliveEmbed: true; type: 'destroy' };
| { photoliveEmbed: true; type: 'destroy' }
/** 照片详情宿主内弹框(用户决策 2026-09-23):url 为观众端深链(同源
* viewer 产物域,宿主 loader 校验后内嵌 iframe 弹框,不新开标签)。 */
| { photoliveEmbed: true; type: 'open-photo'; url: string };
/** 宿主源(referrer 推导);取不到(被隐去/直接打开)为 null。 */
export function embedParentOrigin(): string | null {
+9 -1
View File
@@ -18,6 +18,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useEmbedFeed } from '../embed/useEmbedFeed';
import { clampEmbedIntervalMs, EMBED_CAROUSEL_DEFAULT_MS } from '../embed/feed';
import { postEmbedMessage } from '../embed/postMessage';
import EmbedCarousel from './embed/EmbedCarousel.vue';
import EmbedStream from './embed/EmbedStream.vue';
@@ -131,7 +132,13 @@ function openPhoto(photoId: string): void {
if (albumToken === undefined) return;
const url = new URL(`/a/${encodeURIComponent(albumToken)}/p/${encodeURIComponent(photoId)}`, window.location.origin);
url.searchParams.set('lang', feed.resolvedLocale ?? 'zh-CN');
// noopener:新标签页不持有 opener(sandbox allow-popups-to-escape-sandbox 配合)。
// 宿主内弹框(用户决策 2026-09-23):有父窗(loader 嵌入)→ postMessage
// 由宿主弹框内嵌详情页,不跳出当前网站;无父窗(直接打开/预览)→ 新标签
// noopener(sandbox allow-popups-to-escape-sandbox 配合)。
if (window.parent !== window) {
postEmbedMessage({ photoliveEmbed: true, type: 'open-photo', url: url.toString() });
return;
}
window.open(url, '_blank', 'noopener,noreferrer');
}
</script>
@@ -143,6 +150,7 @@ function openPhoto(photoId: string): void {
<EmbedStream
:photos="feed.status === 'ready' ? feed.photos : []"
:album-token="feed.album?.publicToken ?? null"
:album-title="feed.album?.title ?? ''"
:photo-url="streamPhotoUrl"
:speed="streamSpeed"
:reactions="streamReactions"
@@ -101,6 +101,7 @@ onBeforeUnmount(() => {
ref="streamEl"
:photos="SAMPLE_PHOTOS"
album-token="preview"
album-title="PhotoLive 相册 · 预览"
:photo-url="previewPhotoUrl"
:speed="speed"
:reactions="reactions"
@@ -24,6 +24,8 @@ const props = defineProps<{
photos: PublicEmbedPhoto[];
/** 当前公开相册 token(气泡订阅目标;未解析为 null)。 */
albumToken: string | null;
/** 相册标题(顶部叠加展示,用户决策 2026-09-23)。 */
albumTitle?: string;
/** 观众深链构造器(真实 <a> href;点击经锁定目标受控打开,§4.1)。 */
photoUrl: (photoId: string) => string;
/** 自动上滚速度(CSS px/s)。 */
@@ -402,6 +404,11 @@ onBeforeUnmount(() => {
>
</a>
</div>
<!-- 顶部相册标题(用户决策 2026-09-23):叠加在照片上(不占独立高度,
照片窗仍是全屏照片带),左侧 12px、避让右侧 44px 收起区;渐变底 +
text-shadow 保证可读;空标题(未解析)不渲染。 -->
<p v-if="albumTitle !== ''" class="stream-title" aria-hidden="true">{{ albumTitle }}</p>
<!-- StateOverlay(§2.2):空相册紧凑空态(收起入口保持可用)。 -->
<div v-if="photos.length === 0" class="stream-empty" role="note">
{{ t('embed.comingSoon') }}
@@ -486,6 +493,21 @@ onBeforeUnmount(() => {
background: #f6f8fb;
object-fit: cover;
}
.stream-title {
position: absolute;
top: 11px;
left: 12px;
right: 56px; /* 避让右上收起触区 */
z-index: 2;
margin: 0;
font: 600 12px/1.3 system-ui, -apple-system, 'Segoe UI', 'PingFang SC', sans-serif;
color: rgba(255, 255, 255, 0.94);
text-shadow: 0 1px 3px rgba(9, 14, 20, 0.65);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
}
/* 收起入口(§2.4;效果示例 .collapse):44×44 触区右上、可视圆 24×24
* 内缩 6px(#ffffffd1 底、− 字形 19px #647084)。 */
.stream-collapse {