feat : update info
This commit is contained in:
@@ -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, "次数", ¢er_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, "单价", ¢er_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, "费用合计", ¢er_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, ¢er_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, ¢er_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, ¢er_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, ¢er_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, ¢er_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, ¢er_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, ¢er_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(), ¢er_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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="stylesheet" href="styles.css" />
|
<link rel="stylesheet" href="styles.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Attendance Counter</title>
|
<title>生成考勤表汇总数据</title>
|
||||||
<script type="module" src="/main.js" defer></script>
|
<script type="module" src="/main.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Attendance Counter</h1>
|
<h1>生成考勤表汇总数据</h1>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p>Select the attendance Excel file to process.</p>
|
<p>选择考勤表文件</p>
|
||||||
<button id="select-file-btn" type="button">Select Input File (.xlsx)</button>
|
<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>
|
<p id="status-msg"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
39
src/main.js
39
src/main.js
@@ -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;
|
||||||
|
selectedFileMsg.textContent = "已选择: " + selected;
|
||||||
|
processBtn.disabled = false;
|
||||||
|
statusMsg.textContent = "";
|
||||||
|
statusMsg.className = "";
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
statusMsg.textContent = "选择文件出错: " + err;
|
||||||
|
statusMsg.className = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processFile() {
|
||||||
|
if (!currentFilePath) return;
|
||||||
|
|
||||||
|
statusMsg.textContent = "正在处理...";
|
||||||
statusMsg.className = "processing";
|
statusMsg.className = "processing";
|
||||||
|
processBtn.disabled = true;
|
||||||
|
selectFileBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await invoke("process_attendance", { inputPath: selected });
|
const result = await invoke("process_attendance", { inputPath: currentFilePath });
|
||||||
statusMsg.textContent = result;
|
statusMsg.textContent = result;
|
||||||
statusMsg.className = "success";
|
statusMsg.className = "success";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
statusMsg.textContent = "Error: " + error;
|
statusMsg.textContent = "错误: " + error;
|
||||||
statusMsg.className = "error";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
statusMsg.textContent = "Error selecting file: " + err;
|
|
||||||
statusMsg.className = "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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
BIN
test_output.xlsx
BIN
test_output.xlsx
Binary file not shown.
Reference in New Issue
Block a user