Commit d93730d17a71fa21af7bbb1238908b8550e057cf
Merge branch 'master-jdk17' of https://github.com/YunaiV/ruoyi-vue-pro into master-jdk17
Showing
11 changed files
with
397 additions
and
9 deletions
yudao-module-infra/src/main/resources/codegen/vue/api/api.js.vm
| @@ -73,6 +73,24 @@ export function export${simpleClassName}Excel(params) { | @@ -73,6 +73,24 @@ export function export${simpleClassName}Excel(params) { | ||
| 73 | responseType: 'blob' | 73 | responseType: 'blob' |
| 74 | }) | 74 | }) |
| 75 | } | 75 | } |
| 76 | + | ||
| 77 | +// 下载${table.classComment}导入模板 | ||
| 78 | +export function import${simpleClassName}Template() { | ||
| 79 | + return request({ | ||
| 80 | + url: '${baseURL}/get-import-template', | ||
| 81 | + method: 'get', | ||
| 82 | + responseType: 'blob' | ||
| 83 | + }) | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +// 导入${table.classComment} | ||
| 87 | +export function import${simpleClassName}(data) { | ||
| 88 | + return request({ | ||
| 89 | + url: '${baseURL}/import', | ||
| 90 | + method: 'post', | ||
| 91 | + data | ||
| 92 | + }) | ||
| 93 | +} | ||
| 76 | ## 特殊:主子表专属逻辑 | 94 | ## 特殊:主子表专属逻辑 |
| 77 | #foreach ($subTable in $subTables) | 95 | #foreach ($subTable in $subTables) |
| 78 | #set ($index = $foreach.count - 1) | 96 | #set ($index = $foreach.count - 1) |
| @@ -157,4 +175,4 @@ export function export${simpleClassName}Excel(params) { | @@ -157,4 +175,4 @@ export function export${simpleClassName}Excel(params) { | ||
| 157 | }) | 175 | }) |
| 158 | } | 176 | } |
| 159 | #end | 177 | #end |
| 160 | -#end | ||
| 161 | \ No newline at end of file | 178 | \ No newline at end of file |
| 179 | +#end |
yudao-module-infra/src/main/resources/codegen/vue/views/index.vue.vm
| @@ -50,6 +50,14 @@ | @@ -50,6 +50,14 @@ | ||
| 50 | v-hasPermi="['${permissionPrefix}:create']">新增</el-button> | 50 | v-hasPermi="['${permissionPrefix}:create']">新增</el-button> |
| 51 | </el-col> | 51 | </el-col> |
| 52 | <el-col :span="1.5"> | 52 | <el-col :span="1.5"> |
| 53 | + <el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport" | ||
| 54 | + :loading="importLoading" v-hasPermi="['${permissionPrefix}:import']">导入</el-button> | ||
| 55 | + </el-col> | ||
| 56 | + <el-col :span="1.5"> | ||
| 57 | + <el-button type="info" plain icon="el-icon-document" size="mini" @click="handleImportTemplate" | ||
| 58 | + v-hasPermi="['${permissionPrefix}:import']">导入模板</el-button> | ||
| 59 | + </el-col> | ||
| 60 | + <el-col :span="1.5"> | ||
| 53 | <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading" | 61 | <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading" |
| 54 | v-hasPermi="['${permissionPrefix}:export']">导出</el-button> | 62 | v-hasPermi="['${permissionPrefix}:export']">导出</el-button> |
| 55 | </el-col> | 63 | </el-col> |
| @@ -78,6 +86,7 @@ | @@ -78,6 +86,7 @@ | ||
| 78 | #end | 86 | #end |
| 79 | <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> | 87 | <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> |
| 80 | </el-row> | 88 | </el-row> |
| 89 | + <input ref="importFileRef" type="file" style="display: none" accept=".xls,.xlsx" @change="handleImportFileChange" /> | ||
| 81 | 90 | ||
| 82 | ## 特殊:主子表专属逻辑 | 91 | ## 特殊:主子表专属逻辑 |
| 83 | #if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 ) | 92 | #if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 ) |
| @@ -244,6 +253,8 @@ export default { | @@ -244,6 +253,8 @@ export default { | ||
| 244 | loading: true, | 253 | loading: true, |
| 245 | // 导出遮罩层 | 254 | // 导出遮罩层 |
| 246 | exportLoading: false, | 255 | exportLoading: false, |
| 256 | + // 导入遮罩层 | ||
| 257 | + importLoading: false, | ||
| 247 | // 显示搜索条件 | 258 | // 显示搜索条件 |
| 248 | showSearch: true, | 259 | showSearch: true, |
| 249 | ## 特殊:树表专属逻辑(树不需要分页接口) | 260 | ## 特殊:树表专属逻辑(树不需要分页接口) |
| @@ -322,6 +333,42 @@ export default { | @@ -322,6 +333,42 @@ export default { | ||
| 322 | openForm(id) { | 333 | openForm(id) { |
| 323 | this.#[[$]]#refs["formRef"].open(id); | 334 | this.#[[$]]#refs["formRef"].open(id); |
| 324 | }, | 335 | }, |
| 336 | + /** 导入按钮操作 */ | ||
| 337 | + handleImport() { | ||
| 338 | + this.$refs.importFileRef && this.$refs.importFileRef.click(); | ||
| 339 | + }, | ||
| 340 | + /** 导入模板下载 */ | ||
| 341 | + async handleImportTemplate() { | ||
| 342 | + const data = await ${simpleClassName}Api.import${simpleClassName}Template(); | ||
| 343 | + this.#[[$]]#download.excel(data, '${table.classComment}导入模板.xls'); | ||
| 344 | + }, | ||
| 345 | + /** 导入文件变更 */ | ||
| 346 | + async handleImportFileChange(event) { | ||
| 347 | + const target = event.target; | ||
| 348 | + const file = target.files && target.files[0]; | ||
| 349 | + if (!file) { | ||
| 350 | + return; | ||
| 351 | + } | ||
| 352 | + this.importLoading = true; | ||
| 353 | + try { | ||
| 354 | + const formData = new FormData(); | ||
| 355 | + formData.append('file', file); | ||
| 356 | + const res = await ${simpleClassName}Api.import${simpleClassName}(formData); | ||
| 357 | + const data = res.data || res; | ||
| 358 | + let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';'; | ||
| 359 | + if (data.failureRows) { | ||
| 360 | + Object.keys(data.failureRows).forEach((rowNo) => { | ||
| 361 | + text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'; | ||
| 362 | + }); | ||
| 363 | + } | ||
| 364 | + await this.$alert(text, '${table.classComment}导入结果', { dangerouslyUseHTMLString: true }); | ||
| 365 | + await this.getList(); | ||
| 366 | + } catch { | ||
| 367 | + } finally { | ||
| 368 | + target.value = ''; | ||
| 369 | + this.importLoading = false; | ||
| 370 | + } | ||
| 371 | + }, | ||
| 325 | /** 删除按钮操作 */ | 372 | /** 删除按钮操作 */ |
| 326 | async handleDelete(row) { | 373 | async handleDelete(row) { |
| 327 | const ${primaryColumn.javaField} = row.${primaryColumn.javaField}; | 374 | const ${primaryColumn.javaField} = row.${primaryColumn.javaField}; |
yudao-module-infra/src/main/resources/codegen/vue3_vben/api/api.ts.vm
| @@ -30,3 +30,13 @@ export function delete${simpleClassName}(id: number) { | @@ -30,3 +30,13 @@ export function delete${simpleClassName}(id: number) { | ||
| 30 | export function export${simpleClassName}(params) { | 30 | export function export${simpleClassName}(params) { |
| 31 | return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls') | 31 | return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls') |
| 32 | } | 32 | } |
| 33 | + | ||
| 34 | +// 下载${table.classComment}导入模板 | ||
| 35 | +export function import${simpleClassName}Template() { | ||
| 36 | + return defHttp.download({ url: '${baseURL}/get-import-template' }, '${table.classComment}导入模板.xls') | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +// 导入${table.classComment} | ||
| 40 | +export function import${simpleClassName}(data: FormData) { | ||
| 41 | + return defHttp.post({ url: '${baseURL}/import', data }) | ||
| 42 | +} |
yudao-module-infra/src/main/resources/codegen/vue3_vben/views/index.vue.vm
| 1 | <script lang="ts" setup> | 1 | <script lang="ts" setup> |
| 2 | import ${simpleClassName}Modal from './${simpleClassName}Modal.vue' | 2 | import ${simpleClassName}Modal from './${simpleClassName}Modal.vue' |
| 3 | import { columns, searchFormSchema } from './${classNameVar}.data' | 3 | import { columns, searchFormSchema } from './${classNameVar}.data' |
| 4 | +import { ref } from 'vue' | ||
| 4 | import { useI18n } from '@/hooks/web/useI18n' | 5 | import { useI18n } from '@/hooks/web/useI18n' |
| 5 | import { useMessage } from '@/hooks/web/useMessage' | 6 | import { useMessage } from '@/hooks/web/useMessage' |
| 6 | import { useModal } from '@/components/Modal' | 7 | import { useModal } from '@/components/Modal' |
| 7 | import { IconEnum } from '@/enums/appEnum' | 8 | import { IconEnum } from '@/enums/appEnum' |
| 8 | import { BasicTable, TableAction, useTable } from '@/components/Table' | 9 | import { BasicTable, TableAction, useTable } from '@/components/Table' |
| 9 | -import { delete${simpleClassName}, export${simpleClassName}, get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}' | 10 | +import { |
| 11 | + delete${simpleClassName}, | ||
| 12 | + export${simpleClassName}, | ||
| 13 | + get${simpleClassName}Page, | ||
| 14 | + import${simpleClassName}, | ||
| 15 | + import${simpleClassName}Template, | ||
| 16 | +} from '@/api/${table.moduleName}/${table.businessName}' | ||
| 10 | 17 | ||
| 11 | defineOptions({ name: '${table.className}' }) | 18 | defineOptions({ name: '${table.className}' }) |
| 12 | 19 | ||
| 13 | const { t } = useI18n() | 20 | const { t } = useI18n() |
| 14 | const { createConfirm, createMessage } = useMessage() | 21 | const { createConfirm, createMessage } = useMessage() |
| 15 | const [registerModal, { openModal }] = useModal() | 22 | const [registerModal, { openModal }] = useModal() |
| 23 | +const importFileRef = ref<HTMLInputElement>() | ||
| 24 | +const importLoading = ref(false) | ||
| 16 | 25 | ||
| 17 | const [registerTable, { getForm, reload }] = useTable({ | 26 | const [registerTable, { getForm, reload }] = useTable({ |
| 18 | title: '${table.classComment}列表', | 27 | title: '${table.classComment}列表', |
| @@ -49,6 +58,42 @@ async function handleExport() { | @@ -49,6 +58,42 @@ async function handleExport() { | ||
| 49 | }) | 58 | }) |
| 50 | } | 59 | } |
| 51 | 60 | ||
| 61 | +function handleImport() { | ||
| 62 | + importFileRef.value?.click() | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | +async function handleImportTemplateDownload() { | ||
| 66 | + await import${simpleClassName}Template() | ||
| 67 | + createMessage.success('模板下载已开始') | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +async function handleImportFileChange(event: Event) { | ||
| 71 | + const target = event.target as HTMLInputElement | ||
| 72 | + const file = target.files?.[0] | ||
| 73 | + if (!file) { | ||
| 74 | + return | ||
| 75 | + } | ||
| 76 | + importLoading.value = true | ||
| 77 | + try { | ||
| 78 | + const formData = new FormData() | ||
| 79 | + formData.append('file', file) | ||
| 80 | + const response: any = await import${simpleClassName}(formData) | ||
| 81 | + const data = response?.data ?? response | ||
| 82 | + let text = | ||
| 83 | + '导入成功数量:' + (data?.successCount || 0) + ';导入失败数量:' + (data?.failureCount || 0) + ';' | ||
| 84 | + if (data?.failureRows) { | ||
| 85 | + Object.keys(data.failureRows).forEach((rowNo) => { | ||
| 86 | + text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >' | ||
| 87 | + }) | ||
| 88 | + } | ||
| 89 | + createMessage.success(text) | ||
| 90 | + await reload() | ||
| 91 | + } finally { | ||
| 92 | + target.value = '' | ||
| 93 | + importLoading.value = false | ||
| 94 | + } | ||
| 95 | +} | ||
| 96 | + | ||
| 52 | async function handleDelete(record: Recordable) { | 97 | async function handleDelete(record: Recordable) { |
| 53 | await delete${simpleClassName}(record.id) | 98 | await delete${simpleClassName}(record.id) |
| 54 | createMessage.success(t('common.delSuccessText')) | 99 | createMessage.success(t('common.delSuccessText')) |
| @@ -62,6 +107,12 @@ async function handleDelete(record: Recordable) { | @@ -62,6 +107,12 @@ async function handleDelete(record: Recordable) { | ||
| 62 | <a-button type="primary" v-auth="['${permissionPrefix}:create']" :preIcon="IconEnum.ADD" @click="handleCreate"> | 107 | <a-button type="primary" v-auth="['${permissionPrefix}:create']" :preIcon="IconEnum.ADD" @click="handleCreate"> |
| 63 | {{ t('action.create') }} | 108 | {{ t('action.create') }} |
| 64 | </a-button> | 109 | </a-button> |
| 110 | + <a-button v-auth="['${permissionPrefix}:import']" :loading="importLoading" @click="handleImport"> | ||
| 111 | + 导入 | ||
| 112 | + </a-button> | ||
| 113 | + <a-button v-auth="['${permissionPrefix}:import']" @click="handleImportTemplateDownload"> | ||
| 114 | + 导入模板 | ||
| 115 | + </a-button> | ||
| 65 | <a-button v-auth="['${permissionPrefix}:export']" :preIcon="IconEnum.EXPORT" @click="handleExport"> | 116 | <a-button v-auth="['${permissionPrefix}:export']" :preIcon="IconEnum.EXPORT" @click="handleExport"> |
| 66 | {{ t('action.export') }} | 117 | {{ t('action.export') }} |
| 67 | </a-button> | 118 | </a-button> |
| @@ -87,6 +138,7 @@ async function handleDelete(record: Recordable) { | @@ -87,6 +138,7 @@ async function handleDelete(record: Recordable) { | ||
| 87 | </template> | 138 | </template> |
| 88 | </template> | 139 | </template> |
| 89 | </BasicTable> | 140 | </BasicTable> |
| 141 | + <input ref="importFileRef" type="file" accept=".xls,.xlsx" class="hidden" @change="handleImportFileChange" /> | ||
| 90 | <${simpleClassName}Modal @register="registerModal" @success="reload()" /> | 142 | <${simpleClassName}Modal @register="registerModal" @success="reload()" /> |
| 91 | </div> | 143 | </div> |
| 92 | </template> | 144 | </template> |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/api/api.ts.vm
| @@ -101,6 +101,16 @@ export function export${simpleClassName}(params: any) { | @@ -101,6 +101,16 @@ export function export${simpleClassName}(params: any) { | ||
| 101 | return requestClient.download('${baseURL}/export-excel', { params }); | 101 | return requestClient.download('${baseURL}/export-excel', { params }); |
| 102 | } | 102 | } |
| 103 | 103 | ||
| 104 | +/** 下载${table.classComment}导入模板 */ | ||
| 105 | +export function import${simpleClassName}Template() { | ||
| 106 | + return requestClient.download('${baseURL}/get-import-template'); | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +/** 导入${table.classComment} */ | ||
| 110 | +export function import${simpleClassName}(data: FormData) { | ||
| 111 | + return requestClient.post('${baseURL}/import', data); | ||
| 112 | +} | ||
| 113 | + | ||
| 104 | ## 特殊:主子表专属逻辑 | 114 | ## 特殊:主子表专属逻辑 |
| 105 | #foreach ($subTable in $subTables) | 115 | #foreach ($subTable in $subTables) |
| 106 | #set ($index = $foreach.count - 1) | 116 | #set ($index = $foreach.count - 1) |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm
| @@ -27,9 +27,9 @@ import { getRangePickerDefaultProps } from '#/utils/rangePickerProps'; | @@ -27,9 +27,9 @@ import { getRangePickerDefaultProps } from '#/utils/rangePickerProps'; | ||
| 27 | import { $t } from '#/locales'; | 27 | import { $t } from '#/locales'; |
| 28 | #if (${table.templateType} == 2)## 树表接口 | 28 | #if (${table.templateType} == 2)## 树表接口 |
| 29 | import { handleTree } from '@vben/utils' | 29 | import { handleTree } from '@vben/utils' |
| 30 | -import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; | 30 | +import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName}, import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}'; |
| 31 | #else## 标准表接口 | 31 | #else## 标准表接口 |
| 32 | -import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}'; | 32 | +import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName}, import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}'; |
| 33 | #end | 33 | #end |
| 34 | 34 | ||
| 35 | #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况 | 35 | #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况 |
| @@ -73,6 +73,8 @@ const queryParams = reactive({ | @@ -73,6 +73,8 @@ const queryParams = reactive({ | ||
| 73 | }) | 73 | }) |
| 74 | const queryFormRef = ref() // 搜索的表单 | 74 | const queryFormRef = ref() // 搜索的表单 |
| 75 | const exportLoading = ref(false) // 导出的加载中 | 75 | const exportLoading = ref(false) // 导出的加载中 |
| 76 | +const importLoading = ref(false) // 导入的加载中 | ||
| 77 | +const importFileRef = ref<HTMLInputElement>() | ||
| 76 | 78 | ||
| 77 | /** 查询列表 */ | 79 | /** 查询列表 */ |
| 78 | async function getList() { | 80 | async function getList() { |
| @@ -190,6 +192,44 @@ try { | @@ -190,6 +192,44 @@ try { | ||
| 190 | } | 192 | } |
| 191 | } | 193 | } |
| 192 | 194 | ||
| 195 | +/** 导入模板下载 */ | ||
| 196 | +async function handleImportTemplateDownload() { | ||
| 197 | + const data = await import${simpleClassName}Template(); | ||
| 198 | + downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data }); | ||
| 199 | +} | ||
| 200 | + | ||
| 201 | +/** 导入按钮操作 */ | ||
| 202 | +function handleImport() { | ||
| 203 | + importFileRef.value?.click(); | ||
| 204 | +} | ||
| 205 | + | ||
| 206 | +/** 导入文件变更 */ | ||
| 207 | +async function handleImportFileChange(event: Event) { | ||
| 208 | + const target = event.target as HTMLInputElement; | ||
| 209 | + const file = target.files?.[0]; | ||
| 210 | + if (!file) { | ||
| 211 | + return; | ||
| 212 | + } | ||
| 213 | + importLoading.value = true; | ||
| 214 | + try { | ||
| 215 | + const formData = new FormData(); | ||
| 216 | + formData.append('file', file); | ||
| 217 | + const response: any = await import${simpleClassName}(formData); | ||
| 218 | + const data = response?.data ?? response ?? {}; | ||
| 219 | + let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';'; | ||
| 220 | + if (data.failureRows) { | ||
| 221 | + Object.keys(data.failureRows).forEach((rowNo) => { | ||
| 222 | + text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'; | ||
| 223 | + }); | ||
| 224 | + } | ||
| 225 | + message.info(text); | ||
| 226 | + await getList(); | ||
| 227 | + } finally { | ||
| 228 | + target.value = ''; | ||
| 229 | + importLoading.value = false; | ||
| 230 | + } | ||
| 231 | +} | ||
| 232 | + | ||
| 193 | #if (${table.templateType} == 2) | 233 | #if (${table.templateType} == 2) |
| 194 | /** 切换树形展开/收缩状态 */ | 234 | /** 切换树形展开/收缩状态 */ |
| 195 | const isExpanded = ref(true); | 235 | const isExpanded = ref(true); |
| @@ -209,6 +249,7 @@ onMounted(() => { | @@ -209,6 +249,7 @@ onMounted(() => { | ||
| 209 | <template> | 249 | <template> |
| 210 | <Page auto-content-height> | 250 | <Page auto-content-height> |
| 211 | <FormModal @success="getList" /> | 251 | <FormModal @success="getList" /> |
| 252 | + <input ref="importFileRef" type="file" accept=".xls,.xlsx" class="hidden" @change="handleImportFileChange" /> | ||
| 212 | 253 | ||
| 213 | <Card v-if="!hiddenSearchBar" class="mb-4"> | 254 | <Card v-if="!hiddenSearchBar" class="mb-4"> |
| 214 | <!-- 搜索工作栏 --> | 255 | <!-- 搜索工作栏 --> |
| @@ -315,6 +356,23 @@ onMounted(() => { | @@ -315,6 +356,23 @@ onMounted(() => { | ||
| 315 | {{ $t('ui.actionTitle.create', ['${table.classComment}']) }} | 356 | {{ $t('ui.actionTitle.create', ['${table.classComment}']) }} |
| 316 | </Button> | 357 | </Button> |
| 317 | <Button | 358 | <Button |
| 359 | + class="ml-2" | ||
| 360 | + type="primary" | ||
| 361 | + :loading="importLoading" | ||
| 362 | + @click="handleImport" | ||
| 363 | + v-access:code="['${permissionPrefix}:import']" | ||
| 364 | + > | ||
| 365 | + 导入 | ||
| 366 | + </Button> | ||
| 367 | + <Button | ||
| 368 | + class="ml-2" | ||
| 369 | + type="primary" | ||
| 370 | + @click="handleImportTemplateDownload" | ||
| 371 | + v-access:code="['${permissionPrefix}:import']" | ||
| 372 | + > | ||
| 373 | + 导入模板 | ||
| 374 | + </Button> | ||
| 375 | + <Button | ||
| 318 | :icon="h(Download)" | 376 | :icon="h(Download)" |
| 319 | type="primary" | 377 | type="primary" |
| 320 | class="ml-2" | 378 | class="ml-2" |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm
| @@ -16,6 +16,8 @@ import { | @@ -16,6 +16,8 @@ import { | ||
| 16 | delete${simpleClassName}, | 16 | delete${simpleClassName}, |
| 17 | export${simpleClassName}, | 17 | export${simpleClassName}, |
| 18 | get${simpleClassName}List, | 18 | get${simpleClassName}List, |
| 19 | + import${simpleClassName}, | ||
| 20 | + import${simpleClassName}Template, | ||
| 19 | } from '#/api/${table.moduleName}/${table.businessName}'; | 21 | } from '#/api/${table.moduleName}/${table.businessName}'; |
| 20 | #else## 标准表接口 | 22 | #else## 标准表接口 |
| 21 | import { | 23 | import { |
| @@ -24,6 +26,8 @@ import { | @@ -24,6 +26,8 @@ import { | ||
| 24 | delete${simpleClassName}List,#end | 26 | delete${simpleClassName}List,#end |
| 25 | export${simpleClassName}, | 27 | export${simpleClassName}, |
| 26 | get${simpleClassName}Page, | 28 | get${simpleClassName}Page, |
| 29 | + import${simpleClassName}, | ||
| 30 | + import${simpleClassName}Template, | ||
| 27 | } from '#/api/${table.moduleName}/${table.businessName}'; | 31 | } from '#/api/${table.moduleName}/${table.businessName}'; |
| 28 | #end | 32 | #end |
| 29 | import { $t } from '#/locales'; | 33 | import { $t } from '#/locales'; |
| @@ -50,6 +54,8 @@ const [FormModal, formModalApi] = useVbenModal({ | @@ -50,6 +54,8 @@ const [FormModal, formModalApi] = useVbenModal({ | ||
| 50 | connectedComponent: Form, | 54 | connectedComponent: Form, |
| 51 | destroyOnClose: true, | 55 | destroyOnClose: true, |
| 52 | }); | 56 | }); |
| 57 | +const importLoading = ref(false); | ||
| 58 | +const importFileRef = ref<HTMLInputElement>(); | ||
| 53 | #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩 | 59 | #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩 |
| 54 | 60 | ||
| 55 | /** 切换树形展开/收缩状态 */ | 61 | /** 切换树形展开/收缩状态 */ |
| @@ -135,6 +141,44 @@ async function handleExport() { | @@ -135,6 +141,44 @@ async function handleExport() { | ||
| 135 | downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data }); | 141 | downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data }); |
| 136 | } | 142 | } |
| 137 | 143 | ||
| 144 | +/** 导入模板下载 */ | ||
| 145 | +async function handleImportTemplateDownload() { | ||
| 146 | + const data = await import${simpleClassName}Template(); | ||
| 147 | + downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data }); | ||
| 148 | +} | ||
| 149 | + | ||
| 150 | +/** 导入按钮操作 */ | ||
| 151 | +function handleImport() { | ||
| 152 | + importFileRef.value?.click(); | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +/** 导入文件变更 */ | ||
| 156 | +async function handleImportFileChange(event: Event) { | ||
| 157 | + const target = event.target as HTMLInputElement; | ||
| 158 | + const file = target.files?.[0]; | ||
| 159 | + if (!file) { | ||
| 160 | + return; | ||
| 161 | + } | ||
| 162 | + importLoading.value = true; | ||
| 163 | + try { | ||
| 164 | + const formData = new FormData(); | ||
| 165 | + formData.append('file', file); | ||
| 166 | + const response: any = await import${simpleClassName}(formData); | ||
| 167 | + const data = response?.data ?? response ?? {}; | ||
| 168 | + let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';'; | ||
| 169 | + if (data.failureRows) { | ||
| 170 | + Object.keys(data.failureRows).forEach((rowNo) => { | ||
| 171 | + text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'; | ||
| 172 | + }); | ||
| 173 | + } | ||
| 174 | + message.info(text); | ||
| 175 | + handleRefresh(); | ||
| 176 | + } finally { | ||
| 177 | + target.value = ''; | ||
| 178 | + importLoading.value = false; | ||
| 179 | + } | ||
| 180 | +} | ||
| 181 | + | ||
| 138 | const [Grid, gridApi] = useVbenVxeGrid({ | 182 | const [Grid, gridApi] = useVbenVxeGrid({ |
| 139 | formOptions: { | 183 | formOptions: { |
| 140 | schema: useGridFormSchema(), | 184 | schema: useGridFormSchema(), |
| @@ -210,6 +254,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ | @@ -210,6 +254,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ | ||
| 210 | <template> | 254 | <template> |
| 211 | <Page auto-content-height> | 255 | <Page auto-content-height> |
| 212 | <FormModal @success="handleRefresh" /> | 256 | <FormModal @success="handleRefresh" /> |
| 257 | + <input ref="importFileRef" type="file" accept=".xls,.xlsx" class="hidden" @change="handleImportFileChange" /> | ||
| 213 | #if ($table.templateType == 11) ## erp情况 | 258 | #if ($table.templateType == 11) ## erp情况 |
| 214 | <div> | 259 | <div> |
| 215 | #end | 260 | #end |
| @@ -248,6 +293,19 @@ const [Grid, gridApi] = useVbenVxeGrid({ | @@ -248,6 +293,19 @@ const [Grid, gridApi] = useVbenVxeGrid({ | ||
| 248 | }, | 293 | }, |
| 249 | #end | 294 | #end |
| 250 | { | 295 | { |
| 296 | + label: '导入', | ||
| 297 | + type: 'primary', | ||
| 298 | + auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'], | ||
| 299 | + disabled: importLoading.value, | ||
| 300 | + onClick: handleImport, | ||
| 301 | + }, | ||
| 302 | + { | ||
| 303 | + label: '导入模板', | ||
| 304 | + type: 'primary', | ||
| 305 | + auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'], | ||
| 306 | + onClick: handleImportTemplateDownload, | ||
| 307 | + }, | ||
| 308 | + { | ||
| 251 | label: $t('ui.actionTitle.export'), | 309 | label: $t('ui.actionTitle.export'), |
| 252 | type: 'primary', | 310 | type: 'primary', |
| 253 | icon: ACTION_ICON.DOWNLOAD, | 311 | icon: ACTION_ICON.DOWNLOAD, |
| @@ -318,4 +376,4 @@ const [Grid, gridApi] = useVbenVxeGrid({ | @@ -318,4 +376,4 @@ const [Grid, gridApi] = useVbenVxeGrid({ | ||
| 318 | </div> | 376 | </div> |
| 319 | #end | 377 | #end |
| 320 | </Page> | 378 | </Page> |
| 321 | -</template> | ||
| 322 | \ No newline at end of file | 379 | \ No newline at end of file |
| 380 | +</template> |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/api/api.ts.vm
| @@ -101,6 +101,16 @@ export function export${simpleClassName}(params: any) { | @@ -101,6 +101,16 @@ export function export${simpleClassName}(params: any) { | ||
| 101 | return requestClient.download('${baseURL}/export-excel', { params }); | 101 | return requestClient.download('${baseURL}/export-excel', { params }); |
| 102 | } | 102 | } |
| 103 | 103 | ||
| 104 | +/** 下载${table.classComment}导入模板 */ | ||
| 105 | +export function import${simpleClassName}Template() { | ||
| 106 | + return requestClient.download('${baseURL}/get-import-template'); | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +/** 导入${table.classComment} */ | ||
| 110 | +export function import${simpleClassName}(data: FormData) { | ||
| 111 | + return requestClient.post('${baseURL}/import', data); | ||
| 112 | +} | ||
| 113 | + | ||
| 104 | ## 特殊:主子表专属逻辑 | 114 | ## 特殊:主子表专属逻辑 |
| 105 | #foreach ($subTable in $subTables) | 115 | #foreach ($subTable in $subTables) |
| 106 | #set ($index = $foreach.count - 1) | 116 | #set ($index = $foreach.count - 1) |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/general/views/index.vue.vm
| @@ -26,12 +26,11 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table'; | @@ -26,12 +26,11 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table'; | ||
| 26 | import { $t } from '#/locales'; | 26 | import { $t } from '#/locales'; |
| 27 | #if (${table.templateType} == 2)## 树表接口 | 27 | #if (${table.templateType} == 2)## 树表接口 |
| 28 | import { handleTree,isEmpty } from '@vben/utils' | 28 | import { handleTree,isEmpty } from '@vben/utils' |
| 29 | -import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; | 29 | +import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName}, import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; |
| 30 | #else## 标准表接口 | 30 | #else## 标准表接口 |
| 31 | import { isEmpty } from '@vben/utils'; | 31 | import { isEmpty } from '@vben/utils'; |
| 32 | -import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; | 32 | +import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName}, import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}'; |
| 33 | #end | 33 | #end |
| 34 | -import { downloadFileFromBlobPart } from '@vben/utils'; | ||
| 35 | 34 | ||
| 36 | #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况 | 35 | #if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况 |
| 37 | /** 子表的列表 */ | 36 | /** 子表的列表 */ |
| @@ -74,6 +73,8 @@ const queryParams = reactive({ | @@ -74,6 +73,8 @@ const queryParams = reactive({ | ||
| 74 | }) | 73 | }) |
| 75 | const queryFormRef = ref() // 搜索的表单 | 74 | const queryFormRef = ref() // 搜索的表单 |
| 76 | const exportLoading = ref(false) // 导出的加载中 | 75 | const exportLoading = ref(false) // 导出的加载中 |
| 76 | +const importLoading = ref(false) // 导入的加载中 | ||
| 77 | +const importFileRef = ref<HTMLInputElement>() | ||
| 77 | 78 | ||
| 78 | /** 查询列表 */ | 79 | /** 查询列表 */ |
| 79 | const getList = async () => { | 80 | const getList = async () => { |
| @@ -189,6 +190,44 @@ try { | @@ -189,6 +190,44 @@ try { | ||
| 189 | } | 190 | } |
| 190 | } | 191 | } |
| 191 | 192 | ||
| 193 | +/** 导入模板下载 */ | ||
| 194 | +async function handleImportTemplateDownload() { | ||
| 195 | + const data = await import${simpleClassName}Template(); | ||
| 196 | + downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data }); | ||
| 197 | +} | ||
| 198 | + | ||
| 199 | +/** 导入按钮操作 */ | ||
| 200 | +function handleImport() { | ||
| 201 | + importFileRef.value?.click(); | ||
| 202 | +} | ||
| 203 | + | ||
| 204 | +/** 导入文件变更 */ | ||
| 205 | +async function handleImportFileChange(event: Event) { | ||
| 206 | + const target = event.target as HTMLInputElement; | ||
| 207 | + const file = target.files?.[0]; | ||
| 208 | + if (!file) { | ||
| 209 | + return; | ||
| 210 | + } | ||
| 211 | + importLoading.value = true; | ||
| 212 | + try { | ||
| 213 | + const formData = new FormData(); | ||
| 214 | + formData.append('file', file); | ||
| 215 | + const response: any = await import${simpleClassName}(formData); | ||
| 216 | + const data = response?.data ?? response ?? {}; | ||
| 217 | + let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';'; | ||
| 218 | + if (data.failureRows) { | ||
| 219 | + Object.keys(data.failureRows).forEach((rowNo) => { | ||
| 220 | + text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'; | ||
| 221 | + }); | ||
| 222 | + } | ||
| 223 | + ElMessage.info(text); | ||
| 224 | + await getList(); | ||
| 225 | + } finally { | ||
| 226 | + target.value = ''; | ||
| 227 | + importLoading.value = false; | ||
| 228 | + } | ||
| 229 | +} | ||
| 230 | + | ||
| 192 | #if (${table.templateType} == 2) | 231 | #if (${table.templateType} == 2) |
| 193 | /** 切换树形展开/收缩状态 */ | 232 | /** 切换树形展开/收缩状态 */ |
| 194 | const isExpanded = ref(true); | 233 | const isExpanded = ref(true); |
| @@ -208,6 +247,7 @@ onMounted(() => { | @@ -208,6 +247,7 @@ onMounted(() => { | ||
| 208 | <template> | 247 | <template> |
| 209 | <Page auto-content-height> | 248 | <Page auto-content-height> |
| 210 | <FormModal @success="getList" /> | 249 | <FormModal @success="getList" /> |
| 250 | + <input ref="importFileRef" type="file" accept=".xls,.xlsx" class="hidden" @change="handleImportFileChange" /> | ||
| 211 | 251 | ||
| 212 | <ContentWrap v-if="!hiddenSearchBar"> | 252 | <ContentWrap v-if="!hiddenSearchBar"> |
| 213 | <!-- 搜索工作栏 --> | 253 | <!-- 搜索工作栏 --> |
| @@ -317,6 +357,23 @@ onMounted(() => { | @@ -317,6 +357,23 @@ onMounted(() => { | ||
| 317 | {{ $t('ui.actionTitle.create', ['${table.classComment}']) }} | 357 | {{ $t('ui.actionTitle.create', ['${table.classComment}']) }} |
| 318 | </el-button> | 358 | </el-button> |
| 319 | <el-button | 359 | <el-button |
| 360 | + class="ml-2" | ||
| 361 | + type="primary" | ||
| 362 | + :loading="importLoading" | ||
| 363 | + @click="handleImport" | ||
| 364 | + v-access:code="['${permissionPrefix}:import']" | ||
| 365 | + > | ||
| 366 | + 导入 | ||
| 367 | + </el-button> | ||
| 368 | + <el-button | ||
| 369 | + class="ml-2" | ||
| 370 | + type="primary" | ||
| 371 | + @click="handleImportTemplateDownload" | ||
| 372 | + v-access:code="['${permissionPrefix}:import']" | ||
| 373 | + > | ||
| 374 | + 导入模板 | ||
| 375 | + </el-button> | ||
| 376 | + <el-button | ||
| 320 | :icon="h(Download)" | 377 | :icon="h(Download)" |
| 321 | type="primary" | 378 | type="primary" |
| 322 | class="ml-2" | 379 | class="ml-2" |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/api/api.ts.vm
| @@ -114,6 +114,16 @@ export function export${simpleClassName}(params: any) { | @@ -114,6 +114,16 @@ export function export${simpleClassName}(params: any) { | ||
| 114 | return requestClient.download('${baseURL}/export-excel', { params }); | 114 | return requestClient.download('${baseURL}/export-excel', { params }); |
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | +/** 下载${table.classComment}导入模板 */ | ||
| 118 | +export function import${simpleClassName}Template() { | ||
| 119 | + return requestClient.download('${baseURL}/get-import-template'); | ||
| 120 | +} | ||
| 121 | + | ||
| 122 | +/** 导入${table.classComment} */ | ||
| 123 | +export function import${simpleClassName}(data: FormData) { | ||
| 124 | + return requestClient.post('${baseURL}/import', data); | ||
| 125 | +} | ||
| 126 | + | ||
| 117 | ## 特殊:主子表专属逻辑 | 127 | ## 特殊:主子表专属逻辑 |
| 118 | #foreach ($subTable in $subTables) | 128 | #foreach ($subTable in $subTables) |
| 119 | #set ($index = $foreach.count - 1) | 129 | #set ($index = $foreach.count - 1) |
yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm
| @@ -16,6 +16,8 @@ import { | @@ -16,6 +16,8 @@ import { | ||
| 16 | delete${simpleClassName}, | 16 | delete${simpleClassName}, |
| 17 | export${simpleClassName}, | 17 | export${simpleClassName}, |
| 18 | get${simpleClassName}List, | 18 | get${simpleClassName}List, |
| 19 | + import${simpleClassName}, | ||
| 20 | + import${simpleClassName}Template, | ||
| 19 | } from '#/api/${table.moduleName}/${table.businessName}'; | 21 | } from '#/api/${table.moduleName}/${table.businessName}'; |
| 20 | #else## 标准表接口 | 22 | #else## 标准表接口 |
| 21 | import { | 23 | import { |
| @@ -24,6 +26,8 @@ import { | @@ -24,6 +26,8 @@ import { | ||
| 24 | delete${simpleClassName}List,#end | 26 | delete${simpleClassName}List,#end |
| 25 | export${simpleClassName}, | 27 | export${simpleClassName}, |
| 26 | get${simpleClassName}Page, | 28 | get${simpleClassName}Page, |
| 29 | + import${simpleClassName}, | ||
| 30 | + import${simpleClassName}Template, | ||
| 27 | } from '#/api/${table.moduleName}/${table.businessName}'; | 31 | } from '#/api/${table.moduleName}/${table.businessName}'; |
| 28 | #end | 32 | #end |
| 29 | import { $t } from '#/locales'; | 33 | import { $t } from '#/locales'; |
| @@ -50,6 +54,8 @@ const [FormModal, formModalApi] = useVbenModal({ | @@ -50,6 +54,8 @@ const [FormModal, formModalApi] = useVbenModal({ | ||
| 50 | connectedComponent: Form, | 54 | connectedComponent: Form, |
| 51 | destroyOnClose: true, | 55 | destroyOnClose: true, |
| 52 | }); | 56 | }); |
| 57 | +const importLoading = ref(false); | ||
| 58 | +const importFileRef = ref<HTMLInputElement>(); | ||
| 53 | #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩 | 59 | #if (${table.templateType} == 2)## 树表特有:控制表格展开收缩 |
| 54 | 60 | ||
| 55 | /** 切换树形展开/收缩状态 */ | 61 | /** 切换树形展开/收缩状态 */ |
| @@ -133,6 +139,44 @@ async function handleExport() { | @@ -133,6 +139,44 @@ async function handleExport() { | ||
| 133 | downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data }); | 139 | downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data }); |
| 134 | } | 140 | } |
| 135 | 141 | ||
| 142 | +/** 导入模板下载 */ | ||
| 143 | +async function handleImportTemplateDownload() { | ||
| 144 | + const data = await import${simpleClassName}Template(); | ||
| 145 | + downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data }); | ||
| 146 | +} | ||
| 147 | + | ||
| 148 | +/** 导入按钮操作 */ | ||
| 149 | +function handleImport() { | ||
| 150 | + importFileRef.value?.click(); | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +/** 导入文件变更 */ | ||
| 154 | +async function handleImportFileChange(event: Event) { | ||
| 155 | + const target = event.target as HTMLInputElement; | ||
| 156 | + const file = target.files?.[0]; | ||
| 157 | + if (!file) { | ||
| 158 | + return; | ||
| 159 | + } | ||
| 160 | + importLoading.value = true; | ||
| 161 | + try { | ||
| 162 | + const formData = new FormData(); | ||
| 163 | + formData.append('file', file); | ||
| 164 | + const response: any = await import${simpleClassName}(formData); | ||
| 165 | + const data = response?.data ?? response ?? {}; | ||
| 166 | + let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';'; | ||
| 167 | + if (data.failureRows) { | ||
| 168 | + Object.keys(data.failureRows).forEach((rowNo) => { | ||
| 169 | + text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'; | ||
| 170 | + }); | ||
| 171 | + } | ||
| 172 | + ElMessage.info(text); | ||
| 173 | + handleRefresh(); | ||
| 174 | + } finally { | ||
| 175 | + target.value = ''; | ||
| 176 | + importLoading.value = false; | ||
| 177 | + } | ||
| 178 | +} | ||
| 179 | + | ||
| 136 | const [Grid, gridApi] = useVbenVxeGrid({ | 180 | const [Grid, gridApi] = useVbenVxeGrid({ |
| 137 | formOptions: { | 181 | formOptions: { |
| 138 | schema: useGridFormSchema(), | 182 | schema: useGridFormSchema(), |
| @@ -208,6 +252,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ | @@ -208,6 +252,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ | ||
| 208 | <template> | 252 | <template> |
| 209 | <Page auto-content-height> | 253 | <Page auto-content-height> |
| 210 | <FormModal @success="handleRefresh" /> | 254 | <FormModal @success="handleRefresh" /> |
| 255 | + <input ref="importFileRef" type="file" accept=".xls,.xlsx" class="hidden" @change="handleImportFileChange" /> | ||
| 211 | #if ($table.templateType == 11) ## erp情况 | 256 | #if ($table.templateType == 11) ## erp情况 |
| 212 | <div> | 257 | <div> |
| 213 | #end | 258 | #end |
| @@ -246,6 +291,19 @@ const [Grid, gridApi] = useVbenVxeGrid({ | @@ -246,6 +291,19 @@ const [Grid, gridApi] = useVbenVxeGrid({ | ||
| 246 | }, | 291 | }, |
| 247 | #end | 292 | #end |
| 248 | { | 293 | { |
| 294 | + label: '导入', | ||
| 295 | + type: 'primary', | ||
| 296 | + auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'], | ||
| 297 | + disabled: importLoading.value, | ||
| 298 | + onClick: handleImport, | ||
| 299 | + }, | ||
| 300 | + { | ||
| 301 | + label: '导入模板', | ||
| 302 | + type: 'primary', | ||
| 303 | + auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'], | ||
| 304 | + onClick: handleImportTemplateDownload, | ||
| 305 | + }, | ||
| 306 | + { | ||
| 249 | label: $t('ui.actionTitle.export'), | 307 | label: $t('ui.actionTitle.export'), |
| 250 | type: 'primary', | 308 | type: 'primary', |
| 251 | icon: ACTION_ICON.DOWNLOAD, | 309 | icon: ACTION_ICON.DOWNLOAD, |
| @@ -317,4 +375,4 @@ const [Grid, gridApi] = useVbenVxeGrid({ | @@ -317,4 +375,4 @@ const [Grid, gridApi] = useVbenVxeGrid({ | ||
| 317 | </div> | 375 | </div> |
| 318 | #end | 376 | #end |
| 319 | </Page> | 377 | </Page> |
| 320 | -</template> | ||
| 321 | \ No newline at end of file | 378 | \ No newline at end of file |
| 379 | +</template> |