This commit is contained in:
parent
5ff511f898
commit
b7e227820c
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -158,22 +158,36 @@ 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,
|
||||
)
|
||||
|
||||
duration = 0 # 已添加音频素材的持续时长
|
||||
while duration < target_duration:
|
||||
# 构建音频片段
|
||||
audio_segment = pyJianYingDraft.AudioSegment(
|
||||
material=material_path.as_posix(),
|
||||
material=audio_material,
|
||||
target_timerange=pyJianYingDraft.trange(
|
||||
*(
|
||||
target_timerange
|
||||
if target_timerange
|
||||
else (0, self.draft_duration)
|
||||
)
|
||||
start=duration,
|
||||
duration=min(
|
||||
(target_duration - duration), audio_material_duration
|
||||
),
|
||||
),
|
||||
source_timerange=(
|
||||
pyJianYingDraft.trange(*source_timerange)
|
||||
|
|
@ -190,10 +204,11 @@ class JianYingDraft:
|
|||
# 向指定音频轨道添加音频片段
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import edge_tts
|
|||
from mutagen.mp3 import MP3
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class EdgeTTS:
|
||||
"""
|
||||
EdgeTTS模块,支持:
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
||||
# 初始化所有工作流
|
||||
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("已完成")
|
||||
|
||||
if "add_subtitles" in self.workflow:
|
||||
# 高亮关键词
|
||||
self._highlight_keywords(draft_name=draft_name)
|
||||
exit()
|
||||
|
||||
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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,40 +387,52 @@
|
|||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-container">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
<th>已识别</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for image_index,image in obj["images_layer"].items() %}
|
||||
{% for image_index, image in obj["images_layer"].items() %}
|
||||
<tr>
|
||||
<td>{{ image_index }}</td>
|
||||
<td>{{ image["image_name"] }}</td>
|
||||
|
|
@ -423,119 +444,121 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>赔案层</h2>
|
||||
<div class="card-container">
|
||||
</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 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 class="info-value">
|
||||
<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>
|
||||
<div class="card-title">可理赔责任</div>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>团单号</th>
|
||||
<th>理赔责任</th>
|
||||
<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>
|
||||
<td>
|
||||
{{ liability["commencement_date"] | datetime_to_str }} 至 {{
|
||||
liability["termination_date"] | datetime_to_str }}
|
||||
</td>
|
||||
<td>{{ liability["liability"] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -543,32 +566,8 @@
|
|||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-block">
|
||||
<h2>票据层</h2>
|
||||
{% for receipt in obj["receipts_layer"] %}
|
||||
<div class="invoice-card">
|
||||
|
|
|
|||
Loading…
Reference in New Issue