writeForm.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <template>
  2. <div class="app-container p-2">
  3. <el-row :gutter="20">
  4. <el-col :lg="30" :xs="24">
  5. <el-row :span="24" :gutter="10">
  6. <el-col :span="18">
  7. <h2 v-if="detailData.title" style="font-weight: bolder">{{ detailData.title }}</h2>
  8. <p class="report-period">【填报周期】:{{ detailData.start }} 至 {{ detailData.end }}</p>
  9. </el-col>
  10. <el-col :span="1.5">
  11. <el-button type="primary" @click="handleReport">上报</el-button>
  12. </el-col>
  13. <el-col :span="1.5">
  14. <el-button type="primary" @click="exportToExcel">导出表格</el-button>
  15. </el-col>
  16. <el-col :span="1.5">
  17. <el-button type="primary" @click="handleSave">保存</el-button>
  18. </el-col>
  19. <el-col :span="1.5">
  20. <el-button type="danger" @click="handleReturn">返回</el-button>
  21. </el-col>
  22. </el-row>
  23. </el-col>
  24. <el-row :gutter="10" class="mb8">
  25. <el-col :span="1.5">
  26. <!-- <el-button type="primary" @click="handleAdd">导入</el-button>-->
  27. <el-upload class="upload-demo" :http-request="handleAdd" :before-upload="beforeUpload" accept=".xlsx, .xls">
  28. <template #trigger>
  29. <el-button size="big" type="primary">导入</el-button>
  30. </template>
  31. </el-upload>
  32. </el-col>
  33. <el-col :span="1.5">
  34. <el-button type="primary" @click="handleDownloadEmptyTable">下载空表格</el-button>
  35. </el-col>
  36. <el-col :span="1.5">
  37. <el-button type="primary" @click="handleAddRow">新增一行</el-button>
  38. </el-col>
  39. </el-row>
  40. <el-col :lg="30" :xs="24">
  41. <div :style="{ height: tableHeight + 'px' }">
  42. <hot-table ref="wrapper" :data="tableData" :settings="hotSettings" />
  43. </div>
  44. <!-- <div :style="{ height: tableHeight + 'px' }" style="height: 400px">-->
  45. <!-- <hot-table :settings="hotSettings">-->
  46. <!-- <hot-column v-for="item in editableHeaders" :title="item.field_comment">-->
  47. <!-- </hot-column>-->
  48. <!-- </hot-table>-->
  49. <!-- </div>-->
  50. </el-col>
  51. </el-row>
  52. </div>
  53. </template>
  54. <script setup lang="ts">
  55. import * as XLSX from 'xlsx';
  56. import { writeView, submitFill } from '@/api/dataFilling/datafilling';
  57. import { HotTable, HotColumn } from '@handsontable/vue3';
  58. import 'handsontable/languages';
  59. import 'handsontable/dist/handsontable.full.css';
  60. import { registerAllModules } from 'handsontable/registry';
  61. import { ElButton } from 'element-plus';
  62. import { ref } from 'vue';
  63. import { fillingReport } from '@/api/dataFilling/fillingManage';
  64. registerAllModules();
  65. const props = defineProps({
  66. eventId: String
  67. });
  68. const emits = defineEmits(['close']);
  69. let wrapper = ref();
  70. let tableHeight = window.innerHeight - 230
  71. const detailData = ref({
  72. title: '表单数据',
  73. start: '2024-10-15 17:02:22',
  74. end: '2024-10-15 18:00:00'
  75. });
  76. const fileList = ref([]);
  77. const { proxy } = getCurrentInstance();
  78. const editableHeaders = ref<string[]>([]);
  79. const tableData = ref<any[]>([]);
  80. const headerMapping = ref<{ [key: string]: string }>({});
  81. const fields = ref();
  82. // 获取表头数据
  83. const fetchHeaders = async () => {
  84. const response = await writeView({ report_id: props.eventId });
  85. fields.value = response.fields;
  86. const headers = response.fields.map((field) => field.field_comment);
  87. editableHeaders.value = headers;
  88. let arr1 = [];
  89. let arr2 = [];
  90. let arr3 = [];
  91. // tableData.value = response.tableData;
  92. //如果tableData是空就用下满这段,tableData有数据就用tableData的内容
  93. if (!response.tableData || response.tableData.length === 0) {
  94. headers.forEach((item) => {
  95. arr1.push('');
  96. arr2.push('');
  97. arr3.push('');
  98. })
  99. tableData.value = [arr1, arr2 ,arr3];
  100. wrapper.value.hotInstance.loadData([arr1, arr2 ,arr3]);
  101. } else {
  102. tableData.value = response.tableData
  103. wrapper.value.hotInstance.loadData(response.tableData);
  104. }
  105. // 填充映射
  106. // response.fields.forEach(field => {
  107. // headerMapping.value[field.field_comment] = field.field_name;
  108. // });
  109. };
  110. const addDefaultRow = () => {
  111. const newRow = {};
  112. editableHeaders.value.forEach((header) => {
  113. newRow[header] = '';
  114. });
  115. tableData.value.push(newRow);
  116. };
  117. const saveEdit = (rowIndex, header, value) => {
  118. tableData.value[rowIndex][header] = value;
  119. };
  120. const saveHeader = (header, newValue) => {
  121. const index = editableHeaders.value.indexOf(header);
  122. if (index !== -1) {
  123. editableHeaders.value.splice(index, 1, newValue);
  124. }
  125. };
  126. const exportToExcel = () => {
  127. let resultList = [];
  128. resultList.push(editableHeaders.value);
  129. // editableHeaders.value.forEach((header) => {
  130. // resultList.push(header);
  131. // })
  132. tableData.value.forEach((item) => {
  133. resultList.push(item);
  134. })
  135. const worksheet = XLSX.utils.aoa_to_sheet(resultList);
  136. const workbook = { Sheets: { data: worksheet }, SheetNames: ['data'] };
  137. const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
  138. const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  139. const link = document.createElement('a');
  140. const url = window.URL.createObjectURL(blob);
  141. link.href = url;
  142. link.download = 'SheetJS.xlsx';
  143. link.click();
  144. window.URL.revokeObjectURL(url); // 清理
  145. };
  146. const handleAdd = ({file}) => {
  147. const reader = new FileReader();
  148. reader.onload = (event) => {
  149. nextTick(() => {
  150. const data = new Uint8Array(event.target.result);
  151. const workbook = XLSX.read(data, { type: 'array' });
  152. const firstSheetName = workbook.SheetNames[0];
  153. const worksheet = workbook.Sheets[firstSheetName];
  154. const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
  155. tableData.value = jsonData.slice(1); // 假设第一行是标题行,我们跳过它
  156. wrapper.value.hotInstance.loadData(tableData.value);
  157. });
  158. };
  159. reader.readAsArrayBuffer(file);
  160. fileList.value = [];
  161. return { success: true, file };
  162. };
  163. const beforeUpload = (file) => {
  164. const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.type === 'application/vnd.ms-excel';
  165. if (!isExcel) {
  166. proxy.$modal.msgError('只能上传xlsx/xls文件!');
  167. }
  168. return isExcel;
  169. };
  170. const handleDownloadEmptyTable = () => console.log('下载空表格');
  171. const handleAddRow = () => {
  172. wrapper.value.hotInstance.alter('insert_row_below', wrapper.value.hotInstance.countRows()); // 在末尾插入空行
  173. }
  174. const handleSave = () => {
  175. // localStorage.setItem('tableData', JSON.stringify(tableData.value));
  176. // alert('数据已保存');
  177. };
  178. const handleReturn = () => {
  179. emits('close');
  180. };
  181. const dataTemp = ref()
  182. const formattedData = () => {
  183. dataTemp.value = tableData.value.map(row =>
  184. fields.value.reduce((obj, field, index) => {
  185. const key = field.file_name === 'xmbz' ? 'bz' : field.file_name;
  186. obj[key] = row[index];
  187. return obj;
  188. }, {})
  189. )
  190. };
  191. const handleReport = async () => {
  192. formattedData();
  193. // fillingReport(props.eventId)
  194. // try {
  195. // const mappedData = tableData.value.map((row) => {
  196. // const mappedRow: any = {};
  197. // Object.keys(row).forEach((header) => {
  198. // const fieldName = headerMapping.value[header];
  199. // if (fieldName) {
  200. // mappedRow[fieldName] = row[header];
  201. // }
  202. // });
  203. // return mappedRow;
  204. // });
  205. //
  206. // const data = {
  207. // report_id: props.eventId,
  208. // data: mappedData
  209. // };
  210. //
  211. // const response = await submitFill(data);
  212. // if (response.code === 200) {
  213. // alert('数据上报成功');
  214. // } else {
  215. // alert('数据上报失败: ' + response.msg);
  216. // }
  217. // } catch (error) {
  218. // console.error('上报数据时发生错误:', error);
  219. // alert('数据上报失败,请稍后再试');
  220. // }
  221. };
  222. // 组件挂载时调用
  223. onMounted(() => {
  224. fetchHeaders();
  225. addDefaultRow();
  226. });
  227. const hotSettings = reactive({
  228. data: [['1','2']],
  229. language: 'zh-CN',
  230. colHeaders: editableHeaders,
  231. rowHeaders: true,
  232. autoColumnSize: true,
  233. width: '100%', // auto or 100%
  234. height: '100%', // auto or 100%
  235. licenseKey: 'non-commercial-and-evaluation', // 隐藏版权文字
  236. colWidths: 129, // 默认单元格宽度
  237. rowHeights: 28, // 默认单元格高度
  238. wordWrap: true, // 单元格文字是否换行展示
  239. contextMenu: {
  240. // 自定义右键菜单
  241. items: {
  242. 'row_above': {
  243. name: '向上插一行'
  244. },
  245. 'row_below': {
  246. name: '向下插一行'
  247. },
  248. 'col_left': {
  249. name: '向左插一列'
  250. },
  251. 'col_right': {
  252. name: '向右插一列'
  253. },
  254. 'hsep1': '---------', // 分隔线
  255. 'remove_row': {
  256. name: '删除当前行'
  257. },
  258. 'remove_col': {
  259. name: '删除当前列'
  260. },
  261. 'clear_column': {
  262. name: '清空当前列'
  263. },
  264. 'hsep2': '---------', // 必须和上次的变量名不一样
  265. 'undo': {
  266. name: '撤销'
  267. },
  268. 'cut': {
  269. name: '剪切'
  270. },
  271. 'copy': {
  272. name: '复制'
  273. },
  274. 'alignment': {
  275. name: '对齐'
  276. },
  277. 'hsep3': '---------',
  278. 'commentsAddEdit': {
  279. // 必须开启 comments: true
  280. name: '添加备注'
  281. },
  282. 'commentsRemove': {
  283. // 必须开启 comments: true
  284. name: '删除备注'
  285. },
  286. 'freeze_column': {
  287. // 必须开启 manualColumnFreeze: true
  288. name: '固定列'
  289. },
  290. 'unfreeze_column': {
  291. // 必须开启 manualColumnFreeze: true
  292. name: '取消固定列'
  293. }
  294. }
  295. }
  296. });
  297. </script>
  298. <style scoped>
  299. .app-container {
  300. font-family: Avenir, Helvetica, Arial, sans-serif;
  301. -webkit-font-smoothing: antialiased;
  302. -moz-osx-font-smoothing: grayscale;
  303. color: #2c3e50;
  304. }
  305. .report-period {
  306. margin-top: 10px;
  307. font-size: 14px;
  308. color: #606266;
  309. }
  310. .editable-span {
  311. cursor: pointer;
  312. display: inline-block;
  313. white-space: nowrap;
  314. overflow: hidden;
  315. text-overflow: ellipsis;
  316. }
  317. .editable-span[contenteditable='true'] {
  318. white-space: normal;
  319. outline: none; /* 移除编辑时的焦点边框 */
  320. }
  321. .editable-span[contenteditable='true']:empty::before {
  322. content: attr(data-placeholder); /* 可选:为空时显示占位符 */
  323. color: #999;
  324. }
  325. .editable-header {
  326. cursor: pointer;
  327. display: inline-block;
  328. white-space: nowrap;
  329. overflow: hidden;
  330. text-overflow: ellipsis;
  331. }
  332. .editable-header[contenteditable='true'] {
  333. white-space: normal;
  334. outline: none; /* 移除编辑时的焦点边框 */
  335. }
  336. .editable-header[contenteditable='true']:empty::before {
  337. content: attr(data-placeholder); /* 可选:为空时显示占位符 */
  338. color: #999;
  339. }
  340. </style>