2025-07-03 17:50:42 +08:00
|
|
|
|
"""
|
|
|
|
|
Table-related operations for Word Document Server.
|
|
|
|
|
"""
|
|
|
|
|
from docx.oxml.shared import OxmlElement, qn
|
|
|
|
|
from docx.oxml.ns import nsdecls
|
|
|
|
|
from docx.oxml import parse_xml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_cell_border(cell, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
Set cell border properties.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
cell: The cell to modify
|
|
|
|
|
**kwargs: Border properties (top, bottom, left, right, val, color)
|
|
|
|
|
"""
|
|
|
|
|
tc = cell._tc
|
|
|
|
|
tcPr = tc.get_or_add_tcPr()
|
|
|
|
|
|
|
|
|
|
# Create border elements
|
|
|
|
|
for key, value in kwargs.items():
|
|
|
|
|
if key in ['top', 'left', 'bottom', 'right']:
|
|
|
|
|
tag = 'w:{}'.format(key)
|
|
|
|
|
|
|
|
|
|
element = OxmlElement(tag)
|
|
|
|
|
element.set(qn('w:val'), kwargs.get('val', 'single'))
|
|
|
|
|
element.set(qn('w:sz'), kwargs.get('sz', '4'))
|
|
|
|
|
element.set(qn('w:space'), kwargs.get('space', '0'))
|
|
|
|
|
element.set(qn('w:color'), kwargs.get('color', 'auto'))
|
|
|
|
|
|
|
|
|
|
tcBorders = tcPr.first_child_found_in("w:tcBorders")
|
|
|
|
|
if tcBorders is None:
|
|
|
|
|
tcBorders = OxmlElement('w:tcBorders')
|
|
|
|
|
tcPr.append(tcBorders)
|
|
|
|
|
|
|
|
|
|
tcBorders.append(element)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_table_style(table, has_header_row=False, border_style=None, shading=None):
|
|
|
|
|
"""
|
|
|
|
|
Apply formatting to a table.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
table: The table to format
|
|
|
|
|
has_header_row: If True, formats the first row as a header
|
|
|
|
|
border_style: Style for borders ('none', 'single', 'double', 'thick')
|
|
|
|
|
shading: 2D list of cell background colors (by row and column)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if successful, False otherwise
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Format header row if requested
|
|
|
|
|
if has_header_row and table.rows:
|
|
|
|
|
header_row = table.rows[0]
|
|
|
|
|
for cell in header_row.cells:
|
|
|
|
|
for paragraph in cell.paragraphs:
|
|
|
|
|
if paragraph.runs:
|
|
|
|
|
for run in paragraph.runs:
|
|
|
|
|
run.bold = True
|
|
|
|
|
|
|
|
|
|
# Apply border style if specified
|
|
|
|
|
if border_style:
|
|
|
|
|
val_map = {
|
|
|
|
|
'none': 'nil',
|
|
|
|
|
'single': 'single',
|
|
|
|
|
'double': 'double',
|
|
|
|
|
'thick': 'thick'
|
|
|
|
|
}
|
|
|
|
|
val = val_map.get(border_style.lower(), 'single')
|
|
|
|
|
|
|
|
|
|
# Apply to all cells
|
|
|
|
|
for row in table.rows:
|
|
|
|
|
for cell in row.cells:
|
|
|
|
|
set_cell_border(
|
|
|
|
|
cell,
|
|
|
|
|
top=True,
|
|
|
|
|
bottom=True,
|
|
|
|
|
left=True,
|
|
|
|
|
right=True,
|
|
|
|
|
val=val,
|
|
|
|
|
color="000000"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Apply cell shading if specified
|
|
|
|
|
if shading:
|
|
|
|
|
for i, row_colors in enumerate(shading):
|
|
|
|
|
if i >= len(table.rows):
|
|
|
|
|
break
|
|
|
|
|
for j, color in enumerate(row_colors):
|
|
|
|
|
if j >= len(table.rows[i].cells):
|
|
|
|
|
break
|
|
|
|
|
try:
|
|
|
|
|
# Apply shading to cell
|
|
|
|
|
cell = table.rows[i].cells[j]
|
|
|
|
|
shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color}"/>')
|
|
|
|
|
cell._tc.get_or_add_tcPr().append(shading_elm)
|
|
|
|
|
except:
|
|
|
|
|
# Skip if color format is invalid
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
2025-07-29 18:01:15 +08:00
|
|
|
|
|
2025-07-30 17:31:18 +08:00
|
|
|
|
def copy_table(source_table, target_doc, ifadjustheight=True, height = 1, if_merge = True, REPORT_ENUM = 'DT'):
|
2025-07-03 17:50:42 +08:00
|
|
|
|
"""
|
|
|
|
|
Copy a table from one document to another.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
source_table: The table to copy
|
|
|
|
|
target_doc: The document to copy the table to
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The new table in the target document
|
|
|
|
|
"""
|
|
|
|
|
# Create a new table with the same dimensions
|
|
|
|
|
new_table = target_doc.add_table(rows=len(source_table.rows), cols=len(source_table.columns))
|
|
|
|
|
# Try to apply the same style
|
|
|
|
|
try:
|
|
|
|
|
if source_table.style:
|
|
|
|
|
new_table.style = 'Table Grid'
|
|
|
|
|
except:
|
|
|
|
|
# Fall back to default grid style
|
|
|
|
|
try:
|
|
|
|
|
new_table.style = 'Table Grid'
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
from docx.enum.table import WD_TABLE_ALIGNMENT
|
2025-07-29 18:01:15 +08:00
|
|
|
|
from docx.enum.table import WD_ALIGN_VERTICAL, WD_ROW_HEIGHT_RULE
|
2025-07-03 17:50:42 +08:00
|
|
|
|
from docx.shared import Pt, Inches, Cm, RGBColor
|
2025-07-29 18:01:15 +08:00
|
|
|
|
|
2025-07-03 17:50:42 +08:00
|
|
|
|
# Copy cell contents
|
|
|
|
|
for i, row in enumerate(source_table.rows):
|
|
|
|
|
for j, cell in enumerate(row.cells):
|
|
|
|
|
for paragraph in cell.paragraphs:
|
|
|
|
|
average_char_width_in_points = 6
|
|
|
|
|
if paragraph.text:
|
|
|
|
|
new_table.cell(i,j).text = paragraph.text
|
|
|
|
|
new_table.cell(i,j).paragraphs[0].runs[0].font.name = "Times New Roman" #设置英文字体
|
|
|
|
|
new_table.cell(i,j).paragraphs[0].runs[0].font.size = Pt(10.5) # 字体大小
|
|
|
|
|
new_table.cell(i,j).paragraphs[0].runs[0]._element.rPr.rFonts.set(qn('w:eastAsia'), '仿宋') #设置中文字体
|
|
|
|
|
new_table.cell(i,j).paragraphs[0].paragraph_format.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
|
|
|
new_table.cell(i,j).vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
2025-07-29 18:01:15 +08:00
|
|
|
|
#new_table.cell(i,j).width = cell.width
|
2025-07-30 17:31:18 +08:00
|
|
|
|
if REPORT_ENUM == 'JF':
|
|
|
|
|
new_table.cell(i,j).paragraphs[0].runs[0]._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') #设置中文字体
|
|
|
|
|
new_table.cell(i,j).paragraphs[0].runs[0].font.size = Pt(12) # 字体大小
|
2025-07-03 17:50:42 +08:00
|
|
|
|
"""
|
|
|
|
|
待添加:如何让表格自适应大小(autofit目前不知为何没有作用)
|
|
|
|
|
"""
|
2025-07-29 18:01:15 +08:00
|
|
|
|
#new_table.rows[i].height = row.height
|
|
|
|
|
#new_table.rows[i].height_rule = WD_ROW_HEIGHT_RULE.EXACTLY
|
|
|
|
|
|
|
|
|
|
if not ifadjustheight:
|
|
|
|
|
new_table.auto_fit = True
|
2025-07-30 17:31:18 +08:00
|
|
|
|
if if_merge:
|
|
|
|
|
try:
|
|
|
|
|
new_table = merge_tables(new_table)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"合并表格失败:{e}")
|
2025-07-29 18:01:15 +08:00
|
|
|
|
return target_doc
|
2025-07-03 17:50:42 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from collections import deque
|
|
|
|
|
|
|
|
|
|
def merge_tables(table):
|
|
|
|
|
"""BFS遍历,将相邻且相同单元格合并
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
table: 表格,docx库的Table类型
|
|
|
|
|
Returns:
|
|
|
|
|
合并后的表格
|
|
|
|
|
"""
|
|
|
|
|
if not table or len(table.rows) == 0:
|
|
|
|
|
return table
|
|
|
|
|
|
|
|
|
|
rows = len(table.rows)
|
|
|
|
|
cols = len(table.columns)
|
|
|
|
|
|
|
|
|
|
# 创建访问标记矩阵
|
|
|
|
|
visited = [[False for _ in range(cols)] for _ in range(rows)]
|
|
|
|
|
|
|
|
|
|
# 定义四个方向的移动:上、右、下、左
|
|
|
|
|
directions = [(0, 0), (0, 1), (1, 0), (0, 0)]
|
|
|
|
|
|
|
|
|
|
for i in range(rows):
|
|
|
|
|
for j in range(cols):
|
|
|
|
|
if not visited[i][j]:
|
|
|
|
|
current_cell = table.cell(i, j)
|
|
|
|
|
current_text = current_cell.text.strip()
|
|
|
|
|
|
|
|
|
|
if not current_text: # 跳过空单元格
|
|
|
|
|
visited[i][j] = True
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# BFS队列
|
|
|
|
|
queue = deque()
|
|
|
|
|
queue.append((i, j))
|
|
|
|
|
visited[i][j] = True
|
|
|
|
|
|
|
|
|
|
# 记录需要合并的单元格
|
|
|
|
|
cells_to_merge = []
|
|
|
|
|
|
|
|
|
|
while queue:
|
|
|
|
|
x, y = queue.popleft()
|
|
|
|
|
cells_to_merge.append((x, y))
|
|
|
|
|
|
|
|
|
|
for dx, dy in directions:
|
|
|
|
|
nx, ny = x + dx, y + dy
|
|
|
|
|
|
|
|
|
|
# 检查边界和访问状态
|
|
|
|
|
if 0 <= nx < rows and 0 <= ny < cols and not visited[nx][ny]:
|
|
|
|
|
neighbor_cell = table.cell(nx, ny)
|
|
|
|
|
neighbor_text = neighbor_cell.text.strip()
|
|
|
|
|
|
|
|
|
|
if neighbor_text == current_text:
|
|
|
|
|
visited[nx][ny] = True
|
|
|
|
|
queue.append((nx, ny))
|
|
|
|
|
|
|
|
|
|
# 如果有需要合并的单元格
|
|
|
|
|
if len(cells_to_merge) > 1:
|
|
|
|
|
# 按行和列排序,确保左上角是第一个单元格
|
|
|
|
|
cells_to_merge.sort()
|
|
|
|
|
min_row, min_col = cells_to_merge[0]
|
|
|
|
|
max_row, max_col = cells_to_merge[-1]
|
|
|
|
|
|
|
|
|
|
# 清空所有待合并单元格(包括换行符)
|
|
|
|
|
for x, y in cells_to_merge[1:]:
|
|
|
|
|
cell = table.cell(x, y)
|
|
|
|
|
# 删除所有段落(彻底清空)
|
|
|
|
|
for paragraph in list(cell.paragraphs):
|
|
|
|
|
p = paragraph._element
|
|
|
|
|
p.getparent().remove(p)
|
|
|
|
|
# 可选:添加一个空段落防止格式问题
|
|
|
|
|
cell.add_paragraph()
|
|
|
|
|
|
|
|
|
|
# 执行合并
|
|
|
|
|
if max_row > min_row or max_col > min_col:
|
|
|
|
|
table.cell(min_row, min_col).merge(table.cell(max_row, max_col))
|
|
|
|
|
|
|
|
|
|
return table
|
|
|
|
|
|
|
|
|
|
def fill_tables(Y_table_list, row, col, Y_Table_num, Y):
|
|
|
|
|
"""根据前端返回json块填写表格list,并实时跟进已填写表格数量
|
|
|
|
|
目前只支持固定的缺陷图的填写
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
Y_table_list (list): 前端返回的json块
|
|
|
|
|
row (int): 表格行数
|
|
|
|
|
col (int): 表格列数
|
|
|
|
|
Y_Table_num: json块中有几个表格
|
|
|
|
|
Xu_Hao: 是第几个json块
|
|
|
|
|
Y: 其他参数
|
|
|
|
|
|
|
|
|
|
Return:
|
|
|
|
|
Y1_TABLES: 三维,表和对应元素
|
|
|
|
|
table_index_to_images: 字典,表索引到图片路径列表的映射
|
|
|
|
|
Xu_Hao:到达第几个表了
|
|
|
|
|
"""
|
|
|
|
|
table_index_to_images = {}
|
|
|
|
|
Y_TABLES = [[["" for _ in range(row)] for _ in range(col)] for _ in range(Y_Table_num)]
|
|
|
|
|
|
|
|
|
|
# 处理前端返回数据
|
|
|
|
|
for l, table_dict in enumerate(Y_table_list):
|
|
|
|
|
if table_dict:
|
|
|
|
|
Y_TABLES[l][1][0] = Y
|
|
|
|
|
Y_TABLES[l][1][1] = table_dict["QueXianLeiXing"]
|
|
|
|
|
Y_TABLES[l][1][2] = table_dict["QueXianWeiZhi"]
|
|
|
|
|
Y_TABLES[l][1][3] = table_dict["QueXianChiCun"]
|
|
|
|
|
Y_TABLES[l][3][0] = table_dict["WeiZongDengJi"]
|
|
|
|
|
Y_TABLES[l][3][1] = table_dict["visibility"]
|
|
|
|
|
Y_TABLES[l][3][2] = table_dict["urgency"]
|
|
|
|
|
Y_TABLES[l][3][3] = table_dict["repair_suggestion"]
|
|
|
|
|
|
|
|
|
|
# 获取图片路径
|
|
|
|
|
image_path = table_dict['Tupian_Dir']
|
|
|
|
|
if image_path:
|
|
|
|
|
# 确保路径是字符串形式
|
|
|
|
|
if isinstance(image_path, list):
|
|
|
|
|
table_index_to_images[l] = image_path.copy()
|
|
|
|
|
else:
|
|
|
|
|
table_index_to_images[l] = [str(image_path)]
|
|
|
|
|
|
|
|
|
|
return Y_TABLES, table_index_to_images
|
|
|
|
|
|
|
|
|
|
|