Skip to content

Commit

Permalink
Merge pull request #19 from ywywZhou/import-export
Browse files Browse the repository at this point in the history
feat: 决策表支持导入导出 --story=119248396
  • Loading branch information
luofann authored Sep 12, 2024
2 parents 9b7da08 + caf3c0e commit 756bb03
Show file tree
Hide file tree
Showing 7 changed files with 562 additions and 8 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"vuedraggable": "^2.24.3",
"vuex": "^2.4.0",
"xlsx": "^0.18.5",
"xlsx-js-style": "^1.2.0",
"xss": "^1.0.15"
},
"devDependencies": {
Expand Down
170 changes: 170 additions & 0 deletions frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<template>
<bk-button
text
theme="primary"
:disabled="isDisabled"
@click="handleClick">
{{ '导出' }}
</bk-button>
</template>
<script>
import XLSX from 'xlsx-js-style';
import moment from 'moment-timezone';
import { getCellText } from './dataTransfer.js';
export default {
props: {
name: {
type: String,
default: '',
},
data: {
type: Object,
default: () => ({}),
},
},
data() {
return {
};
},
computed: {
isDisabled() {
return !this.data.records.length;
},
},
methods: {
handleClick() {
const { inputs, outputs, records } = this.data;
// 设置单元格的样式
const cellStyles = {
alignment: {
horizontal: 'center', // 水平居中
vertical: 'center', // 垂直居中
},
border: {
top: { style: 'thin', color: { rgb: '000000' } },
bottom: { style: 'thin', color: { rgb: '000000' } },
left: { style: 'thin', color: { rgb: '000000' } },
right: { style: 'thin', color: { rgb: '000000' } },
},
};
// 定义表头和注释
const headers = [
{
label: 'Input',
children: inputs.map(item => ({ label: `${item.name}(${item.id})`, description: JSON.stringify(item) })),
},
{
label: 'Output',
children: outputs.map(item => ({ label: `${item.name}(${item.id})`, description: JSON.stringify(item) })),
},
];
const data = records.reduce((acc, cur) => {
// 暂时过滤【条件组合】类型!!!
if (cur.inputs.type !== 'common') return acc;
const arr = [];
cur.inputs.conditions.forEach((item) => {
arr.push({ v: getCellText(item), t: 's', s: cellStyles });
});
outputs.forEach((item) => {
arr.push({ v: cur.outputs[item.id], t: 's', s: cellStyles });
});
acc.push(arr);
return acc;
}, []);
const wb = XLSX.utils.book_new();
// 定义工作表数据
const wsData = [];
// 表头
const topHeader = [];
const subHeader = [];
const comments = [];
headers.forEach((header, headerIndex) => {
topHeader.push({
v: header.label,
t: 's',
s: {
...cellStyles,
font: { bold: true, sz: 16 },
fill: { fgColor: { rgb: '9FE3FF' } },
},
});
for (let i = 1; i < header.children.length; i++) {
topHeader.push(null);
}
header.children.forEach((child, childIndex) => {
subHeader.push({
v: child.label,
t: 's',
s: {
...cellStyles,
font: { bold: true },
fill: { fgColor: { rgb: '9FE3FF' } },
},
});
comments.push({
cell: XLSX.utils.encode_cell({ c: headerIndex * header.children.length + childIndex, r: 1 }),
comment: child.description,
});
});
});
wsData.push(topHeader);
wsData.push(subHeader);
// 填充数据
wsData.push(...data);
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 合并单元格设置
let colIndex = 0;
headers.forEach((header) => {
ws['!merges'] = ws['!merges'] || [];
ws['!merges'].push({
s: { r: 0, c: colIndex },
e: { r: 0, c: colIndex + header.children.length - 1 },
});
colIndex += header.children.length;
});
// 添加注释
comments.forEach(({ cell, comment }) => {
if (!ws[cell].c) ws[cell].c = [];
ws[cell].c.hidden = true;
ws[cell].c.push({ t: comment });
});
// 调整列高宽
ws['!cols'] = subHeader.map(() => ({ wch: 40 }));
ws['!rows'] = [
{ hpx: 40 },
{ hpx: 25 },
...data.map(() => ({ hpx: 20 })),
];
// 将工作表添加到工作簿中
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
// 导出工作簿
XLSX.writeFile(wb, `${this.name || 'Decision'}_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
},
},
};
</script>
<style lang="scss" scoped>
.bk-button-text {
font-size: 12px;
color: #63656e;
&.is-disabled {
color: #dcdee5;
}
}
</style>
191 changes: 191 additions & 0 deletions frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<template>
<div>
<label
:for="isDisabled ? '' : 'xls-file'"
:class="['file-label', { 'is-disabled': isDisabled }]">
{{ '导入' }}
</label>
<input
id="xls-file"
ref="fileInput"
type="file"
accept=".xlsx, .xls"
style="display: none;"
@change="handleFile">
</div>
</template>
<script>
import * as XLSX from 'xlsx';
import tools from '@/utils/tools.js';
import { validateFiled, parseValue, validateValue, getValueRight } from './dataTransfer.js';
export default {
props: {
data: {
type: Object,
default: () => ({}),
},
},
computed: {
isDisabled() {
const { inputs, outputs, records } = this.data;
return inputs.length > 0 || outputs.length > 0 || records.length > 0;
},
},
methods: {
handleFile(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = this.processFile;
reader.onerror = () => {
this.showMessage('读取文件失败');
};
reader.readAsArrayBuffer(file);
// 处理完文件后,重置文件输入字段
this.$refs.fileInput.value = '';
},
processFile(e) {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
// 读取第一个工作表
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// 将工作表转换为JSON
let jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
jsonData = jsonData.filter(row => (!row.every(cell => !cell))); // 过滤空行
// 表格至少三层结构 类型、字段、数据
if (jsonData.length < 3) {
this.showMessage('数据结构不对');
return;
}
const sheetValue = Object.values(worksheet);
this.parseSheetData(jsonData, sheetValue);
},
parseSheetData(jsonData, sheetValue) {
const inputs = [];
const outputs = [];
const records = [];
const result = jsonData.some((row, rIndex) => {
// 类型
if (rIndex === 0) return false;
// 字段
if (rIndex === 1) {
const { header, result } = this.getHeader(row, sheetValue);
if (!result) return true;
// 校验header
const message = validateFiled(header);
if (message) {
this.showMessage(message);
return true;
}
header.forEach((item) => {
if (item.from === 'inputs') {
inputs.push(item);
} else {
outputs.push(item);
}
});
return false;
}
// 数据
const header = [...inputs, ...outputs];
const record = this.getRecord(row, header);
if (!record.result) return true;
delete record.result;
records.push(record);
return false;
});
if (result) return;
this.$emit('updateData', { inputs, outputs, records });
},
getHeader(row, sheetValue) {
const header = [];
const result = row.every((cell) => {
const comment = sheetValue.find(value => Object.prototype.toString.call(value) === '[object Object]' && value.v === cell);
if (!comment || !comment.c) {
this.showMessage(`表格【${cell}】列缺少相应的配置注释`);
return false;
}
const { t } = comment.c[0];
if (!tools.checkIsJSON(t)) {
this.showMessage(`表格【${cell}】列的配置注释不是json格式`);
return false;
}
header.push(JSON.parse(t));
return true;
});
return { header, result };
},
getRecord(row, header) {
const inputs = {
conditions: [],
type: 'common',
};
const outputs = {};
const result = header.every((col, colIndex) => {
// 解析value和操作方式
const { value, type } = parseValue(row[colIndex]);
// 校验value
const message = validateValue(value, col);
if (message) {
this.showMessage(message);
return false;
}
// 生成record
if (col.from === 'outputs') {
outputs[col.id] = value;
}
if (col.from === 'inputs') {
inputs.conditions.push({
compare: type,
right: getValueRight(value, type, col),
});
}
return true;
});
return { inputs, outputs, result };
},
showMessage(message, theme = 'error') {
this.$bkMessage({ message, theme });
},
},
};
</script>
<style lang="scss" scoped>
.file-label {
line-height: 24px;
font-size: 12px;
color: #63656e;
cursor: pointer;
&:hover {
color: #3a84ff;
}
&.is-disabled {
color: #dcdee5;
cursor: not-allowed;
}
}
</style>
Loading

0 comments on commit 756bb03

Please sign in to comment.