2026-04-25 00:01:40 +08:00
|
|
|
|
use clap::{Parser, Subcommand};
|
|
|
|
|
|
use std::path::PathBuf;
|
2026-04-25 23:51:05 +08:00
|
|
|
|
use umya_spreadsheet::*;
|
|
|
|
|
|
use std::fs;
|
|
|
|
|
|
use std::io::Write;
|
2026-04-25 23:58:23 +08:00
|
|
|
|
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<NaiveDateTime> {
|
|
|
|
|
|
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))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- 错误处理 (原有) ---
|
2026-04-25 23:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
#[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<T> = std::result::Result<T, DexcelError>;
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
2026-04-25 23:58:23 +08:00
|
|
|
|
// --- CLI 定义 (原有,未改动) ---
|
|
|
|
|
|
|
2026-04-25 00:01:40 +08:00
|
|
|
|
#[derive(Parser)]
|
|
|
|
|
|
#[command(name = "dexcel")]
|
2026-04-25 23:51:05 +08:00
|
|
|
|
#[command(about = "Excel 读写工具 (样式保留版)", long_about = None)]
|
2026-04-25 00:01:40 +08:00
|
|
|
|
struct Cli {
|
|
|
|
|
|
/// Excel 文件路径
|
2026-04-25 23:51:05 +08:00
|
|
|
|
#[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<PathBuf>,
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
|
command: Commands,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Subcommand)]
|
|
|
|
|
|
enum Commands {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 查询数据 (只读)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Query {
|
|
|
|
|
|
#[arg(default_value_t = 0, allow_hyphen_values = true)]
|
|
|
|
|
|
count: i32,
|
|
|
|
|
|
#[arg(long, allow_hyphen_values = true)]
|
|
|
|
|
|
start: Option<i32>,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
last: bool,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
sheet: Option<String>,
|
|
|
|
|
|
},
|
|
|
|
|
|
/// 统计行数
|
|
|
|
|
|
Count {
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
sheet: Option<String>,
|
|
|
|
|
|
},
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 创建新文件
|
|
|
|
|
|
New {
|
|
|
|
|
|
name: String,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
sheet: Option<String>,
|
|
|
|
|
|
},
|
|
|
|
|
|
/// 插入行 (保留样式)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Insert {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 要插入的值 (用 | 分隔)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
value: String,
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 插入位置 (空则追加)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
#[arg(long, allow_hyphen_values = true)]
|
|
|
|
|
|
row: Option<i32>,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
sheet: Option<String>,
|
|
|
|
|
|
#[arg(long, default_value = "|")]
|
|
|
|
|
|
split: String,
|
|
|
|
|
|
},
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 更新数据 (保留样式)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Update {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 新值
|
2026-04-25 00:01:40 +08:00
|
|
|
|
value: String,
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 行号
|
2026-04-25 00:01:40 +08:00
|
|
|
|
#[arg(long, allow_hyphen_values = true)]
|
|
|
|
|
|
row: i32,
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 起始列号 (默认1)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
cell: Option<u32>,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
sheet: Option<String>,
|
|
|
|
|
|
#[arg(long, default_value = "|")]
|
|
|
|
|
|
split: String,
|
|
|
|
|
|
},
|
2026-04-25 23:51:05 +08:00
|
|
|
|
/// 删除行
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Delete {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
#[arg(num_args = 1..=2, allow_hyphen_values = true, value_delimiter = ' ')]
|
|
|
|
|
|
range: Vec<i32>,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
sheet: Option<String>,
|
|
|
|
|
|
},
|
|
|
|
|
|
/// 设置样式
|
|
|
|
|
|
Style {
|
2026-04-25 00:01:40 +08:00
|
|
|
|
#[arg(long, allow_hyphen_values = true)]
|
2026-04-25 23:51:05 +08:00
|
|
|
|
rows: Vec<i32>,
|
2026-04-25 00:01:40 +08:00
|
|
|
|
#[arg(long)]
|
2026-04-25 23:51:05 +08:00
|
|
|
|
cols: Vec<u32>,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
row_height: Option<f64>,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
col_width: Option<f64>,
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
wrap_text: bool,
|
2026-04-25 00:01:40 +08:00
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
sheet: Option<String>,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:58:23 +08:00
|
|
|
|
// --- 核心工具函数 (原有 + 修改 get_cell_value) ---
|
|
|
|
|
|
|
|
|
|
|
|
// 安全保存辅助
|
2026-04-25 23:51:05 +08:00
|
|
|
|
fn safe_save_umya(book: &mut Spreadsheet, target: &PathBuf) -> Result<()> {
|
2026-04-26 00:01:16 +08:00
|
|
|
|
let backup = if target.exists() {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
let backup = target.with_extension("xlsx.bak");
|
|
|
|
|
|
let _ = fs::remove_file(&backup);
|
|
|
|
|
|
fs::copy(target, &backup)?;
|
2026-04-26 00:01:16 +08:00
|
|
|
|
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()))
|
|
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn get_sheet_mut<'a>(book: &'a mut Spreadsheet, name: &Option<String>) -> 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<String>) -> 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<usize> {
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:58:23 +08:00
|
|
|
|
// --- 修改版:获取单元格值 (支持日期格式化显示) ---
|
|
|
|
|
|
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 入口 (原有,未改动) ---
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
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)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
Commands::Count { sheet } => {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
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)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
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);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
writer::xlsx::write(&book, &path).map_err(|e| DexcelError::Umya(e.to_string()))?;
|
|
|
|
|
|
println!("✓ 已创建: {}", path.display());
|
|
|
|
|
|
Ok(())
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
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)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
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)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
Commands::Delete { range, 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, range, sheet)?;
|
|
|
|
|
|
safe_save_umya(&mut book, file)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
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)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
if let Err(e) = result {
|
|
|
|
|
|
eprintln!("❌ 错误: {}", e);
|
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:58:23 +08:00
|
|
|
|
// --- 具体实现 (修改了 Insert 和 Update 的写入逻辑) ---
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
fn query_excel(book: &Spreadsheet, count: i32, start: Option<i32>, last: bool, sheet_name: &Option<String>) -> Result<()> {
|
|
|
|
|
|
let sheet = get_sheet(book, sheet_name)?;
|
2026-04-25 23:58:23 +08:00
|
|
|
|
let (_, max_row) = sheet.get_highest_column_and_row(); // 注意:umya 返回 (col, row)
|
|
|
|
|
|
let total_rows = max_row as usize;
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
|
|
|
|
|
if total_rows == 0 {
|
|
|
|
|
|
println!("表格为空");
|
|
|
|
|
|
return Ok(());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:58:23 +08:00
|
|
|
|
// 计算范围逻辑
|
2026-04-25 00:01:40 +08:00
|
|
|
|
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 {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
(actual_start, (actual_start + (-count) as usize).min(total_rows))
|
2026-04-25 00:01:40 +08:00
|
|
|
|
} else {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
let max = 20;
|
|
|
|
|
|
(actual_start, (actual_start + max).min(total_rows))
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else if count > 0 {
|
|
|
|
|
|
(0, count as usize)
|
|
|
|
|
|
} else if count < 0 {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
let abs = (-count) as usize;
|
|
|
|
|
|
let start = if abs >= total_rows { 0 } else { total_rows - abs };
|
2026-04-25 00:01:40 +08:00
|
|
|
|
(start, total_rows)
|
|
|
|
|
|
} else {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
let max = 20;
|
|
|
|
|
|
((total_rows - max).max(0), total_rows)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
let (max_col, _) = sheet.get_highest_column_and_row();
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
|
|
|
|
|
use tabled::builder::Builder;
|
|
|
|
|
|
let mut builder = Builder::default();
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
for r in start_row..end_row.min(total_rows) {
|
|
|
|
|
|
let row: Vec<String> = (0..max_col as usize)
|
|
|
|
|
|
.map(|c| get_cell_value(sheet, r, c))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
builder.push_record(row);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
|
2026-04-25 00:01:40 +08:00
|
|
|
|
let mut table = builder.build();
|
|
|
|
|
|
table.with(tabled::settings::Style::modern());
|
|
|
|
|
|
println!("{}", table);
|
2026-04-25 23:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn count_excel(book: &Spreadsheet, sheet_name: &Option<String>) -> Result<()> {
|
|
|
|
|
|
let sheet = get_sheet(book, sheet_name)?;
|
|
|
|
|
|
let (_, row) = sheet.get_highest_column_and_row();
|
|
|
|
|
|
println!("{}", row);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:58:23 +08:00
|
|
|
|
// --- 修改版:插入行 (支持日期写入) ---
|
2026-04-25 00:01:40 +08:00
|
|
|
|
fn insert_excel(
|
2026-04-25 23:51:05 +08:00
|
|
|
|
book: &mut Spreadsheet,
|
2026-04-25 00:01:40 +08:00
|
|
|
|
value: &str,
|
|
|
|
|
|
row: Option<i32>,
|
|
|
|
|
|
sheet_name: &Option<String>,
|
|
|
|
|
|
split: &str,
|
2026-04-25 23:51:05 +08:00
|
|
|
|
) -> 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 {
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Some(r) => {
|
|
|
|
|
|
if r < 0 {
|
2026-04-25 23:58:23 +08:00
|
|
|
|
(max_row as i32 + r + 2).max(1) as u32
|
2026-04-25 00:01:40 +08:00
|
|
|
|
} else {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
r.max(1) as u32
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
None => max_row + 1,
|
2026-04-25 00:01:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
if insert_idx_1based <= max_row {
|
|
|
|
|
|
sheet.insert_new_row(&insert_idx_1based, &1);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:58:23 +08:00
|
|
|
|
// 写入数据 (逻辑增强)
|
2026-04-25 23:51:05 +08:00
|
|
|
|
let vals: Vec<&str> = value.split(split).collect();
|
|
|
|
|
|
for (i, val) in vals.iter().enumerate() {
|
|
|
|
|
|
let col = (i + 1) as u32;
|
2026-04-25 23:58:23 +08:00
|
|
|
|
let cell = sheet.get_cell_mut((col, insert_idx_1based));
|
|
|
|
|
|
let val_trimmed = val.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if !val_trimmed.is_empty() {
|
|
|
|
|
|
// 新增逻辑:尝试解析日期
|
|
|
|
|
|
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 {
|
|
|
|
|
|
cell.set_value(val_trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
println!("✓ 已在第 {} 行插入数据 (样式已保留)", insert_idx_1based);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:58:23 +08:00
|
|
|
|
// --- 修改版:更新行 (支持日期写入) ---
|
2026-04-25 00:01:40 +08:00
|
|
|
|
fn update_excel(
|
2026-04-25 23:51:05 +08:00
|
|
|
|
book: &mut Spreadsheet,
|
2026-04-25 00:01:40 +08:00
|
|
|
|
value: &str,
|
|
|
|
|
|
row: i32,
|
|
|
|
|
|
cell: Option<u32>,
|
|
|
|
|
|
sheet_name: &Option<String>,
|
|
|
|
|
|
split: &str,
|
2026-04-25 23:51:05 +08:00
|
|
|
|
) -> 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);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
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));
|
2026-04-25 23:58:23 +08:00
|
|
|
|
let val_trimmed = val.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if !val_trimmed.is_empty() {
|
|
|
|
|
|
// 新增逻辑:尝试解析日期
|
|
|
|
|
|
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 {
|
|
|
|
|
|
cell_obj.set_value(val_trimmed);
|
|
|
|
|
|
}
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
}
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
println!("✓ 已更新第 {} 行 (样式未改变)", row);
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
fn delete_excel(
|
|
|
|
|
|
book: &mut Spreadsheet,
|
|
|
|
|
|
range: &[i32],
|
|
|
|
|
|
sheet_name: &Option<String>,
|
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
|
let sheet = get_sheet_mut(book, sheet_name)?;
|
|
|
|
|
|
let (_, max_row) = sheet.get_highest_column_and_row();
|
|
|
|
|
|
|
|
|
|
|
|
let (start_del, end_del) = match range {
|
|
|
|
|
|
[n] if n > &0 => (1, *n as u32),
|
|
|
|
|
|
[n] if n < &0 => {
|
|
|
|
|
|
let count = (-n) as u32;
|
|
|
|
|
|
(max_row - count + 1, max_row)
|
|
|
|
|
|
},
|
|
|
|
|
|
[a, b] => {
|
|
|
|
|
|
if a <= &0 || b < a { return Err(DexcelError::Operation("范围无效".into())); }
|
|
|
|
|
|
(*a as u32, *b as u32)
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
2026-04-25 23:51:05 +08:00
|
|
|
|
_ => return Err(DexcelError::Operation("参数错误".into())),
|
2026-04-25 00:01:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
eprintln!("⚠ 即将删除第 {} 至 {} 行", start_del, end_del);
|
|
|
|
|
|
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(());
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
let num_to_del = end_del - start_del + 1;
|
|
|
|
|
|
sheet.remove_row(&start_del, &num_to_del);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
println!("✓ 删除完成");
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
fn style_excel(
|
|
|
|
|
|
book: &mut Spreadsheet,
|
|
|
|
|
|
rows: &[i32],
|
|
|
|
|
|
cols: &[u32],
|
|
|
|
|
|
row_height: Option<f64>,
|
|
|
|
|
|
col_width: Option<f64>,
|
|
|
|
|
|
wrap_text: bool,
|
2026-04-25 00:01:40 +08:00
|
|
|
|
sheet_name: &Option<String>,
|
2026-04-25 23:51:05 +08:00
|
|
|
|
) -> 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<u32> = 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()
|
2026-04-25 00:01:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
let target_cols: Vec<u32> = if cols.is_empty() {
|
|
|
|
|
|
(1..=max_col).collect()
|
2026-04-25 00:01:40 +08:00
|
|
|
|
} else {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
cols.to_vec()
|
2026-04-25 00:01:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
for &r in &target_rows {
|
|
|
|
|
|
if let Some(h) = row_height {
|
|
|
|
|
|
let row = sheet.get_row_dimension_mut(&r);
|
|
|
|
|
|
row.set_height(h);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
if wrap_text {
|
2026-04-25 23:58:23 +08:00
|
|
|
|
use umya_spreadsheet::structs::HorizontalAlignmentValues;
|
|
|
|
|
|
for r in &target_rows {
|
2026-04-25 23:51:05 +08:00
|
|
|
|
for c in &target_cols {
|
2026-04-25 23:58:23 +08:00
|
|
|
|
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);
|
2026-04-25 00:01:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 23:51:05 +08:00
|
|
|
|
println!("✓ 样式应用成功");
|
2026-04-25 00:01:40 +08:00
|
|
|
|
Ok(())
|
2026-04-25 23:51:05 +08:00
|
|
|
|
}
|