From b7e227820c82b4c7e3e582b046205d62c1506cb6 Mon Sep 17 00:00:00 2001 From: liubiren Date: Thu, 15 Jan 2026 22:38:08 +0800 Subject: [PATCH] 1 --- utils/html_render.py | 83 ++++--- utils/sqlite.py | 4 +- 短视频合成自动化.zip | Bin 0 -> 33916 bytes 短视频合成自动化/draft.py | 92 +++++--- 短视频合成自动化/edgetts.py | 1 - 短视频合成自动化/export.py | 224 +++++++++++++++---- 短视频合成自动化/main.py | 4 +- 票据理赔自动化/template.html | 419 +++++++++++++++++------------------ 8 files changed, 493 insertions(+), 334 deletions(-) create mode 100644 短视频合成自动化.zip diff --git a/utils/html_render.py b/utils/html_render.py index abe3b9b..83c20da 100644 --- a/utils/html_render.py +++ b/utils/html_render.py @@ -5,38 +5,13 @@ HTML渲染器 from datetime import datetime from pathlib import Path -from typing import Any, Dict +import re +from decimal import Decimal +from typing import Any, Dict, Optional, Union, List from jinja2 import Environment, FileSystemLoader -def datetime_to_str(field): - """ - 渲染模板时,若字段为datetime对象则转为字符串 - :param field: 字段 - :return: 字符串 - """ - if isinstance(field, datetime): - if field == datetime(9999, 12, 31): - return "长期" - if field.hour == 0 and field.minute == 0 and field.second == 0: - return field.strftime("%Y-%m-%d") - return field.strftime("%Y-%m-%d %H:%M:%S") - else: - return field - - -def str_to_str(field): - """ - 渲染模板时,若字段为字符串则转空字符串 - :param field: 字段 - :return: 字符串 - """ - if isinstance(field, str): - return field - return "" - - class HTMLRenderer: """ HTML渲染器,支持: @@ -52,13 +27,6 @@ class HTMLRenderer: self.environment = Environment( loader=FileSystemLoader(searchpath=template_path.parent) ) - # 设置过滤器 - self.environment.filters.update( - { - "datetime_to_str": datetime_to_str, - "str_to_str": str_to_str, - } - ) # 加载指定模板 self.template = self.environment.get_template(template_path.name) @@ -75,7 +43,50 @@ class HTMLRenderer: mode="w", encoding="utf-8", ) as file: - file.write(self.template.render(obj=obj)) # 在模板中需以obj获取键值 + file.write(self.template.render(obj=self._format(obj))) + # 在模板中需以obj获取键值 except Exception as exception: print(f"根据数据字典渲染HTML文档发生异常:{str(exception)}") + + def _format( + self, + input: Union[str, None, datetime, Decimal, List[Any], Dict[str, Any]], + format: Optional[str] = None, + ) -> Any: + """ + 格式化数据 + :param input: 数据 + :param format: 格式,默认为空值 + :return: 格式化后的数据 + """ + match input: + # 若为字典,则递归格式化 + case dict(): + for key, value in input.items(): + # 若键值为datetime + if isinstance(value, datetime): + # 若键名后缀形如_time,则格式为"%Y-%m-%d %H:%M:%S",否则默认为"%Y-%m-%d" + if re.search(r"_time$", key): + format = r"%Y-%m-%d %H:%M:%S" + + input[key] = self._format(value, format=format) + return input + + # 若为列表,则递归格式化 + case list(): + for idx, item in enumerate(input): + input[idx] = self._format(item) + return input + + case None: + return "" + + case datetime(): + # 若键值为datetime(9999, 12, 31),则返回字符串"长期" + if input == datetime(9999, 12, 31): + return "长期" + return input.strftime(format=(format or r"%Y-%m-%d")) + + case _: + return input diff --git a/utils/sqlite.py b/utils/sqlite.py index 6a13b8e..afee3f7 100644 --- a/utils/sqlite.py +++ b/utils/sqlite.py @@ -3,10 +3,10 @@ SQLite客户端 """ +from pathlib import Path import sqlite3 import threading -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple class SQLite: diff --git a/短视频合成自动化.zip b/短视频合成自动化.zip new file mode 100644 index 0000000000000000000000000000000000000000..ef924b71ef884ff5da6ff86d5e7518e7c460bcaa GIT binary patch literal 33916 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 literal 0 HcmV?d00001 diff --git a/短视频合成自动化/draft.py b/短视频合成自动化/draft.py index fbbaa84..128f2fe 100644 --- a/短视频合成自动化/draft.py +++ b/短视频合成自动化/draft.py @@ -158,42 +158,57 @@ class JianYingDraft: :return: 无 """ try: + # 音频素材 + audio_material = pyJianYingDraft.AudioMaterial( + path=material_path.as_posix() + ) + # 音频素材的持续时长 + audio_material_duration = audio_material.duration + # 若草稿持续时长为0,则将第一个音频素材持续时长作为草稿持续时长 + + # 获取持续时间 + target_duration = pyJianYingDraft.time_util.tim( + (target_timerange if target_timerange else (0, self.draft_duration))[1] + ) + if add_track: # 添加音频轨道 self.draft.add_track( - track_type=pyJianYingDraft.TrackType.video, + track_type=pyJianYingDraft.TrackType.audio, track_name=track_name, ) - # 构建音频片段 - audio_segment = pyJianYingDraft.AudioSegment( - material=material_path.as_posix(), - target_timerange=pyJianYingDraft.trange( - *( - target_timerange - if target_timerange - else (0, self.draft_duration) - ) - ), - source_timerange=( - pyJianYingDraft.trange(*source_timerange) - if source_timerange - else None - ), - speed=speed, - volume=volume, - ) - # 添加淡入淡出 - if fade: - audio_segment.add_fade(*fade) + duration = 0 # 已添加音频素材的持续时长 + while duration < target_duration: + # 构建音频片段 + audio_segment = pyJianYingDraft.AudioSegment( + material=audio_material, + target_timerange=pyJianYingDraft.trange( + start=duration, + duration=min( + (target_duration - duration), audio_material_duration + ), + ), + source_timerange=( + pyJianYingDraft.trange(*source_timerange) + if source_timerange + else None + ), + speed=speed, + volume=volume, + ) + # 添加淡入淡出 + if fade: + audio_segment.add_fade(*fade) - # 向指定音频轨道添加音频片段 - self.draft.add_segment(segment=audio_segment, track_name=track_name) + # 向指定音频轨道添加音频片段 + self.draft.add_segment(segment=audio_segment, track_name=track_name) + + duration += audio_material_duration except Exception as exception: raise RuntimeError(str(exception)) from exception - # pylint: disable=too-many-locals def add_video_segment( self, track_name: str, @@ -226,23 +241,28 @@ class JianYingDraft: :return: 无 """ try: - # 添加视频轨道 - self.draft.add_track( - track_type=pyJianYingDraft.TrackType.video, - track_name=track_name, - ) - - # 获取持续时间 - target_duration = pyJianYingDraft.time_util.tim( - (target_timerange if target_timerange else (0, self.draft_duration))[1] - ) - # 视频素材 video_material = pyJianYingDraft.VideoMaterial( path=material_path.as_posix() ) # 视频素材的持续时长 video_material_duration = video_material.duration + # 若草稿持续时长为0,则将第一个视频素材持续时长作为草稿持续时长 + relative_index = 0 + if not self.draft_duration: + relative_index = 1 # 视频轨道相对索引 + self.draft_duration = video_material_duration + # 获取持续时间 + target_duration = pyJianYingDraft.time_util.tim( + (target_timerange if target_timerange else (0, self.draft_duration))[1] + ) + + # 添加视频轨道 + self.draft.add_track( + track_type=pyJianYingDraft.TrackType.video, + track_name=track_name, + relative_index=relative_index, + ) duration = 0 # 已添加视频素材的持续时长 while duration < target_duration: diff --git a/短视频合成自动化/edgetts.py b/短视频合成自动化/edgetts.py index b215d35..ebe55be 100644 --- a/短视频合成自动化/edgetts.py +++ b/短视频合成自动化/edgetts.py @@ -12,7 +12,6 @@ import edge_tts from mutagen.mp3 import MP3 -# pylint: disable=too-few-public-methods class EdgeTTS: """ EdgeTTS模块,支持: diff --git a/短视频合成自动化/export.py b/短视频合成自动化/export.py index f87acdb..5e18102 100644 --- a/短视频合成自动化/export.py +++ b/短视频合成自动化/export.py @@ -11,6 +11,7 @@ import re import subprocess import time from typing import Any, Dict, Optional +from uuid import uuid4 import pyJianYingDraft import win32con @@ -19,8 +20,6 @@ import win32gui from draft import JianYingDraft -# pylint: disable=too-few-public-methods -# pylint: disable=too-many-instance-attributes class JianYingExport: """ 封装 pyJianYingDraft.JianyingController库,支持: @@ -74,7 +73,7 @@ class JianYingExport: self.exports_folder_path = Path( self.materials_folder_path.as_posix().replace("materials", "exports") ) - # self.exports_folder_path.mkdir() # 若导出文件夹存在则抛出异常,需手动处理 + self.exports_folder_path.mkdir() # 若导出文件夹存在则抛出异常,需手动处理 self.materials = {} # 初始化素材文件夹内所有素材 @@ -82,15 +81,26 @@ class JianYingExport: # 构建项目名称 self.project_name = self.materials_folder_path.stem - # 初始化工作流,其目的是按照工作流和工作配置拼接素材,先生成草稿再导出 - self.workflow = [ - "add_subtitles", - "add_background_video", - "add_statement", - "add_sticker1", - "add_sticker2", - "save", - ] + + # 初始化所有工作流 + self.workflows = { + "0000": [ + "add_subtitles", + "add_background_video", + "add_statement", + "add_sticker1", + "add_sticker2", + "save", + ], + "0001": [ + "add_subtitles_video", # 以此作为草稿持续时长 + "add_background_video", + "add_background_audio", + "add_statement_video", + "save", + ], + } + # 初始化工作配置 self.configuration = { "add_subtitles": { @@ -124,13 +134,27 @@ class JianYingExport: {"effect_id": "7127828216647011592"}, ], # 花字设置 }, # 添加字幕工作配置 - "add_background_video": { - "material_path": self.materials["background_video_material_path"], - "volume": [0.3, 0.4, 0.5], + "add_subtitles_video": { + "material_path": self.materials["subtitles_video_material_path"], + "volume": [1.0], # 播放音量 "clip_settings": [ None, ], # 图像调节设置 + }, # 添加字幕视频工作配置 + "add_background_video": { + "material_path": self.materials["background_video_material_path"], + "volume": [1.0], # 播放音量 + "clip_settings": [ + { + "scale_x": 1.5, + "scale_y": 1.5, + }, + ], # 图像调节设置 }, # 添加背景视频工作配置 + "add_background_audio": { + "material_path": self.materials["background_audio_material_path"], + "volume": [0.6], # 播放音量 + }, # 添加背景音频工作配置 "add_logo": { "material_path": self.materials["logo_material_path"], "clip_settings": [ @@ -192,6 +216,13 @@ class JianYingExport: }, ], # 图像调节设置 }, # 添加声明工作配置 + "add_statement_video": { + "material_path": self.materials["statement_video_material_path"], + "volume": [1.0], # 播放音量 + "clip_settings": [ + None, + ], # 图像调节设置 + }, # 添加声明视频工作配置 "add_sticker1": { "resource_id": [ "7110124379568098568", @@ -229,7 +260,7 @@ class JianYingExport: "transform_y": 0.75, }, ], # 图像调节设置 - }, # 添加贴纸工作配置1(不包含箭头类) + }, # 添加贴纸1工作配置(不包含箭头类) "add_sticker2": { "resource_id": [ "7143078914989018379", @@ -246,9 +277,12 @@ class JianYingExport: "transform_y": -0.62, }, ], # 图像调节设置 - }, # 添加贴纸工作配置2(箭头类) + }, # 添加贴纸2工作配置 } + # 初始化工作流 + self.workflow = [] + self.video_width, self.video_height = video_width, video_height self.video_fps = video_fps @@ -264,8 +298,8 @@ class JianYingExport: 初始化素材文件夹内所有素材 :return: 无 """ - # 字幕文本 - subtitles_path = self.materials_folder_path / "字幕文本.txt" + # 字幕(文本) + subtitles_path = self.materials_folder_path / "字幕.txt" if subtitles_path.exists() and subtitles_path.is_file(): with open(subtitles_path, "r", encoding="utf-8") as file: subtitles_text = file.readlines() @@ -273,33 +307,59 @@ class JianYingExport: raise RuntimeError("字幕文本为空") self.materials["subtitles_text"] = subtitles_text else: - raise RuntimeError("字幕文本不存在") + self.materials["subtitles_text"] = [] - # 背景视频 - background_videos_path = self.materials_folder_path / "背景视频" - if background_videos_path.exists() and background_videos_path.is_dir(): - background_video_material_path = [ + # 字幕(视频) + subtitles_video_folder_path = self.materials_folder_path / "字幕视频" + if ( + subtitles_video_folder_path.exists() + and subtitles_video_folder_path.is_dir() + ): + self.materials["subtitles_video_material_path"] = [ file_path - for file_path in background_videos_path.rglob("*.mp4") + for file_path in subtitles_video_folder_path.rglob("*.mov") if file_path.is_file() ] - if not background_video_material_path: - raise RuntimeError("背景视频为空") - self.materials["background_video_material_path"] = ( - background_video_material_path - ) else: - raise RuntimeError("背景视频文件夹不存在") + self.materials["subtitles_video_material_path"] = [] + + # 背景视频 + background_video_folder_path = self.materials_folder_path / "背景视频" + if ( + background_video_folder_path.exists() + and background_video_folder_path.is_dir() + ): + self.materials["background_video_material_path"] = [ + file_path + for file_path in background_video_folder_path.rglob("*.mp4") + if file_path.is_file() + ] + else: + self.materials["background_video_material_path"] = [] + + # 背景音频 + background_audio_folder_path = self.materials_folder_path / "背景音频" + if ( + background_audio_folder_path.exists() + and background_audio_folder_path.is_dir() + ): + self.materials["background_audio_material_path"] = [ + file_path + for file_path in background_audio_folder_path.rglob("*.mp3") + if file_path.is_file() + ] + else: + self.materials["background_audio_material_path"] = [] # 标识 logo_path = self.materials_folder_path / "标识.png" if logo_path.exists() and logo_path.is_file(): self.materials["logo_material_path"] = [logo_path] # 有且只有一张标识 else: - raise RuntimeError("标识不存在") + self.materials["logo_material_path"] = [] - # 声明文本 - statement_path = self.materials_folder_path / "声明文本.txt" + # 声明(声明) + statement_path = self.materials_folder_path / "声明.txt" if statement_path.exists() and statement_path.is_file(): with open(statement_path, "r", encoding="utf-8") as file: statement_text = file.readlines() @@ -307,16 +367,38 @@ class JianYingExport: raise RuntimeError("声明文本为空") self.materials["statement_text"] = statement_text else: - raise RuntimeError("声明不存在") + self.materials["statement_text"] = [] - def export(self, batch_draft_counts: int = 1): + # 声明(视频) + statement_video_folder_path = self.materials_folder_path / "声明视频" + if ( + statement_video_folder_path.exists() + and statement_video_folder_path.is_dir() + ): + self.materials["statement_video_material_path"] = [ + file_path + for file_path in statement_video_folder_path.rglob("*.mov") + if file_path.is_file() + ] + else: + self.materials["statement_video_material_path"] = [] + + def export(self, workflow_name: str = "0001", batch_draft_counts: int = 1): """ 导出草稿 + :param workflow_name: 工作流名称 :param batch_draft_counts: 每批次导出草稿数 """ + if workflow_name not in self.workflows: + raise RuntimeError(f"未配置该工作流") + self.workflow = self.workflows[workflow_name] + + # 若工作流包含添加背景音频,则将添加背景视频工作配置中播放音量设置为0 + if "add_background_audio" in self.workflow: + self.configuration["add_background_video"]["volume"] = [0.0] + # 按照工作流和工作配置拼接素材,批量生成草稿 self._generate_drafts() - exit() # 批次导出 for batch_start in range(0, self.draft_counts, batch_draft_counts): @@ -375,31 +457,63 @@ class JianYingExport: video_fps=self.video_fps, materials_folder_path=self.materials_folder_path, ) + + # 初始化当前草稿工作配置 + configuration = { + "materials_folder_path": self.materials_folder_path.stem, + "draft_name": draft_name, + "workflows": [], + } for work in self.workflow: + # 获取工作配置 + parameters = self._get_parameters(work=work) + configuration["workflows"].append( + { + "work": work, + "parameters": parameters, + } + ) + match work: + # 添加字幕 case "add_subtitles": print("-> 正在添加字幕...", end="") - draft.add_subtitles(**self._get_parameters(work=work)) + draft.add_subtitles(**parameters) + print("已完成") + # 添加字幕 + case "add_subtitles_video": + print("-> 正在添加字幕视频...", end="") + draft.add_video_segment(**parameters) print("已完成") # 添加背景视频 case "add_background_video": print("-> 正在添加背景视频...", end="") - draft.add_video_segment(**self._get_parameters(work=work)) + draft.add_video_segment(**parameters) + print("已完成") + # 添加背景音频 + case "add_background_audio": + print("-> 正在添加背景音频...", end="") + draft.add_audio_segment(**parameters) print("已完成") # 添加标识 case "add_logo": print("-> 正在添加标识...", end="") - draft.add_video_segment(**self._get_parameters(work=work)) + draft.add_video_segment(**parameters) print("已完成") # 添加声明 case "add_statement": print("-> 正在添加声明...", end="") - draft.add_text_segment(**self._get_parameters(work=work)) + draft.add_text_segment(**parameters) + print("已完成") + # 添加视频 + case "add_statement_video": + print("-> 正在添加声明视频...", end="") + draft.add_video_segment(**parameters) print("已完成") # 添加贴纸 case _ if work.startswith("add_sticker"): print("-> 正在添加贴纸...", end="") - draft.add_sticker(**self._get_parameters(work=work)) + draft.add_sticker(**parameters) print("已完成") # 将草稿保存至剪映草稿文件夹内 case "save": @@ -407,11 +521,17 @@ class JianYingExport: draft.save() print("已完成") - # 高亮关键词 - self._highlight_keywords(draft_name=draft_name) - exit() + if "add_subtitles" in self.workflow: + # 高亮关键词 + self._highlight_keywords(draft_name=draft_name) self.draft_names.append(draft_name) + with open( + file=self.exports_folder_path / f"{uuid4().hex.upper()}.txt", + mode="w", + encoding="utf-8", + ) as file: + file.write(f"{configuration}") print("已完成") print() @@ -424,10 +544,13 @@ class JianYingExport: work: str, ) -> Dict[str, Any]: """ - 获取工作配置项 + 获取工作配置 :param work: 工作,包括添加字幕、添加背景视频、添加标识、添加声明和添加贴纸 :return: 工作配置 """ + if work == "save": + return {} + parameters = { key: random.choice(value) for key, value in self.configuration[work].items() } # TODO: 考虑融合贝叶斯优化 @@ -522,7 +645,14 @@ class JianYingExport: mode="w", encoding="utf-8", ) as file: - file.write(json.dumps(obj=draft_content, ensure_ascii=False, indent=4)) + file.write( + json.dumps( + obj=draft_content, + default=lambda x: x.name if isinstance(x, Path) else x, + ensure_ascii=False, + indent=4, + ) + ) def _start_process(self, timeout: int = 60) -> None: """ diff --git a/短视频合成自动化/main.py b/短视频合成自动化/main.py index 0af11ee..d58be7e 100644 --- a/短视频合成自动化/main.py +++ b/短视频合成自动化/main.py @@ -8,9 +8,9 @@ from export import JianYingExport if __name__ == "__main__": # 实例化 JianYingExport jianying_export = JianYingExport( - materials_folder_path=r"E:\jianying\materials\260104", + materials_folder_path=r"E:\jianying\materials\淘宝闪购模版001", draft_counts=1, ) # 导出草稿 - jianying_export.export() + jianying_export.export(workflow_name="0001") diff --git a/票据理赔自动化/template.html b/票据理赔自动化/template.html index 5eb63b4..2fd547d 100644 --- a/票据理赔自动化/template.html +++ b/票据理赔自动化/template.html @@ -66,20 +66,24 @@ --color-success: var(--green-6); --color-warning: var(--orange-6); --color-danger: var(--red-6); - --color-text: var(--gray-10); - --color-text-secondary: var(--gray-8); + --color-text: var(--gray-9); + --color-text-secondary: var(--gray-7); --color-border: var(--gray-3); - --color-bg: var(--gray-1); - --color-bg-secondary: #ffffff; + --color-background: var(--gray-1); + --color-background-secondary: #ffffff; --border-radius-small: 4px; --border-radius-medium: 6px; --border-radius-large: 8px; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; + --font-size-medium: 14px; + --font-size-large: 20px; + --font-weight-medium: 400; + --font-weight-large: 600; --box-shadow: 0 4px 10px rgba(0, 0, 0, 0.04); --box-shadow-hover: 0 8px 20px rgba(0, 0, 0, 0.08); --spacing-xs: 4px; - --spacing-sm: 8px; + --spacing-small: 8px; --spacing-md: 16px; --spacing-lg: 24px; --spacing-xl: 32px; @@ -93,7 +97,7 @@ } body { - background-color: var(--color-bg); + background-color: var(--color-background); color: var(--color-text); line-height: 1.6; padding: var(--spacing-lg); @@ -103,84 +107,85 @@ .container { max-width: 1200px; margin: 0 auto; - background: var(--color-bg-secondary); + background: var(--color-background-secondary); border-radius: var(--border-radius-large); box-shadow: var(--box-shadow); overflow: hidden; } - header { - background: var(--color-primary); - color: white; - padding: var(--spacing-xl); + /* 头块 */ + .header-block { position: relative; - } - - .header-container { + background: var(--color-primary); /* 背景色 */ + padding: var(--spacing-xl); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; } - h1 { - font-size: 20px; - font-weight: 600; - margin-bottom: var(--spacing-sm); + .header-block-title { + flex-basis: 100%; /* 独占一行 */ + margin-bottom: var(--spacing-small); /* 外边距 */ + color: var(--gray-1); /* 文字颜色 */ + font-size: var(--font-size-large); /* 文字字号 */ + font-weight: var(--font-weight-large); /* 文字粗细 */ } - .header-info { - font-size: 14px; - opacity: 0.9; + .header-block-content { + color: var(--gray-1); /* 文字颜色 */ + font-size: var(--font-size-medium); /* 文字字号 */ + font-weight: var(--font-weight-medium); /* 文字粗细 */ + opacity: 0.8; /* 文字透明度 */ } - .insurance-logo { - background: white; - padding: var(--spacing-xs) var(--spacing-md); - border-radius: 50px; - font-weight: 500; - color: var(--color-primary); - box-shadow: var(--box-shadow); - } - - .section { + .section-block { padding: var(--spacing-lg); border-bottom: 1px solid var(--color-border); } - .section:last-child { + .section-block:last-child { border-bottom: none; } - h2 { + .section-block-title { color: var(--color-primary); - font-size: 16px; + font-size: var(--font-size-medium); + font-weight: var(--font-weight-medium); margin-bottom: var(--spacing-lg); - padding-bottom: var(--spacing-sm); + padding-bottom: var(--spacing-small); border-bottom: 1px solid var(--color-primary-light); display: flex; align-items: center; - font-weight: 500; } - h2:before { + .section-block-title:before { content: ""; display: inline-block; width: 3px; height: 16px; background: var(--color-primary); border-radius: var(--border-radius-small); - margin-right: var(--spacing-sm); + margin-right: var(--spacing-small); } - .card-container { + .section-block-content { + color: var(--color-text); /* 文字颜色 */ + font-size: var(--font-size-medium); /* 文字字号 */ + font-weight: var(--font-weight-large); /* 文字粗细 */ + line-height: 1.6; /* 文字行高 */ + margin-bottom: var(--spacing-md); + padding: var(--spacing-small) 0; + } + + .section-block-inner { display: grid; grid-template-columns: 1fr; gap: var(--spacing-md); } .card { - background: var(--color-bg-secondary); + background: var(--color-background-secondary); border-radius: var(--border-radius-medium); padding: var(--spacing-md); border: 1px solid var(--color-border); @@ -188,44 +193,46 @@ transition: transform 0.2s ease, box-shadow 0.2s ease; } - .card h3 { + .card-title { color: var(--color-primary); - font-size: 14px; + font-size: var(--font-size-medium); + font-weight: var(--font-weight-large); margin-bottom: var(--spacing-md); - padding-bottom: var(--spacing-sm); + padding-bottom: var(--spacing-small); border-bottom: 1px dashed var(--color-border); - font-weight: 500; } /* 字段布局 */ - .info-grid { + .card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-md) var(--spacing-lg); font-size: 14px; } - .info-item { + .card-item { display: flex; flex-direction: column; margin-bottom: 12px; } - .info-label { - font-size: 12px; + .card-item-label { color: var(--color-text-secondary); + font-size: var(--font-size-medium); + font-weight: var(--font-weight-medium); margin-bottom: 4px; } - .info-value { - font-size: 15px; - font-weight: 500; + .card-item-value { + color: var(--color-text); + font-size: var(--font-size-medium); + font-weight: var(--font-weight-large); word-break: break-word; padding: 4px 0; } .invoice-card { - background: var(--color-bg-secondary); + background: var(--color-background-secondary); border-radius: var(--border-radius-medium); padding: var(--spacing-md); margin-bottom: var(--spacing-md); @@ -238,7 +245,7 @@ justify-content: space-between; align-items: flex-start; margin-bottom: var(--spacing-md); - padding-bottom: var(--spacing-sm); + padding-bottom: var(--spacing-small); border-bottom: 1px solid var(--color-border); } @@ -318,18 +325,21 @@ table { width: 100%; margin-top: var(--spacing-md); - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0; border-radius: var(--border-radius-medium); overflow: hidden; box-shadow: var(--box-shadow); - font-size: 14px; table-layout: fixed; + border: 1px solid var(--color-border); + background-color: var(--color-background-secondary); } th { background-color: var(--color-primary-light); color: var(--color-primary); - font-weight: 500; + font-size: var(--font-size-medium); + font-weight: var(--font-weight-large); padding: 12px; text-align: left; width: 20%; @@ -337,6 +347,9 @@ } td { + color: var(--color-text); + font-size: var(--font-size-medium); + font-weight: var(--font-weight-large); padding: 12px; border-bottom: 1px solid var(--color-border); width: 20%; @@ -344,11 +357,7 @@ } tr:nth-child(even) { - background-color: var(--color-bg); - } - - tr:hover { - background-color: var(--color-primary-light); + background-color: var(--color-background); } .amount-total { @@ -366,7 +375,7 @@ padding: var(--spacing-md); color: var(--color-text-secondary); font-size: 12px; - background: var(--color-bg); + background: var(--color-background); border-top: 1px solid var(--color-border); } @@ -378,197 +387,187 @@
-
-
-
-

理赔报告

-
-

- 保险分公司名称: {{ obj["report_layer"]["insurer_company"] }} | - 报案时间: {{ obj["report_layer"]["report_time"] | - datetime_to_str }} | 赔案号:{{ - obj["report_layer"]["case_number"] }} -

-
-
+
+
理赔报告
+
+ {{ obj["report_layer"]["insurer_company"] }} + | + {{ obj["report_layer"]["report_time"] }} + | + {{ obj["report_layer"]["case_number"] }}
-
-
-

影像件层

-
- 签收影像件{{ obj["report_layer"]["images_counts"] }}张,其中:已分类{{ - obj["classified_images_counts"] }}张,已识别{{ - obj["recognized_images_counts"] }}张 -
- - - - - - - - - - - - {% for image_index,image in obj["images_layer"].items() %} - - - - - - - - {% endfor %} - -
影像件编号影像件路径影像件类型已分类已识别
{{ image_index }}{{ image["image_name"] }}{{ image["image_type"] }}{{ image["image_classified"] }}{{ image["image_recognized"] }}
-
-

赔案层

-
+
+
理赔结论层
+
+ 经理赔流程初步审核,本案结论为 + {{ obj["adjustment_layer"]["conclusion"] }} + ,理赔金额为 + {{ obj["adjustment_layer"]["adjustment_amount"] }} + 元,其依据为 + {{ obj["adjustment_layer"]["explanation"] }} + +
+
+
+
影像件层
+
+ 本案签收 + {{ obj["report_layer"]["images_counts"] }} + 张影像件,其中已分类 + {{ obj["classified_images_counts"] }} + 张,已识别 + {{ obj["recognized_images_counts"] }} + 张。具体情况如下: +
+
+ + + + + + + + + + + + {% for image_index, image in obj["images_layer"].items() %} + + + + + + + + {% endfor %} + +
影像件编号影像件名称影像件类型已分类已识别
{{ image_index }}{{ image["image_name"] }}{{ image["image_type"] }}{{ image["image_classified"] }}{{ image["image_recognized"] }}
+
+
+
+
赔案层
+
-

出险人(亦被保险人)信息

-
-
-
姓名
-
+
出险人(亦被保险人)信息
+
+
+
姓名
+
{{ obj["insured_person_layer"]["insured_person"] }}
-
-
出生
-
- {{ obj["insured_person_layer"]["birth_date"] | datetime_to_str - }} | {{ obj["insured_person_layer"]["age"] }}岁 +
+
出生
+
+ {{ obj["insured_person_layer"]["birth_date"] }} + | + {{ obj["insured_person_layer"]["age"] }}
-
-
性别
-
+
+
性别
+
{{ obj["insured_person_layer"]["gender"] }}
-
-
证件类型
-
+
+
证件类型
+
{{ obj["insured_person_layer"]["identity_type"] }}
-
-
证件号码
-
+
+
证件号码
+
{{ obj["insured_person_layer"]["identity_number"] }}
-
-
证件有效期
-
- {{ obj["insured_person_layer"]["commencement_date"] | - datetime_to_str }} 至 {{ - obj["insured_person_layer"]["termination_date"] | - datetime_to_str }} +
+
证件有效期至
+
+ {{ obj["insured_person_layer"]["termination_date"] }}
-
-
手机号
-
+
+
手机号
+
{{ obj["insured_person_layer"]["phone_number"] }}
-
-
住址
-
- {{ obj["insured_person_layer"]["province"] }} {{ - obj["insured_person_layer"]["city"] }} {{ - obj["insured_person_layer"]["district"] }} -
-
- {{ obj["insured_person_layer"]["detailed_address"] }} +
+
住址
+
+
+ {{ obj["insured_person_layer"]["province"] }} + | + {{ obj["insured_person_layer"]["city"] }} + | + {{ obj["insured_person_layer"]["district"] }} +
+
+ {{ obj["insured_person_layer"]["detailed_address"] }} +
+
-

领款信息

-
-
-
开户银行
-
+
领款信息
+
+
+
开户银行
+
{{ obj["insured_person_layer"]["account_bank"] }}
-
-
户名
-
+
+
户名
+
{{ obj["insured_person_layer"]["account"] }}
-
-
户号
-
+
+
户号
+
{{ obj["insured_person_layer"]["account_number"] }}
-

可理赔责任

- - - - - - - - - - - - - {% for liability in obj["liabilities_layer"] %} - - - - - - - - - {% endfor %} - -
团单号主被保险人被保险人与主被保险人关系保险期理赔责任
{{ liability["group_policy"] }}{{ liability["master_insured_person"] }}{{ liability["insured_person"] }}{{ liability["relationship"] }} - {{ liability["commencement_date"] | datetime_to_str }} 至 {{ - liability["termination_date"] | datetime_to_str }} - {{ liability["liability"] }}
-
-
-
-
-

结论层

-
-
-
-
理赔结论
-
- {{ obj["adjustment_layer"]["conclusion"] }} -
-
-
-
理算金额
-
- {{ obj["adjustment_layer"]["adjustment_amount"] }} -
-
-
-
结论说明
-
- {{ obj["adjustment_layer"]["explanation"] }} -
+
可理赔责任
+
+ + + + + + + + + + + + + {% for liability in obj["liabilities_layer"] %} + + + + + + + + + {% endfor %} + +
团单号理赔责任保险期至主被保险人被保险人与主被保险人关系
{{ liability["group_policy"] }}{{ liability["liability"] }}{{ liability["termination_date"] }}{{ liability["master_insured_person"] }}{{ liability["insured_person"] }}{{ liability["relationship"] }}
-
+

票据层

{% for receipt in obj["receipts_layer"] %}