feat: 主站 /ai-web 嵌入 AI 创作子应用并修正路径与通信

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-08 15:32:02 +08:00
parent b9ed5e17c6
commit 3fa1ef95ac
9 changed files with 77 additions and 45 deletions

View File

@ -1,3 +1,6 @@
# 开发环境 # 开发环境
VITE_API_BASE_URL=/api VITE_API_BASE_URL=/api
# AI 绘本子项目本地 dev servervite proxy /ai-web → 该地址)
VITE_AI_CLIENT_DEV_URL=http://localhost:3001
# 兼容旧名:与 VITE_AI_CLIENT_DEV_URL 二选一即可,优先前者
VITE_AI_POST_MESSAGE_URL=http://localhost:3001 VITE_AI_POST_MESSAGE_URL=http://localhost:3001

View File

@ -20,7 +20,12 @@ import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { leaiApi } from '@/api/public' import { leaiApi } from '@/api/public'
const VITE_AI_POST_MESSAGE_URL = import.meta.env.VITE_AI_POST_MESSAGE_URL;
/** 子应用路径前缀,须与 lesingle-aicreate-client 的 Vite base、主站 /ai-web 代理一致;末尾必须有 / */
const LEAI_EMBED_BASE = `${location.origin}/ai-web/`
/** postMessage 的 origin 仅为 scheme+host+port不含路径 */
const LEAI_PARENT_ORIGIN = location.origin
const router = useRouter() const router = useRouter()
const leaiFrame = ref<HTMLIFrameElement | null>(null) const leaiFrame = ref<HTMLIFrameElement | null>(null)
const iframeSrc = ref<string>('') const iframeSrc = ref<string>('')
@ -31,10 +36,10 @@ const loadError = ref<string>('')
const initLeai = async () => { const initLeai = async () => {
loading.value = true loading.value = true
loadError.value = '' loadError.value = ''
console.log('VITE_AI_POST_MESSAGE_URL', VITE_AI_POST_MESSAGE_URL); console.log('[创作工坊] 嵌入地址', LEAI_EMBED_BASE)
try { try {
const data = await leaiApi.getToken() const data = await leaiApi.getToken()
iframeSrc.value = `${VITE_AI_POST_MESSAGE_URL}?token=${encodeURIComponent(data.token)}&orgId=${encodeURIComponent(data.orgId)}&phone=${encodeURIComponent(data.phone)}&embed=1` iframeSrc.value = `${LEAI_EMBED_BASE}?token=${encodeURIComponent(data.token)}&orgId=${encodeURIComponent(data.orgId)}&phone=${encodeURIComponent(data.phone)}&embed=1`
} catch (err: any) { } catch (err: any) {
const errMsg = err?.response?.data?.message || err?.message || '加载失败' const errMsg = err?.response?.data?.message || err?.message || '加载失败'
loadError.value = errMsg loadError.value = errMsg
@ -48,8 +53,8 @@ const initLeai = async () => {
/** 监听乐读派 H5 postMessage 事件 */ /** 监听乐读派 H5 postMessage 事件 */
const handleMessage = (event: MessageEvent) => { const handleMessage = (event: MessageEvent) => {
if (event.origin !== VITE_AI_POST_MESSAGE_URL) { if (event.origin !== LEAI_PARENT_ORIGIN) {
console.log('event.origin', event.origin); console.log('event.origin', event.origin)
return return
} }
const msg = event.data const msg = event.data
@ -99,7 +104,7 @@ const handleTokenExpired = async (payload: any) => {
orgId: data.orgId, orgId: data.orgId,
phone: data.phone, phone: data.phone,
}, },
}, VITE_AI_POST_MESSAGE_URL) }, LEAI_PARENT_ORIGIN)
console.log('[创作工坊] Token已刷新') console.log('[创作工坊] Token已刷新')
} catch (err) { } catch (err) {
console.error('[创作工坊] Token刷新失败:', err) console.error('[创作工坊] Token刷新失败:', err)

View File

@ -1,4 +1,4 @@
import { defineConfig } from "vite"; import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { resolve } from "path"; import { resolve } from "path";
@ -16,6 +16,14 @@ const getBase = (mode: string) => {
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
// vite.config 顶层无法从 .env 读取 VITE_*,需在回调内 loadEnv
const env = loadEnv(mode, process.cwd(), "");
// AI 绘本子项目 Vite 开发服务地址(主站 /ai-web 代理到此地址,需与 lesingle-aicreate-client 端口一致)
const aiProxyTarget =
env.VITE_AI_CLIENT_DEV_URL ||
env.VITE_AI_POST_MESSAGE_URL ||
"http://localhost:3001";
return { return {
base: getBase(mode), base: getBase(mode),
plugins: [vue()], plugins: [vue()],
@ -33,6 +41,11 @@ export default defineConfig(({ mode }) => {
changeOrigin: true, changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''), // rewrite: (path) => path.replace(/^\/api/, ''),
}, },
"/ai-web": {
target: aiProxyTarget,
changeOrigin: true,
// 子项目 base 为 /ai-web/,不重写路径,直接转发到子 dev server
},
}, },
}, },
}; };

View File

@ -1,3 +1,5 @@
# 开发环境 # 开发环境
# 与主项目同源的父页面 originiframe postMessage 校验目标),局域网调试可改为 http://192.168.1.119:3000
VITE_CREATE_POST_MESSAGE_URL=http://localhost:3000 VITE_CREATE_POST_MESSAGE_URL=http://localhost:3000
# 子应用部署路径前缀,须与主项目代理路径 /ai-web 一致
VITE_APP_BASE=/ai-web/

View File

@ -1,2 +1,4 @@
# 生产环境 # 生产环境
VITE_CREATE_POST_MESSAGE_URL=http://localhost:3000 VITE_CREATE_POST_MESSAGE_URL=http://localhost:3000
# 若静态资源挂在主站 /ai-web 下,保持与开发一致;独立域名部署可改为 /
VITE_APP_BASE=/ai-web/

View File

@ -44,7 +44,7 @@ function handleTokenExpired_standalone() {
window.location.href = redirectUrl + (redirectUrl.includes('?') ? '&' : '?') window.location.href = redirectUrl + (redirectUrl.includes('?') ? '&' : '?')
+ 'returnPath=' + returnPath + '&orgId=' + encodeURIComponent(store.orgId) + 'returnPath=' + returnPath + '&orgId=' + encodeURIComponent(store.orgId)
} else { } else {
window.location.href = '/' window.location.href = import.meta.env.BASE_URL || '/' // 与 Vite base/ai-web/)一致
} }
setTimeout(() => { isRefreshing = false }, 3000) setTimeout(() => { isRefreshing = false }, 3000)
} }

View File

@ -1,10 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import config from '@/utils/config'
import { store } from '@/utils/store' import { store } from '@/utils/store'
import bridge from '@/utils/bridge'
// base 由 Vite 的 import.meta.env.BASE_URL如 /ai-web/)注入,路由表内不要再写 /ai-web 前缀
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/', path: '/',
@ -79,7 +78,7 @@ router.beforeEach((to, from, next) => {
// ─── 全局 token 初始化:从 URL query 读取 ─── // ─── 全局 token 初始化:从 URL query 读取 ───
// 支持 iframe src 直接带 token 加载任意页面 // 支持 iframe src 直接带 token 加载任意页面
// 如: /edit-info/190xxx?token=xxx&orgId=xxx&phone=xxx&embed=1 // 例: /edit-info/190xxx?token=xxx&orgId=xxx&phone=xxx&embed=1
const searchParams = new URLSearchParams(window.location.search) const searchParams = new URLSearchParams(window.location.search)
const urlToken = searchParams.get('token') const urlToken = searchParams.get('token')
const urlOrgId = searchParams.get('orgId') const urlOrgId = searchParams.get('orgId')

View File

@ -3,7 +3,8 @@
* 封装 H5 与企业父页面的双向通信 * 封装 H5 与企业父页面的双向通信
*/ */
import config from './config' import config from './config'
const VITE_CREATE_POST_MESSAGE_URL = import.meta.env.VITE_CREATE_POST_MESSAGE_URL; // const VITE_CREATE_POST_MESSAGE_URL = import.meta.env.VITE_CREATE_POST_MESSAGE_URL;
const VITE_AI_POST_MESSAGE_URL = location.origin;
const SOURCE = 'leai-creation' const SOURCE = 'leai-creation'
const VERSION = 1 const VERSION = 1
const TIMEOUT = 30000 const TIMEOUT = 30000
@ -17,7 +18,7 @@ const targetOrigin = config.parentOrigins.length > 0 ? config.parentOrigins[0] :
export function send(type, payload = {}) { export function send(type, payload = {}) {
if (!isEmbedded) return if (!isEmbedded) return
window.parent.postMessage({ source: SOURCE, version: VERSION, type, payload }, VITE_CREATE_POST_MESSAGE_URL) window.parent.postMessage({ source: SOURCE, version: VERSION, type, payload }, VITE_AI_POST_MESSAGE_URL)
} }
export function request(type, payload = {}) { export function request(type, payload = {}) {
if (!isEmbedded) return Promise.reject(new Error('Not in iframe')) if (!isEmbedded) return Promise.reject(new Error('Not in iframe'))
@ -41,7 +42,7 @@ function onMessage(event) {
console.log('event.origin', event.origin); console.log('event.origin', event.origin);
return return
} }
console.log('onMessage',event); console.log('onMessage', event);
if (config.parentOrigins.length > 0 && !config.parentOrigins.includes(event.origin)) { if (config.parentOrigins.length > 0 && !config.parentOrigins.includes(event.origin)) {
return return
} }

View File

@ -1,36 +1,43 @@
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
// HTTPS: 启动时加 --https 参数,或设环境变量 VITE_HTTPS=true // 与主项目 frontend 的 /ai-web 代理一致;可通过 VITE_APP_BASE 覆盖
// 默认 HTTP局域网测试友好无证书问题 export default defineConfig(async ({ mode }) => {
const useHttps = process.argv.includes('--https') || process.env.VITE_HTTPS === 'true' const env = loadEnv(mode, process.cwd(), '')
let sslPlugin = [] const base = env.VITE_APP_BASE || '/ai-web/'
if (useHttps) {
try {
const basicSsl = (await import('@vitejs/plugin-basic-ssl')).default
sslPlugin = [basicSsl()]
} catch { /* basicSsl not installed, skip */ }
}
export default defineConfig({ const useHttps = process.argv.includes('--https') || env.VITE_HTTPS === 'true'
plugins: [vue(), ...sslPlugin], let sslPlugin = []
resolve: { if (useHttps) {
alias: { try {
'@': fileURLToPath(new URL('./src', import.meta.url)) const basicSsl = (await import('@vitejs/plugin-basic-ssl')).default
sslPlugin = [basicSsl()]
} catch {
/* basicSsl not installed, skip */
} }
}, }
server: {
port: 3001, return {
host: '0.0.0.0', base,
proxy: { plugins: [vue(), ...sslPlugin],
'/api': { resolve: {
target: 'http://localhost:8080', alias: {
changeOrigin: true '@': fileURLToPath(new URL('./src', import.meta.url))
}, }
'/ws': { },
target: 'http://localhost:8080', server: {
ws: true port: 3001,
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/ws': {
target: 'http://localhost:8080',
ws: true
}
} }
} }
} }