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:
Binary file not shown.
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 93 KiB |
@@ -422,7 +422,7 @@
|
||||
"bannerPickerHint": "Tap to pick a banner image (shown atop the viewer page)",
|
||||
"bannerSelected": "Banner selected — will upload on save",
|
||||
"bannerUploadFailed": "Banner upload failed",
|
||||
"shareCardMode": "Share to WeChat / other apps",
|
||||
"shareCardMode": "Share to...",
|
||||
"sharePosterMode": "Generate QR poster",
|
||||
"posterDownload": "Download poster image",
|
||||
"posterSaved": "Poster saved to gallery",
|
||||
|
||||
@@ -1985,7 +1985,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @shareCardMode.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'分享到微信 / 其他应用'**
|
||||
/// **'分享到...'**
|
||||
String get shareCardMode;
|
||||
|
||||
/// No description provided for @sharePosterMode.
|
||||
|
||||
@@ -1015,7 +1015,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get bannerUploadFailed => 'Banner upload failed';
|
||||
|
||||
@override
|
||||
String get shareCardMode => 'Share to WeChat / other apps';
|
||||
String get shareCardMode => 'Share to...';
|
||||
|
||||
@override
|
||||
String get sharePosterMode => 'Generate QR poster';
|
||||
|
||||
@@ -982,7 +982,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get bannerUploadFailed => '宣传图上传失败';
|
||||
|
||||
@override
|
||||
String get shareCardMode => '分享到微信 / 其他应用';
|
||||
String get shareCardMode => '分享到...';
|
||||
|
||||
@override
|
||||
String get sharePosterMode => '生成二维码海报';
|
||||
|
||||
@@ -422,7 +422,7 @@
|
||||
"bannerPickerHint": "点击选择宣传图(展示在观众页顶部)",
|
||||
"bannerSelected": "已选择宣传图——保存时上传",
|
||||
"bannerUploadFailed": "宣传图上传失败",
|
||||
"shareCardMode": "分享到微信 / 其他应用",
|
||||
"shareCardMode": "分享到...",
|
||||
"sharePosterMode": "生成二维码海报",
|
||||
"posterDownload": "下载海报图",
|
||||
"posterSaved": "海报已保存到相册",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fluwx/fluwx.dart';
|
||||
|
||||
import 'app/app.dart';
|
||||
import 'app/locale_controller.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// 微信 SDK(卡片分享):WECHAT_APPID 经 --dart-define 注入(微信开放平台
|
||||
// 注册的应用 ID);未配置(空)时跳过注册——分享回退系统分享面板。
|
||||
// Android 需保证打包签名与微信平台注册签名一致,否则拉不起微信。
|
||||
const wechatAppId = String.fromEnvironment('WECHAT_APPID');
|
||||
if (wechatAppId.isNotEmpty) {
|
||||
await Fluwx().registerApi(appId: wechatAppId, doOnIOS: false);
|
||||
}
|
||||
// 启动先读语言偏好再 runApp:首帧即正确语言(无「system 闪变」,
|
||||
// P3 Task 10)。读取失败由 readPersisted 内部兜底为 system。
|
||||
final localeChoice = await LocaleSettings.readPersisted();
|
||||
|
||||
@@ -10,10 +10,12 @@ library;
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fluwx/fluwx.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
@@ -47,17 +49,72 @@ final urlLauncherProvider = Provider<UrlLauncher>(
|
||||
(url) => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication),
|
||||
);
|
||||
|
||||
/// 系统分享注入点(生产=share_plus;测试 override)。
|
||||
typedef ShareLauncher = Future<void> Function(String text);
|
||||
/// 分享请求(卡片字段:微信 SDK 用全集;系统分享回退用 title+url)。
|
||||
class ShareCardRequest {
|
||||
const ShareCardRequest({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.url,
|
||||
this.thumbnailUrl,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final String url;
|
||||
|
||||
/// 卡片缩略图(封面 URL;null=无图,微信显示默认图标)。
|
||||
final String? thumbnailUrl;
|
||||
}
|
||||
|
||||
/// 分享注入点(生产:微信已配置且安装→WXWebpage 卡片;否则系统分享面板。
|
||||
/// 测试 override)。
|
||||
typedef ShareLauncher = Future<void> Function(ShareCardRequest card);
|
||||
|
||||
final shareLauncherProvider = Provider<ShareLauncher>(
|
||||
// share_plus 11:SharePlus.instance.share(旧 Share.share 已废弃)。
|
||||
(ref) =>
|
||||
(text) async => SharePlus.instance.share(
|
||||
ShareParams(text: text, subject: text.split('\n').first),
|
||||
),
|
||||
(ref) => (card) async {
|
||||
const wechatAppId = String.fromEnvironment('WECHAT_APPID');
|
||||
if (wechatAppId.isNotEmpty && await Fluwx().isWeChatInstalled) {
|
||||
final thumb = await compressShareThumbnail(card.thumbnailUrl);
|
||||
await Fluwx().share(
|
||||
WeChatShareWebPageModel(
|
||||
card.url,
|
||||
title: card.title,
|
||||
description: card.description,
|
||||
thumbData: thumb,
|
||||
scene: WeChatScene.session,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 回退:系统分享面板(文本=标题+链接;接收应用自行渲染链接)。
|
||||
// share_plus 11:SharePlus.instance.share(旧 Share.share 已废弃)。
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(text: '${card.title}\n${card.url}', subject: card.title),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// 微信卡片缩略图:下载→缩到 240px→PNG(微信 thumbData 限制 ≤32KB)。
|
||||
Future<Uint8List?> compressShareThumbnail(String? url) async {
|
||||
if (url == null) return null;
|
||||
try {
|
||||
final resp = await Dio().get<List<int>>(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
final data = Uint8List.fromList(resp.data!);
|
||||
final codec = await ui.instantiateImageCodec(data, targetWidth: 240);
|
||||
final frame = await codec.getNextFrame();
|
||||
final resized = await frame.image.toByteData(
|
||||
format: ui.ImageByteFormat.png,
|
||||
);
|
||||
return resized?.buffer.asUint8List();
|
||||
} on Object {
|
||||
// 缩略图失败不阻塞分享(无图卡片仍可发)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 海报保存相册注入点(生产=gal;测试 override)。
|
||||
typedef PosterSaver = Future<void> Function(ui.Image image);
|
||||
|
||||
@@ -89,7 +146,28 @@ class ShareQrPage extends ConsumerWidget {
|
||||
String url,
|
||||
String title,
|
||||
) async {
|
||||
await ref.read(shareLauncherProvider)('$title\n$url');
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final localeName = l10n.localeName;
|
||||
final start = formatAlbumDateTime(
|
||||
album.startAt,
|
||||
album.timezone,
|
||||
localeName,
|
||||
);
|
||||
final end = album.endAt == null
|
||||
? null
|
||||
: formatAlbumDateTime(album.endAt!, album.timezone, localeName);
|
||||
final dateRange = end == null ? start : '$start ~ $end';
|
||||
final venue = albumDisplayVenue(album, localeName);
|
||||
final description = venue == null ? dateRange : '$dateRange · $venue';
|
||||
// 缩略图用封面(列表即有签名 URL,减少一次串行请求)。
|
||||
await ref.read(shareLauncherProvider)(
|
||||
ShareCardRequest(
|
||||
title: title,
|
||||
description: description,
|
||||
url: url,
|
||||
thumbnailUrl: album.cover?.url,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -418,6 +418,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fluwx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluwx
|
||||
sha256: "4b526d281be8560a490bd1b945373a23ab2a12088c8fc5997ffb31cc6fb41082"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
||||
@@ -38,6 +38,7 @@ dependencies:
|
||||
url_launcher: ^6.3.2
|
||||
share_plus: ^13.3.0
|
||||
gal: ^2.3.3
|
||||
fluwx: ^6.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -64,7 +64,7 @@ void main() {
|
||||
reason: '开始时间(相册时区;结束未定不伪造区间)+ 场地(展示回退链)',
|
||||
);
|
||||
// UI06 三方式:系统分享(默认)/ 生成二维码海报 / 复制链接。
|
||||
expect(find.text('Share to WeChat / other apps'), findsOneWidget);
|
||||
expect(find.text('Share to...'), findsOneWidget);
|
||||
expect(find.text('Generate QR poster'), findsOneWidget);
|
||||
expect(find.text('Copy link'), findsOneWidget);
|
||||
// 可选中链接(二维码与复制共用同一 URL)。
|
||||
|
||||
@@ -321,11 +321,24 @@ test.describe.serial('E04 interactions: pause, click lock, keyboard, destroy', (
|
||||
const target = inside.locator('a.stream-photo').first();
|
||||
const photoId = await target.getAttribute('data-photo-id');
|
||||
expect(photoId).toBeTruthy();
|
||||
const popupPromise = page.waitForEvent('popup', { timeout: 5000 });
|
||||
// 宿主内弹框(用户决策 2026-09-23):不新开标签,宿主页覆盖层 + 详情 iframe。
|
||||
let popupOpened = false;
|
||||
page.once('popup', () => {
|
||||
popupOpened = true;
|
||||
});
|
||||
await target.click();
|
||||
const popup = await popupPromise;
|
||||
expect(popup.url()).toContain(`/p/${photoId}`);
|
||||
await popup.close();
|
||||
const modalFrame = page.locator('iframe[title="PhotoLive photo detail"]');
|
||||
await modalFrame.waitFor({ timeout: 5000 });
|
||||
expect(popupOpened).toBe(false);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const f = page.frames().find((fr) => (fr.url().includes('/p/') ?? false));
|
||||
return f?.url() ?? '';
|
||||
})
|
||||
.toContain(`/p/${photoId}`);
|
||||
await page.screenshot({ path: `${OUT_ROOT}/actual/server/photo-modal.png` });
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(modalFrame).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('drag beyond 8px cancels click (§4.1)', async ({ page }) => {
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- 无 JS 时的兜底标题;运行时由 App.vue 按 i18n 覆写 -->
|
||||
<title>现场相册</title>
|
||||
<!-- 分享链接卡片的站点级 og 标签(UI06):微信/QQ 等抓取器不执行 JS,
|
||||
读源码生成链接预览——此处提供全站默认;每相册动态 og(标题/封面)
|
||||
需服务端按 token 注入,属后续增强。 -->
|
||||
<meta property="og:site_name" content="PhotoLive" />
|
||||
<meta property="og:title" content="现场相册 · PhotoLive" />
|
||||
<meta property="og:description" content="扫码或点击查看活动现场照片直播" />
|
||||
<meta property="og:type" content="website" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
@@ -27,6 +27,9 @@ import {
|
||||
V2_HEIGHT,
|
||||
} from './params.js';
|
||||
|
||||
// v2 上行白名单 = v1 白名单 + open-photo(宿主内详情弹框,用户决策 2026-09-23)。
|
||||
var V2_MESSAGE_TYPES = MESSAGE_TYPES.concat(['open-photo']);
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
try {
|
||||
@@ -281,10 +284,121 @@ import {
|
||||
var i = instances.indexOf(inst);
|
||||
if (i >= 0) instances.splice(i, 1);
|
||||
delete seenSlots[inst.config.slot];
|
||||
closePhotoModal();
|
||||
if (instances.length === 0) w.removeEventListener('message', onMessage);
|
||||
scheduleRelayout();
|
||||
}
|
||||
|
||||
// ── 照片详情宿主内弹框(用户决策 2026-09-23):点击滚动照片 → 宿主页
|
||||
// 覆盖层 + 居中卡片内嵌 viewer 深链 iframe(不新开标签)。安全:URL
|
||||
// 必须与组件 iframe 同源(frameOrigin)才打开——iframe 被攻破也无法
|
||||
// 诱导宿主内嵌任意第三方页面;Esc/遮罩/× 关闭;单实例(重复点击换 src)。
|
||||
var photoModal = null;
|
||||
|
||||
function closePhotoModal() {
|
||||
if (photoModal === null) return;
|
||||
if (photoModal.parentNode) photoModal.parentNode.removeChild(photoModal);
|
||||
photoModal = null;
|
||||
w.removeEventListener('keydown', onModalKeydown);
|
||||
}
|
||||
|
||||
function onModalKeydown(e) {
|
||||
if (e.key === 'Escape') closePhotoModal();
|
||||
}
|
||||
|
||||
function openPhotoModal(inst, rawUrl) {
|
||||
if (typeof rawUrl !== 'string' || rawUrl === '') return;
|
||||
var target;
|
||||
try {
|
||||
target = new URL(rawUrl);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (target.origin !== inst.frameOrigin) return; // 同源校验(防任意嵌入)
|
||||
if (photoModal === null) {
|
||||
photoModal = document.createElement('div');
|
||||
var os = photoModal.style;
|
||||
os.position = 'fixed';
|
||||
os.inset = '0';
|
||||
os.zIndex = String(inst.config.zIndex + 1);
|
||||
os.background = 'rgba(9, 14, 20, 0.6)';
|
||||
os.display = 'flex';
|
||||
os.alignItems = 'center';
|
||||
os.justifyContent = 'center';
|
||||
os.padding = '16px';
|
||||
photoModal.addEventListener('pointerdown', function (e) {
|
||||
if (e.target === photoModal) closePhotoModal();
|
||||
});
|
||||
|
||||
var card = document.createElement('div');
|
||||
var cs = card.style;
|
||||
cs.position = 'relative';
|
||||
cs.width = 'min(92vw, 420px)';
|
||||
cs.height = 'min(86vh, 640px)';
|
||||
cs.borderRadius = '12px';
|
||||
cs.border = '1px solid rgba(255,255,255,0.80)';
|
||||
cs.background = '#10161e';
|
||||
cs.boxShadow = '0 12px 40px rgba(15,23,42,0.35)';
|
||||
cs.overflow = 'hidden';
|
||||
|
||||
var detailFrame = document.createElement('iframe');
|
||||
detailFrame.setAttribute(
|
||||
'sandbox',
|
||||
'allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin'
|
||||
);
|
||||
detailFrame.setAttribute('referrerpolicy', 'no-referrer-when-downgrade');
|
||||
detailFrame.setAttribute('title', 'PhotoLive photo detail');
|
||||
var dfs = detailFrame.style;
|
||||
dfs.width = '100%';
|
||||
dfs.height = '100%';
|
||||
dfs.border = '0';
|
||||
dfs.display = 'block';
|
||||
card.appendChild(detailFrame);
|
||||
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.type = 'button';
|
||||
closeBtn.setAttribute('aria-label', isZh(inst.config.lang) ? '关闭照片详情' : 'Close photo detail');
|
||||
var bs = closeBtn.style;
|
||||
bs.position = 'absolute';
|
||||
bs.top = '6px';
|
||||
bs.right = '6px';
|
||||
bs.width = '44px';
|
||||
bs.height = '44px';
|
||||
bs.border = '0';
|
||||
bs.background = 'transparent';
|
||||
bs.color = '#647084';
|
||||
bs.cursor = 'pointer';
|
||||
bs.padding = '0';
|
||||
var dot = document.createElement('span');
|
||||
var dss = dot.style;
|
||||
dss.position = 'absolute';
|
||||
dss.top = '6px';
|
||||
dss.right = '6px';
|
||||
dss.width = '24px';
|
||||
dss.height = '24px';
|
||||
dss.borderRadius = '50%';
|
||||
dss.background = 'rgba(255,255,255,0.82)';
|
||||
dss.display = 'flex';
|
||||
dss.alignItems = 'center';
|
||||
dss.justifyContent = 'center';
|
||||
dot.textContent = '×';
|
||||
dot.style.fontSize = '19px';
|
||||
dot.style.lineHeight = '1';
|
||||
closeBtn.appendChild(dot);
|
||||
closeBtn.addEventListener('click', closePhotoModal);
|
||||
card.appendChild(closeBtn);
|
||||
|
||||
photoModal.appendChild(card);
|
||||
document.body.appendChild(photoModal);
|
||||
w.addEventListener('keydown', onModalKeydown);
|
||||
photoModal.__frame = detailFrame;
|
||||
// 焦点移入弹框关闭钮:键盘事件落到宿主文档(Esc 可关);否则焦点
|
||||
// 残留在小组件 iframe 内,宿主 keydown 收不到。
|
||||
closeBtn.focus();
|
||||
}
|
||||
photoModal.__frame.setAttribute('src', target.href);
|
||||
}
|
||||
|
||||
/** 后台设置应用(config 消息已过 parseConfigMessageV2 版本闸):非 data-*
|
||||
* 显式字段立即生效;collapsedDefault 仅在无用户选择时应用。 */
|
||||
function applyConfig(inst, version, settings) {
|
||||
@@ -374,7 +488,7 @@ import {
|
||||
var data = event.data;
|
||||
if (!data || data.photoliveEmbed !== true) return;
|
||||
var type = data.type;
|
||||
if (MESSAGE_TYPES.indexOf(type) < 0) return;
|
||||
if (V2_MESSAGE_TYPES.indexOf(type) < 0) return;
|
||||
for (var i = 0; i < instances.length; i++) {
|
||||
var inst = instances[i];
|
||||
if (!inst.frame.contentWindow || event.source !== inst.frame.contentWindow) continue;
|
||||
@@ -390,6 +504,9 @@ import {
|
||||
if (parsed !== null) applyConfig(inst, parsed.configVersion, parsed.settings);
|
||||
} else if (type === 'destroy') {
|
||||
destroyInstance(inst);
|
||||
closePhotoModal();
|
||||
} else if (type === 'open-photo') {
|
||||
openPhotoModal(inst, data.url);
|
||||
}
|
||||
// 'resize':v2 窗口固定,显式忽略(§7.3)。
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user