完善 README 文档,添加功能特性、使用说明及开发脚本;在转换逻辑中支持嵌套对象构建

This commit is contained in:
lirui
2026-02-09 20:13:40 +08:00
parent 36f5d247b1
commit c7f97f77c7
3 changed files with 143 additions and 32 deletions

117
README.md
View File

@@ -1,42 +1,107 @@
# sv # excel2json
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 一个基于 **SvelteKit + TypeScript** 的 Excel 转 JSON 可视化工具。
支持上传 Excel/CSV、列级映射配置、实时 JSON 预览、模板导入导出与一键复制/下载。
## Creating a project ## 功能特性
If you're seeing this, you've probably already done this step. Congrats! - 支持文件格式:`.xlsx``.xls``.csv`
- 左侧表格预览 Excel 数据,右侧实时预览转换后的 JSON
- 每列可配置:
- 目标字段名(`target`
- 数据类型(`string` / `number` / `boolean` / `date`
- 日期格式(如 `YYYY-MM-DD``YYYY-MM-DD HH:mm:ss``timestamp`
- 空值策略(空值时移除字段 / 使用默认值)
- 是否启用该列输出
- 支持嵌套字段:`user.address.city` 会生成嵌套对象,而不是带点字符串键名
- 支持映射模板导入/导出JSON
- 支持导出 JSON 文件与复制 JSON 到剪贴板
- 包含单元测试(`vitest`
```sh ## 技术栈
# create a new project
npx sv create my-app - `Svelte 5` + `SvelteKit 2`
- `TypeScript`
- `Tailwind CSS 4`
- `xlsx`SheetJS
- `dayjs`
- `Vitest`
## 快速开始
### 1) 安装依赖
```bash
npm install
``` ```
To recreate this project with the same configuration: ### 2) 启动开发环境
```sh ```bash
# recreate this project
npx sv create --template minimal --types ts --add vitest="usages:unit,component" tailwindcss="plugins:typography,forms" devtools-json mcp="ide:claude-code,vscode,other,gemini,cursor,opencode+setup:remote" --install yarn .
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
``` ```
## Building 默认地址通常为:`http://localhost:5173`
To create a production version of your app: ### 3) 构建与预览
```sh ```bash
npm run build npm run build
npm run preview
``` ```
You can preview the production build with `npm run preview`. ## 使用说明
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 1. 上传或拖拽 Excel/CSV 文件
2. 在左侧点击列头“设置”调整映射规则
3. 右侧实时查看 JSON 结果
4. 可导出当前映射为模板,供后续复用
5. 使用“下载 JSON”或“复制 JSON”输出结果
## 映射模板格式
模板是一个 JSON 数组,每一项对应一列配置。示例:
```json
[
{
"source": "姓名",
"target": "user.name",
"type": "string",
"excludeIfEmpty": false,
"defaultValue": ""
},
{
"source": "入职日期",
"target": "user.joinDate",
"type": "date",
"format": "YYYY-MM-DD",
"excludeIfEmpty": true
}
]
```
## 开发脚本
- `npm run dev`:启动开发服务器
- `npm run build`:生产构建
- `npm run preview`:预览构建产物
- `npm run check`:类型与 Svelte 检查
- `npm run test`:运行测试(单次)
- `npm run test:unit`:运行 Vitest交互模式
## 项目结构(核心)
- `src/routes/+page.svelte`:主页面与整体交互
- `src/lib/excel.ts`Excel/CSV 读取与解析
- `src/lib/converter.ts`:映射转换核心逻辑
- `src/lib/types.ts`:类型定义(`MappingConfig``RowData` 等)
- `src/lib/components/ExcelTable.svelte`:左侧数据表格
- `src/lib/components/ColumnConfig.svelte`:列映射配置面板
- `src/lib/components/JsonPreview.svelte`:右侧 JSON 预览
## 注意事项
- 日期字段会尽量兼容 Excel 序列日期(数字日期)
-`excludeIfEmpty = true` 时,空值字段不会出现在输出 JSON 中
- 若目标字段包含 `.`,会按路径写入嵌套对象

View File

@@ -100,6 +100,27 @@ describe('convertData', () => {
const result = convertData(rows, mappings); const result = convertData(rows, mappings);
expect(result[0]).toEqual({ full_name: 'Alice' }); expect(result[0]).toEqual({ full_name: 'Alice' });
}); });
it('builds nested objects from dot-separated target keys', () => {
const mappings: MappingConfig[] = [
{ source: 'name', target: 'user.name', type: 'string', excludeIfEmpty: false, defaultValue: '', enabled: true },
{ source: 'age', target: 'user.age', type: 'number', excludeIfEmpty: false, defaultValue: '', enabled: true },
{ source: 'active', target: 'meta.active', type: 'boolean', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({
user: { name: 'Alice', age: 30 },
meta: { active: true }
});
});
it('builds deeply nested objects', () => {
const mappings: MappingConfig[] = [
{ source: 'name', target: 'a.b.c.d', type: 'string', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({ a: { b: { c: { d: 'Alice' } } } });
});
}); });
describe('date conversion', () => { describe('date conversion', () => {

View File

@@ -63,6 +63,31 @@ function formatDate(value: unknown, format?: string): string | number {
return date.format(format || 'YYYY-MM-DD'); return date.format(format || 'YYYY-MM-DD');
} }
/**
* Set a value at a dot-separated path, creating nested objects as needed.
* e.g. setNested(obj, "user.address.city", "Beijing")
* => obj = { user: { address: { city: "Beijing" } } }
*/
function setNested(obj: Record<string, unknown>, path: string, value: unknown): void {
if (!path.includes('.')) {
obj[path] = value;
return;
}
const keys = path.split('.');
let current: Record<string, unknown> = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (current[key] === undefined || current[key] === null || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key] as Record<string, unknown>;
}
current[keys[keys.length - 1]] = value;
}
/** /**
* Convert raw Excel rows to JSON objects based on mapping configs. * Convert raw Excel rows to JSON objects based on mapping configs.
*/ */
@@ -83,13 +108,13 @@ export function convertData(
continue; continue;
} }
if (isEmptyVal) { const finalValue = isEmptyVal
obj[mapping.target] = mapping.defaultValue !== undefined && mapping.defaultValue !== '' ? (mapping.defaultValue !== undefined && mapping.defaultValue !== ''
? convertValue(mapping.defaultValue, mapping.type, mapping.format) ? convertValue(mapping.defaultValue, mapping.type, mapping.format)
: null; : null)
} else { : convertValue(rawValue, mapping.type, mapping.format);
obj[mapping.target] = convertValue(rawValue, mapping.type, mapping.format);
} setNested(obj, mapping.target, finalValue);
} }
return obj; return obj;