This commit is contained in:
liubiren 2026-01-15 22:38:08 +08:00
parent 5ff511f898
commit b7e227820c
8 changed files with 493 additions and 334 deletions

View File

@ -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

View File

@ -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:

Binary file not shown.

View File

@ -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:

View File

@ -12,7 +12,6 @@ import edge_tts
from mutagen.mp3 import MP3
# pylint: disable=too-few-public-methods
class EdgeTTS:
"""
EdgeTTS模块支持

View File

@ -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:
"""

View File

@ -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")

View File

@ -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 @@
</head>
<body>
<div class="container">
<header>
<div class="header-container">
<div>
<h1>理赔报告</h1>
<div class="header-info">
<p>
保险分公司名称: {{ obj["report_layer"]["insurer_company"] }} |
报案时间: {{ obj["report_layer"]["report_time"] |
datetime_to_str }} | 赔案号:{{
obj["report_layer"]["case_number"] }}
</p>
</div>
</div>
<div class="header-block">
<div class="header-block-title">理赔报告</div>
<div class="header-block-content">
<span>{{ obj["report_layer"]["insurer_company"] }}</span>
<span>|</span>
<span>{{ obj["report_layer"]["report_time"] }}</span>
<span>|</span>
<span>{{ obj["report_layer"]["case_number"] }}</span>
</div>
</header>
<div class="section">
<h2>影像件层</h2>
<div>
签收影像件{{ obj["report_layer"]["images_counts"] }}张,其中:已分类{{
obj["classified_images_counts"] }}张,已识别{{
obj["recognized_images_counts"] }}张
</div>
<table>
<thead>
<tr>
<th>影像件编号</th>
<th>影像件路径</th>
<th>影像件类型</th>
<th>已分类</th>
<th>已识别</th>
</tr>
</thead>
<tbody>
{% for image_index,image in obj["images_layer"].items() %}
<tr>
<td>{{ image_index }}</td>
<td>{{ image["image_name"] }}</td>
<td>{{ image["image_type"] }}</td>
<td>{{ image["image_classified"] }}</td>
<td>{{ image["image_recognized"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="section">
<h2>赔案层</h2>
<div class="card-container">
<div class="section-block">
<div class="section-block-title">理赔结论层</div>
<div class="section-block-content">
<span>经理赔流程初步审核,本案结论为</span>
<span>{{ obj["adjustment_layer"]["conclusion"] }}</span>
<span>,理赔金额为</span>
<span>{{ obj["adjustment_layer"]["adjustment_amount"] }}</span>
<span>元,其依据为</span>
<span>{{ obj["adjustment_layer"]["explanation"] }}</span>
<span></span>
</div>
</div>
<div class="section-block">
<div class="section-block-title">影像件层</div>
<div class="section-block-content">
<span>本案签收</span>
<span>{{ obj["report_layer"]["images_counts"] }}</span>
<span>张影像件,其中已分类</span>
<span>{{ obj["classified_images_counts"] }}</span>
<span>张,已识别</span>
<span>{{ obj["recognized_images_counts"] }}</span>
<span>张。具体情况如下:</span>
</div>
<div>
<table>
<thead>
<tr>
<th>影像件编号</th>
<th>影像件名称</th>
<th>影像件类型</th>
<th>已分类</th>
<th>已识别</th>
</tr>
</thead>
<tbody>
{% for image_index, image in obj["images_layer"].items() %}
<tr>
<td>{{ image_index }}</td>
<td>{{ image["image_name"] }}</td>
<td>{{ image["image_type"] }}</td>
<td>{{ image["image_classified"] }}</td>
<td>{{ image["image_recognized"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="section-block">
<div class="section-block-title">赔案层</div>
<div class="section-block-inner">
<div class="card">
<h3>出险人(亦被保险人)信息</h3>
<div class="info-grid">
<div class="info-item">
<div class="info-label">姓名</div>
<div class="info-value">
<div class="card-title">出险人(亦被保险人)信息</div>
<div class="card-grid">
<div class="card-item">
<div class="card-item-label">姓名</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["insured_person"] }}
</div>
</div>
<div class="info-item">
<div class="info-label">出生</div>
<div class="info-value">
{{ obj["insured_person_layer"]["birth_date"] | datetime_to_str
}} | {{ obj["insured_person_layer"]["age"] }}岁
<div class="card-item">
<div class="card-item-label">出生</div>
<div class="card-item-value">
<span>{{ obj["insured_person_layer"]["birth_date"] }}</span>
<span>|</span>
<span>{{ obj["insured_person_layer"]["age"] }}</span>
</div>
</div>
<div class="info-item">
<div class="info-label">性别</div>
<div class="info-value">
<div class="card-item">
<div class="card-item-label">性别</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["gender"] }}
</div>
</div>
<div class="info-item">
<div class="info-label">证件类型</div>
<div class="info-value">
<div class="card-item">
<div class="card-item-label">证件类型</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["identity_type"] }}
</div>
</div>
<div class="info-item">
<div class="info-label">证件号码</div>
<div class="info-value">
<div class="card-item">
<div class="card-item-label">证件号码</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["identity_number"] }}
</div>
</div>
<div class="info-item">
<div class="info-label">证件有效期</div>
<div class="info-value">
{{ obj["insured_person_layer"]["commencement_date"] |
datetime_to_str }} 至 {{
obj["insured_person_layer"]["termination_date"] |
datetime_to_str }}
<div class="card-item">
<div class="card-item-label">证件有效期至</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["termination_date"] }}
</div>
</div>
<div class="info-item">
<div class="info-label">手机号</div>
<div class="info-value">
<div class="card-item">
<div class="card-item-label">手机号</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["phone_number"] }}
</div>
</div>
<div class="info-item">
<div class="info-label">住址</div>
<div class="info-value">
{{ obj["insured_person_layer"]["province"] }} {{
obj["insured_person_layer"]["city"] }} {{
obj["insured_person_layer"]["district"] }}
</div>
<div class="info-value">
{{ obj["insured_person_layer"]["detailed_address"] }}
<div class="card-item">
<div class="card-item-label">住址</div>
<div class="card-item-value">
<div>
<span>{{ obj["insured_person_layer"]["province"] }}</span>
<span>|</span>
<span>{{ obj["insured_person_layer"]["city"] }}</span>
<span>|</span>
<span>{{ obj["insured_person_layer"]["district"] }}</span>
</div>
<div>
{{ obj["insured_person_layer"]["detailed_address"] }}
</div>
</div>
<div class="card-item-value"></div>
</div>
</div>
</div>
<div class="card">
<h3>领款信息</h3>
<div class="info-grid">
<div class="info-item">
<div class="info-label">开户银行</div>
<div class="info-value">
<div class="card-title">领款信息</div>
<div class="card-grid">
<div class="card-item">
<div class="card-item-label">开户银行</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["account_bank"] }}
</div>
</div>
<div class="info-item">
<div class="info-label">户名</div>
<div class="info-value">
<div class="card-item">
<div class="card-item-label">户名</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["account"] }}
</div>
</div>
<div class="info-item">
<div class="info-label">户号</div>
<div class="info-value">
<div class="card-item">
<div class="card-item-label">户号</div>
<div class="card-item-value">
{{ obj["insured_person_layer"]["account_number"] }}
</div>
</div>
</div>
</div>
<div class="card">
<h3>可理赔责任</h3>
<table>
<thead>
<tr>
<th>团单号</th>
<th>主被保险人</th>
<th>被保险人</th>
<th>与主被保险人关系</th>
<th>保险期</th>
<th>理赔责任</th>
</tr>
</thead>
<tbody>
{% for liability in obj["liabilities_layer"] %}
<tr>
<td>{{ liability["group_policy"] }}</td>
<td>{{ liability["master_insured_person"] }}</td>
<td>{{ liability["insured_person"] }}</td>
<td>{{ liability["relationship"] }}</td>
<td>
{{ liability["commencement_date"] | datetime_to_str }} 至 {{
liability["termination_date"] | datetime_to_str }}
</td>
<td>{{ liability["liability"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="section">
<h2>结论层</h2>
<div class="invoice-card">
<div class="invoice-details">
<div class="detail-item">
<div class="detail-label">理赔结论</div>
<div class="detail-value">
{{ obj["adjustment_layer"]["conclusion"] }}
</div>
</div>
<div class="detail-item">
<div class="detail-label">理算金额</div>
<div class="detail-value">
{{ obj["adjustment_layer"]["adjustment_amount"] }}
</div>
</div>
<div class="detail-item">
<div class="detail-label">结论说明</div>
<div class="detail-value">
{{ obj["adjustment_layer"]["explanation"] }}
</div>
<div class="card-title">可理赔责任</div>
<div>
<table>
<thead>
<tr>
<th>团单号</th>
<th>理赔责任</th>
<th>保险期至</th>
<th>主被保险人</th>
<th>被保险人</th>
<th>与主被保险人关系</th>
</tr>
</thead>
<tbody>
{% for liability in obj["liabilities_layer"] %}
<tr>
<td>{{ liability["group_policy"] }}</td>
<td>{{ liability["liability"] }}</td>
<td>{{ liability["termination_date"] }}</td>
<td>{{ liability["master_insured_person"] }}</td>
<td>{{ liability["insured_person"] }}</td>
<td>{{ liability["relationship"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-block">
<h2>票据层</h2>
{% for receipt in obj["receipts_layer"] %}
<div class="invoice-card">