1
0
Code Issues Pull Requests Packages Projects Releases Wiki Activity GitHub Gitee
Files
epp/frontend/src/components/manage-list.vue

709 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="manage-list-container">
<div v-if="tableData">
<!-- {{ query }} -->
<!-- 筛选 -->
<div class="handle-box">
<template v-for="field in searchFields">
<el-input v-if="field.searchType == 'input'" v-model="query[field.field]"
@keyup.enter.native="handleSearch" :placeholder="field.placeholder" :prefix-icon="Filter"
class="handle-input mr10"></el-input>
<el-select v-else-if="field.searchType == 'select'" v-model="query[field.field]" :clearable="true"
@change="handleSearch" :placeholder="field.placeholder" class="handle-select mr10">
<template #prefix>
<el-icon>
<Filter />
</el-icon>
</template>
<el-option v-for="optKey in Object.keys(field.options)" :key="optKey" :label="field.options[optKey]"
:value="optKey"></el-option>
</el-select>
<el-date-picker v-else-if="field.searchType == 'time-interval'" v-model="query[field.field]"
type="datetimerange" :shortcuts="dateTimePickerRangeShotcut" range-separator=""
:start-placeholder="field.placeholder + ' 起始时间'" :end-placeholder="field.placeholder + '结束时间'"
:prefix-icon="Filter" style="width: 350px; margin-right: 10px;" />
<template v-else>{{ field }}</template>
</template>
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button type="primary" :icon="Plus" @click="handleNew" v-permiss="props.editPermiss"
v-if="props.addFunc">新增记录</el-button>
<el-button type="primary" :icon="Download" @click="exportFormVisible = true" v-permiss="props.editPermiss"
v-if="props.exportFunc">导出到文件</el-button>
</div>
<!-- 表格 -->
<el-table :data="tableData" border class="table" ref="multipleTable" header-cell-class-name="table-header">
<el-table-column prop="id" label="ID" align="center"></el-table-column>
<el-table-column v-for="(field, index) in tableFields" :prop="field.prop" :label="field.label" :key="index"
align="center">
<template #default="scope" v-if="field.type == 'image'">
<el-image style="width: 100%; height: 100%;" :src="scope.row[field.prop]" fit="cover" />
</template>
<template #default="scope" v-else-if="field.type == 'time'">
{{
new Date(scope.row[field.prop] + 8 * 3600 * 1000).toISOString().replace('T', ' ').substring(0,19)
}}
</template>
<template #default="scope" v-else-if="field.type == 'longtext'">
<el-tooltip placement="top">
<template #content>
<p v-for="line in scope.row[field.prop].split(/[\r\n]/g)" style="max-width: 300px;">
{{ line }}
</p>
</template>
<div class="oneLine">
{{ scope.row[field.prop] }}
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" v-if="props.editFunc || props.deleteFunc">
<template #default="scope">
<el-button text :icon="Edit" @click="handleEdit(scope.$index, scope.row)"
v-permiss="props.editPermiss" v-if="props.editFunc">
编辑
</el-button>
<el-button text :icon="Delete" class="red" @click="handleDelete(scope.$index, scope.row)"
v-permiss="props.editPermiss" v-if="props.deleteFunc">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination background layout="total, prev, pager, next" :current-page="query.pageIndex"
:page-size="query.pageSize" :total="pageTotal" @current-change="handlePageChange"></el-pagination>
</div>
</div>
<div v-else style="padding-top: 11vh;">
<el-empty description="暂无数据" />
</div>
<!-- 新增 / 编辑弹出框 -->
<el-dialog :title="formId > 0 ? '编辑' : '新增'" v-model="editVisible" style="width: 40%; min-width: 280px;">
<!-- {{ form }} -->
<el-form ref="editForm" label-width="80px" :rules="rules" :model="form">
<el-form-item v-if="editVisible" v-for="field in dialogFields" :label="field.label" :prop="field.field">
<el-input v-if="(formId > 0 ? field.editType : field.addType) == 'input'"
:placeholder="formId > 0 ? field.editPlaceholder : field.addPlaceholder" class="popup-item"
v-model="form[field.field]"></el-input>
<el-input v-else-if="(formId > 0 ? field.editType : field.addType) == 'textarea'"
:placeholder="formId > 0 ? field.editPlaceholder : field.addPlaceholder" class="popup-item"
v-model="form[field.field]" type="textarea" :rows="4"></el-input>
<el-select v-else-if="(formId > 0 ? field.editType : field.addType) == 'select'" class="popup-item"
v-model="form[field.field]" :clearable="true">
<el-option v-for="optKey in Object.keys(field.options)" :key="optKey" :label="field.options[optKey]"
:value="optKey"></el-option>
</el-select>
<ImageUpload v-else-if="(formId > 0 ? field.editType : field.addType) == 'image'"
:imageUrl="form[field.field]" @change="(value: any) => form[field.field] = value" />
<el-input v-else-if="(formId > 0 ? field.editType : field.addType) == 'plainText'" class="popup-item"
v-model="form[field.field]" :disabled="true"></el-input>
<!-- {{ field }} -->
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="doMockData(formId > 0)" type="danger" round>随机填充测试数据</el-button>
<el-button @click="editVisible = false"> </el-button>
<el-button type="primary" @click="saveEdit(editForm)"> </el-button>
</span>
</template>
</el-dialog>
<!-- 导出 Excel 弹窗 -->
<el-dialog v-model="exportFormVisible" title="导出选项">
<!-- {{ exportConfig }} -->
<el-form :model="form" label-width="80px">
<el-form-item label="数据范围">
<el-radio-group v-model="exportConfig.withFilter">
<el-radio :label="false">全部数据</el-radio>
<el-radio :label="true">满足筛选条件的数据</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="文件格式">
<el-radio-group v-model="exportConfig.ext">
<el-radio v-for="ext in supportExportFormatList" :label="ext">{{ ext }}</el-radio>
</el-radio-group>
</el-form-item>
<el-alert v-if="exportInfo" :title="exportInfo.info" :type="exportInfo.type" :closable="false" show-icon />
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="exportFormVisible = false">取消</el-button>
<el-button type="primary" @click="handleExport">导出</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { FormInstance, FormRules, ElMessage, ElMessageBox } from 'element-plus';
import { Delete, Edit, Search, Plus, Filter, Download } from '@element-plus/icons-vue';
import * as xlsx from 'xlsx';
import Mock from 'mockjs';
import ImageUpload from './image-upload.vue';
const props = defineProps({
// 获取列表 接口函数
'listFunc': {
type: Function,
required: true,
},
// 新增 接口函数
'addFunc': {
type: Function,
required: false,
},
// 修改 接口函数
'editFunc': {
type: Function,
required: false,
},
// 删除 接口函数
'deleteFunc': {
type: Function,
required: false,
},
// 导出 接口函数
'exportFunc': {
type: Function,
required: true,
},
// 修改、删除、导出权限
'editPermiss': {
type: String,
required: true,
}
})
// 筛选条件
const searchFields: any = ref([]); // 筛选条件字段
const query = reactive({
pageIndex: 1,
pageSize: 10
// 其他筛选条件
}); // 筛选条件的值
// 字段翻译字典(如 roleId -> roleName
let fieldsMapper: any;
// 表格
const tableData: any = ref(null); // 表格数据
const tableFields: any = ref([]); // 表格列
const pageTotal = ref(0); // 总页数
// 新增/编辑 弹窗
const dialogFields: any = ref([]); // 弹窗字段
const editVisible = ref(false); // 是否显示弹窗
let form = reactive({} as any); // 弹窗字段的值
let idx: number = -1; // 表格中编辑行的 index; 新增为 -1
let formId: number = -1; // 数据库 id; 新增为 -1
let idFieldName: string = ''; // id列列名
// 添加/修改记录时表单验证
const editForm = ref<FormInstance>();
// const rules: FormRules = {};
const rules = ref({} as FormRules);
// mock数据
let mockData: any = []
// 导出 Excel 弹窗
const exportFormVisible: any = ref(false);
let exportFields: any = [] // 导出 excel 列表对应中文
const supportExportFormatList = ref(['xlsx', 'xls', 'csv', 'html', 'txt', 'json', 'rtf'] as Array<String>); // 支持的导出格式
const exportConfig = ref({
withFilter: false, // 导出时是否携带筛选条件
ext: "xlsx", // 所选导出格式
} as any);
const exportInfo = computed(() => {
if (['xlsx', 'xls'].includes(exportConfig.value.ext)) {
return null
}
let info = '建议选择 xlsx 或 xls 格式'
let type = 'info'
if (['rtf'].includes(exportConfig.value.ext)) {
// 不推荐的导出格式
info = '该格式易出现编码问题,' + info
type = 'warning'
} else if (['json'].includes(exportConfig.value.ext)) {
info = '该格式方便程序读取,若您不了解该格式,建议选择其他格式进行导出'
type = 'warning'
}
return { info, type }
})
// 获取表格数据
const getData = async () => {
props.listFunc(query).then((data: any) => {
console.log(data)
// 深拷贝一份
let _tableData = JSON.parse(JSON.stringify(data.list));
// 字段映射
fieldsMapper = data.fieldMapper;
if (fieldsMapper) {
// 对于每一个需要映射的字段
for (let m of fieldsMapper) {
// console.log("mapper", m)
_tableData = _tableData.map((row: any) => {
let oldValue = row[m.key] // 取得旧值
let newValue = m.mapper[oldValue] // 翻译新值
row[m.value] = newValue // 塞回新值
return row
})
}
}
console.log("data.columns", data.columns)
// 表格列
tableFields.value = data.columns
.filter((field: any) => field.fieldType != "null")
.map((field: any) => {
// query 填充默认空字符串
if (typeof (query[field.field]) === "undefined") {
query[field.field] = ''
}
return { prop: field.prop, label: field.label, type: field.fieldType }
});
console.log("tableFields", tableFields.value)
// 表格数据
tableData.value = _tableData;
// 总页数
pageTotal.value = data.total;
// id列列名
idFieldName = data.idFieldName;
console.log("idFieldName", idFieldName)
// 筛选字段
searchFields.value = data.columns
.filter((field: any) => field.searchType != "null")
.map((field: any) => {
let f: any = {
placeholder: field.label,
field: field.field,
searchType: field.searchType,
}
switch (field.searchType) {
case 'select':
// 如果是下拉框,那么找到下拉框的 option 数据
f.options = fieldsMapper.find((field: any) => field.key == f.field).mapper
break;
default:
break;
}
return f
});
console.log("searchFields", searchFields.value);
// 新增/修改弹窗列
dialogFields.value = data.columns
.map((field: any) => {
let f: any = {
label: field.label,
field: field.field,
addType: field.addType,
addPlaceholder: field.addPlaceholder,
editType: field.editType,
editPlaceholder: field.editPlaceholder,
default: field.default,
}
switch (field.searchType) {
case 'select':
// 如果是下拉框,那么找到下拉框的 option 数据
f.options = fieldsMapper.find((field: any) => field.key == f.field).mapper
break;
default:
break;
}
return f
});
console.log("dialogFields", dialogFields.value);
// 导出 excel 字段映射
exportFields = data.columns
.filter((field: any) => field.fieldType != "null")
.map((field: any) => {
return { field: field.field, label: field.label }
});
console.log("exportFields", exportFields)
// 表单验证
for (let field of data.columns) {
rules.value[field.field] = field.validateRules
}
console.log("rules", rules.value)
// 测试数据
mockData = data.columns
.filter((field: any) => field.mockRegex)
.map((field: any) => {
let spaceIndex = field.mockRegex.indexOf(' ')
if (spaceIndex == -1) return
let type = field.mockRegex.substring(0, spaceIndex)
let str = field.mockRegex.substring(spaceIndex + 1)
return {
canEdit: field.editType != "plainText",
field: field.field,
type: type,
str: str,
}
})
console.log("mockData", mockData)
})
};
// 查询按钮
const handleSearch = () => {
query.pageIndex = 1;
getData();
};
// 分页导航
const handlePageChange = (val: number) => {
query.pageIndex = val;
getData();
};
// 点击进入编辑框
const handleEdit = (index: number, row: any) => {
idx = index;
formId = row[idFieldName];
form[idFieldName] = formId;
// 遍历所有编辑项,并将 row 的值赋给表格
for (let f of dialogFields.value) {
switch (f.editType) {
case "select":
// 下拉框的值要为 string 类型
form[f.field] = String(row[f.field]);
break;
default:
form[f.field] = row[f.field];
break;
}
}
editVisible.value = true;
};
// 点击进入新建框
const handleNew = () => {
idx = -1;
formId = -1;
form[idFieldName] = -1;
// 遍历所有编辑项,并将 row 的值赋给表格
for (let f of dialogFields.value) {
switch (f.editType) {
case "select":
// 下拉框的值要为 string 类型
form[f.field] = String(f.default);
break;
default:
form[f.field] = f.default;
break;
}
}
editVisible.value = true;
};
// 编辑保存到数据库
const saveEdit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
console.log("formEl", formEl);
formEl.validate(async (valid: boolean, invalidFields: any) => {
// 表单校验
if (!valid) {
// console.log("invalidFields", invalidFields);
// 对表单中的每一个不合法输入框进行遍历
Object.values(invalidFields).forEach((input: any) => {
// 对该不合法输入框的提示信息进行遍历
input.forEach((element: any) => {
ElMessage.error({ message: element.message, grouping: true });
});
});
return;
}
var result;
if (formId > 0) {
// 修改记录
var result = await props.editFunc(form)
} else {
// 新增记录
var result = await props.addFunc(form)
query.pageIndex = Math.ceil((pageTotal.value + 1) / query.pageSize);
}
console.log("result", result)
if (!result) {
ElMessage.error({ message: "添加失败" });
return
}
// 关闭弹窗
editVisible.value = false;
// 更新表格中数据
await getData();
ElMessage.success(formId > 0 ? `修改成功` : `添加成功`);
});
};
// 删除操作
const handleDelete = (index: number, row: any) => {
idx = index;
// 二次确认删除
ElMessageBox.confirm('确定要删除吗?', '提示', { type: 'warning' })
.then(async () => {
var result = await props.deleteFunc({
id: row[idFieldName],
})
if (result) {
ElMessage.success('删除成功');
if (tableData.value.length == 1) {
// 删除了当前页面的最后一条记录,那么往前翻一页
if (query.pageIndex > 0) {
query.pageIndex -= 1;
}
}
tableData.value.splice(idx, 1);
// 更新表格中数据
await getData();
} else {
ElMessage.error('删除失败');
}
})
.catch(() => { });
};
// 导出数据到文件
const handleExport = async () => {
let params = {};
// 如果选择仅导出满肚条件的数据,那么这里就填上查询参数
if (exportConfig.value.withFilter) {
params = JSON.parse(JSON.stringify(query))
// 去除分页参数
delete params["pageIndex"]
delete params["pageSize"]
}
props.exportFunc(params).then((data: any) => {
console.log("exportData", data)
let sheetName = data.sheetName
let fileName = data.fileName
console.log("data.list", data.list)
// 字段翻译
let dataList = JSON.parse(JSON.stringify(data.list))
if (fieldsMapper) {
// 对于每一个需要映射的字段
for (let m of fieldsMapper) {
// console.log("mapper", m)
dataList = dataList.map((row: any) => {
let oldValue = row[m.key] // 取得旧值
let newValue = m.mapper[oldValue] // 翻译新值
row[m.key] = newValue // 直接在旧 key 塞回新值
// 因为后续是通过翻译前的 key 拿的数据,所以这里不翻译 key 只翻译 value
return row
})
}
}
console.log("dataList", dataList)
// 所有列的 field
let fieldNameList = exportFields.map((f: any) => f.field)
fieldNameList.unshift(idFieldName)
// 第一行表头
let firstRow = exportFields.map((f: any) => f.label)
firstRow.unshift("ID")
// 列宽
let rowWidth = exportFields.map(() => { return { wch: 13 } })
rowWidth.unshift({ wch: 5 })
// 数据部分
let excelList = dataList.map((row: any) => {
// 通过翻译前的 key 拿数据
return fieldNameList.map((field: any) => String(row[field]))
})
excelList.unshift(firstRow) // 插入表头
console.log("excelList", excelList)
function downloadFile(text: string, fileName: string, contentType: string) {
const blob = new Blob([text], { type: contentType });
const href = URL.createObjectURL(blob);
const alink = document.createElement("a");
alink.style.display = "none";
alink.download = fileName; // 下载后文件名
alink.href = href;
document.body.appendChild(alink);
alink.click();
document.body.removeChild(alink); // 下载完成移除元素
URL.revokeObjectURL(href); // 释放掉blob对象
}
// 生成文件
let _fileName: string = `${fileName || '数据导出'}.${exportConfig.value.ext || 'xlsx'}`
if (exportConfig.value.ext === "json") {
let text = JSON.stringify(excelList)
downloadFile(text, _fileName, "application/json;charset=utf-8")
} else if (exportConfig.value.ext === "txt") {
let text = excelList.map((row: Array<any>) => row.join('\t')).join('\n')
downloadFile(text, _fileName, "text/plain;charset=utf-8")
} else {
let worksheet = xlsx.utils.aoa_to_sheet(excelList)
let workbook = xlsx.utils.book_new()
worksheet["!cols"] = rowWidth; // 指定列宽
xlsx.utils.book_append_sheet(workbook, worksheet, sheetName || 'Sheet1')
xlsx.writeFile(workbook, _fileName)
}
ElMessage.success({ message: "导出成功" })
exportFormVisible.value = false
})
}
// 生成测试数据
const doMockData = (isEdit: boolean) => {
for (let mock of mockData) {
if (isEdit && !mock.canEdit) continue; // 跳过不允许修改的列,例如 用户名
switch (mock.type) {
case "DTD":
let regexp = new RegExp(mock.str.substring(1, mock.str.length - 2))
// console.log(regexp)
form[mock.field] = Mock.mock({ 'regexp': regexp })['regexp']
break;
case "DPD":
form[mock.field] = Mock.mock(mock.str)
break;
case "IMG":
form[mock.field] = Mock.Random.dataImage(...mock.str.split(','))
break;
}
}
}
// 网页加载完成后加载数据
onMounted(() => {
getData();
});
// 附加内容
const dateTimePickerRangeShotcut = [
{
text: '今天',
value: () => {
const start = new Date()
start.setHours(0)
start.setMinutes(0)
start.setSeconds(0)
start.setMilliseconds(0)
const end = new Date(start.getTime() + 24 * 3600 * 1000)
return [start, end]
},
},
{
text: '昨天',
value: () => {
const end = new Date()
end.setHours(0)
end.setMinutes(0)
end.setSeconds(0)
end.setMilliseconds(0)
const start = new Date(end.getTime() - 24 * 3600 * 1000)
return [start, end]
},
},
{
text: '过去24小时',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 1)
return [start, end]
},
},
{
text: '过去7天',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
return [start, end]
},
},
{
text: '过去30天',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
return [start, end]
},
},
{
text: '过去90天',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
return [start, end]
},
},
]
</script>
<style scoped>
.handle-box {
margin-bottom: 20px;
line-height: 2.5em;
}
.handle-select {
width: 150px;
}
.handle-input {
width: 150px;
}
.table {
width: 100%;
font-size: 14px;
}
.red {
color: #F56C6C;
}
.mr10 {
margin-right: 10px;
}
.table-td-thumb {
display: block;
margin: auto;
width: 40px;
height: 40px;
}
.popup-item {
width: 100%;
}
.oneLine {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>