Files
tools/dexcel/src/main.rs
T

459 lines
14 KiB
Rust
Raw Normal View History

use clap::{Parser, Subcommand};
use std::path::PathBuf;
use umya_spreadsheet::*;
use std::fs;
use std::io::Write;
#[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>;
#[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<PathBuf>,
#[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<i32>,
#[arg(long)]
last: bool,
#[arg(long)]
sheet: Option<String>,
},
/// 统计行数
Count {
#[arg(long)]
sheet: Option<String>,
},
/// 创建新文件
New {
name: String,
#[arg(long)]
sheet: Option<String>,
},
/// 插入行 (保留样式)
Insert {
/// 要插入的值 (用 | 分隔)
value: String,
/// 插入位置 (空则追加)
#[arg(long, allow_hyphen_values = true)]
row: Option<i32>,
#[arg(long)]
sheet: Option<String>,
#[arg(long, default_value = "|")]
split: String,
},
/// 更新数据 (保留样式)
Update {
/// 新值
value: String,
/// 行号
#[arg(long, allow_hyphen_values = true)]
row: i32,
/// 起始列号 (默认1)
#[arg(long)]
cell: Option<u32>,
#[arg(long)]
sheet: Option<String>,
#[arg(long, default_value = "|")]
split: String,
},
/// 删除行
Delete {
#[arg(num_args = 1..=2, allow_hyphen_values = true, value_delimiter = ' ')]
range: Vec<i32>,
#[arg(long)]
sheet: Option<String>,
},
/// 设置样式
Style {
#[arg(long, allow_hyphen_values = true)]
rows: Vec<i32>,
#[arg(long)]
cols: Vec<u32>,
#[arg(long)]
row_height: Option<f64>,
#[arg(long)]
col_width: Option<f64>,
#[arg(long)]
wrap_text: bool,
#[arg(long)]
sheet: Option<String>,
},
}
// 安全保存辅助:利用 umya 直接保存,但为了安全,依然先备份
fn safe_save_umya(book: &mut Spreadsheet, target: &PathBuf) -> Result<()> {
// 1. 备份原文件
if target.exists() {
let backup = target.with_extension("xlsx.bak");
let _ = fs::remove_file(&backup);
fs::copy(target, &backup)?;
}
// 2. 直接写入 (umya 内部处理了 zip 结构,保留所有内容)
writer::xlsx::write(book, target).map_err(|e| DexcelError::Umya(e.to_string()))?;
Ok(())
}
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)
}
}
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 { 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)
}
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(())
}
// --- 具体实现 ---
fn get_cell_value(sheet: &Worksheet, row: usize, col: usize) -> String {
// umya 的坐标是 (col, row) 且从 1 开始
if let Some(c) = sheet.get_cell((col as u32 + 1, row as u32 + 1)) {
c.get_value().to_string()
} else {
String::new()
}
}
fn query_excel(book: &Spreadsheet, count: i32, start: Option<i32>, last: bool, sheet_name: &Option<String>) -> Result<()> {
let sheet = get_sheet(book, sheet_name)?;
let (h, _) = sheet.get_highest_column_and_row();
let total_rows = h 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<String> = (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<String>) -> Result<()> {
let sheet = get_sheet(book, sheet_name)?;
let (_, row) = sheet.get_highest_column_and_row();
println!("{}", row);
Ok(())
}
// 核心:插入行 (umya 支持 insert_row,会自动下移并保留样式)
fn insert_excel(
book: &mut Spreadsheet,
value: &str,
row: Option<i32>,
sheet_name: &Option<String>,
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;
sheet.get_cell_mut((col, insert_idx_1based)).set_value(val.trim());
}
println!("✓ 已在第 {} 行插入数据 (样式已保留)", insert_idx_1based);
Ok(())
}
// 核心:更新数据 (只改值,不动格式)
fn update_excel(
book: &mut Spreadsheet,
value: &str,
row: i32,
cell: Option<u32>,
sheet_name: &Option<String>,
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));
// 仅设置值,不触碰格式属性
if !val.trim().is_empty() {
cell_obj.set_value(val.trim());
}
}
println!("✓ 已更新第 {} 行 (样式未改变)", row);
Ok(())
}
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 _total = max_row as usize;
// 解析范围 (转为 1-based)
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)
}
_ => return Err(DexcelError::Operation("参数错误".into())),
};
// 确认
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(());
}
// 调用 umya 的删除方法 (会自动上移下方内容)
// 注意:umya 似乎没有直接的 range delete,我们循环删除或者从后往前删
// 最稳妥的是从后往前删
let num_to_del = end_del - start_del + 1;
sheet.remove_row(&start_del, &num_to_del);
println!("✓ 删除完成");
Ok(())
}
fn style_excel(
book: &mut Spreadsheet,
rows: &[i32],
cols: &[u32],
row_height: Option<f64>,
col_width: Option<f64>,
wrap_text: bool,
sheet_name: &Option<String>,
) -> 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()
};
let target_cols: Vec<u32> = 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 {
// 需要对每个单元格设置
for r in target_rows {
for c in &target_cols {
let cell = sheet.get_cell_mut((*c, r));
let alignment = cell.get_style().get_alignment();
if let Some(align) = alignment {
let mut new_alignment = align.clone();
new_alignment.set_horizontal(HorizontalAlignmentValues::Left);
new_alignment.set_wrap_text(true);
cell.get_style_mut().set_alignment(new_alignment);
}
}
}
}
println!("✓ 样式应用成功");
Ok(())
}