feat : update info

This commit is contained in:
2025-12-03 09:53:10 +08:00
parent e09f1f38e6
commit b088d9c8ee
6 changed files with 178 additions and 68 deletions

View File

@@ -7,31 +7,67 @@ pub fn write_report<P: AsRef<Path>>(path: P, reports: Vec<ClassReport>) -> Resul
let mut workbook = Workbook::new(); let mut workbook = Workbook::new();
let worksheet = workbook.add_worksheet(); let worksheet = workbook.add_worksheet();
// Define Colors
let header_bg_color = rust_xlsxwriter::Color::RGB(0x4472C4); // Blue for main headers
let subheader_bg_color = rust_xlsxwriter::Color::RGB(0x4874CB); // Slightly different blue for subheaders
let text_color = rust_xlsxwriter::Color::White; // White text on blue background
let border_color = rust_xlsxwriter::Color::Black;
// Formats // Formats
let header_format = Format::new().set_bold().set_align(rust_xlsxwriter::FormatAlign::Center);
let center_format = Format::new().set_align(rust_xlsxwriter::FormatAlign::Center); // 1. Main Header Format (Class Name, Row 1 Headers)
// Songti, 16pt, Bold, Blue Fill, White Text, Center, Thin Borders
let main_header_format = Format::new()
.set_font_name("宋体")
.set_font_size(16)
.set_bold()
.set_background_color(header_bg_color)
.set_font_color(text_color)
.set_align(rust_xlsxwriter::FormatAlign::Center)
.set_align(rust_xlsxwriter::FormatAlign::VerticalCenter)
.set_border(rust_xlsxwriter::FormatBorder::Thin)
.set_border_color(border_color);
// 2. Sub Header Format (Row 2 Headers: 次数, 单价, 费用合计)
// Songti, 11pt, Bold, Blue Fill, White Text, Center, Thin Borders
let sub_header_format = Format::new()
.set_font_name("宋体")
.set_font_size(11)
.set_bold()
.set_background_color(subheader_bg_color)
.set_font_color(text_color)
.set_align(rust_xlsxwriter::FormatAlign::Center)
.set_align(rust_xlsxwriter::FormatAlign::VerticalCenter)
.set_border(rust_xlsxwriter::FormatBorder::Thin)
.set_border_color(border_color);
// 3. Data Cell Format
// Songti, 10pt, Center, Thin Borders
let data_format = Format::new()
.set_font_name("宋体")
.set_font_size(10)
.set_align(rust_xlsxwriter::FormatAlign::Center)
.set_align(rust_xlsxwriter::FormatAlign::VerticalCenter)
.set_border(rust_xlsxwriter::FormatBorder::Thin)
.set_border_color(border_color);
let mut current_row = 0; let mut current_row = 0;
for report in reports { for report in reports {
// Write Class Name // Write Class Name (Merged across columns A-E?)
worksheet.write_string(current_row, 0, &report.class_name)?; // Actually, looking at res.xlsx, "思维B" is in A147.
// Let's merge A to E for the class name to look nice, or just put in A.
// The user sample shows it in A.
worksheet.write_string_with_format(current_row, 0, &report.class_name, &main_header_format)?;
// Apply empty format to B-E to show borders/bg if we want, but let's stick to simple first.
current_row += 1; current_row += 1;
// Write Headers // Write Headers
let headers = ["序号", "入学年份", "年级", "班级", "姓名"]; let headers = ["序号", "入学年份", "年级", "班级", "姓名"];
for (i, header) in headers.iter().enumerate() { for (i, header) in headers.iter().enumerate() {
worksheet.write_string_with_format(current_row, i as u16, *header, &header_format)?; worksheet.write_string_with_format(current_row, i as u16, *header, &main_header_format)?;
} }
// Determine all unique months across all students in this class to create columns
// Actually, the requirement implies we list months for each student?
// Looking at res.xlsx:
// Row 148: ..., 9月陈南岚老师, None, None, 10月陈南岚老师, ...
// Row 149: ..., 次数, 单价, 费用合计, 次数, 单价, 费用合计
// It seems the columns are dynamic based on months.
// We need to find all months present in this class and create columns for them.
// Collect all months // Collect all months
let mut all_months = std::collections::BTreeSet::new(); let mut all_months = std::collections::BTreeSet::new();
for student in &report.students { for student in &report.students {
@@ -46,9 +82,8 @@ pub fn write_report<P: AsRef<Path>>(path: P, reports: Vec<ClassReport>) -> Resul
for month in &all_months { for month in &all_months {
// Write Month Header (merged 3 cells) // Write Month Header (merged 3 cells)
// We don't know the teacher name, so just use "X月"
let month_str = format!("{}", month); let month_str = format!("{}", month);
worksheet.merge_range(current_row, col_idx, current_row, col_idx + 2, &month_str, &header_format)?; worksheet.merge_range(current_row, col_idx, current_row, col_idx + 2, &month_str, &main_header_format)?;
month_col_map.insert(*month, col_idx); month_col_map.insert(*month, col_idx);
col_idx += 3; col_idx += 3;
@@ -57,16 +92,17 @@ pub fn write_report<P: AsRef<Path>>(path: P, reports: Vec<ClassReport>) -> Resul
current_row += 1; current_row += 1;
// Write Sub-headers (次数, 单价, 费用合计) // Write Sub-headers (次数, 单价, 费用合计)
for _ in &all_months { // Columns A-E are empty in this row in the sample?
// We need to know the column index for this month // Sample Row 149: [None, None, None, None, None, '次数', ...]
// But we are iterating linearly. // So we leave A-E empty or apply a default format?
} // Let's apply data format to keep borders consistent if needed, or just leave blank.
// Actually, let's iterate columns again // Sample shows blank.
let mut sub_col_idx = 5; let mut sub_col_idx = 5;
for _ in &all_months { for _ in &all_months {
worksheet.write_string_with_format(current_row, sub_col_idx, "次数", &center_format)?; worksheet.write_string_with_format(current_row, sub_col_idx, "次数", &sub_header_format)?;
worksheet.write_string_with_format(current_row, sub_col_idx + 1, "单价", &center_format)?; worksheet.write_string_with_format(current_row, sub_col_idx + 1, "单价", &sub_header_format)?;
worksheet.write_string_with_format(current_row, sub_col_idx + 2, "费用合计", &center_format)?; worksheet.write_string_with_format(current_row, sub_col_idx + 2, "费用合计", &sub_header_format)?;
sub_col_idx += 3; sub_col_idx += 3;
} }
@@ -74,36 +110,67 @@ pub fn write_report<P: AsRef<Path>>(path: P, reports: Vec<ClassReport>) -> Resul
// Write Student Data // Write Student Data
for (i, student) in report.students.iter().enumerate() { for (i, student) in report.students.iter().enumerate() {
worksheet.write_number_with_format(current_row, 0, (i + 1) as f64, &center_format)?; worksheet.write_number_with_format(current_row, 0, (i + 1) as f64, &data_format)?;
if let Some(year) = &student.student.year { if let Some(year) = &student.student.year {
worksheet.write_string_with_format(current_row, 1, year, &center_format)?; worksheet.write_string_with_format(current_row, 1, year, &data_format)?;
} else {
worksheet.write_blank(current_row, 1, &data_format)?;
} }
if let Some(grade) = &student.student.grade { if let Some(grade) = &student.student.grade {
worksheet.write_string_with_format(current_row, 2, grade, &center_format)?; worksheet.write_string_with_format(current_row, 2, grade, &data_format)?;
} else {
worksheet.write_blank(current_row, 2, &data_format)?;
} }
if let Some(class_info) = &student.student.class { if let Some(class_info) = &student.student.class {
worksheet.write_string_with_format(current_row, 3, class_info, &center_format)?; worksheet.write_string_with_format(current_row, 3, class_info, &data_format)?;
} else {
worksheet.write_blank(current_row, 3, &data_format)?;
} }
worksheet.write_string_with_format(current_row, 4, &student.student.name, &center_format)?;
worksheet.write_string_with_format(current_row, 4, &student.student.name, &data_format)?;
for record in &student.monthly_records { for record in &student.monthly_records {
if let Some(&start_col) = month_col_map.get(&record.month) { if let Some(&start_col) = month_col_map.get(&record.month) {
worksheet.write_number_with_format(current_row, start_col, record.count as f64, &center_format)?; worksheet.write_number_with_format(current_row, start_col, record.count as f64, &data_format)?;
worksheet.write_number_with_format(current_row, start_col + 1, record.price, &center_format)?; worksheet.write_number_with_format(current_row, start_col + 1, record.price, &data_format)?;
// Formula: =PRODUCT(次数:单价)
// Column indices are 0-based.
// rust_xlsxwriter uses (row, col)
let count_cell = rust_xlsxwriter::utility::row_col_to_cell(current_row, start_col); let count_cell = rust_xlsxwriter::utility::row_col_to_cell(current_row, start_col);
let price_cell = rust_xlsxwriter::utility::row_col_to_cell(current_row, start_col + 1); let price_cell = rust_xlsxwriter::utility::row_col_to_cell(current_row, start_col + 1);
let formula = format!("=PRODUCT({}:{})", count_cell, price_cell); let formula = format!("=PRODUCT({}:{})", count_cell, price_cell);
worksheet.write_formula_with_format(current_row, start_col + 2, formula.as_str(), &center_format)?; worksheet.write_formula_with_format(current_row, start_col + 2, formula.as_str(), &data_format)?;
} }
} }
current_row += 1; current_row += 1;
} }
// Add Total Row
// Sample Row 162: [None, ..., =SUM(...)]
// We need to sum the columns.
// Let's add a "Total" row.
// Columns F, H, I, K, etc. (Count and Total Cost)
// We need to iterate through all month columns again
for month in &all_months {
if let Some(&start_col) = month_col_map.get(month) {
// Sum Count (start_col)
let start_cell = rust_xlsxwriter::utility::row_col_to_cell(current_row - report.students.len() as u32, start_col);
let end_cell = rust_xlsxwriter::utility::row_col_to_cell(current_row - 1, start_col);
let formula = format!("=SUM({}:{})", start_cell, end_cell);
worksheet.write_formula_with_format(current_row, start_col, formula.as_str(), &data_format)?;
// Sum Total Cost (start_col + 2)
let start_cell = rust_xlsxwriter::utility::row_col_to_cell(current_row - report.students.len() as u32, start_col + 2);
let end_cell = rust_xlsxwriter::utility::row_col_to_cell(current_row - 1, start_col + 2);
let formula = format!("=SUM({}:{})", start_cell, end_cell);
worksheet.write_formula_with_format(current_row, start_col + 2, formula.as_str(), &data_format)?;
}
}
current_row += 1;
current_row += 2; // Spacing between classes current_row += 2; // Spacing between classes
} }

View File

@@ -41,7 +41,7 @@ async fn process_attendance(input_path: String) -> Result<String, String> {
excel_writer::write_report(&output_path, reports) excel_writer::write_report(&output_path, reports)
.map_err(|e| format!("Failed to write Excel: {}", e))?; .map_err(|e| format!("Failed to write Excel: {}", e))?;
Ok(format!("Successfully saved to {:?}", output_path)) Ok(format!("保存成功,文件保存到 {:?}", output_path))
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,21 +1,26 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Attendance Counter</title>
<script type="module" src="/main.js" defer></script>
</head>
<body>
<div class="container">
<h1>Attendance Counter</h1>
<div class="card"> <head>
<p>Select the attendance Excel file to process.</p> <meta charset="UTF-8" />
<button id="select-file-btn" type="button">Select Input File (.xlsx)</button> <link rel="stylesheet" href="styles.css" />
<p id="status-msg"></p> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</div> <title>生成考勤表汇总数据</title>
<script type="module" src="/main.js" defer></script>
</head>
<body>
<div class="container">
<h1>生成考勤表汇总数据</h1>
<div class="card">
<p>选择考勤表文件</p>
<button id="select-file-btn" type="button">选择文件 (.xlsx)</button>
<p id="selected-file" class="file-path"></p>
<button id="process-btn" type="button" disabled>开始生成</button>
<p id="status-msg"></p>
</div> </div>
</body> </div>
</body>
</html> </html>

View File

@@ -2,7 +2,10 @@ const { invoke } = window.__TAURI__.core;
const { open } = window.__TAURI__.dialog; const { open } = window.__TAURI__.dialog;
let selectFileBtn; let selectFileBtn;
let processBtn;
let statusMsg; let statusMsg;
let selectedFileMsg;
let currentFilePath = null;
async function selectFile() { async function selectFile() {
try { try {
@@ -15,29 +18,47 @@ async function selectFile() {
}); });
if (selected) { if (selected) {
statusMsg.textContent = "Processing..."; currentFilePath = selected;
statusMsg.className = "processing"; selectedFileMsg.textContent = "已选择: " + selected;
processBtn.disabled = false;
try { statusMsg.textContent = "";
const result = await invoke("process_attendance", { inputPath: selected }); statusMsg.className = "";
statusMsg.textContent = result;
statusMsg.className = "success";
} catch (error) {
console.error(error);
statusMsg.textContent = "Error: " + error;
statusMsg.className = "error";
}
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
statusMsg.textContent = "Error selecting file: " + err; statusMsg.textContent = "选择文件出错: " + err;
statusMsg.className = "error"; statusMsg.className = "error";
} }
} }
async function processFile() {
if (!currentFilePath) return;
statusMsg.textContent = "正在处理...";
statusMsg.className = "processing";
processBtn.disabled = true;
selectFileBtn.disabled = true;
try {
const result = await invoke("process_attendance", { inputPath: currentFilePath });
statusMsg.textContent = result;
statusMsg.className = "success";
} catch (error) {
console.error(error);
statusMsg.textContent = "错误: " + error;
statusMsg.className = "error";
} finally {
processBtn.disabled = false;
selectFileBtn.disabled = false;
}
}
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
selectFileBtn = document.querySelector("#select-file-btn"); selectFileBtn = document.querySelector("#select-file-btn");
processBtn = document.querySelector("#process-btn");
statusMsg = document.querySelector("#status-msg"); statusMsg = document.querySelector("#status-msg");
selectedFileMsg = document.querySelector("#selected-file");
selectFileBtn.addEventListener("click", selectFile); selectFileBtn.addEventListener("click", selectFile);
processBtn.addEventListener("click", processFile);
}); });

View File

@@ -53,6 +53,23 @@ button:focus-visible {
outline: 4px auto -webkit-focus-ring-color; outline: 4px auto -webkit-focus-ring-color;
} }
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
#process-btn {
margin-top: 10px;
background-color: #4472C4; /* Match Excel header color */
}
.file-path {
font-size: 0.9em;
color: #666;
margin: 10px 0;
word-break: break-all;
}
#status-msg { #status-msg {
margin-top: 20px; margin-top: 20px;
font-weight: bold; font-weight: bold;

Binary file not shown.