use clap::{Parser, Subcommand}; use std::path::PathBuf; use umya_spreadsheet::*; use std::fs; use std::io::Write; use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; // --- 日期时间辅助逻辑 (新增) --- /// 尝试将字符串解析为日期时间,并返回对应的 Excel 格式代码 fn try_parse_datetime(s: &str) -> Option<(NaiveDateTime, &'static str)> { const FORMATS: &[(&str, &str)] = &[ ("%Y-%m-%d %H:%M:%S", "yyyy-mm-dd hh:mm:ss"), ("%Y/%m/%d %H:%M:%S", "yyyy/mm/dd hh:mm:ss"), ("%Y-%m-%d", "yyyy-mm-dd"), ("%Y/%m/%d", "yyyy/mm/dd"), ("%H:%M:%S", "hh:mm:ss"), ("%H:%M", "hh:mm"), ]; for (fmt_str, excel_fmt) in FORMATS { // 1. 尝试解析为完整 DateTime if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt_str) { return Some((dt, excel_fmt)); } // 2. 尝试解析为 Date (默认时间 00:00:00) if let Ok(d) = NaiveDate::parse_from_str(s, fmt_str) { let dt = d.and_hms_opt(0, 0, 0).unwrap(); return Some((dt, excel_fmt)); } // 3. 尝试解析为 Time (默认日期 1899-12-30,Excel 纪元) if let Ok(t) = NaiveTime::parse_from_str(s, fmt_str) { let dt = NaiveDate::from_ymd_opt(1899, 12, 30).unwrap().and_time(t); return Some((dt, excel_fmt)); } } None } /// 将 Chrono 时间转换为 Excel 序列号 (f64) fn datetime_to_excel_serial(dt: &NaiveDateTime) -> f64 { // Excel 的 Windows 纪元是 1899-12-30 (由于历史 Bug) let excel_epoch = NaiveDate::from_ymd_opt(1899, 12, 30) .unwrap() .and_hms_opt(0, 0, 0) .unwrap(); let duration = dt.signed_duration_since(excel_epoch); let days = duration.num_days() as f64; let secs = (duration.num_seconds() % 86400).abs() as f64; days + (secs / 86400.0) } /// 将 Excel 序列号转换回 Chrono 时间 fn excel_serial_to_datetime(serial: f64) -> Option { let excel_epoch = NaiveDate::from_ymd_opt(1899, 12, 30) .unwrap() .and_hms_opt(0, 0, 0) .unwrap(); let days = serial.trunc() as i64; let fraction = serial.fract().abs(); let secs = (fraction * 86400.0).round() as i64; excel_epoch .checked_add_signed(chrono::Duration::days(days))? .checked_add_signed(chrono::Duration::seconds(secs)) } // --- 错误处理 (原有) --- #[derive(Debug, thiserror::Error)] enum DexcelError { #[error("IO错误: {0}")] Io(#[from] std::io::Error), #[error("Excel错误: {0}")] Umya(String), #[error("操作错误: {0}")] Operation(String), } type Result = std::result::Result; // --- CLI 定义 (原有,未改动) --- #[derive(Parser)] #[command(name = "dexcel")] #[command(about = "Excel 读写工具 (样式保留版)", long_about = None)] struct Cli { /// Excel 文件路径 #[arg(required_if_eq("command", "query"))] #[arg(required_if_eq("command", "count"))] #[arg(required_if_eq("command", "insert"))] #[arg(required_if_eq("command", "update"))] #[arg(required_if_eq("command", "delete"))] #[arg(required_if_eq("command", "style"))] file: Option, #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// 查询数据 (只读) Query { #[arg(default_value_t = 0, allow_hyphen_values = true)] count: i32, #[arg(long, allow_hyphen_values = true)] start: Option, #[arg(long)] last: bool, #[arg(long)] sheet: Option, }, /// 统计行数 Count { #[arg(long)] sheet: Option, }, /// 创建新文件 New { name: String, #[arg(long)] sheet: Option, }, /// 插入行 (保留样式) Insert { /// 要插入的值 (用 | 分隔) value: String, /// 插入位置 (空则追加) #[arg(long, allow_hyphen_values = true)] row: Option, #[arg(long)] sheet: Option, #[arg(long, default_value = "|")] split: String, }, /// 更新数据 (保留样式) Update { /// 新值 value: String, /// 行号 #[arg(long, allow_hyphen_values = true)] row: i32, /// 起始列号 (默认1) #[arg(long)] cell: Option, #[arg(long)] sheet: Option, #[arg(long, default_value = "|")] split: String, }, /// 删除行 Delete { /// 起始行号(支持负数) #[arg(allow_hyphen_values = true)] start: i32, /// 删除的行数(默认1) #[arg(long, default_value_t = 1)] count: u32, /// 跳过确认 #[arg(long, short = 'y')] yes: bool, #[arg(long)] sheet: Option, }, /// 设置样式 Style { #[arg(long, allow_hyphen_values = true)] rows: Vec, #[arg(long)] cols: Vec, #[arg(long)] row_height: Option, #[arg(long)] col_width: Option, #[arg(long)] wrap_text: bool, #[arg(long)] sheet: Option, }, } // --- 核心工具函数 (原有 + 修改 get_cell_value) --- // 安全保存辅助 fn safe_save_umya(book: &mut Spreadsheet, target: &PathBuf) -> Result<()> { let backup = if target.exists() { let backup = target.with_extension("xlsx.bak"); let _ = fs::remove_file(&backup); fs::copy(target, &backup)?; Some(backup) } else { None }; // 尝试写入文件 match writer::xlsx::write(book, target) { Ok(_) => { // 写入成功,删除备份文件 if let Some(bak) = backup { let _ = fs::remove_file(bak); } Ok(()) } Err(e) => { // 写入失败,恢复备份 if let Some(bak) = &backup { if bak.exists() { let _ = fs::copy(bak, target); } } Err(DexcelError::Umya(e.to_string())) } } } fn get_sheet_mut<'a>(book: &'a mut Spreadsheet, name: &Option) -> Result<&'a mut Worksheet> { match name { Some(n) => book.get_sheet_by_name_mut(n).ok_or_else(|| DexcelError::Operation(format!("找不到Sheet: {}", n))), None => Ok(book.get_sheet_mut(&0).ok_or_else(|| DexcelError::Operation("找不到Sheet".to_string()))?), } } fn get_sheet<'a>(book: &'a Spreadsheet, name: &Option) -> Result<&'a Worksheet> { match name { Some(n) => book.get_sheet_by_name(n).ok_or_else(|| DexcelError::Operation(format!("找不到Sheet: {}", n))), None => Ok(book.get_sheet(&0).ok_or_else(|| DexcelError::Operation("找不到Sheet".to_string()))?), } } // 解析行号 (1-based -> 0-based) fn resolve_row(input: i32, max: usize) -> Result { let max_i = max as i32; let idx = if input < 0 { max_i + input } else { input - 1 }; if idx < 0 || idx >= max_i { Err(DexcelError::Operation(format!("行号 {} 超出范围", input))) } else { Ok(idx as usize) } } // --- 修改版:获取单元格值 (支持日期格式化显示) --- fn get_cell_value(sheet: &Worksheet, row: usize, col: usize) -> String { // 坐标是 (col, row) 1-based let coord = (col as u32 + 1, row as u32 + 1); if let Some(c) = sheet.get_cell(coord) { let value = c.get_value(); // 尝试判断是否为日期格式的数字 if let Some(serial) = c.get_value_number() { if let Some(num_fmt) = c.get_style().get_number_format() { let fmt_code = num_fmt.get_format_code(); // 简单 heuristic:如果格式码包含日期关键字,尝试转换 let is_date_like = fmt_code.contains("yyyy") || fmt_code.contains("mm") || fmt_code.contains("dd") || fmt_code.contains("hh") || fmt_code.contains("ss"); if is_date_like { if let Some(dt) = excel_serial_to_datetime(serial) { // 根据 Excel 格式的类型返回不同的 Rust 格式 if fmt_code.contains("yyyy") && fmt_code.contains("hh") { return dt.format("%Y-%m-%d %H:%M:%S").to_string(); } else if fmt_code.contains("yyyy") { return dt.format("%Y-%m-%d").to_string(); } else if fmt_code.contains("hh") && fmt_code.contains("ss") { return dt.format("%H:%M:%S").to_string(); } else if fmt_code.contains("hh") { return dt.format("%H:%M").to_string(); } } } } } // 默认返回 value.to_string() } else { String::new() } } // --- Main 入口 (原有,未改动) --- fn main() -> Result<()> { let cli = Cli::parse(); let result = match &cli.command { Commands::Query { count, start, last, sheet } => { let file = cli.file.as_ref().unwrap(); let book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?; query_excel(&book, *count, *start, *last, sheet) } Commands::Count { sheet } => { let file = cli.file.as_ref().unwrap(); let book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?; count_excel(&book, sheet) } Commands::New { name, sheet } => { let path = if name.ends_with(".xlsx") { PathBuf::from(name) } else { PathBuf::from(format!("{}.xlsx", name)) }; let mut book = new_file(); if let Some(s) = sheet { book.get_sheet_mut(&0).unwrap().set_name(s); } writer::xlsx::write(&book, &path).map_err(|e| DexcelError::Umya(e.to_string()))?; println!("✓ 已创建: {}", path.display()); Ok(()) } Commands::Insert { value, row, sheet, split } => { let file = cli.file.as_ref().unwrap(); let mut book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?; insert_excel(&mut book, value, *row, sheet, split)?; safe_save_umya(&mut book, file) } Commands::Update { value, row, cell, sheet, split } => { let file = cli.file.as_ref().unwrap(); let mut book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?; update_excel(&mut book, value, *row, *cell, sheet, split)?; safe_save_umya(&mut book, file) } Commands::Delete { start, count, yes, sheet } => { let file = cli.file.as_ref().unwrap(); let mut book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?; delete_excel(&mut book, *start, *count, *yes, sheet)?; safe_save_umya(&mut book, file) } Commands::Style { rows, cols, row_height, col_width, wrap_text, sheet } => { let file = cli.file.as_ref().unwrap(); let mut book = reader::xlsx::read(file).map_err(|e| DexcelError::Umya(e.to_string()))?; style_excel(&mut book, rows, cols, *row_height, *col_width, *wrap_text, sheet)?; safe_save_umya(&mut book, file) } }; if let Err(e) = result { eprintln!("❌ 错误: {}", e); std::process::exit(1); } Ok(()) } // --- 具体实现 (修改了 Insert 和 Update 的写入逻辑) --- fn query_excel(book: &Spreadsheet, count: i32, start: Option, last: bool, sheet_name: &Option) -> Result<()> { let sheet = get_sheet(book, sheet_name)?; let (_, max_row) = sheet.get_highest_column_and_row(); // 注意:umya 返回 (col, row) let total_rows = max_row as usize; if total_rows == 0 { println!("表格为空"); return Ok(()); } // 计算范围逻辑 let (start_row, end_row) = if last { (total_rows - 1, total_rows) } else if let Some(s) = start { let actual_start = if s < 0 { let pos = total_rows as i32 + s; if pos < 0 { 0 } else { pos as usize } } else { (s as usize).saturating_sub(1) }; if count > 0 { (actual_start, (actual_start + count as usize).min(total_rows)) } else if count < 0 { (actual_start, (actual_start + (-count) as usize).min(total_rows)) } else { let max = 20; (actual_start, (actual_start + max).min(total_rows)) } } else if count > 0 { (0, count as usize) } else if count < 0 { let abs = (-count) as usize; let start = if abs >= total_rows { 0 } else { total_rows - abs }; (start, total_rows) } else { let max = 20; ((total_rows - max).max(0), total_rows) }; let (max_col, _) = sheet.get_highest_column_and_row(); use tabled::builder::Builder; let mut builder = Builder::default(); for r in start_row..end_row.min(total_rows) { let row: Vec = (0..max_col as usize) .map(|c| get_cell_value(sheet, r, c)) .collect(); builder.push_record(row); } let mut table = builder.build(); table.with(tabled::settings::Style::modern()); println!("{}", table); Ok(()) } fn count_excel(book: &Spreadsheet, sheet_name: &Option) -> Result<()> { let sheet = get_sheet(book, sheet_name)?; let (_, row) = sheet.get_highest_column_and_row(); println!("{}", row); Ok(()) } // --- 修改版:插入行 (支持日期写入) --- fn insert_excel( book: &mut Spreadsheet, value: &str, row: Option, sheet_name: &Option, split: &str, ) -> Result<()> { let sheet = get_sheet_mut(book, sheet_name)?; let (_, max_row) = sheet.get_highest_column_and_row(); // 确定插入位置 (umya 是 1-based) let insert_idx_1based = match row { Some(r) => { if r < 0 { (max_row as i32 + r + 2).max(1) as u32 } else { r.max(1) as u32 } } None => max_row + 1, }; if insert_idx_1based <= max_row { sheet.insert_new_row(&insert_idx_1based, &1); } // 写入数据 (逻辑增强) let vals: Vec<&str> = value.split(split).collect(); for (i, val) in vals.iter().enumerate() { let col = (i + 1) as u32; let cell = sheet.get_cell_mut((col, insert_idx_1based)); let val_trimmed = val.trim(); if !val_trimmed.is_empty() { // 1. 尝试解析日期 if let Some((dt, excel_fmt)) = try_parse_datetime(val_trimmed) { let serial = datetime_to_excel_serial(&dt); cell.set_value_number(serial); // 关键:设置单元格格式,这样 Excel 才会把它当日期看 cell.get_style_mut().get_number_format_mut().set_format_code(excel_fmt); } else { // 2. 写入文本 cell.set_value(val_trimmed); } } } println!("✓ 已在第 {} 行插入数据 (样式已保留)", insert_idx_1based); Ok(()) } // --- 修改版:更新行 (支持日期写入) --- fn update_excel( book: &mut Spreadsheet, value: &str, row: i32, cell: Option, sheet_name: &Option, split: &str, ) -> Result<()> { let sheet = get_sheet_mut(book, sheet_name)?; let (_, max_row) = sheet.get_highest_column_and_row(); let target_row = resolve_row(row, max_row as usize)? as u32 + 1; // 转为 1-based let start_col = cell.unwrap_or(1); let vals: Vec<&str> = value.split(split).collect(); for (i, val) in vals.iter().enumerate() { let c = start_col + i as u32; let cell_obj = sheet.get_cell_mut((c, target_row)); let val_trimmed = val.trim(); if !val_trimmed.is_empty() { // 1. 尝试解析日期 if let Some((dt, excel_fmt)) = try_parse_datetime(val_trimmed) { let serial = datetime_to_excel_serial(&dt); cell_obj.set_value_number(serial); cell_obj.get_style_mut().get_number_format_mut().set_format_code(excel_fmt); } else { // 2. 写入文本 cell_obj.set_value(val_trimmed); } } } println!("✓ 已更新第 {} 行 (样式未改变)", row); Ok(()) } fn delete_excel( book: &mut Spreadsheet, start: i32, count: u32, yes: bool, sheet_name: &Option, ) -> Result<()> { let sheet = get_sheet_mut(book, sheet_name)?; let (_, max_row) = sheet.get_highest_column_and_row(); // 解析起始行 (转为 1-based) let start_del = if start < 0 { // 负数:从后往前数 let pos = (max_row as i32 + start + 1).max(1) as u32; pos } else { // 正数:直接使用 start.max(1) as u32 }; let end_del = (start_del + count - 1).min(max_row); // 确认 if !yes { eprintln!("⚠ 即将删除第 {} 至 {} 行(共 {} 行)", start_del, end_del, count); eprint!("确认? (y/N): "); std::io::stdout().flush()?; let mut input = String::new(); std::io::stdin().read_line(&mut input)?; if !input.trim().eq_ignore_ascii_case("y") { println!("操作已取消"); return Ok(()); } } // 调用 umya 的删除方法 sheet.remove_row(&start_del, &count); println!("✓ 已删除第 {} 至 {} 行(共 {} 行)", start_del, end_del, count); Ok(()) } fn style_excel( book: &mut Spreadsheet, rows: &[i32], cols: &[u32], row_height: Option, col_width: Option, wrap_text: bool, sheet_name: &Option, ) -> Result<()> { let sheet = get_sheet_mut(book, sheet_name)?; let (_, max_row) = sheet.get_highest_column_and_row(); let (max_col, _) = sheet.get_highest_column_and_row(); let target_rows: Vec = if rows.is_empty() { (1..=max_row).collect() } else { rows.iter().filter_map(|&r| resolve_row(r, max_row as usize).ok()).map(|x| x as u32 + 1).collect() }; let target_cols: Vec = if cols.is_empty() { (1..=max_col).collect() } else { cols.to_vec() }; for &r in &target_rows { if let Some(h) = row_height { let row = sheet.get_row_dimension_mut(&r); row.set_height(h); } } for &c in &target_cols { if let Some(w) = col_width { let col = sheet.get_column_dimension_by_number_mut(&c); col.set_width(w); } } if wrap_text { use umya_spreadsheet::structs::HorizontalAlignmentValues; for r in &target_rows { for c in &target_cols { let cell = sheet.get_cell_mut((*c, *r)); let mut align = cell.get_style().get_alignment().cloned().unwrap_or_default(); align.set_horizontal(HorizontalAlignmentValues::Left); align.set_wrap_text(true); cell.get_style_mut().set_alignment(align); } } } println!("✓ 样式应用成功"); Ok(()) }