158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
import json
|
||
import logging
|
||
import os
|
||
|
||
from odoo import http, api, SUPERUSER_ID
|
||
from odoo.http import request, Response
|
||
|
||
_logger = logging.getLogger(__name__)
|
||
|
||
# H5 模板文件路径
|
||
_H5_TEMPLATE_PATH = os.path.join(
|
||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||
'static', 'src', 'html', 'report_template.html'
|
||
)
|
||
|
||
|
||
class AiReportController(http.Controller):
|
||
"""AI 动态报表公开控制器"""
|
||
|
||
def _get_report_by_token(self, token):
|
||
"""根据 token 查找并验证报表"""
|
||
report = request.env['ai.report'].sudo().search([
|
||
('access_token', '=', token),
|
||
], limit=1)
|
||
if not report:
|
||
_logger.warning('报表不存在: token=%s', token[:12])
|
||
return None, '报表不存在或已被删除'
|
||
if not report.is_valid():
|
||
_logger.warning('报表已过期: token=%s, expire=%s, active=%s',
|
||
token[:12], report.expire_date, report.active)
|
||
return None, '报表已过期或已停用'
|
||
return report, None
|
||
|
||
@http.route('/ai/report/<string:token>', type='http', auth='none',
|
||
methods=['GET'], csrf=False)
|
||
def report_page(self, token, **kw):
|
||
"""渲染 H5 报表页面(增强版:动态注入 REPORT_CONFIG)"""
|
||
report, error = self._get_report_by_token(token)
|
||
if error:
|
||
return Response(
|
||
self._render_error_page(error),
|
||
content_type='text/html; charset=utf-8'
|
||
)
|
||
|
||
# 1. 获取基础数据
|
||
data = report.fetch_data()
|
||
|
||
# 2. 构建 REPORT_CONFIG
|
||
config = {}
|
||
template = report.template_id
|
||
|
||
# 2a. 标题
|
||
config['title'] = report.name
|
||
|
||
# 2b. 列配置:优先使用模板的 report_config.columns
|
||
if template and template.report_config:
|
||
try:
|
||
tmpl_config = json.loads(template.report_config)
|
||
config['columns'] = tmpl_config.get('columns', [])
|
||
config['cards'] = tmpl_config.get('cards', [])
|
||
config['groupBy'] = tmpl_config.get('groupBy', [])
|
||
config['defaultVisible'] = tmpl_config.get('defaultVisible', 10)
|
||
config['colorRules'] = tmpl_config.get('colorRules', [])
|
||
config['filters'] = tmpl_config.get('filters', [])
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# 2c. 如果没有模板或模板没有列配置,从 report 自身字段生成
|
||
if not config.get('columns'):
|
||
fields = json.loads(report.field_list or '[]')
|
||
labels = {}
|
||
if report.field_labels:
|
||
labels = json.loads(report.field_labels)
|
||
config['columns'] = [
|
||
{'k': f, 'l': labels.get(f, f), 'w': 120}
|
||
for f in fields
|
||
]
|
||
|
||
# 2d. 处理数据加载失败
|
||
if not data.get('success'):
|
||
config['data'] = []
|
||
config['description'] = data.get('error', '数据加载失败')
|
||
else:
|
||
# 注入数据
|
||
config['data'] = data.get('records', [])
|
||
config['description'] = "共 {} 条记录".format(data.get('count', 0))
|
||
|
||
# 2e. 子标题(数据概要)
|
||
fields = data.get('fields', [])
|
||
records_count = data.get('count', 0)
|
||
config['subtitle'] = "{} 个字段 · {} 条记录".format(len(fields), records_count)
|
||
|
||
# 3. 读取 H5 模板
|
||
try:
|
||
with open(_H5_TEMPLATE_PATH, 'r', encoding='utf-8') as f:
|
||
html_template = f.read()
|
||
except FileNotFoundError:
|
||
return Response('模板文件未找到', content_type='text/plain')
|
||
|
||
# 4. 注入占位符
|
||
config_json_str = json.dumps(config, ensure_ascii=False, default=str)
|
||
html_content = html_template.replace('{{REPORT_CONFIG_JSON}}', config_json_str)
|
||
html_content = html_content.replace('{{DATA_URL}}', '/ai/report/{}/data'.format(token))
|
||
html_content = html_content.replace('{{REPORT_TITLE}}', report.name or '动态报表')
|
||
|
||
return Response(html_content, content_type='text/html; charset=utf-8')
|
||
|
||
@http.route('/ai/report/<string:token>/data', type='http', auth='none',
|
||
methods=['GET'], csrf=False)
|
||
def report_data(self, token, **kw):
|
||
"""返回报表数据(JSON API)"""
|
||
report, error = self._get_report_by_token(token)
|
||
if error:
|
||
return Response(
|
||
json.dumps({'error': error}, ensure_ascii=False),
|
||
content_type='application/json; charset=utf-8'
|
||
)
|
||
|
||
data = report.fetch_data()
|
||
return Response(
|
||
json.dumps(data, ensure_ascii=False, default=str),
|
||
content_type='application/json; charset=utf-8'
|
||
)
|
||
|
||
@staticmethod
|
||
def _render_error_page(message):
|
||
"""渲染简单的错误页面"""
|
||
return f'''<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>报表错误</title>
|
||
<style>
|
||
body {{
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
display: flex; justify-content: center; align-items: center;
|
||
min-height: 100vh; margin: 0;
|
||
background: #f5f5f5; color: #333;
|
||
}}
|
||
.error-box {{
|
||
text-align: center; padding: 40px;
|
||
background: white; border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||
}}
|
||
.error-icon {{ font-size: 48px; margin-bottom: 16px; }}
|
||
.error-msg {{ font-size: 16px; color: #666; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="error-box">
|
||
<div class="error-icon">⚠</div>
|
||
<div class="error-msg">{message}</div>
|
||
</div>
|
||
</body>
|
||
</html>'''
|