diff --git a/miniprogram/pages/web-view/index.js b/miniprogram/pages/web-view/index.js index 91de532d..b417a495 100644 --- a/miniprogram/pages/web-view/index.js +++ b/miniprogram/pages/web-view/index.js @@ -12,6 +12,9 @@ const { } = require('../../config'); const { appendHashParams, + buildWebViewSharePath, + buildWebViewShareTimelineQuery, + resolveShareTargetFromWebViewMessage, resolveWebViewUrlFromRuntimeConfig, } = require('./index.shared'); @@ -23,7 +26,6 @@ const AUTH_RESULT_STORAGE_KEY = 'genarrative:mini-program-auth-result'; const AUTH_ACTION_LOGIN = 'login'; const PAY_RESULT_RECHECK_DELAY_MS = 120; const WEB_VIEW_SHARE_TITLE = '陶泥儿'; -const WEB_VIEW_SHARE_PATH = '/pages/web-view/index'; function showWebViewShareMenu() { if (typeof wx.showShareMenu !== 'function') { @@ -36,17 +38,25 @@ function showWebViewShareMenu() { }); } -function buildWebViewShareAppMessage() { +function resolveNativeShareQuery(page) { + return ( + (page && page._currentShareTarget) || + (page && page._lastLaunchQuery) || + {} + ); +} + +function buildWebViewShareAppMessage(query = {}) { return { title: WEB_VIEW_SHARE_TITLE, - path: WEB_VIEW_SHARE_PATH, + path: buildWebViewSharePath(query), }; } -function buildWebViewShareTimeline() { +function buildWebViewShareTimeline(query = {}) { return { title: WEB_VIEW_SHARE_TITLE, - query: '', + query: buildWebViewShareTimelineQuery(query), }; } @@ -669,15 +679,19 @@ Page({ }, handleWebViewMessage(event) { + const shareTarget = resolveShareTargetFromWebViewMessage(event.detail); + if (shareTarget) { + this._currentShareTarget = shareTarget; + } // 中文注释:支付和订阅消息都由独立 native 页面承接,web-view 消息只保留调试输出。 console.info('[web-view] message', event.detail); }, onShareAppMessage() { - return buildWebViewShareAppMessage(); + return buildWebViewShareAppMessage(resolveNativeShareQuery(this)); }, onShareTimeline() { - return buildWebViewShareTimeline(); + return buildWebViewShareTimeline(resolveNativeShareQuery(this)); }, }); diff --git a/miniprogram/pages/web-view/index.shared.js b/miniprogram/pages/web-view/index.shared.js index 6357960c..5f05096a 100644 --- a/miniprogram/pages/web-view/index.shared.js +++ b/miniprogram/pages/web-view/index.shared.js @@ -1,4 +1,6 @@ const ALLOWED_TARGET_PATHS = new Set(['/works/detail']); +const SHARE_TARGET_MESSAGE_TYPE = 'genarrative:share-target'; +const WEB_VIEW_SHARE_PATH = '/pages/web-view/index'; function trimTrailingSlash(value) { return String(value || '').trim().replace(/\/+$/u, ''); @@ -75,6 +77,60 @@ function resolveLaunchTargetQuery(query) { }; } +function buildWebViewSharePath(query = {}, basePath = WEB_VIEW_SHARE_PATH) { + const launchTarget = resolveLaunchTargetQuery(query); + if (!launchTarget.targetPath) { + return basePath; + } + + return appendQuery(basePath, { + targetPath: launchTarget.targetPath, + work: launchTarget.work, + }); +} + +function buildWebViewShareTimelineQuery(query = {}) { + const launchTarget = resolveLaunchTargetQuery(query); + if (!launchTarget.targetPath) { + return ''; + } + + return new URLSearchParams({ + targetPath: launchTarget.targetPath, + work: launchTarget.work, + }).toString(); +} + +function normalizeShareTargetMessageData(value) { + const message = value && value.data ? value.data : value; + if (!message || message.type !== SHARE_TARGET_MESSAGE_TYPE) { + return null; + } + + const payload = message.payload || {}; + const launchTarget = resolveLaunchTargetQuery(payload); + if (!launchTarget.targetPath) { + return null; + } + + return { + ...launchTarget, + title: String(payload.title || '').trim(), + }; +} + +function resolveShareTargetFromWebViewMessage(detail) { + const dataList = detail && Array.isArray(detail.data) ? detail.data : []; + for (let index = dataList.length - 1; index >= 0; index -= 1) { + const target = normalizeShareTargetMessageData(dataList[index]); + if (target) { + return target; + } + } + + return normalizeShareTargetMessageData(detail); +} + function appendLaunchTargetToEntryUrl(entryUrl, query) { const launchTarget = resolveLaunchTargetQuery(query); if (!launchTarget.targetPath) { @@ -123,7 +179,10 @@ module.exports = { appendHashParams, appendLaunchTargetToEntryUrl, appendQuery, + buildWebViewSharePath, + buildWebViewShareTimelineQuery, normalizeTargetPath, + resolveShareTargetFromWebViewMessage, resolveLaunchTargetQuery, resolveWebViewUrlFromRuntimeConfig, }; diff --git a/miniprogram/pages/web-view/index.test.js b/miniprogram/pages/web-view/index.test.js index a677d747..a04adbc5 100644 --- a/miniprogram/pages/web-view/index.test.js +++ b/miniprogram/pages/web-view/index.test.js @@ -4,6 +4,9 @@ import webViewBridge from './index.shared.js'; const { appendLaunchTargetToEntryUrl, + buildWebViewSharePath, + buildWebViewShareTimelineQuery, + resolveShareTargetFromWebViewMessage, resolveWebViewUrlFromRuntimeConfig, } = webViewBridge; @@ -53,4 +56,55 @@ describe('mini program web-view launch target', () => { expect(url.pathname).toBe('/'); expect(url.searchParams.get('work')).toBeNull(); }); + + test('keeps public work params in native mini program share paths', () => { + const sharePath = buildWebViewSharePath({ + targetPath: '/works/detail', + work: 'BB-12345678', + }); + const url = new URL(sharePath, 'https://mini.test'); + + expect(url.pathname).toBe('/pages/web-view/index'); + expect(url.searchParams.get('targetPath')).toBe('/works/detail'); + expect(url.searchParams.get('work')).toBe('BB-12345678'); + expect( + buildWebViewShareTimelineQuery({ + targetPath: '/works/detail', + work: 'BB-12345678', + }), + ).toBe('targetPath=%2Fworks%2Fdetail&work=BB-12345678'); + }); + + test('reads the latest H5 recommended work share target from web-view messages', () => { + expect( + resolveShareTargetFromWebViewMessage({ + data: [ + { + data: { + type: 'genarrative:share-target', + payload: { + targetPath: '/works/detail', + work: 'PZ-0001', + title: '旧作品', + }, + }, + }, + { + data: { + type: 'genarrative:share-target', + payload: { + targetPath: '/works/detail', + work: 'BB-12345678', + title: '汪汪声浪', + }, + }, + }, + ], + }), + ).toEqual({ + targetPath: '/works/detail', + work: 'BB-12345678', + title: '汪汪声浪', + }); + }); }); diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index cadbbe54..2c531f71 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -365,6 +365,7 @@ import { updateVisualNovelWork, } from '../../services/visual-novel-works'; import { requestGenerationResultSubscribePermission } from '../../services/wechatMiniProgramSubscribe'; +import { postWechatMiniProgramShareTarget } from '../../services/wechatMiniProgramShareTarget'; import { woodenFishClient, type WoodenFishGalleryCardResponse, @@ -741,6 +742,25 @@ function resolveRecommendEntryShareStage( return 'work-detail'; } +function postRecommendEntryMiniProgramShareTarget( + entry: PlatformPublicGalleryCard | null | undefined, +) { + if (!entry) { + return false; + } + + const publicWorkCode = resolvePlatformPublicWorkCode(entry)?.trim(); + if (!publicWorkCode) { + return false; + } + + return postWechatMiniProgramShareTarget({ + targetPath: '/works/detail', + work: publicWorkCode, + title: entry.worldName, + }); +} + function resolveUnsupportedPublicWorkActionMessage( entry: PlatformPublicGalleryCard, actionLabel: string, @@ -5250,6 +5270,7 @@ export function PlatformEntryFlowShellImpl({ return; } + postRecommendEntryMiniProgramShareTarget(entry); openPublishShareModal({ title: entry.worldName, publicWorkCode, @@ -16473,6 +16494,22 @@ export function PlatformEntryFlowShellImpl({ woodenFishRun, }); + useEffect(() => { + if ( + selectionStage !== 'platform' || + platformBootstrap.platformTab !== 'home' || + !activeRecommendEntry + ) { + return; + } + + postRecommendEntryMiniProgramShareTarget(activeRecommendEntry); + }, [ + activeRecommendEntry, + platformBootstrap.platformTab, + selectionStage, + ]); + useEffect(() => { if ( isDesktopLayout || diff --git a/src/services/wechatMiniProgramShareTarget.test.ts b/src/services/wechatMiniProgramShareTarget.test.ts new file mode 100644 index 00000000..2a8398da --- /dev/null +++ b/src/services/wechatMiniProgramShareTarget.test.ts @@ -0,0 +1,67 @@ +/* @vitest-environment jsdom */ + +import { afterEach, describe, expect, test, vi } from 'vitest'; + +import { + buildWechatMiniProgramShareTargetMessage, + postWechatMiniProgramShareTarget, +} from './wechatMiniProgramShareTarget'; + +afterEach(() => { + vi.restoreAllMocks(); + Reflect.deleteProperty(window, 'wx'); + window.history.replaceState(null, '', '/'); +}); + +describe('wechatMiniProgramShareTarget', () => { + test('builds a compact share target message for mini program native share', () => { + expect( + buildWechatMiniProgramShareTargetMessage({ + targetPath: '/works/detail', + work: ' BB-12345678 ', + title: ' 汪汪声浪 ', + }), + ).toEqual({ + type: 'genarrative:share-target', + payload: { + targetPath: '/works/detail', + work: 'BB-12345678', + title: '汪汪声浪', + }, + }); + }); + + test('posts the current recommended work to mini program web-view host', () => { + const postMessage = vi.fn(); + window.history.replaceState( + null, + '', + '/?clientRuntime=wechat_mini_program', + ); + window.wx = { + miniProgram: { + postMessage, + }, + }; + + expect( + postWechatMiniProgramShareTarget({ + targetPath: '/works/detail', + work: 'BB-12345678', + title: '汪汪声浪', + }), + ).toBe(true); + + expect(postMessage).toHaveBeenCalledWith({ + data: { + type: 'genarrative:share-target', + payload: { + targetPath: '/works/detail', + work: 'BB-12345678', + title: '汪汪声浪', + }, + }, + }); + }); +}); + diff --git a/src/services/wechatMiniProgramShareTarget.ts b/src/services/wechatMiniProgramShareTarget.ts new file mode 100644 index 00000000..6ca0dfd9 --- /dev/null +++ b/src/services/wechatMiniProgramShareTarget.ts @@ -0,0 +1,61 @@ +import { isWechatMiniProgramWebViewRuntime } from './authService'; + +const MESSAGE_TYPE = 'genarrative:share-target'; + +export type WechatMiniProgramShareTarget = { + targetPath: '/works/detail'; + work: string; + title?: string | null; +}; + +function normalizeShareTarget( + target: WechatMiniProgramShareTarget | null | undefined, +) { + const work = target?.work?.trim() ?? ''; + if (!work) { + return null; + } + + return { + targetPath: '/works/detail' as const, + work, + title: target?.title?.trim() || undefined, + }; +} + +export function buildWechatMiniProgramShareTargetMessage( + target: WechatMiniProgramShareTarget | null | undefined, +) { + const normalizedTarget = normalizeShareTarget(target); + return normalizedTarget + ? { + type: MESSAGE_TYPE, + payload: normalizedTarget, + } + : null; +} + +export function postWechatMiniProgramShareTarget( + target: WechatMiniProgramShareTarget | null | undefined, +) { + if ( + typeof window === 'undefined' || + !isWechatMiniProgramWebViewRuntime() || + typeof window.wx?.miniProgram?.postMessage !== 'function' + ) { + return false; + } + + const message = buildWechatMiniProgramShareTargetMessage(target); + if (!message) { + return false; + } + + // 中文注释:微信 web-view 会在分享等时机把 postMessage 数据交给原生页, + // 小程序页据此把右上角系统分享指向当前推荐作品。 + window.wx.miniProgram.postMessage({ + data: message, + }); + return true; +} +