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 0000000..ef924b7 Binary files /dev/null and b/短视频合成自动化.zip differ 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"] %}