""" Quant-UX prototype -> single self-contained interactive HTML exporter (Plan A). Fetches the app model + its image assets through the REST API, then emits one standalone .html file containing an embedded lightweight renderer. The file works fully offline: double-click it in any browser and click through the prototype (button -> screen navigation via the model's flow lines). Supported widget types: Box, Label, Button, TextBox, Password, TextArea, Image, Icon (basic), HotSpot, CheckBox, RadioBox, Switch, ToggleButton, SegmentButton, SegmentPicker, DropDown. Interactions: click/dblclick/mouseover flow lines (widget or group -> screen/widget), input typing, toggle controls. Advanced widgets (charts, data grids, logic/data blocks) render as their background box and are listed as unsupported. Usage as CLI: QUX_ADMIN_EMAIL=... QUX_ADMIN_PASSWORD=... \ python quantux_export.py --app [--out out.html] Usage as library: from quantux_export import QuantUXExporter path = QuantUXExporter(client).export(app_id) """ import argparse import base64 import json import os import re from quantux_client import QuantUXClient, QuantUXError MIME = { "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif", "svg": "image/svg+xml", "webp": "image/webp", "bmp": "image/bmp", "ico": "image/x-icon", } # --------------------------------------------------------------------------- # HTML template. {{MODEL}} / {{IMAGES}} / {{TITLE}} are replaced by the # exporter (never use .format() on this string). # --------------------------------------------------------------------------- HTML_TEMPLATE = r""" {{TITLE}}
Quant-UX 离线导出
""" def _sanitize(name): return re.sub(r"[^\w\u4e00-\u9fff\-]+", "_", name).strip("_") or "prototype" class QuantUXExporter: """Export a Quant-UX app to a self-contained interactive HTML file.""" def __init__(self, client): self.client = client # ------------------------------------------------------------- images def collect_image_urls(self, model): urls = {} def add(bg): if not bg: return u = bg.get("url") or bg.get("src") if isinstance(bg, dict) else bg if isinstance(u, str) and u and u not in urls: urls[u] = u for scr in model.get("screens", {}).values(): add((scr.get("style") or {}).get("backgroundImage")) for w in model.get("widgets", {}).values(): add((w.get("style") or {}).get("backgroundImage")) p = w.get("props") or {} for key in ("url", "src", "image"): if isinstance(p.get(key), str): add(p[key]) return list(urls.keys()) def fetch_image(self, url): token = self.client.token or "" sep = "&" if "?" in url else "?" u = f"{self.client.base_url}/rest/images/{url}{sep}token={token}" resp = self.client.session.get(u, timeout=self.client.timeout) if resp.status_code != 200: raise QuantUXError(f"image fetch HTTP {resp.status_code} for {url}") return resp.content def fetch_all_images(self, urls): result = {} for url in urls: try: data = self.fetch_image(url) ext = url.rsplit(".", 1)[-1].lower() if "." in url else "png" mime = MIME.get(ext, "image/png") result[url] = "data:" + mime + ";base64," + base64.b64encode(data).decode() except Exception as exc: # noqa: BLE001 print(f"WARN: could not fetch image {url}: {exc}") return result # --------------------------------------------------------------- html def build_html(self, model, images): title = model.get("name") or "Quant-UX Prototype" html = HTML_TEMPLATE html = html.replace("{{TITLE}}", title) html = html.replace( "{{MODEL}}", json.dumps(model, ensure_ascii=False) ) html = html.replace( "{{IMAGES}}", json.dumps(images, ensure_ascii=False) ) return html def export(self, app_id, out_path=None): model = self.client.get_app(app_id) urls = self.collect_image_urls(model) images = self.fetch_all_images(urls) html = self.build_html(model, images) if not out_path: out_path = _sanitize(model.get("name") or app_id) + ".html" with open(out_path, "w", encoding="utf-8") as fh: fh.write(html) return out_path # ---------------------------------------------------------------------- CLI def main(): parser = argparse.ArgumentParser(description="Export a Quant-UX prototype to a local interactive HTML file") parser.add_argument("--app", required=True, help="Quant-UX app id") parser.add_argument("--out", default="", help="output html path") parser.add_argument("--base", default=os.environ.get("QUX_BASE_URL", "http://127.0.0.1:8082")) parser.add_argument("--email", default=os.environ.get("QUX_ADMIN_EMAIL", "")) parser.add_argument("--password", default=os.environ.get("QUX_ADMIN_PASSWORD", "")) args = parser.parse_args() client = QuantUXClient(args.base) if args.email and args.password: client.login(args.email, args.password) else: raise SystemExit("login required: pass --email/--password or set QUX_ADMIN_EMAIL/QUX_ADMIN_PASSWORD") path = QuantUXExporter(client).export(args.app, args.out) print(f"exported: {path} ({os.path.getsize(path)} bytes)") if __name__ == "__main__": main()