Showing
1 changed file
with
639 additions
and
0 deletions
| @@ -49,6 +49,7 @@ class HTMLRenderer: | @@ -49,6 +49,7 @@ class HTMLRenderer: | ||
| 49 | "figure", | 49 | "figure", |
| 50 | "kpiGrid", | 50 | "kpiGrid", |
| 51 | "swotTable", | 51 | "swotTable", |
| 52 | + "pestTable", | ||
| 52 | "engineQuote", | 53 | "engineQuote", |
| 53 | } | 54 | } |
| 54 | INLINE_ARTIFACT_KEYS = { | 55 | INLINE_ARTIFACT_KEYS = { |
| @@ -1023,6 +1024,7 @@ class HTMLRenderer: | @@ -1023,6 +1024,7 @@ class HTMLRenderer: | ||
| 1023 | "list": self._render_list, | 1024 | "list": self._render_list, |
| 1024 | "table": self._render_table, | 1025 | "table": self._render_table, |
| 1025 | "swotTable": self._render_swot_table, | 1026 | "swotTable": self._render_swot_table, |
| 1027 | + "pestTable": self._render_pest_table, | ||
| 1026 | "blockquote": self._render_blockquote, | 1028 | "blockquote": self._render_blockquote, |
| 1027 | "engineQuote": self._render_engine_quote, | 1029 | "engineQuote": self._render_engine_quote, |
| 1028 | "hr": lambda b: "<hr />", | 1030 | "hr": lambda b: "<hr />", |
| @@ -1426,6 +1428,254 @@ class HTMLRenderer: | @@ -1426,6 +1428,254 @@ class HTMLRenderer: | ||
| 1426 | </li> | 1428 | </li> |
| 1427 | """ | 1429 | """ |
| 1428 | 1430 | ||
| 1431 | + # ==================== PEST 分析块 ==================== | ||
| 1432 | + | ||
| 1433 | + def _render_pest_table(self, block: Dict[str, Any]) -> str: | ||
| 1434 | + """ | ||
| 1435 | + 渲染四维度的PEST分析,同时生成两种布局: | ||
| 1436 | + 1. 卡片布局(用于HTML网页显示)- 横向条状堆叠 | ||
| 1437 | + 2. 表格布局(用于PDF导出)- 结构化表格,支持分页 | ||
| 1438 | + | ||
| 1439 | + PEST分析维度: | ||
| 1440 | + - P: Political(政治因素) | ||
| 1441 | + - E: Economic(经济因素) | ||
| 1442 | + - S: Social(社会因素) | ||
| 1443 | + - T: Technological(技术因素) | ||
| 1444 | + """ | ||
| 1445 | + title = block.get("title") or "PEST 分析" | ||
| 1446 | + summary = block.get("summary") | ||
| 1447 | + | ||
| 1448 | + # ========== 卡片布局(HTML用)========== | ||
| 1449 | + card_html = self._render_pest_card_layout(block, title, summary) | ||
| 1450 | + | ||
| 1451 | + # ========== 表格布局(PDF用)========== | ||
| 1452 | + table_html = self._render_pest_pdf_table_layout(block, title, summary) | ||
| 1453 | + | ||
| 1454 | + # 返回包含两种布局的容器 | ||
| 1455 | + return f""" | ||
| 1456 | + <div class="pest-container"> | ||
| 1457 | + {card_html} | ||
| 1458 | + {table_html} | ||
| 1459 | + </div> | ||
| 1460 | + """ | ||
| 1461 | + | ||
| 1462 | + def _render_pest_card_layout(self, block: Dict[str, Any], title: str, summary: str | None) -> str: | ||
| 1463 | + """渲染PEST卡片布局(用于HTML网页显示)- 横向条状堆叠设计""" | ||
| 1464 | + dimensions = [ | ||
| 1465 | + ("political", "政治因素 Political", "P", "political"), | ||
| 1466 | + ("economic", "经济因素 Economic", "E", "economic"), | ||
| 1467 | + ("social", "社会因素 Social", "S", "social"), | ||
| 1468 | + ("technological", "技术因素 Technological", "T", "technological"), | ||
| 1469 | + ] | ||
| 1470 | + strips_html = "" | ||
| 1471 | + for idx, (key, label, code, css) in enumerate(dimensions): | ||
| 1472 | + items = self._normalize_pest_items(block.get(key)) | ||
| 1473 | + caption_text = f"{len(items)} 条要点" if items else "待补充" | ||
| 1474 | + list_html = "".join(self._render_pest_item(item) for item in items) if items else '<li class="pest-empty">尚未填入要点</li>' | ||
| 1475 | + first_strip_class = " pest-strip--first" if idx == 0 else "" | ||
| 1476 | + strips_html += f""" | ||
| 1477 | + <div class="pest-strip pest-strip--pageable {css}{first_strip_class}" data-pest-key="{key}"> | ||
| 1478 | + <div class="pest-strip__indicator {css}"> | ||
| 1479 | + <span class="pest-code">{self._escape_html(code)}</span> | ||
| 1480 | + </div> | ||
| 1481 | + <div class="pest-strip__content"> | ||
| 1482 | + <div class="pest-strip__header"> | ||
| 1483 | + <div class="pest-strip__title">{self._escape_html(label)}</div> | ||
| 1484 | + <div class="pest-strip__caption">{self._escape_html(caption_text)}</div> | ||
| 1485 | + </div> | ||
| 1486 | + <ul class="pest-list">{list_html}</ul> | ||
| 1487 | + </div> | ||
| 1488 | + </div>""" | ||
| 1489 | + summary_html = f'<p class="pest-card__summary">{self._escape_html(summary)}</p>' if summary else "" | ||
| 1490 | + title_html = f'<div class="pest-card__title">{self._escape_html(title)}</div>' if title else "" | ||
| 1491 | + legend = """ | ||
| 1492 | + <div class="pest-legend"> | ||
| 1493 | + <span class="pest-legend__item political">P 政治</span> | ||
| 1494 | + <span class="pest-legend__item economic">E 经济</span> | ||
| 1495 | + <span class="pest-legend__item social">S 社会</span> | ||
| 1496 | + <span class="pest-legend__item technological">T 技术</span> | ||
| 1497 | + </div> | ||
| 1498 | + """ | ||
| 1499 | + return f""" | ||
| 1500 | + <div class="pest-card pest-card--html"> | ||
| 1501 | + <div class="pest-card__head"> | ||
| 1502 | + <div>{title_html}{summary_html}</div> | ||
| 1503 | + {legend} | ||
| 1504 | + </div> | ||
| 1505 | + <div class="pest-strips">{strips_html}</div> | ||
| 1506 | + </div> | ||
| 1507 | + """ | ||
| 1508 | + | ||
| 1509 | + def _render_pest_pdf_table_layout(self, block: Dict[str, Any], title: str, summary: str | None) -> str: | ||
| 1510 | + """ | ||
| 1511 | + 渲染PEST表格布局(用于PDF导出) | ||
| 1512 | + | ||
| 1513 | + 设计说明: | ||
| 1514 | + - 整体为一个大表格,包含标题行和4个维度区域 | ||
| 1515 | + - 每个维度有自己的子标题行和内容行 | ||
| 1516 | + - 使用合并单元格来显示维度标题 | ||
| 1517 | + - 通过CSS控制分页行为 | ||
| 1518 | + """ | ||
| 1519 | + dimensions = [ | ||
| 1520 | + ("political", "P", "政治因素 Political", "pest-pdf-political", "#8e44ad"), | ||
| 1521 | + ("economic", "E", "经济因素 Economic", "pest-pdf-economic", "#16a085"), | ||
| 1522 | + ("social", "S", "社会因素 Social", "pest-pdf-social", "#e84393"), | ||
| 1523 | + ("technological", "T", "技术因素 Technological", "pest-pdf-technological", "#2980b9"), | ||
| 1524 | + ] | ||
| 1525 | + | ||
| 1526 | + # 标题和摘要 | ||
| 1527 | + summary_row = "" | ||
| 1528 | + if summary: | ||
| 1529 | + summary_row = f""" | ||
| 1530 | + <tr class="pest-pdf-summary-row"> | ||
| 1531 | + <td colspan="4" class="pest-pdf-summary">{self._escape_html(summary)}</td> | ||
| 1532 | + </tr>""" | ||
| 1533 | + | ||
| 1534 | + # 生成四个维度的表格内容 | ||
| 1535 | + dimension_tables = "" | ||
| 1536 | + for idx, (key, code, label, css_class, color) in enumerate(dimensions): | ||
| 1537 | + items = self._normalize_pest_items(block.get(key)) | ||
| 1538 | + | ||
| 1539 | + # 生成每个维度的内容行 | ||
| 1540 | + items_rows = "" | ||
| 1541 | + if items: | ||
| 1542 | + for item_idx, item in enumerate(items): | ||
| 1543 | + item_title = item.get("title") or item.get("label") or item.get("text") or "未命名要点" | ||
| 1544 | + item_detail = item.get("detail") or item.get("description") or "" | ||
| 1545 | + item_source = item.get("source") or item.get("evidence") or "" | ||
| 1546 | + item_trend = item.get("trend") or item.get("impact") or "" | ||
| 1547 | + | ||
| 1548 | + # 构建详情内容 | ||
| 1549 | + detail_parts = [] | ||
| 1550 | + if item_detail: | ||
| 1551 | + detail_parts.append(item_detail) | ||
| 1552 | + if item_source: | ||
| 1553 | + detail_parts.append(f"来源:{item_source}") | ||
| 1554 | + detail_text = "<br/>".join(detail_parts) if detail_parts else "-" | ||
| 1555 | + | ||
| 1556 | + # 构建标签 | ||
| 1557 | + tags = [] | ||
| 1558 | + if item_trend: | ||
| 1559 | + tags.append(f'<span class="pest-pdf-tag">{self._escape_html(item_trend)}</span>') | ||
| 1560 | + tags_html = " ".join(tags) | ||
| 1561 | + | ||
| 1562 | + # 第一行需要合并维度标题单元格 | ||
| 1563 | + if item_idx == 0: | ||
| 1564 | + rowspan = len(items) | ||
| 1565 | + items_rows += f""" | ||
| 1566 | + <tr class="pest-pdf-item-row {css_class}"> | ||
| 1567 | + <td rowspan="{rowspan}" class="pest-pdf-dimension-label {css_class}"> | ||
| 1568 | + <span class="pest-pdf-code">{code}</span> | ||
| 1569 | + <span class="pest-pdf-label-text">{self._escape_html(label.split()[0])}</span> | ||
| 1570 | + </td> | ||
| 1571 | + <td class="pest-pdf-item-num">{item_idx + 1}</td> | ||
| 1572 | + <td class="pest-pdf-item-title">{self._escape_html(item_title)}</td> | ||
| 1573 | + <td class="pest-pdf-item-detail">{detail_text}</td> | ||
| 1574 | + <td class="pest-pdf-item-tags">{tags_html}</td> | ||
| 1575 | + </tr>""" | ||
| 1576 | + else: | ||
| 1577 | + items_rows += f""" | ||
| 1578 | + <tr class="pest-pdf-item-row {css_class}"> | ||
| 1579 | + <td class="pest-pdf-item-num">{item_idx + 1}</td> | ||
| 1580 | + <td class="pest-pdf-item-title">{self._escape_html(item_title)}</td> | ||
| 1581 | + <td class="pest-pdf-item-detail">{detail_text}</td> | ||
| 1582 | + <td class="pest-pdf-item-tags">{tags_html}</td> | ||
| 1583 | + </tr>""" | ||
| 1584 | + else: | ||
| 1585 | + # 没有内容时显示占位 | ||
| 1586 | + items_rows = f""" | ||
| 1587 | + <tr class="pest-pdf-item-row {css_class}"> | ||
| 1588 | + <td class="pest-pdf-dimension-label {css_class}"> | ||
| 1589 | + <span class="pest-pdf-code">{code}</span> | ||
| 1590 | + <span class="pest-pdf-label-text">{self._escape_html(label.split()[0])}</span> | ||
| 1591 | + </td> | ||
| 1592 | + <td class="pest-pdf-item-num">-</td> | ||
| 1593 | + <td colspan="3" class="pest-pdf-empty">暂无要点</td> | ||
| 1594 | + </tr>""" | ||
| 1595 | + | ||
| 1596 | + # 每个维度作为一个独立的tbody,便于分页控制 | ||
| 1597 | + dimension_tables += f""" | ||
| 1598 | + <tbody class="pest-pdf-dimension {css_class}"> | ||
| 1599 | + {items_rows} | ||
| 1600 | + </tbody>""" | ||
| 1601 | + | ||
| 1602 | + return f""" | ||
| 1603 | + <div class="pest-pdf-wrapper"> | ||
| 1604 | + <table class="pest-pdf-table"> | ||
| 1605 | + <caption class="pest-pdf-caption">{self._escape_html(title)}</caption> | ||
| 1606 | + <thead class="pest-pdf-thead"> | ||
| 1607 | + <tr> | ||
| 1608 | + <th class="pest-pdf-th-dimension">维度</th> | ||
| 1609 | + <th class="pest-pdf-th-num">序号</th> | ||
| 1610 | + <th class="pest-pdf-th-title">要点</th> | ||
| 1611 | + <th class="pest-pdf-th-detail">详细说明</th> | ||
| 1612 | + <th class="pest-pdf-th-tags">趋势/影响</th> | ||
| 1613 | + </tr> | ||
| 1614 | + {summary_row} | ||
| 1615 | + </thead> | ||
| 1616 | + {dimension_tables} | ||
| 1617 | + </table> | ||
| 1618 | + </div> | ||
| 1619 | + """ | ||
| 1620 | + | ||
| 1621 | + def _normalize_pest_items(self, raw: Any) -> List[Dict[str, Any]]: | ||
| 1622 | + """将PEST条目规整为统一结构,兼容字符串/对象两种写法""" | ||
| 1623 | + normalized: List[Dict[str, Any]] = [] | ||
| 1624 | + if raw is None: | ||
| 1625 | + return normalized | ||
| 1626 | + if isinstance(raw, (str, int, float)): | ||
| 1627 | + text = self._safe_text(raw).strip() | ||
| 1628 | + if text: | ||
| 1629 | + normalized.append({"title": text}) | ||
| 1630 | + return normalized | ||
| 1631 | + if not isinstance(raw, list): | ||
| 1632 | + return normalized | ||
| 1633 | + for entry in raw: | ||
| 1634 | + if isinstance(entry, (str, int, float)): | ||
| 1635 | + text = self._safe_text(entry).strip() | ||
| 1636 | + if text: | ||
| 1637 | + normalized.append({"title": text}) | ||
| 1638 | + continue | ||
| 1639 | + if not isinstance(entry, dict): | ||
| 1640 | + continue | ||
| 1641 | + title = entry.get("title") or entry.get("label") or entry.get("text") | ||
| 1642 | + detail = entry.get("detail") or entry.get("description") | ||
| 1643 | + source = entry.get("source") or entry.get("evidence") | ||
| 1644 | + trend = entry.get("trend") or entry.get("impact") | ||
| 1645 | + if not title and isinstance(detail, str): | ||
| 1646 | + title = detail | ||
| 1647 | + detail = None | ||
| 1648 | + if not (title or detail or source): | ||
| 1649 | + continue | ||
| 1650 | + normalized.append( | ||
| 1651 | + { | ||
| 1652 | + "title": title, | ||
| 1653 | + "detail": detail, | ||
| 1654 | + "source": source, | ||
| 1655 | + "trend": trend, | ||
| 1656 | + } | ||
| 1657 | + ) | ||
| 1658 | + return normalized | ||
| 1659 | + | ||
| 1660 | + def _render_pest_item(self, item: Dict[str, Any]) -> str: | ||
| 1661 | + """输出单个PEST条目的HTML片段""" | ||
| 1662 | + title = item.get("title") or item.get("label") or item.get("text") or "未命名要点" | ||
| 1663 | + detail = item.get("detail") or item.get("description") | ||
| 1664 | + source = item.get("source") or item.get("evidence") | ||
| 1665 | + trend = item.get("trend") or item.get("impact") | ||
| 1666 | + tags: List[str] = [] | ||
| 1667 | + if trend: | ||
| 1668 | + tags.append(f'<span class="pest-tag">{self._escape_html(trend)}</span>') | ||
| 1669 | + tags_html = f'<span class="pest-item-tags">{"".join(tags)}</span>' if tags else "" | ||
| 1670 | + detail_html = f'<div class="pest-item-desc">{self._escape_html(detail)}</div>' if detail else "" | ||
| 1671 | + source_html = f'<div class="pest-item-source">来源:{self._escape_html(source)}</div>' if source else "" | ||
| 1672 | + return f""" | ||
| 1673 | + <li class="pest-item"> | ||
| 1674 | + <div class="pest-item-title">{self._escape_html(title)}{tags_html}</div> | ||
| 1675 | + {detail_html}{source_html} | ||
| 1676 | + </li> | ||
| 1677 | + """ | ||
| 1678 | + | ||
| 1429 | def _normalize_table_rows(self, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: | 1679 | def _normalize_table_rows(self, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: |
| 1430 | """ | 1680 | """ |
| 1431 | 检测并修正仅有单列的竖排表,转换为标准网格。 | 1681 | 检测并修正仅有单列的竖排表,转换为标准网格。 |
| @@ -2702,6 +2952,33 @@ class HTMLRenderer: | @@ -2702,6 +2952,33 @@ class HTMLRenderer: | ||
| 2702 | --swot-cell-opportunity-border: rgba(31,90,179,0.35); | 2952 | --swot-cell-opportunity-border: rgba(31,90,179,0.35); |
| 2703 | --swot-cell-threat-border: rgba(179,107,22,0.35); | 2953 | --swot-cell-threat-border: rgba(179,107,22,0.35); |
| 2704 | --swot-item-border: rgba(0,0,0,0.05); | 2954 | --swot-item-border: rgba(0,0,0,0.05); |
| 2955 | + /* PEST 分析变量 - 紫青色系 */ | ||
| 2956 | + --pest-political: #8e44ad; | ||
| 2957 | + --pest-economic: #16a085; | ||
| 2958 | + --pest-social: #e84393; | ||
| 2959 | + --pest-technological: #2980b9; | ||
| 2960 | + --pest-on-light: #1a1a2e; | ||
| 2961 | + --pest-on-dark: #f8f9ff; | ||
| 2962 | + --pest-text: var(--text-color); | ||
| 2963 | + --pest-muted: rgba(0,0,0,0.55); | ||
| 2964 | + --pest-surface: rgba(255,255,255,0.88); | ||
| 2965 | + --pest-chip-bg: rgba(0,0,0,0.05); | ||
| 2966 | + --pest-tag-border: var(--border-color); | ||
| 2967 | + --pest-card-bg: linear-gradient(145deg, rgba(142,68,173,0.03), rgba(22,160,133,0.04)), var(--card-bg); | ||
| 2968 | + --pest-card-border: var(--border-color); | ||
| 2969 | + --pest-card-shadow: 0 16px 32px var(--shadow-color); | ||
| 2970 | + --pest-card-blur: none; | ||
| 2971 | + --pest-strip-base: linear-gradient(90deg, rgba(255,255,255,0.95), rgba(255,255,255,0.7)); | ||
| 2972 | + --pest-strip-border: rgba(0,0,0,0.06); | ||
| 2973 | + --pest-strip-political-bg: linear-gradient(90deg, rgba(142,68,173,0.08), rgba(255,255,255,0.85)), var(--card-bg); | ||
| 2974 | + --pest-strip-economic-bg: linear-gradient(90deg, rgba(22,160,133,0.08), rgba(255,255,255,0.85)), var(--card-bg); | ||
| 2975 | + --pest-strip-social-bg: linear-gradient(90deg, rgba(232,67,147,0.08), rgba(255,255,255,0.85)), var(--card-bg); | ||
| 2976 | + --pest-strip-technological-bg: linear-gradient(90deg, rgba(41,128,185,0.08), rgba(255,255,255,0.85)), var(--card-bg); | ||
| 2977 | + --pest-strip-political-border: rgba(142,68,173,0.4); | ||
| 2978 | + --pest-strip-economic-border: rgba(22,160,133,0.4); | ||
| 2979 | + --pest-strip-social-border: rgba(232,67,147,0.4); | ||
| 2980 | + --pest-strip-technological-border: rgba(41,128,185,0.4); | ||
| 2981 | + --pest-item-border: rgba(0,0,0,0.06); | ||
| 2705 | }} | 2982 | }} |
| 2706 | .dark-mode {{ | 2983 | .dark-mode {{ |
| 2707 | --bg-color: #121212; | 2984 | --bg-color: #121212; |
| @@ -2751,6 +3028,33 @@ class HTMLRenderer: | @@ -2751,6 +3028,33 @@ class HTMLRenderer: | ||
| 2751 | --swot-cell-opportunity-border: rgba(31,90,179,0.68); | 3028 | --swot-cell-opportunity-border: rgba(31,90,179,0.68); |
| 2752 | --swot-cell-threat-border: rgba(179,107,22,0.68); | 3029 | --swot-cell-threat-border: rgba(179,107,22,0.68); |
| 2753 | --swot-item-border: rgba(255,255,255,0.14); | 3030 | --swot-item-border: rgba(255,255,255,0.14); |
| 3031 | + /* PEST 分析变量 - 暗色模式 */ | ||
| 3032 | + --pest-political: #a569bd; | ||
| 3033 | + --pest-economic: #48c9b0; | ||
| 3034 | + --pest-social: #f06292; | ||
| 3035 | + --pest-technological: #5dade2; | ||
| 3036 | + --pest-on-light: #1a1a2e; | ||
| 3037 | + --pest-on-dark: #f0f4ff; | ||
| 3038 | + --pest-text: #f0f4ff; | ||
| 3039 | + --pest-muted: rgba(240,244,255,0.7); | ||
| 3040 | + --pest-surface: rgba(255,255,255,0.06); | ||
| 3041 | + --pest-chip-bg: rgba(255,255,255,0.12); | ||
| 3042 | + --pest-tag-border: rgba(255,255,255,0.22); | ||
| 3043 | + --pest-card-bg: radial-gradient(130% 130% at 15% 15%, rgba(165,105,189,0.16), transparent 50%), radial-gradient(110% 130% at 85% 5%, rgba(72,201,176,0.14), transparent 48%), linear-gradient(155deg, #12162a 0%, #161b30 50%, #0f1425 100%); | ||
| 3044 | + --pest-card-border: rgba(255,255,255,0.12); | ||
| 3045 | + --pest-card-shadow: 0 28px 65px rgba(0, 0, 0, 0.55); | ||
| 3046 | + --pest-card-blur: blur(10px); | ||
| 3047 | + --pest-strip-base: linear-gradient(90deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02)); | ||
| 3048 | + --pest-strip-border: rgba(255,255,255,0.18); | ||
| 3049 | + --pest-strip-political-bg: linear-gradient(90deg, rgba(142,68,173,0.25), rgba(142,68,173,0.1)), var(--pest-strip-base); | ||
| 3050 | + --pest-strip-economic-bg: linear-gradient(90deg, rgba(22,160,133,0.25), rgba(22,160,133,0.1)), var(--pest-strip-base); | ||
| 3051 | + --pest-strip-social-bg: linear-gradient(90deg, rgba(232,67,147,0.25), rgba(232,67,147,0.1)), var(--pest-strip-base); | ||
| 3052 | + --pest-strip-technological-bg: linear-gradient(90deg, rgba(41,128,185,0.25), rgba(41,128,185,0.1)), var(--pest-strip-base); | ||
| 3053 | + --pest-strip-political-border: rgba(165,105,189,0.6); | ||
| 3054 | + --pest-strip-economic-border: rgba(72,201,176,0.6); | ||
| 3055 | + --pest-strip-social-border: rgba(240,98,146,0.6); | ||
| 3056 | + --pest-strip-technological-border: rgba(93,173,226,0.6); | ||
| 3057 | + --pest-item-border: rgba(255,255,255,0.12); | ||
| 2754 | }} | 3058 | }} |
| 2755 | * {{ box-sizing: border-box; }} | 3059 | * {{ box-sizing: border-box; }} |
| 2756 | body {{ | 3060 | body {{ |
| @@ -3482,6 +3786,313 @@ table th {{ | @@ -3482,6 +3786,313 @@ table th {{ | ||
| 3482 | page-break-inside: avoid; | 3786 | page-break-inside: avoid; |
| 3483 | }} | 3787 | }} |
| 3484 | }} | 3788 | }} |
| 3789 | + | ||
| 3790 | +/* ==================== PEST 分析样式 ==================== */ | ||
| 3791 | +.pest-card {{ | ||
| 3792 | + margin: 28px 0; | ||
| 3793 | + padding: 20px 20px 16px; | ||
| 3794 | + border-radius: 18px; | ||
| 3795 | + border: 1px solid var(--pest-card-border); | ||
| 3796 | + background: var(--pest-card-bg); | ||
| 3797 | + box-shadow: var(--pest-card-shadow); | ||
| 3798 | + color: var(--pest-text); | ||
| 3799 | + backdrop-filter: var(--pest-card-blur); | ||
| 3800 | + position: relative; | ||
| 3801 | + overflow: hidden; | ||
| 3802 | +}} | ||
| 3803 | +.pest-card__head {{ | ||
| 3804 | + display: flex; | ||
| 3805 | + justify-content: space-between; | ||
| 3806 | + gap: 16px; | ||
| 3807 | + align-items: flex-start; | ||
| 3808 | + flex-wrap: wrap; | ||
| 3809 | + margin-bottom: 16px; | ||
| 3810 | +}} | ||
| 3811 | +.pest-card__title {{ | ||
| 3812 | + font-size: 1.18rem; | ||
| 3813 | + font-weight: 750; | ||
| 3814 | + margin-bottom: 4px; | ||
| 3815 | + background: linear-gradient(135deg, var(--pest-political), var(--pest-technological)); | ||
| 3816 | + -webkit-background-clip: text; | ||
| 3817 | + -webkit-text-fill-color: transparent; | ||
| 3818 | + background-clip: text; | ||
| 3819 | +}} | ||
| 3820 | +.pest-card__summary {{ | ||
| 3821 | + margin: 0; | ||
| 3822 | + color: var(--pest-text); | ||
| 3823 | + opacity: 0.8; | ||
| 3824 | +}} | ||
| 3825 | +.pest-legend {{ | ||
| 3826 | + display: flex; | ||
| 3827 | + gap: 8px; | ||
| 3828 | + flex-wrap: wrap; | ||
| 3829 | + align-items: center; | ||
| 3830 | +}} | ||
| 3831 | +.pest-legend__item {{ | ||
| 3832 | + padding: 6px 14px; | ||
| 3833 | + border-radius: 8px; | ||
| 3834 | + font-weight: 700; | ||
| 3835 | + font-size: 0.85rem; | ||
| 3836 | + color: var(--pest-on-dark); | ||
| 3837 | + border: 1px solid var(--pest-tag-border); | ||
| 3838 | + box-shadow: 0 4px 14px rgba(0,0,0,0.18); | ||
| 3839 | + text-shadow: 0 1px 2px rgba(0,0,0,0.3); | ||
| 3840 | +}} | ||
| 3841 | +.pest-legend__item.political {{ background: var(--pest-political); }} | ||
| 3842 | +.pest-legend__item.economic {{ background: var(--pest-economic); }} | ||
| 3843 | +.pest-legend__item.social {{ background: var(--pest-social); }} | ||
| 3844 | +.pest-legend__item.technological {{ background: var(--pest-technological); }} | ||
| 3845 | +.pest-strips {{ | ||
| 3846 | + display: flex; | ||
| 3847 | + flex-direction: column; | ||
| 3848 | + gap: 14px; | ||
| 3849 | +}} | ||
| 3850 | +.pest-strip {{ | ||
| 3851 | + display: flex; | ||
| 3852 | + border-radius: 14px; | ||
| 3853 | + border: 1px solid var(--pest-strip-border); | ||
| 3854 | + background: var(--pest-strip-base); | ||
| 3855 | + overflow: hidden; | ||
| 3856 | + box-shadow: 0 6px 16px rgba(0,0,0,0.06); | ||
| 3857 | + transition: transform 0.2s ease, box-shadow 0.2s ease; | ||
| 3858 | +}} | ||
| 3859 | +.pest-strip:hover {{ | ||
| 3860 | + transform: translateY(-2px); | ||
| 3861 | + box-shadow: 0 10px 24px rgba(0,0,0,0.1); | ||
| 3862 | +}} | ||
| 3863 | +.pest-strip.political {{ border-color: var(--pest-strip-political-border); background: var(--pest-strip-political-bg); }} | ||
| 3864 | +.pest-strip.economic {{ border-color: var(--pest-strip-economic-border); background: var(--pest-strip-economic-bg); }} | ||
| 3865 | +.pest-strip.social {{ border-color: var(--pest-strip-social-border); background: var(--pest-strip-social-bg); }} | ||
| 3866 | +.pest-strip.technological {{ border-color: var(--pest-strip-technological-border); background: var(--pest-strip-technological-bg); }} | ||
| 3867 | +.pest-strip__indicator {{ | ||
| 3868 | + display: flex; | ||
| 3869 | + align-items: center; | ||
| 3870 | + justify-content: center; | ||
| 3871 | + width: 56px; | ||
| 3872 | + min-width: 56px; | ||
| 3873 | + padding: 16px 8px; | ||
| 3874 | + color: var(--pest-on-dark); | ||
| 3875 | + text-shadow: 0 2px 4px rgba(0,0,0,0.25); | ||
| 3876 | +}} | ||
| 3877 | +.pest-strip__indicator.political {{ background: linear-gradient(180deg, var(--pest-political), rgba(142,68,173,0.8)); }} | ||
| 3878 | +.pest-strip__indicator.economic {{ background: linear-gradient(180deg, var(--pest-economic), rgba(22,160,133,0.8)); }} | ||
| 3879 | +.pest-strip__indicator.social {{ background: linear-gradient(180deg, var(--pest-social), rgba(232,67,147,0.8)); }} | ||
| 3880 | +.pest-strip__indicator.technological {{ background: linear-gradient(180deg, var(--pest-technological), rgba(41,128,185,0.8)); }} | ||
| 3881 | +.pest-code {{ | ||
| 3882 | + font-size: 1.6rem; | ||
| 3883 | + font-weight: 900; | ||
| 3884 | + letter-spacing: 0.02em; | ||
| 3885 | +}} | ||
| 3886 | +.pest-strip__content {{ | ||
| 3887 | + flex: 1; | ||
| 3888 | + padding: 14px 16px; | ||
| 3889 | + min-width: 0; | ||
| 3890 | +}} | ||
| 3891 | +.pest-strip__header {{ | ||
| 3892 | + display: flex; | ||
| 3893 | + justify-content: space-between; | ||
| 3894 | + align-items: baseline; | ||
| 3895 | + gap: 12px; | ||
| 3896 | + margin-bottom: 10px; | ||
| 3897 | + flex-wrap: wrap; | ||
| 3898 | +}} | ||
| 3899 | +.pest-strip__title {{ | ||
| 3900 | + font-weight: 700; | ||
| 3901 | + font-size: 1rem; | ||
| 3902 | + color: var(--pest-text); | ||
| 3903 | +}} | ||
| 3904 | +.pest-strip__caption {{ | ||
| 3905 | + font-size: 0.85rem; | ||
| 3906 | + color: var(--pest-text); | ||
| 3907 | + opacity: 0.65; | ||
| 3908 | +}} | ||
| 3909 | +.pest-list {{ | ||
| 3910 | + list-style: none; | ||
| 3911 | + padding: 0; | ||
| 3912 | + margin: 0; | ||
| 3913 | + display: flex; | ||
| 3914 | + flex-direction: column; | ||
| 3915 | + gap: 8px; | ||
| 3916 | +}} | ||
| 3917 | +.pest-item {{ | ||
| 3918 | + padding: 10px 14px; | ||
| 3919 | + border-radius: 10px; | ||
| 3920 | + background: var(--pest-surface); | ||
| 3921 | + border: 1px solid var(--pest-item-border); | ||
| 3922 | + box-shadow: 0 8px 18px rgba(0,0,0,0.06); | ||
| 3923 | +}} | ||
| 3924 | +.pest-item-title {{ | ||
| 3925 | + display: flex; | ||
| 3926 | + justify-content: space-between; | ||
| 3927 | + gap: 8px; | ||
| 3928 | + font-weight: 650; | ||
| 3929 | + color: var(--pest-text); | ||
| 3930 | +}} | ||
| 3931 | +.pest-item-tags {{ | ||
| 3932 | + display: inline-flex; | ||
| 3933 | + gap: 6px; | ||
| 3934 | + flex-wrap: wrap; | ||
| 3935 | + font-size: 0.82rem; | ||
| 3936 | +}} | ||
| 3937 | +.pest-tag {{ | ||
| 3938 | + display: inline-block; | ||
| 3939 | + padding: 3px 8px; | ||
| 3940 | + border-radius: 6px; | ||
| 3941 | + background: var(--pest-chip-bg); | ||
| 3942 | + color: var(--pest-text); | ||
| 3943 | + border: 1px solid var(--pest-tag-border); | ||
| 3944 | + box-shadow: 0 4px 10px rgba(0,0,0,0.08); | ||
| 3945 | + line-height: 1.2; | ||
| 3946 | +}} | ||
| 3947 | +.pest-item-desc {{ | ||
| 3948 | + margin-top: 5px; | ||
| 3949 | + color: var(--pest-text); | ||
| 3950 | + opacity: 0.88; | ||
| 3951 | + font-size: 0.95rem; | ||
| 3952 | +}} | ||
| 3953 | +.pest-item-source {{ | ||
| 3954 | + margin-top: 4px; | ||
| 3955 | + font-size: 0.88rem; | ||
| 3956 | + color: var(--secondary-color); | ||
| 3957 | + opacity: 0.9; | ||
| 3958 | +}} | ||
| 3959 | +.pest-empty {{ | ||
| 3960 | + padding: 14px; | ||
| 3961 | + border-radius: 10px; | ||
| 3962 | + border: 1px dashed var(--pest-card-border); | ||
| 3963 | + text-align: center; | ||
| 3964 | + color: var(--pest-muted); | ||
| 3965 | + opacity: 0.65; | ||
| 3966 | +}} | ||
| 3967 | + | ||
| 3968 | +/* ========== PEST PDF表格布局样式(默认隐藏)========== */ | ||
| 3969 | +.pest-pdf-wrapper {{ | ||
| 3970 | + display: none; | ||
| 3971 | +}} | ||
| 3972 | + | ||
| 3973 | +/* PEST PDF表格样式定义(用于PDF渲染时显示) */ | ||
| 3974 | +.pest-pdf-table {{ | ||
| 3975 | + width: 100%; | ||
| 3976 | + border-collapse: collapse; | ||
| 3977 | + margin: 20px 0; | ||
| 3978 | + font-size: 13px; | ||
| 3979 | + table-layout: fixed; | ||
| 3980 | +}} | ||
| 3981 | +.pest-pdf-caption {{ | ||
| 3982 | + caption-side: top; | ||
| 3983 | + text-align: left; | ||
| 3984 | + font-size: 1.15rem; | ||
| 3985 | + font-weight: 700; | ||
| 3986 | + padding: 12px 0; | ||
| 3987 | + color: var(--text-color); | ||
| 3988 | +}} | ||
| 3989 | +.pest-pdf-thead th {{ | ||
| 3990 | + background: #f5f3f7; | ||
| 3991 | + padding: 10px 8px; | ||
| 3992 | + text-align: left; | ||
| 3993 | + font-weight: 600; | ||
| 3994 | + border: 1px solid #e0dce3; | ||
| 3995 | + color: #4a4458; | ||
| 3996 | +}} | ||
| 3997 | +.pest-pdf-th-dimension {{ width: 85px; }} | ||
| 3998 | +.pest-pdf-th-num {{ width: 50px; text-align: center; }} | ||
| 3999 | +.pest-pdf-th-title {{ width: 22%; }} | ||
| 4000 | +.pest-pdf-th-detail {{ width: auto; }} | ||
| 4001 | +.pest-pdf-th-tags {{ width: 100px; text-align: center; }} | ||
| 4002 | +.pest-pdf-summary {{ | ||
| 4003 | + padding: 12px; | ||
| 4004 | + background: #f8f6fa; | ||
| 4005 | + color: #666; | ||
| 4006 | + font-style: italic; | ||
| 4007 | + border: 1px solid #e0dce3; | ||
| 4008 | +}} | ||
| 4009 | +.pest-pdf-dimension {{ | ||
| 4010 | + break-inside: avoid; | ||
| 4011 | + page-break-inside: avoid; | ||
| 4012 | +}} | ||
| 4013 | +.pest-pdf-dimension-label {{ | ||
| 4014 | + text-align: center; | ||
| 4015 | + vertical-align: middle; | ||
| 4016 | + padding: 12px 8px; | ||
| 4017 | + font-weight: 700; | ||
| 4018 | + border: 1px solid #e0dce3; | ||
| 4019 | + writing-mode: horizontal-tb; | ||
| 4020 | +}} | ||
| 4021 | +.pest-pdf-dimension-label.pest-pdf-political {{ background: rgba(142,68,173,0.12); color: #8e44ad; border-left: 4px solid #8e44ad; }} | ||
| 4022 | +.pest-pdf-dimension-label.pest-pdf-economic {{ background: rgba(22,160,133,0.12); color: #16a085; border-left: 4px solid #16a085; }} | ||
| 4023 | +.pest-pdf-dimension-label.pest-pdf-social {{ background: rgba(232,67,147,0.12); color: #e84393; border-left: 4px solid #e84393; }} | ||
| 4024 | +.pest-pdf-dimension-label.pest-pdf-technological {{ background: rgba(41,128,185,0.12); color: #2980b9; border-left: 4px solid #2980b9; }} | ||
| 4025 | +.pest-pdf-code {{ | ||
| 4026 | + display: block; | ||
| 4027 | + font-size: 1.5rem; | ||
| 4028 | + font-weight: 800; | ||
| 4029 | + margin-bottom: 4px; | ||
| 4030 | +}} | ||
| 4031 | +.pest-pdf-label-text {{ | ||
| 4032 | + display: block; | ||
| 4033 | + font-size: 0.75rem; | ||
| 4034 | + font-weight: 600; | ||
| 4035 | + letter-spacing: 0.02em; | ||
| 4036 | +}} | ||
| 4037 | +.pest-pdf-item-row td {{ | ||
| 4038 | + padding: 10px 8px; | ||
| 4039 | + border: 1px solid #e0dce3; | ||
| 4040 | + vertical-align: top; | ||
| 4041 | +}} | ||
| 4042 | +.pest-pdf-item-row.pest-pdf-political td {{ background: rgba(142,68,173,0.03); }} | ||
| 4043 | +.pest-pdf-item-row.pest-pdf-economic td {{ background: rgba(22,160,133,0.03); }} | ||
| 4044 | +.pest-pdf-item-row.pest-pdf-social td {{ background: rgba(232,67,147,0.03); }} | ||
| 4045 | +.pest-pdf-item-row.pest-pdf-technological td {{ background: rgba(41,128,185,0.03); }} | ||
| 4046 | +.pest-pdf-item-num {{ | ||
| 4047 | + text-align: center; | ||
| 4048 | + font-weight: 600; | ||
| 4049 | + color: #6c757d; | ||
| 4050 | +}} | ||
| 4051 | +.pest-pdf-item-title {{ | ||
| 4052 | + font-weight: 600; | ||
| 4053 | + color: #212529; | ||
| 4054 | +}} | ||
| 4055 | +.pest-pdf-item-detail {{ | ||
| 4056 | + color: #495057; | ||
| 4057 | + line-height: 1.5; | ||
| 4058 | +}} | ||
| 4059 | +.pest-pdf-item-tags {{ | ||
| 4060 | + text-align: center; | ||
| 4061 | +}} | ||
| 4062 | +.pest-pdf-tag {{ | ||
| 4063 | + display: inline-block; | ||
| 4064 | + padding: 3px 8px; | ||
| 4065 | + border-radius: 4px; | ||
| 4066 | + font-size: 0.75rem; | ||
| 4067 | + background: #ece9f1; | ||
| 4068 | + color: #5a4f6a; | ||
| 4069 | + margin: 2px; | ||
| 4070 | +}} | ||
| 4071 | +.pest-pdf-empty {{ | ||
| 4072 | + text-align: center; | ||
| 4073 | + color: #adb5bd; | ||
| 4074 | + font-style: italic; | ||
| 4075 | +}} | ||
| 4076 | + | ||
| 4077 | +/* 打印模式下的PEST分页控制 */ | ||
| 4078 | +@media print {{ | ||
| 4079 | + .pest-card {{ | ||
| 4080 | + break-inside: auto; | ||
| 4081 | + page-break-inside: auto; | ||
| 4082 | + }} | ||
| 4083 | + .pest-card__head {{ | ||
| 4084 | + break-after: avoid; | ||
| 4085 | + page-break-after: avoid; | ||
| 4086 | + }} | ||
| 4087 | + .pest-pdf-dimension {{ | ||
| 4088 | + break-inside: avoid; | ||
| 4089 | + page-break-inside: avoid; | ||
| 4090 | + }} | ||
| 4091 | + .pest-strip {{ | ||
| 4092 | + break-inside: avoid; | ||
| 4093 | + page-break-inside: avoid; | ||
| 4094 | + }} | ||
| 4095 | +}} | ||
| 3485 | .callout {{ | 4096 | .callout {{ |
| 3486 | border-left: 4px solid var(--primary-color); | 4097 | border-left: 4px solid var(--primary-color); |
| 3487 | padding: 16px; | 4098 | padding: 16px; |
| @@ -3705,6 +4316,7 @@ pre.code-block {{ | @@ -3705,6 +4316,7 @@ pre.code-block {{ | ||
| 3705 | .chart-card, | 4316 | .chart-card, |
| 3706 | .kpi-grid, | 4317 | .kpi-grid, |
| 3707 | .swot-card, | 4318 | .swot-card, |
| 4319 | +.pest-card, | ||
| 3708 | .table-wrap, | 4320 | .table-wrap, |
| 3709 | figure, | 4321 | figure, |
| 3710 | blockquote {{ | 4322 | blockquote {{ |
| @@ -3767,6 +4379,33 @@ blockquote {{ | @@ -3767,6 +4379,33 @@ blockquote {{ | ||
| 3767 | min-width: 240px; | 4379 | min-width: 240px; |
| 3768 | height: auto; | 4380 | height: auto; |
| 3769 | }} | 4381 | }} |
| 4382 | + /* PEST 打印样式 */ | ||
| 4383 | + .pest-card, | ||
| 4384 | + .pest-strip {{ | ||
| 4385 | + break-inside: avoid; | ||
| 4386 | + page-break-inside: avoid; | ||
| 4387 | + }} | ||
| 4388 | + .pest-card {{ | ||
| 4389 | + color: var(--pest-text); | ||
| 4390 | + break-inside: auto !important; | ||
| 4391 | + page-break-inside: auto !important; | ||
| 4392 | + }} | ||
| 4393 | + .pest-card__head {{ | ||
| 4394 | + break-after: avoid; | ||
| 4395 | + page-break-after: avoid; | ||
| 4396 | + }} | ||
| 4397 | + .pest-strips {{ | ||
| 4398 | + break-before: avoid; | ||
| 4399 | + page-break-before: avoid; | ||
| 4400 | + break-inside: auto; | ||
| 4401 | + page-break-inside: auto; | ||
| 4402 | + }} | ||
| 4403 | + .pest-legend {{ | ||
| 4404 | + display: none !important; | ||
| 4405 | + }} | ||
| 4406 | + .pest-strip {{ | ||
| 4407 | + flex-direction: row; | ||
| 4408 | + }} | ||
| 3770 | .table-wrap {{ | 4409 | .table-wrap {{ |
| 3771 | overflow-x: auto; | 4410 | overflow-x: auto; |
| 3772 | max-width: 100%; | 4411 | max-width: 100%; |
-
Please register or login to post a comment