fix: iframe link click - add allow-popups, use target=_blank
Previous fix injected base target=_top but the sandbox attribute lacked allow-top-navigation, so the browser blocked link clicks and the user had to Ctrl+Click as a workaround. Fix: sandbox allow-same-origin allow-popups + base target=_blank. Links now open in new tab (better UX, no content loss). Also fix deploy/rebuild.ps1 to read jar from target/ instead of deleted deploy/baota/. Add agent_test/ scripts: create_7_test_projects, upload_test_html, manual_package.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""Create 7 test projects on the production server to demonstrate carousel pagination."""
|
||||
import requests
|
||||
import json
|
||||
|
||||
API_BASE = "http://www.1415243231.top:30081/api"
|
||||
|
||||
# Existing projects on server (don't touch)
|
||||
existing = [
|
||||
{"id": 1, "name": "MiniMax 套餐用量"},
|
||||
{"id": 2, "name": "GitHub每日热点-1"},
|
||||
{"id": 3, "name": "AI每日热点-1"},
|
||||
]
|
||||
|
||||
# 7 new test projects with varied metadata
|
||||
new_projects = [
|
||||
{
|
||||
"name": "产品设计周报",
|
||||
"description": "每周产品迭代总结,含 UI 改版、用户反馈分析",
|
||||
},
|
||||
{
|
||||
"name": "技术分享月刊",
|
||||
"description": "团队技术分享、代码评审要点、最佳实践",
|
||||
},
|
||||
{
|
||||
"name": "运营数据日报",
|
||||
"description": "DAU/MAU、转化漏斗、付费用户行为",
|
||||
},
|
||||
{
|
||||
"name": "AI 模型评测",
|
||||
"description": "GPT-4 / Claude / Gemini / 文心一言横向对比",
|
||||
},
|
||||
{
|
||||
"name": "客户调研记录",
|
||||
"description": "用户访谈纪要、需求池、痛点分析",
|
||||
},
|
||||
{
|
||||
"name": "团队 OKR 复盘",
|
||||
"description": "Q2 目标完成情况、Q3 规划、风险清单",
|
||||
},
|
||||
{
|
||||
"name": "行业研究报告",
|
||||
"description": "SaaS / AI Agent / 垂直行业趋势分析",
|
||||
},
|
||||
]
|
||||
|
||||
print(f"现有项目 {len(existing)} 个: {[p['name'] for p in existing]}")
|
||||
print(f"\n准备创建 {len(new_projects)} 个测试项目...")
|
||||
|
||||
created = []
|
||||
for p in new_projects:
|
||||
try:
|
||||
r = requests.post(f"{API_BASE}/projects", json=p, timeout=15)
|
||||
if r.status_code == 201:
|
||||
data = r.json()
|
||||
created.append(data)
|
||||
print(f" [OK] id={data['id']} {data['name']}")
|
||||
else:
|
||||
print(f" [FAIL {r.status_code}] {p['name']}: {r.text[:200]}")
|
||||
except Exception as e:
|
||||
print(f" [ERROR] {p['name']}: {e}")
|
||||
|
||||
print(f"\n成功创建 {len(created)} 个")
|
||||
|
||||
# Verify final state
|
||||
r = requests.get(f"{API_BASE}/projects", timeout=10)
|
||||
all_projects = r.json()
|
||||
print(f"\n服务器现有项目总数: {len(all_projects)}")
|
||||
print("项目列表:")
|
||||
for p in all_projects:
|
||||
print(f" - id={p['id']:2d} {p['name']:20s} 报告数={p.get('reportCount', 0):3d}")
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Manually package the deploy zip (workaround for PowerShell encoding issues)."""
|
||||
import shutil
|
||||
import os
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
|
||||
PROJECT_ROOT = r"D:\Idea Project\publish"
|
||||
TMP = os.path.join(os.environ["TEMP"], f"publish_deploy_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
||||
FRONTEND_DIR = os.path.join(TMP, "frontend")
|
||||
JAR_SRC = os.path.join(PROJECT_ROOT, "target", "daily-report-distribution-1.0.0.jar")
|
||||
ZIP_OUT = os.path.join(PROJECT_ROOT, "deploy", "publish_deploy.zip")
|
||||
|
||||
# Verify inputs
|
||||
assert os.path.exists(JAR_SRC), f"Missing jar: {JAR_SRC}"
|
||||
assert os.path.exists(os.path.join(PROJECT_ROOT, "dist", "index.html")), "Missing dist/index.html"
|
||||
assert os.path.exists(os.path.join(PROJECT_ROOT, "server.js")), "Missing server.js"
|
||||
|
||||
# Clean & create
|
||||
shutil.rmtree(TMP, ignore_errors=True)
|
||||
os.makedirs(FRONTEND_DIR)
|
||||
|
||||
# Copy frontend build
|
||||
shutil.copy2(os.path.join(PROJECT_ROOT, "dist", "index.html"), FRONTEND_DIR)
|
||||
shutil.copytree(os.path.join(PROJECT_ROOT, "dist", "assets"),
|
||||
os.path.join(FRONTEND_DIR, "assets"))
|
||||
shutil.copy2(os.path.join(PROJECT_ROOT, "server.js"), FRONTEND_DIR)
|
||||
print(" [OK] Frontend files staged")
|
||||
|
||||
# Copy backend jar
|
||||
shutil.copy2(JAR_SRC, os.path.join(TMP, "app.jar"))
|
||||
print(" [OK] Backend jar staged")
|
||||
|
||||
# Zip
|
||||
if os.path.exists(ZIP_OUT):
|
||||
os.remove(ZIP_OUT)
|
||||
with zipfile.ZipFile(ZIP_OUT, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for root, dirs, files in os.walk(TMP):
|
||||
for f in files:
|
||||
full = os.path.join(root, f)
|
||||
arc = os.path.relpath(full, TMP)
|
||||
zf.write(full, arc)
|
||||
|
||||
size_mb = os.path.getsize(ZIP_OUT) / 1024 / 1024
|
||||
print(f" [OK] Packaged: {size_mb:.1f} MB")
|
||||
|
||||
# Cleanup
|
||||
shutil.rmtree(TMP, ignore_errors=True)
|
||||
print(f"\nDone. Output: {ZIP_OUT}")
|
||||
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>链接跳转测试报告</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; line-height: 1.6; color: #1a1a1a; }
|
||||
h1 { color: #FF7A45; border-bottom: 3px solid #FF7A45; padding-bottom: 8px; }
|
||||
h2 { color: #1a1a1a; margin-top: 32px; }
|
||||
.test-box { background: #FFF7E6; border-left: 4px solid #FF7A45; padding: 12px 16px; margin: 12px 0; border-radius: 8px; }
|
||||
a { color: #FF7A45; text-decoration: underline; font-weight: 500; }
|
||||
a:hover { color: #1a1a1a; }
|
||||
.info { background: #f0f0f0; padding: 8px 12px; border-radius: 6px; font-size: 14px; color: #555; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>HTML 链接跳转测试</h1>
|
||||
|
||||
<p class="info">
|
||||
<strong>测试目标:</strong>验证在 iframe 中点击链接,<strong>不需要 Ctrl+点击</strong>,链接应该正常打开。
|
||||
</p>
|
||||
|
||||
<h2>1. 外部链接(应该打开新标签)</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="https://www.google.com" target="_blank">Google</a> —— 普通外部链接</p>
|
||||
<p><a href="https://github.com" target="_blank">GitHub</a> —— 另一个外部链接</p>
|
||||
<p><a href="https://www.bing.com" target="_self">Bing</a> —— 强制 target=_self(应该走 base 默认行为)</p>
|
||||
</div>
|
||||
|
||||
<h2>2. 不同 target 行为对比</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="https://www.baidu.com" target="_blank">→ target="_blank"(新标签)</a></p>
|
||||
<p><a href="https://www.zhihu.com" target="_self">→ target="_self"(iframe 内)</a></p>
|
||||
<p><a href="https://www.example.com">→ 无 target(继承 base,默认新标签)</a></p>
|
||||
</div>
|
||||
|
||||
<h2>3. 锚点链接(页面内跳转)</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="#section-bottom">跳到页面底部</a></p>
|
||||
<p><a href="#section-middle">跳到中间部分</a></p>
|
||||
</div>
|
||||
|
||||
<h2 id="section-middle">中间部分(锚点目标 1)</h2>
|
||||
<p>这里是页面的中间部分。Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
||||
|
||||
<h2>4. 相对路径链接</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="/">→ 根路径</a></p>
|
||||
<p><a href="./other-page">→ 当前目录</a></p>
|
||||
<p><a href="../parent">→ 父目录</a></p>
|
||||
</div>
|
||||
|
||||
<h2>5. 邮件链接</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="mailto:test@example.com">发送邮件给 test@example.com</a></p>
|
||||
</div>
|
||||
|
||||
<h2 id="section-bottom">页面底部(锚点目标 2)</h2>
|
||||
<p>恭喜你滚动到了底部。所有链接测试完成!</p>
|
||||
<p><a href="#">↑ 回到顶部</a></p>
|
||||
</body>
|
||||
</html
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Upload a test HTML report with multiple links to verify the iframe link fix."""
|
||||
import requests
|
||||
|
||||
API_BASE = "http://www.1415243231.top:30081/api"
|
||||
PROJECT_ID = 4 # "产品设计周报" - new test project
|
||||
|
||||
# Test HTML with various link types
|
||||
html_content = """<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>链接跳转测试报告</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; line-height: 1.6; color: #1a1a1a; }
|
||||
h1 { color: #FF7A45; border-bottom: 3px solid #FF7A45; padding-bottom: 8px; }
|
||||
h2 { color: #1a1a1a; margin-top: 32px; }
|
||||
.test-box { background: #FFF7E6; border-left: 4px solid #FF7A45; padding: 12px 16px; margin: 12px 0; border-radius: 8px; }
|
||||
a { color: #FF7A45; text-decoration: underline; font-weight: 500; }
|
||||
a:hover { color: #1a1a1a; }
|
||||
.info { background: #f0f0f0; padding: 8px 12px; border-radius: 6px; font-size: 14px; color: #555; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>HTML 链接跳转测试</h1>
|
||||
|
||||
<p class="info">
|
||||
<strong>测试目标:</strong>验证在 iframe 中点击链接,<strong>不需要 Ctrl+点击</strong>,链接应该正常打开。
|
||||
</p>
|
||||
|
||||
<h2>1. 外部链接(应该打开新标签)</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="https://www.google.com" target="_blank">Google</a> —— 普通外部链接</p>
|
||||
<p><a href="https://github.com" target="_blank">GitHub</a> —— 另一个外部链接</p>
|
||||
<p><a href="https://www.bing.com" target="_self">Bing</a> —— 强制 target=_self(应该走 base 默认行为)</p>
|
||||
</div>
|
||||
|
||||
<h2>2. 不同 target 行为对比</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="https://www.baidu.com" target="_blank">→ target="_blank"(新标签)</a></p>
|
||||
<p><a href="https://www.zhihu.com" target="_self">→ target="_self"(iframe 内)</a></p>
|
||||
<p><a href="https://www.example.com">→ 无 target(继承 base,默认新标签)</a></p>
|
||||
</div>
|
||||
|
||||
<h2>3. 锚点链接(页面内跳转)</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="#section-bottom">跳到页面底部</a></p>
|
||||
<p><a href="#section-middle">跳到中间部分</a></p>
|
||||
</div>
|
||||
|
||||
<h2 id="section-middle">中间部分(锚点目标 1)</h2>
|
||||
<p>这里是页面的中间部分。Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
||||
|
||||
<h2>4. 相对路径链接</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="/">→ 根路径</a></p>
|
||||
<p><a href="./other-page">→ 当前目录</a></p>
|
||||
<p><a href="../parent">→ 父目录</a></p>
|
||||
</div>
|
||||
|
||||
<h2>5. 邮件链接</h2>
|
||||
<div class="test-box">
|
||||
<p><a href="mailto:test@example.com">发送邮件给 test@example.com</a></p>
|
||||
</div>
|
||||
|
||||
<h2 id="section-bottom">页面底部(锚点目标 2)</h2>
|
||||
<p>恭喜你滚动到了底部。所有链接测试完成!</p>
|
||||
<p><a href="#">↑ 回到顶部</a></p>
|
||||
</body>
|
||||
</html"""
|
||||
|
||||
# Save to temp file
|
||||
test_path = r"D:\Idea Project\publish\agent_test\test_link_click.html"
|
||||
with open(test_path, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
|
||||
# Upload via API
|
||||
print(f"正在上传测试 HTML 到项目 {PROJECT_ID}...")
|
||||
with open(test_path, "rb") as f:
|
||||
files = {"file": ("test_link_click.html", f, "text/html")}
|
||||
data = {"projectId": PROJECT_ID, "fileType": "HTML"}
|
||||
r = requests.post(f"{API_BASE}/reports", files=files, data=data, timeout=30)
|
||||
|
||||
if r.status_code == 201:
|
||||
result = r.json()
|
||||
print(f"\n[OK] 上传成功")
|
||||
print(f" - Report ID: {result['id']}")
|
||||
print(f" - File name: {result['fileName']}")
|
||||
print(f" - File size: {result.get('fileSize', 'N/A')} bytes")
|
||||
print(f"\n验证步骤:")
|
||||
print(f" 1. 打开 https://www.1415243231.top/publish_dishboard")
|
||||
print(f" 2. 进入项目 #{PROJECT_ID} '产品设计周报'")
|
||||
print(f" 3. 点击左侧 'test_link_click.html'")
|
||||
print(f" 4. 在右侧预览区,点击任意链接 → 应该直接打开新标签,无需 Ctrl")
|
||||
else:
|
||||
print(f"[FAIL] {r.status_code}: {r.text[:500]}")
|
||||
+11
-6
@@ -8,17 +8,22 @@ Write-Host '=== 打包 publish_deploy.zip ===' -ForegroundColor Cyan
|
||||
New-Item -ItemType Directory -Path "$tmpDir\frontend" -Force | Out-Null
|
||||
|
||||
# 2. 复制前端构建产物
|
||||
Copy-Item "$ProjectRoot\dist\index.html" "$tmpDir\frontend\" -Force
|
||||
Copy-Item "$ProjectRoot\dist\assets" "$tmpDir\frontend\" -Recurse -Force
|
||||
Copy-Item "$ProjectRoot\server.js" "$tmpDir\frontend\" -Force
|
||||
Copy-Item "D:\Idea Project\publish\dist\index.html" "$tmpDir\frontend\" -Force
|
||||
Copy-Item "D:\Idea Project\publish\dist\assets" "$tmpDir\frontend\" -Recurse -Force
|
||||
Copy-Item "D:\Idea Project\publish\server.js" "$tmpDir\frontend\" -Force
|
||||
Write-Host ' [OK] 前端文件就位' -ForegroundColor Green
|
||||
|
||||
# 3. 复制后端 jar
|
||||
Copy-Item "$ProjectRoot\deploy\baota\app.jar" "$tmpDir\" -Force
|
||||
# 3. 复制后端 jar(从 Maven build output)
|
||||
$jarPath = "D:\Idea Project\publish\target\daily-report-distribution-1.0.0.jar"
|
||||
if (-not (Test-Path $jarPath)) {
|
||||
Write-Host " [ERROR] Backend jar not found at $jarPath. Run 'mvnw.cmd package' first." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Copy-Item $jarPath "$tmpDir\app.jar" -Force
|
||||
Write-Host ' [OK] 后端 jar 就位' -ForegroundColor Green
|
||||
|
||||
# 4. 打包
|
||||
$outputZip = "$ProjectRoot\deploy\publish_deploy.zip"
|
||||
$outputZip = "D:\Idea Project\publish\deploy\publish_deploy.zip"
|
||||
Compress-Archive -Path "$tmpDir\*" -DestinationPath $outputZip -Force
|
||||
$size = [math]::Round((Get-Item $outputZip).Length / 1MB, 1)
|
||||
Write-Host " [OK] 打包完成: $size MB" -ForegroundColor Green
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
ref="iframeRef"
|
||||
:srcdoc="htmlContent"
|
||||
class="w-full h-full"
|
||||
sandbox="allow-same-origin"
|
||||
sandbox="allow-same-origin allow-popups"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
@@ -135,19 +135,19 @@ const renderedMarkdown = computed(() => {
|
||||
return marked(props.content)
|
||||
})
|
||||
|
||||
// Inject <base target="_top"> so links open in the parent window, not the iframe
|
||||
// Inject <base target="_blank"> so links open in a new tab instead of being trapped in the iframe.
|
||||
// Combined with sandbox="allow-popups", this lets users click links normally (no Ctrl+Click needed).
|
||||
const htmlContent = computed(() => {
|
||||
if (!props.content) return ''
|
||||
const base = '<base target="_top">'
|
||||
// If content already has <head>, inject base right after <head> tag
|
||||
const base = '<base target="_blank">'
|
||||
if (/<head[^>]*>/i.test(props.content)) {
|
||||
return props.content.replace(/(<head[^>]*>)/i, `$1\n${base}`)
|
||||
}
|
||||
// Otherwise prepend before the body or first element
|
||||
if (/<body[^>]*>/i.test(props.content)) {
|
||||
return props.content.replace(/(<body[^>]*>)/i, `${base}\n$1`)
|
||||
}
|
||||
return base + props.content
|
||||
// Fragment (no <head>/<body>): wrap in a full document so the base tag is valid
|
||||
return `<!DOCTYPE html><html><head>${base}</head><body>${props.content}</body></html>`
|
||||
})
|
||||
|
||||
const formatUploadTime = (isoString) => {
|
||||
|
||||
@@ -29,8 +29,9 @@ describe('FilePreview.vue', () => {
|
||||
|
||||
const iframe = wrapper.find('iframe')
|
||||
expect(iframe.exists()).toBe(true)
|
||||
// srcdoc now has <base target="_top"> injected for click-out-of-iframe
|
||||
expect(iframe.attributes('srcdoc')).toContain('target="_top"')
|
||||
// srcdoc now has <base target="_blank"> injected so links open in new tab
|
||||
// (combined with sandbox allow-popups, this enables normal click without Ctrl+Click)
|
||||
expect(iframe.attributes('srcdoc')).toContain('target="_blank"')
|
||||
expect(iframe.attributes('srcdoc')).toContain('Test')
|
||||
})
|
||||
|
||||
@@ -48,7 +49,9 @@ describe('FilePreview.vue', () => {
|
||||
})
|
||||
|
||||
const iframe = wrapper.find('iframe')
|
||||
expect(iframe.attributes('sandbox')).toBe('allow-same-origin')
|
||||
// sandbox must allow popups so target="_blank" links can actually open a new tab
|
||||
expect(iframe.attributes('sandbox')).toContain('allow-same-origin')
|
||||
expect(iframe.attributes('sandbox')).toContain('allow-popups')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user