Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
S
skills
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
allen.wang
skills
Commits
efff934e
Commit
efff934e
authored
Apr 18, 2026
by
allen.wang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix:优化报表效果
parent
1027cb42
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
764 additions
and
0 deletions
+764
-0
cli.config.json
vip-report/.playwright/cli.config.json
+7
-0
vip-report-recrop-render.ps1
vip-report/bin/vip-report-recrop-render.ps1
+97
-0
requirements.txt
vip-report/requirements.txt
+6
-0
recrop_from_raw.py
vip-report/scripts/recrop_from_raw.py
+429
-0
tableau_export.py
vip-report/scripts/tableau_export.py
+225
-0
No files found.
vip-report/.playwright/cli.config.json
0 → 100644
View file @
efff934e
{
"browser"
:
{
"launchOptions"
:
{
"args"
:
[
"--no-proxy-server"
]
}
}
}
vip-report/bin/vip-report-recrop-render.ps1
0 → 100644
View file @
efff934e
param
(
[
Parameter
(
Mandatory
=
$true
)]
[
string
]
$Slide
,
[
Parameter
(
Mandatory
=
$true
)]
[
string
]
$ReportMonth
,
[
string
]
$ConfigPath
=
""
,
[
int
]
$ReportYear
=
0,
[
int
]
$CompareYear
=
0,
[
ValidateSet
(
"cumulative"
,
"single"
)]
[
string
]
$MonthMode
=
"cumulative"
,
[
string
]
$TemplatePath
=
""
,
[
string
]
$OutputPath
=
""
)
$ErrorActionPreference
=
"Stop"
$root
=
Split-Path
-Parent
$PSScriptRoot
function
Resolve-ProjectPath
{
param
([
string
]
$PathValue
)
if
(
-not
$PathValue
)
{
return
$PathValue
}
if
([
System.IO.Path]::IsPathRooted
(
$PathValue
))
{
return
$PathValue
}
return
[
System.IO.Path]::GetFullPath
((
Join-Path
$root
$PathValue
))
}
function
Resolve-PythonCommand
{
if
(
Get-Command py -ErrorAction SilentlyContinue
)
{
return
@
(
"py"
,
"-3"
)
}
if
(
Get-Command python -ErrorAction SilentlyContinue
)
{
return
@
(
"python"
)
}
throw
"Python launcher not found. Please install py or python."
}
if
(
-not
$ConfigPath
)
{
$ConfigPath
=
"config.yaml"
}
if
(
-not
$TemplatePath
)
{
$TemplatePath
=
"Report.pptx"
}
if
(
-not
$OutputPath
)
{
$OutputPath
=
"output\generated-
$(
$Slide
.ToLower
(
)
)-
$ReportMonth
-recrop.pptx"
}
$ConfigPath
=
Resolve-ProjectPath
$ConfigPath
$TemplatePath
=
Resolve-ProjectPath
$TemplatePath
$OutputPath
=
Resolve-ProjectPath
$OutputPath
if
(
-not
(
Test-Path
-LiteralPath
$ConfigPath
))
{
throw
"ConfigPath not found:
$ConfigPath
"
}
if
(
-not
(
Test-Path
-LiteralPath
$TemplatePath
))
{
throw
"TemplatePath not found:
$TemplatePath
"
}
$pythonArgs
=
@
(
"
$root
\scripts\recrop_from_raw.py"
,
"--config"
,
"
$ConfigPath
"
,
"--slide"
,
"
$Slide
"
,
"--report-month"
,
"
$ReportMonth
"
,
"--month-mode"
,
"
$MonthMode
"
)
if
(
$ReportYear
-gt 0
)
{
$pythonArgs
+
=
@
(
"--report-year"
,
"
$ReportYear
"
)
}
if
(
$CompareYear
-gt 0
)
{
$pythonArgs
+
=
@
(
"--compare-year"
,
"
$CompareYear
"
)
}
$pythonCommand
=
Resolve-PythonCommand
if
(
$pythonCommand
.Count -eq 2
)
{
$opsOutput
=
&
$pythonCommand
[
0]
$pythonCommand
[
1] @pythonArgs 2>&1
}
else
{
$opsOutput
=
&
$pythonCommand
[
0] @pythonArgs 2>&1
}
if
(
$LASTEXITCODE
-ne 0
)
{
$details
=
(
$opsOutput
|
ForEach
-Object
{
"
$_
"
})
-join
[
Environment]::NewLine
throw
"recrop_from_raw.py failed with exit code
$LASTEXITCODE
`n
$details
"
}
$opsPath
=
(
$opsOutput
|
Select-Object
-Last 1
)
.ToString
()
.Trim
()
if
(
-not
$opsPath
)
{
throw
"recrop_from_raw.py did not return an operations path."
}
powershell -ExecutionPolicy Bypass -File
"
$root
\bin\vip-report-render.ps1"
-TemplatePath
"
$TemplatePath
"
-OutputPath
"
$OutputPath
"
-OperationsPath
"
$opsPath
"
| Out-Null
if
(
$LASTEXITCODE
-ne 0
)
{
throw
"vip-report-render.ps1 failed with exit code
$LASTEXITCODE
"
}
Write-Output
$OutputPath
vip-report/requirements.txt
0 → 100644
View file @
efff934e
Pillow
PyMySQL
PyYAML
lxml
matplotlib
numpy
vip-report/scripts/recrop_from_raw.py
0 → 100644
View file @
efff934e
This diff is collapsed.
Click to expand it.
vip-report/scripts/tableau_export.py
0 → 100644
View file @
efff934e
from
__future__
import
annotations
import
json
from
pathlib
import
Path
from
PIL
import
Image
DEFAULT_STALE_DIALOG_SELECTORS
=
[
'[data-tb-test-id="FileDownload-Dlg-Dialog-Glass-Root"]'
,
'[data-tb-test-id="FileDownload-Dlg-Dialog-Floater-Root"]'
,
'[data-tb-test-id="detailedErrorDialog-Dialog-Glass-Root"]'
,
'[data-tb-test-id="detailedErrorDialog-Dialog-Glass"]'
,
]
def
build_export_image_js
(
inner_frame_fragment
:
str
,
output_path
:
str
,
*
,
viewport
:
dict
[
str
,
int
]
|
None
=
None
,
tableau_wait_ms
:
int
=
90000
,
download_wait_ms
:
int
=
60000
,
dialog_selectors
:
list
[
str
]
|
None
=
None
,
)
->
str
:
"""Build Playwright code that exports the currently filtered Tableau view as an image."""
payload
=
json
.
dumps
(
{
"inner_frame_fragment"
:
inner_frame_fragment
,
"output_path"
:
output_path
,
"viewport"
:
viewport
or
{},
"tableau_wait_ms"
:
tableau_wait_ms
,
"download_wait_ms"
:
download_wait_ms
,
"dialog_selectors"
:
dialog_selectors
or
DEFAULT_STALE_DIALOG_SELECTORS
,
},
ensure_ascii
=
False
,
)
return
f
"""async function(page) {{
const spec = {payload};
if (spec.viewport && spec.viewport.width && spec.viewport.height) {{
await page.setViewportSize({{
width: Number(spec.viewport.width),
height: Number(spec.viewport.height),
}});
}}
await page.waitForLoadState('domcontentloaded').catch(() => null);
async function inspectFrame(frame, preferredFragment) {{
const frameUrl = typeof frame.url === 'function' ? frame.url() : '';
try {{
const snapshot = await frame.evaluate(() => {{
const vizCount = window.tableau?.VizManager?.getVizs?.().length || 0;
return {{
url: location.href,
title: document.title,
readyState: document.readyState,
hasTableau: !!(window.tableau && window.tableau.VizManager),
vizCount,
}};
}});
return {{
...snapshot,
frameUrl,
matchesTargetFrame: !!(preferredFragment && frameUrl && frameUrl.includes(preferredFragment)),
}};
}} catch (error) {{
return {{
url: frameUrl,
title: null,
readyState: null,
hasTableau: false,
vizCount: 0,
frameUrl,
matchesTargetFrame: !!(preferredFragment && frameUrl && frameUrl.includes(preferredFragment)),
error: String(error && error.message || error),
}};
}}
}}
async function locateVizFrame(timeoutMs, preferredFragment) {{
const deadline = Date.now() + timeoutMs;
let lastSnapshots = [];
while (Date.now() < deadline) {{
const rankedFrames = page.frames()
.map((frame) => {{
const frameUrl = typeof frame.url === 'function' ? frame.url() : '';
const isPreferred = !!(preferredFragment && frameUrl.includes(preferredFragment));
return {{
frame,
rank: isPreferred ? 0 : (frame === page.mainFrame() ? 1 : 2),
}};
}})
.sort((left, right) => left.rank - right.rank);
lastSnapshots = [];
for (const item of rankedFrames) {{
const snapshot = await inspectFrame(item.frame, preferredFragment);
lastSnapshots.push(snapshot);
if (snapshot.vizCount > 0) {{
return {{ frame: item.frame, snapshots: lastSnapshots }};
}}
}}
const stillOnSignin =
(page.url() || '').includes('/#/signin') ||
lastSnapshots.some((snapshot) => (snapshot.url || '').includes('/#/signin'));
if (stillOnSignin) {{
throw new Error('Tableau page is still on sign-in: ' + JSON.stringify({{ pageUrl: page.url(), frames: lastSnapshots }}));
}}
const hasChromeError =
(page.url() || '').startsWith('chrome-error://') ||
lastSnapshots.some((snapshot) => (snapshot.url || '').startsWith('chrome-error://'));
if (hasChromeError) {{
throw new Error('Tableau page failed to load: ' + JSON.stringify({{ pageUrl: page.url(), frames: lastSnapshots }}));
}}
await page.waitForTimeout(1000);
}}
throw new Error('Timed out waiting for Tableau viz: ' + JSON.stringify({{ pageUrl: page.url(), frames: lastSnapshots }}));
}}
const located = await locateVizFrame(Number(spec.tableau_wait_ms || 90000), spec.inner_frame_fragment || '');
const targetFrame = located.frame;
await targetFrame.evaluate((selectors) => {{
for (const selector of selectors || []) {{
document.querySelector(selector)?.remove();
}}
}}, spec.dialog_selectors).catch(() => null);
const exportResult = await targetFrame.evaluate(async (config) => {{
const viz = window.tableau?.VizManager?.getVizs?.()?.[0];
if (!viz) {{
throw new Error('Tableau viz is not ready for export.');
}}
if (typeof viz.showExportImageDialog !== 'function') {{
throw new Error('Tableau export image dialog is not available.');
}}
const workbook = typeof viz.getWorkbook === 'function' ? viz.getWorkbook() : null;
const activeSheet = workbook && typeof workbook.getActiveSheet === 'function'
? workbook.getActiveSheet()
: null;
for (const selector of config.dialog_selectors || []) {{
document.querySelector(selector)?.remove();
}}
viz.showExportImageDialog();
return {{
frameUrl: location.href,
activeSheet: activeSheet && typeof activeSheet.getName === 'function'
? activeSheet.getName()
: null,
}};
}}, {{
dialog_selectors: spec.dialog_selectors,
}});
const downloadScopes = [
page,
targetFrame,
...page.frames().filter((frame) => frame !== targetFrame),
];
const linkCandidates = [
{{
selector: '[role="dialog"] a.tabDownloadFileButton, [role="dialog"] button.tabDownloadFileButton',
text: null,
}},
{{
selector: '[role="dialog"] button, [role="dialog"] a',
text: /^(下载|Download)$/i,
}},
{{
selector: 'a.tabDownloadFileButton, button.tabDownloadFileButton',
text: null,
}},
];
let link = null;
for (const scope of downloadScopes) {{
for (const candidate of linkCandidates) {{
const locator =
candidate.text
? scope.locator(candidate.selector).filter({{ hasText: candidate.text }}).first()
: scope.locator(candidate.selector).first();
try {{
await locator.waitFor({{ state: 'visible', timeout: 5000 }});
link = locator;
break;
}} catch (error) {{}}
}}
if (link) {{
break;
}}
}}
if (!link) {{
throw new Error('Export download button was not found after opening the Tableau image export dialog.');
}}
const [download] = await Promise.all([
page.waitForEvent('download', {{ timeout: Number(spec.download_wait_ms || 60000) }}),
link.click({{ force: true }}),
]);
await download.saveAs(spec.output_path);
return {{
...exportResult,
pageUrl: page.url(),
outputPath: spec.output_path,
suggestedFilename: download.suggestedFilename(),
frameSnapshots: located.snapshots,
}};
}}
"""
def
normalize_image_size
(
path
:
Path
,
size
:
dict
[
str
,
int
]
|
tuple
[
int
,
int
])
->
None
:
"""Resize an exported Tableau image to the expected crop canvas when needed."""
if
isinstance
(
size
,
dict
):
target_size
=
(
int
(
size
[
"width"
]),
int
(
size
[
"height"
]))
else
:
target_size
=
(
int
(
size
[
0
]),
int
(
size
[
1
]))
with
Image
.
open
(
path
)
as
image
:
if
image
.
size
==
target_size
:
return
resized
=
image
.
resize
(
target_size
,
Image
.
Resampling
.
LANCZOS
)
resized
.
save
(
path
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment