菜鸟所在的公司很多流程都需要用到excel
,因为之前没有信息化,所以现在很多功能都是要上传excel
识别,然后对识别内容进行编辑!
这部分菜鸟之前写过一篇文章:vue3 导出数据为 excel 文件 + 导入并识别excel文件
上述这篇文章主要是实现的json
数据导出为excel
,有讲编辑功能,当时想的是直接用el-input
实现,并没有实现完整。后面发现了vxe-table
,但没有切实的使用,所以没有更新那篇文章。这次项目中再次需要用到excel识别后编辑的功能,所以特来补充!
vxe UI 介绍
vxe-table
是 vxe UI 里面的 table组件,很实用,其他的组件需要各位读者去开发,因为菜鸟没怎么使用。菜鸟感觉这个基本和 element plus 差不多,只是功能有的更强、有的又偏弱,基本上 element plus 的都可以替代 vxe UI 里面的组件,但菜鸟感觉 vxe UI 的:表单、vxe-grid
各位读者可以好好看看,因为这两个都有通过数据结构生成内容的能力,对喜欢jsx的帮助应该很大;然后其他组件的话,感觉也比较有用,有element plus里面没有的一些功能!
vxe UI 网址:vxeui.com
为什么 vxe-table 实用?
vxe-table
实用不是因为别的,就因为其使用方式和 element plus 的 el-table
一模一样,但是不同的是,其可以对每一行数据进行编辑!
安装
这里菜鸟感觉有个槽点,按需要引入很奇怪(感觉很超出了认知,是你要用什么自己去引入的这种按需引入,而不是element plus那种自动的),主要是使用vxe-table
以及vxe-table
中会用到的vxe-select
、vxe-input
,大部分只需要引入一点就行了!
菜鸟的main.js部分
// 导入vxe-table --> 必须导入VxeTooltip不然有问题,其他随便
// 导入vxe ui的组件 --> 必须导入VxeUI不然没法使用VxeUI.setI18n等方法
// 导入VxeSelect --> 必须导入VxeInput,不然样式不对,因为VxeSelect中使用了VxeInput
import { VxeUI, VxeSelect, VxeTooltip, VxeInput } from "vxe-pc-ui";
import { VxeTable, VxeColumn, VxeColgroup, VxeGrid, VxeToolbar } from "vxe-table";
// 导入主题变量,也可以重写主题变量
import "vxe-table/styles/cssvar.scss";
import "vxe-pc-ui/styles/cssvar.scss";
// 导入默认的语言
import zhCN from "vxe-pc-ui/lib/language/zh-CN";
VxeUI.setI18n("zh-CN", zhCN);
VxeUI.setLanguage("zh-CN");
function lazyVxe(app) {
app.use(VxeSelect);
app.use(VxeInput);
app.use(VxeTooltip);
app.use(VxeTable);
app.use(VxeColumn);
app.use(VxeColgroup);
app.use(VxeGrid);
app.use(VxeToolbar);
}
app.use(lazyVxe);
实际应用
菜鸟的界面
代码
实现代码 -- 父组件
<script setup>
import { upload_or_download_path } from "@/enums/sampleType.js";
import { isEqual, omit } from "lodash-es";
import { sleep } from "@/utils/coroutine.js";
import SampleNucleinCol from "@/components/Lims/SampleCommon/SampleInput/cols/SampleNucleinCol.vue";
import SampleCellCol from "@/components/Lims/SampleCommon/SampleInput/cols/SampleCellCol.vue";
import SampleLibCol from "@/components/Lims/SampleCommon/SampleInput/cols/SampleLibCol.vue";
import SampleSublibraryCol from "@/components/Lims/SampleCommon/SampleInput/cols/SampleSublibraryCol.vue";
import _ from "lodash-es";
// el-dialog 里面使用 vxe-table 需要设置z-index,不然编辑的弹出会被盖住,导致没法编辑
import domZIndex from "dom-zindex";
domZIndex.setCurrent(10000);
const props = defineProps({
sendSampleType: {
type: [String, Number],
default: -1
},
canEdit: {
type: Boolean,
default: true
},
canSelect: {
type: Boolean,
default: true
},
height: {
type: [String, Number],
default: "600"
},
otherRules: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(["selectChangeEvent", "showSublib", "selectAllEvent"]);
const tableRef = ref();
const tableData = defineModel("tableData", { default: [] });
// 设置能否编辑
const editConfig = ref();
if (props.canEdit) {
editConfig.value = {
trigger: "click",
mode: "cell"
};
} else {
editConfig.value = {
trigger: "none",
mode: "cell"
};
}
const curStrategy = computed(() => {
return (
upload_or_download_path.find(
(v) => v.value.toString() === `${toValue(props.sendSampleType)}`
) ?? {}
);
});
// 新增一行
const addRow = async () => {
const newRow = {
isChecked: true
};
tableData.value.push(newRow);
await nextTick();
const $table = tableRef.value;
if ($table) {
if (props.canEdit) {
await $table.setEditRow(newRow, "sampleName");
unref(tableRef).setCheckboxRow(newRow, true);
}
await sleep(0);
$table.scrollToRow(newRow);
}
};
// 校验规则
const useRules = computed(() => {
if (_.isFunction(curStrategy.value.rules)) {
let rules = curStrategy.value.rules(tableData.value);
return {
...rules,
...props.otherRules
};
}
return { ...props.otherRules };
});
// 获取选中的数据
const getSelectedList = async () => {
const selectedList = await unref(tableRef).getCheckboxRecords();
return selectedList.map((v) =>
omit(v, ["_X_ROW_KEY", "isChecked", "createUserId", "createTime", "updateUserId", "updateTime"])
);
};
// 设置选中列表
const addSelectedList = (_sampleList) => {
unref(tableRef).setCheckboxRow(_sampleList, true);
};
// 清空选中
const clearSelectEvent = () => {
const $table = tableRef.value;
if ($table) {
$table.clearCheckboxRow();
}
};
// 清空数据
const resetSelectedList = () => {
tableData.value = [];
};
// 校验方法 -- true 表示全部校验,false 表示校验变动行
const fullValidEvent = async (bool = true) => {
const $table = tableRef.value;
if ($table) {
const errMap = await $table.validate(bool);
return errMap;
}
};
// 删除行
const removeRow = async (row) => {
tableData.value = tableData.value.filter((item) => !isEqual(item, row));
};
// 获取行id
const getRowIndex = (row) => {
const $table = tableRef.value;
if ($table) {
return $table.getRowIndex(row);
}
};
// 子文库
const showSublib = (row) => {
emit("showSublib", row);
};
// 选择事件
const selectChangeEvent = (row) => {
console.log(row);
emit("selectChangeEvent", row.records);
};
const selectAllEvent = (checked) => {
emit("selectAllEvent", checked);
};
// 暴露方法
defineExpose({
getSelectedList,
addSelectedList,
fullValidEvent,
addRow,
clearSelectEvent,
getRowIndex,
resetSelectedList,
removeRow
});
</script>
<template>
<vxe-table
border
ref="tableRef"
show-overflow
:edit-config="editConfig"
:data="tableData"
:height="height"
:row-config="{ isHover: true }"
:checkbox-config="{ checkField: 'isChecked' }"
:edit-rules="useRules"
@checkbox-change="selectChangeEvent"
@checkbox-all="selectAllEvent"
>
<template #empty>
<el-alert type="info" :closable="false">
当前尚未添加{{ curStrategy.label }}样本数据,请点击右上方的下载{{
curStrategy.label
}}模板按钮下载后并填写,填写完成后点击上传{{
curStrategy.label
}}模板按钮上传.或点击右上方的新增一行按钮添加{{ curStrategy.label }}样本数据手动添加
</el-alert>
</template>
<vxe-column v-if="canSelect" fixed="left" type="checkbox" width="40"></vxe-column>
<!-- 注意,因为菜鸟这里面的列可能有很多变化,所以有很多插槽 -->
<slot name="StorageLocationCol"></slot>
<template v-if="curStrategy.label === '组织' || curStrategy.label === '核酸'">
<SampleNucleinCol :can-edit="canEdit">
<!-- 需要先插入,然后再暴露 -->
<template #userCanAddNucleninCol>
<slot name="userCanAddNucleninCol"></slot>
</template>
</SampleNucleinCol>
</template>
<template v-if="curStrategy.label === '单细胞'">
<SampleCellCol :can-edit="canEdit">
<template #userCanAddCellCol>
<slot name="userCanAddCellCol"></slot>
</template>
</SampleCellCol>
</template>
<template v-if="curStrategy.label === '文库'">
<SampleLibCol :can-edit="canEdit">
<template #userCanAddLibCol>
<slot name="userCanAddLibCol"></slot>
</template>
</SampleLibCol>
</template>
<template v-if="curStrategy.label === '子文库'">
<slot name="sublibFristCol"></slot>
<SampleSublibraryCol :can-edit="canEdit">
<template #userCanAddSublibcol>
<slot name="userCanAddSublibcol"></slot>
</template>
</SampleSublibraryCol>
</template>
<slot name="userCanAddSamplelist"></slot>
<vxe-column
field="remark"
title="备注"
min-width="200"
:edit-render="{ name: 'textarea' }"
></vxe-column>
<vxe-column
v-if="canEdit"
field="action"
title="操作"
fixed="right"
:width="curStrategy.label === '文库' ? '160' : '80'"
>
<template #default="{ row }">
<template v-if="curStrategy.label === '文库'">
<el-button type="primary" text @click="showSublib(row)"
>子文库<span class="numbericon">{{
row?.orderSampleSubLibraryDTOList?.length || 0
}}</span></el-button
>
</template>
<el-button type="danger" :icon="Delete" circle plain @click="removeRow(row)" />
</template>
</vxe-column>
</vxe-table>
</template>
<style lang="scss" scoped>
.numbericon {
border: 1px solid var(--el-color-primary);
border-radius: 50%;
display: inline-block;
font-size: 9px;
width: 15px;
height: 15px;
text-align: center;
line-height: 15px;
margin-left: 5px;
margin-top: 2px;
}
</style>
这里的upload_or_download_path
就是个枚举,用到了策略模式,不然每一个都要写,很麻烦:
import * as sampleSend from "@/api/sample/sampleSend";
import * as sampleInput from "@/api/sample/sampleInput";
import * as sampleListByOrderId from "@/api/sample/sampleList";
import { sublibEnumsNum } from "@/components/Lims/SampleCommon/SampleInput/common";
const rulesObject = {
// 组织/核酸
sampleName: [
{
required: true,
message: "请输入样本名称"
}
],
latinSpeciesName: [
{
required: true,
message: "请输入拉丁学名"
}
],
……
};
export const upload_or_download_path = [
{
label: "组织",
value: 1,
height: "600px",
downloadApi: () => sampleInput.orderSampleDownload(1),
uploadApi: sampleInput.orderSampleUpload,
associated: sampleSend.sampleAssociatedOrder,
detail: sampleListByOrderId.orderSampleList,
itemName: "orderSampleDTOList",
serverDataToLocal: (res) => {
let data = res.data.records;
for (let i of data) {
i.infective = i.infective + "";
}
return data;
},
rules: function () {
return rulesObject;
}
},
{
label: "核酸",
value: 2,
height: "600px",
downloadApi: () => sampleInput.orderSampleDownload(2),
uploadApi: sampleInput.orderSampleUpload,
associated: sampleSend.sampleAssociatedOrder,
detail: sampleListByOrderId.orderSampleList,
itemName: "orderSampleDTOList",
serverDataToLocal: (res) => {
let data = res.data.records;
for (let i of data) {
i.infective = i.infective + "";
}
return data;
},
rules: function () {
return rulesObject;
}
},
{
label: "文库",
value: 3,
height: "500px",
downloadApi: sampleInput.orderSampleLibraryDownload,
uploadApi: sampleInput.orderSampleLibraryUpload,
associated: sampleSend.sampleAssociatedOrderSampleLibrary,
detail: sampleListByOrderId.orderSampleListLibrary,
itemName: "orderSampleLibraryDTOList",
serverDataToLocal: (res) => {
let data = res.data.records;
for (let i of data) {
i.baseBalance = i.baseBalance + "";
i.cyclized = i.cyclized + "";
for (let j of i.orderSampleSubLibraryDTOList) {
j.baseBalance = j.baseBalance + "";
j.libraryEndFivePhosphorylation = j.libraryEndFivePhosphorylation + "";
}
}
return data;
},
rules: function (data) {
const sampleName = [];
sampleName.push({
required: true,
message: "请输入文库名称"
});
sampleName.push({
validator(e) {
// console.log(e);
let sampleNameArr = {};
for (let i = 0; i < data.length; i++) {
if (sampleNameArr[data[i]?.sampleName]) {
sampleNameArr[data[i].sampleName]++;
} else {
sampleNameArr[data[i].sampleName] = 1;
}
}
if (sampleNameArr[e.cellValue] > 1) {
return new Error("存在重名的文库名称");
}
}
});
return Object.assign({}, rulesObject, {
sampleName: sampleName,
latinSpeciesName: [
{
required: true,
message: "请输入来源物种"
}
]
});
}
},
{
label: "单细胞",
value: 4,
height: "450px",
downloadApi: sampleInput.orderSampleCellDownload,
uploadApi: sampleInput.orderSampleCellUpload,
associated: sampleSend.sampleAssociatedOrderSampleCell,
detail: sampleListByOrderId.orderSampleListCell,
itemName: "orderSampleCellDTOList",
serverDataToLocal: (res) => {
let data = res.data.records;
for (let i of data) {
i.sampleRunOut = i.sampleRunOut + "";
i.sampleHasBackup = i.sampleHasBackup + "";
}
return data;
},
rules: function () {
return Object.assign({}, rulesObject, {
latinSpeciesName: [
{
required: true,
message: "请输入来源物种"
}
]
});
}
},
{
label: "子文库",
value: sublibEnumsNum,
height: "500px",
detail: sampleListByOrderId.orderSampleListSubLibrary,
serverDataToLocal: (res) => {
let data = res.data;
for (let i of data) {
i.baseBalance = i.baseBalance + "";
i.libraryEndFivePhosphorylation = i.libraryEndFivePhosphorylation + "";
}
return data;
},
rules: function () {
return Object.assign({}, rulesObject, {
sampleName: [
{
required: true,
message: "请输入文库名称"
}
]
});
}
}
];
实现代码 -- 子组件
子组件都是列之类的,这里就只放一个了SampleCellCol.vue
,作为例子
<script setup>
import { dictData, format, activeVal, inactiveVal } from "../common";
import { serverDateFormat } from "@/utils/date.js";
import DictTag from "@/components/DictTag/index.vue";
defineProps({
canEdit: {
type: Boolean,
default: true
}
});
const {
send_sample_order_sample_preservation_status,
send_sample_order_sample_preservation_medium,
send_sample_order_single_cell_platform,
send_sample_order_single_cell_project_type,
send_sample_order_seq_platform,
send_sample_order_single_cell_hand_method
} = dictData;
</script>
<template>
<vxe-column
field="sampleName"
title="样本名称"
width="160"
:edit-render="{ name: 'input' }"
></vxe-column>
<slot name="userCanAddCellCol"></slot>
<vxe-column
field="sampleCount"
title="样品数量"
width="160"
:edit-render="{ name: 'input' }"
></vxe-column>
<vxe-column
field="latinSpeciesName"
title="来源物种"
width="160"
:edit-render="{ name: 'input' }"
></vxe-column>
<vxe-column
field="organOrigin"
title="组织来源(部位)"
width="180"
:edit-render="{ name: 'input' }"
></vxe-column>
<vxe-column field="samplePreservationStatus" title="样本保存状态" width="200" :edit-render="{}">
<template #edit="{ row }">
<vxe-select
v-model="row.samplePreservationStatus"
:options="send_sample_order_sample_preservation_status"
>
<template #option="{ option }">
<dict-tag
:options="send_sample_order_sample_preservation_status"
:value="option.value"
></dict-tag>
</template>
</vxe-select>
</template>
<template #default="{ row }">
<span>{{
format(row, "samplePreservationStatus", "send_sample_order_sample_preservation_status")
}}</span>
</template>
</vxe-column>
<vxe-column field="samplePreservationMedium" title="样本保存介质" width="200" :edit-render="{}">
<template #edit="{ row }">
<vxe-select
v-model="row.samplePreservationMedium"
:options="send_sample_order_sample_preservation_medium"
>
<template #option="{ option }">
<dict-tag
:options="send_sample_order_sample_preservation_medium"
:value="option.value"
></dict-tag>
</template>
</vxe-select>
</template>
<template #default="{ row }">
<span>{{
format(row, "samplePreservationMedium", "send_sample_order_sample_preservation_medium")
}}</span>
</template>
</vxe-column>
<vxe-column field="sampleGatherTime" title="样本采集时间" width="240" :edit-render="{}">
<template #edit="{ row }">
<el-date-picker
size="small"
v-model="row.sampleGatherTime"
type="datetime"
:value-format="serverDateFormat"
clearable
placeholder="样本采集时间"
/>
</template>
<template #default="{ row }">
<span>{{ row.sampleGatherTime }}</span>
</template>
</vxe-column>
<vxe-column field="sampleHasBackup" title="样本是否有备份" width="160" :edit-render="{}">
<template #edit="{ row }">
<el-switch
v-model="row.sampleHasBackup"
:active-value="activeVal"
:inactive-value="inactiveVal"
></el-switch>
</template>
<template #default="{ row }">
<el-switch
v-model="row.sampleHasBackup"
:disabled="!canEdit"
:active-value="activeVal"
:inactive-value="inactiveVal"
></el-switch>
</template>
</vxe-column>
<vxe-column field="sampleRunOut" title="样本是否可用完" width="160" :edit-render="{}">
<template #edit="{ row }">
<el-switch
v-model="row.sampleRunOut"
:active-value="activeVal"
:inactive-value="inactiveVal"
></el-switch>
</template>
<template #default="{ row }">
<el-switch
v-model="row.sampleRunOut"
:disabled="!canEdit"
:active-value="activeVal"
:inactive-value="inactiveVal"
></el-switch>
</template>
</vxe-column>
<vxe-column field="processWay" title="处理方式" width="160" :edit-render="{}">
<template #edit="{ row }">
<vxe-select v-model="row.processWay" :options="send_sample_order_single_cell_hand_method">
<template #option="{ option }">
<dict-tag
:options="send_sample_order_single_cell_hand_method"
:value="option.value"
></dict-tag>
</template>
</vxe-select>
</template>
<template #default="{ row }">
<span>{{ format(row, "processWay", "send_sample_order_single_cell_hand_method") }}</span>
</template>
</vxe-column>
<vxe-column
field="expectCellCount"
title="预期细胞(核)数量"
width="180"
:edit-render="{ name: 'input' }"
></vxe-column>
<vxe-column field="singleCellPlatform" title="单细胞平台" width="160" :edit-render="{}">
<template #edit="{ row }">
<vxe-select
v-model="row.singleCellPlatform"
:options="send_sample_order_single_cell_platform"
>
<template #option="{ option }">
<dict-tag
:options="send_sample_order_single_cell_platform"
:value="option.value"
></dict-tag>
</template>
</vxe-select>
</template>
<template #default="{ row }">
<span>{{ format(row, "singleCellPlatform", "send_sample_order_single_cell_platform") }}</span>
</template>
</vxe-column>
<vxe-column field="singleCellProjectType" title="单细胞项目类型" width="200" :edit-render="{}">
<template #edit="{ row }">
<vxe-select
v-model="row.singleCellProjectType"
:options="send_sample_order_single_cell_project_type"
>
<template #option="{ option }">
<dict-tag
:options="send_sample_order_single_cell_project_type"
:value="option.value"
></dict-tag>
</template>
</vxe-select>
</template>
<template #default="{ row }">
<span>{{
format(row, "singleCellProjectType", "send_sample_order_single_cell_project_type")
}}</span>
</template>
</vxe-column>
<vxe-column field="seqPlatform" title="测序平台" width="180" :edit-render="{}">
<template #edit="{ row }">
<vxe-select v-model="row.seqPlatform" :options="send_sample_order_seq_platform">
<template #option="{ option }">
<dict-tag :options="send_sample_order_seq_platform" :value="option.value"></dict-tag>
</template>
</vxe-select>
</template>
<template #default="{ row }">
<span>{{ format(row, "seqPlatform", "send_sample_order_seq_platform") }}</span>
</template>
</vxe-column>
<vxe-column
field="seqReadsNum"
title="测序reads数"
width="160"
:edit-render="{ name: 'input' }"
></vxe-column>
</template>
这里菜鸟感觉不是很难,就按照element plus的去搞就行,主要是需要找到对应的文档,具体文档在这里:
- 可编辑 - 插槽式:vxeui.com/#/component…
- 可编辑 - 配置式:vxeui.com/#/component…
- 校验数据:vxeui.com/#/component…
- 设置选择项:vxeui.com/#/component…
复杂表单
如果是复杂表单,后端识别出来,只要给过来的数据结构没啥问题,感觉都可以使用vxe-table
实现编辑!
然后如果是简单excel(没有合并的列),甚至都不用后端识别,菜鸟这里有个办法,见文章:工作+学习 常用的一些东西