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