Vxe-table的导入文档中主要是分别讲解了各种类型文档前端导入的例子和说明。当通过各种文件将数据导入到前端后,我们还得需要将前端的数据持久化到后端,此过程对于少量数据导入比较方便,如果是大量数据,通过导入到前端,然后再更新到后端,就会导致其操作比较繁琐,性能也没有保证;
在实际的开发实践中,我们更倾向于直接文件上传到后端,后端通过必要的检查,然后将数据根据导入模式分别导入到数据库表中。下面我们将实现的一个例子分享给大家,希望能够对您有所帮助!
Vxe-table的导入实现可以参考文档提供的案例:
这个例子中,只是提供了模拟的例子,但是,通过这个例子,我们可以知道,其关键点是设置importConfig
<template>
<div>
<vxe-button @click="importEvent">高级导入</vxe-button>
<vxe-grid ref="gridRef" v-bind="gridOptions"></vxe-grid>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { VxeUI } from 'vxe-table'
const gridRef = ref()
// 模拟接口上传附件导入
const importFileApi = (file) => {
// 模拟后端接口
console.log(file)
return new Promise(resolve => {
setTimeout(() => {
VxeUI.modal.message({
content: '导入2条成功',
status: 'success'
})
resolve([
{ id: Date.now(), name: `Name_Test${Date.now()}`, role: 'Test', sex: 'Women', age: 22, address: 'Guangzhou' },
{ id: Date.now(), name: `Name_Test${Date.now()}`, role: 'PM', sex: 'Man', age: 32, address: 'Shanghai' }
])
}, 1000)
})
}
const gridOptions = reactive({
importConfig: {
remote: true,
async importMethod ({ file }) {
const rest = await importFileApi(file)
// 自定义处理数据
gridOptions.data = rest
}
},
columns: [
{ type: 'seq', width: 70 },
{ field: 'name', title: 'Name' },
{ field: 'sex', title: 'Sex' },
{ field: 'age', title: 'Age' }
],
data: [
{ id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', age: 28, address: 'test abc' },
{ id: 10002, name: 'Test2', role: 'Test', sex: 'Women', age: 22, address: 'Guangzhou' },
{ id: 10003, name: 'Test3', role: 'PM', sex: 'Man', age: 32, address: 'Shanghai' },
{ id: 10004, name: 'Test4', role: 'Designer', sex: 'Women', age: 24, address: 'Shanghai' }
]
})
const importEvent = () => {
const $grid = gridRef.value
if ($grid) {
$grid.openImport()
}
}
</script>
结合我们项目的实际情况,只需要定义如下的importConfig,并开启remote远程导入,models只保留覆盖和底部追加,如果是从顶部追加,涉及问题较多,暂时不考虑。这里我们前端会上传一个excel文件,我们将此文件提交到后端服务,然后由后端服务器处理;
// 导入(服务端)
const importConfig = ref({});
if (toolOptions.import) {
importConfig.value = {
remote: true,
modes: ['covering', 'insertBottom'],
async importMethod({ file, options }) {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('options', JSON.stringify(options));
await API.import(formData);
gridRef.value.commitProxy('query');
} catch (error) {
console.error('error', error);
throw error;
}
},
types: ['xlsx', 'xls'],
};
}
后端我们以Thinkphp为例,我们为此设计了一个服务,完整代码如下:
<?php
namespace app\admin\service\common;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use think\exception\ValidateException;
use think\facade\Db;
class ImportService
{
/**
* @param $tempPath
* @param $menuId
* @param $table
* @param $options
* 'mode'可取值为:'covering, insertBottom, insertTop',
* 因排序规则不是固定的,所以,只允许覆盖和底部追加
* 因顶部追加可能随排序规则而改变,故暂舍弃
*
*/
public function importExcel($tempPath, $menuId, $table, $options, $userInfo): bool
{
try {
// 获取字段映射配置
$fieldConfig = $this->getFieldConfig($menuId);
$titleToField = array_column($fieldConfig, 'field', 'title');
$spreadsheet = IOFactory::load($tempPath);
$sheet = $spreadsheet->getActiveSheet();
$rows = $sheet->toArray();
// 解析标题行
$excelTitles = array_shift($rows);
$columnMap = [];
foreach ($excelTitles as $colIndex => $title) {
if (!isset($titleToField[$title])) {
throw new \Exception("未知字段标题:{$title}");
}
$columnMap[$colIndex] = [
'field' => $titleToField[$title],
'config' => $fieldConfig[array_search($title, array_column($fieldConfig, 'title'))]['table_config']
];
}
// 处理数据行
$validData = [];
foreach ($rows as $rowIndex => $row) {
if (empty(array_filter($row))) continue;
$rowData = [];
foreach ($columnMap as $colIndex => $config) {
$value = $row[$colIndex] ?? null;
// 类型校验与转换
$processedValue = $this->validateField($value, $config['config'], $table);
$rowData[$config['field']] = $processedValue;
}
$rowData['create_time'] = date('Y-m-d H:i:s'); // datetime 格式化
$rowData['create_id'] = $userInfo['id'];
$rowData['create_dept'] = $userInfo['deptId'];
$validData[] = $rowData;
}
// 批量插入(建议用事务)
try {
Db::startTrans();
if ($options['mode'] === 'insertBottom') {
Db::table($table)->insertAll($validData);
}
if ($options['mode'] === 'covering') {
Db::execute("TRUNCATE TABLE $table");
Db::table($table)->insertAll($validData);
}
Db::query("UPDATE $table SET sort = id");
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
throw new ValidateException($e->getMessage());
}
} catch (\Exception $e) {
throw new ValidateException("第{$rowIndex}行错误: " . $e->getMessage());
}
}
private function getFieldConfig($menuId): array
{
return Db::name('dev_field')
->where('menu_id', $menuId)
->where('status', 1)
->whereNull('delete_time')
->field('title,field,config->"$.table_config" as table_config')
->order('sort')
->select()
->map(function ($item) {
$item['table_config'] = json_decode($item['table_config'], true);
return $item;
})->toArray();
}
private function validateField($value, $config, $table)
{
$fieldType = $config['field_type'];
$constraints = $config['field_constraint'] ?? [];
$maxLength = $config['field_length'] ?? null;
// 空值处理
if ($value === null || $value === '') {
if (in_array('NOT NULL', $constraints)) {
throw new \Exception("该字段不允许为空");
}
return $config['field_default'] ?? null;
}
// 类型校验
switch ($fieldType) {
case 'bigint':
case 'int':
if (!is_numeric($value)) {
throw new \Exception("必须为整数值");
}
$value = (int)$value;
break;
case 'varchar':
$value = (string)$value;
if ($maxLength && mb_strlen($value) > $maxLength) {
throw new \Exception("超出最大长度{$maxLength}");
}
break;
case 'datetime':
try {
// 处理Excel日期格式
if (is_numeric($value)) {
$value = Date::excelToDateTimeObject($value)->format('Y-m-d H:i:s');
} else {
new \DateTime($value); // 验证合法性
}
} catch (\Exception $e) {
throw new \Exception("日期格式无效");
}
break;
case 'decimal':
if (!is_numeric($value)) {
throw new \Exception("必须为数字");
}
$value = (float)$value;
break;
// 其他类型扩展...
}
// 约束检查
if (!empty($constraints)) {
if (in_array('UNIQUE', $constraints)) {
$exists = Db::table($table)
->where($config['field'], $value)
->count();
if ($exists) {
throw new \Exception("值必须唯一");
}
}
}
return $value;
}
}
如果您需要科学上网,可以 到这里看看,这是我知道的最佳的软件