Vue模板化配置-解析json数据
实现给定模板后自动加载显示数据
主要实现功能:
1.图片显示全路径,鼠标悬停后显示图片
2.附件显示附件名称,点击附件名称即可下载附件
3.列表可以支持分页显示,设定列表最大显示行数,超过行数则分页显示
1.根据页面要求设计模板
{
"organization": {
"__description": "机构信息",
"__type": "object",
"baseInfo": {
"__description": "基本信息",
"__type": "object",
"name": "名称",
"logo": "头像",
"abbreviation": "缩写",
"type": "类型",
"registrationNumber": "注册编号",
"taxId": "税号",
"foundingDate": "成立日期",
"missionStatement": "使命宣言",
"description": {
"__description": "描述",
"__showWay": "textField"
},
"remark": {
"__description": "简介",
"__showWay": "textField"
}
},
"address": {
"__description": "地址",
"__type": "object",
"street": "街道",
"city": "城市",
"state": "州/省",
"postalCode": "邮政编码",
"country": "国家"
},
"contact": {
"__description": "联系方式",
"__type": "object",
"phone": "电话",
"email": "邮箱",
"website": "网站",
"contract": "合规报告"
},
"socialMediaLinks": {
"__description": "社交媒体链接",
"__type": "object",
"facebook": "Facebook",
"twitter": "Twitter",
"linkedin": "LinkedIn",
"instagram": "Instagram"
},
"members": {
"__description": "成员列表",
"__type": "list",
"0": {
"name": "姓名",
"position": "职位",
"phone": "电话",
"email": "邮箱",
"bio": "简介"
}
},
"paymentRecords": {
"__description": "缴费记录",
"__type": "list",
"0": {
"payType": "缴费类型",
"payMoney": "缴费金额",
"payStatus": "缴费状态",
"payTime": "缴费时间"
}
}
}
}
2.编写代码实现
<template>
<div class="container">
<!-- 增加对 data 和 templateData 的检查 -->
<div v-if="templateData && data && data.organization">
<template v-if="templateData.__type === 'object'">
<h2 class="title">{{ templateData.__description }}</h2>
<div class="details">
<div v-for="(value, key) in templateData" :key="key" class="section">
<template v-if="!['__description', '__type'].includes(key)">
<!-- 嵌套对象 -->
<div v-if="value.__type === 'object'">
<h3 class="section-title">{{ value.__description }}</h3>
<div class="nested-fields">
<div
v-for="(nestedValue, nestedKey) in value"
:key="nestedKey"
class="field"
:class="{ 'full-width': nestedValue.__showWay === 'textField' }"
>
<template v-if="!['__description', '__type'].includes(nestedKey)">
<!-- 文本域 -->
<template v-if="nestedValue.__showWay === 'textField'">
<strong class="field-label">{{ nestedValue.__description }}:</strong>
<textarea
class="field-value textarea"
:value="data.organization[key][nestedKey]"
readonly
></textarea>
</template>
<!-- 图片字段 -->
<template v-else-if="isImage(data.organization[key][nestedKey])">
<strong class="field-label">{{ nestedValue }}:</strong>
<div
class="image-container"
@mouseover="showImagePreview(data.organization[key][nestedKey])"
@mouseleave="hideImagePreview"
>
<span class="field-value">{{ data.organization[key][nestedKey] }}</span>
<div class="image-preview" v-if="hoveredImage === data.organization[key][nestedKey]">
<img :src="data.organization[key][nestedKey]" alt="Image Preview" class="preview-image" />
</div>
</div>
</template>
<!-- 附件字段 -->
<template v-else-if="isAttachment(data.organization[key][nestedKey])">
<strong class="field-label">{{ nestedValue }}:</strong>
<div class="attachment-container">
<a
class="attachment-link"
:href="data.organization[key][nestedKey]"
@click.prevent="downloadFile(data.organization[key][nestedKey])"
>
{{ getFileName(data.organization[key][nestedKey]) }}
</a>
</div>
</template>
<!-- 普通字段 -->
<template v-else>
<strong class="field-label">{{ nestedValue }}:</strong>
<span class="field-value">{{ data.organization[key][nestedKey] }}</span>
</template>
</template>
</div>
</div>
</div>
<!-- 列表 -->
<div v-else-if="value.__type === 'list'">
<h3 class="section-title">{{ value.__description }}</h3>
<table class="data-table">
<thead>
<tr>
<th v-for="(headerValue, headerKey) in value[0]" :key="headerKey">
{{ headerValue }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in paginatedData(data.organization[key], key)" :key="index">
<td v-for="(headerValue, headerKey) in value[0]" :key="headerKey">
{{ item[headerKey] }}
</td>
</tr>
</tbody>
</table>
<!-- 分页控件 -->
<div class="pagination" v-if="data.organization[key].length > itemsPerPage">
<button
v-for="page in Math.ceil(data.organization[key].length / itemsPerPage)"
:key="page"
@click="changePage(page, key)"
:class="{ active: paginationState[key] === page }"
>
{{ page }}
</button>
</div>
</div>
<!-- 普通字段 -->
<template v-else>
<div class="field">
<strong class="field-label">{{ value }}:</strong>
<span class="field-value">{{ data.organization[key] }}</span>
</div>
</template>
</template>
</div>
</div>
</template>
</div>
<div v-else class="loading">
<p>加载中...</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
// 模板集合
const templates = {
"organization": {
"__description": "机构信息",
"__type": "object",
"baseInfo": {
"__description": "基本信息",
"__type": "object",
"name": "名称",
"logo": "头像",
"abbreviation": "缩写",
"type": "类型",
"registrationNumber": "注册编号",
"taxId": "税号",
"foundingDate": "成立日期",
"missionStatement": "使命宣言",
"description": {
"__description": "描述",
"__showWay": "textField"
},
"remark": {
"__description": "简介",
"__showWay": "textField"
}
},
"address": {
"__description": "地址",
"__type": "object",
"street": "街道",
"city": "城市",
"state": "州/省",
"postalCode": "邮政编码",
"country": "国家"
},
"contact": {
"__description": "联系方式",
"__type": "object",
"phone": "电话",
"email": "邮箱",
"website": "网站",
"contract": "合规报告"
},
"socialMediaLinks": {
"__description": "社交媒体链接",
"__type": "object",
"facebook": "Facebook",
"twitter": "Twitter",
"linkedin": "LinkedIn",
"instagram": "Instagram"
},
"members": {
"__description": "成员列表",
"__type": "list",
"0": {
"name": "姓名",
"position": "职位",
"phone": "电话",
"email": "邮箱",
"bio": "简介"
}
},
"paymentRecords": {
"__description": "缴费记录",
"__type": "list",
"0": {
"payType": "缴费类型",
"payMoney": "缴费金额",
"payStatus": "缴费状态",
"payTime": "缴费时间"
}
}
}
};
// 当前模板数据
const templateData = ref(null);
// 当前数据
const data = ref(null);
// 模板名称
const templateName = ref("organization");
// 当前悬停的图片 URL
const hoveredImage = ref(null);
// 分页状态,存储每个列表的当前页码
const paginationState = ref({});
// 每页显示的条数
const itemsPerPage = 5;
// 判断是否为图片 URL
const isImage = (url) => {
return url && /\.(png|jpg|jpeg|gif|webp)$/i.test(url);
};
// 判断是否为附件
const isAttachment = (url) => {
return url && /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/i.test(url);
};
// 显示图片预览
const showImagePreview = (imageUrl) => {
hoveredImage.value = imageUrl;
};
// 隐藏图片预览
const hideImagePreview = () => {
hoveredImage.value = null;
};
// 从 URL 中提取文件名
const getFileName = (url) => {
return url.split("/").pop(); // 获取 URL 的最后一部分作为文件名
};
// 下载文件
const downloadFile = (url) => {
const link = document.createElement("a");
link.href = url;
link.download = getFileName(url); // 使用提取的文件名
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 获取当前列表的分页数据
const paginatedData = (list, listKey) => {
const currentPage = paginationState.value[listKey] || 1;
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
return list.slice(start, end);
};
// 切换页码
const changePage = (page, listKey) => {
paginationState.value[listKey] = page;
};
// 根据模板名称加载模板和数据
const loadTemplateAndData = (name) => {
// 加载模板
templateData.value = templates[name];
// 模拟加载数据
if (name === "organization") {
data.value = {
"organization": {
"baseInfo": {
"name": "云基华海信息技术有限公司",
"logo": "http://10.168.3.13:30013/img/icon1.0a93c87a.png",
"abbreviation": "云基华海",
"type": "科技公司",
"registrationNumber": "123456789",
"taxId": "987654321",
"foundingDate": "2010-05-20",
"missionStatement": "用科技赋能未来",
"description": "一家专注于大数据和人工智能技术的高科技公司。",
"remark": "话说秦钟既死,宝玉痛哭不止,李贵等好容易劝解半日方住,归时还带余哀。贾母帮了几十两银子外,又另备奠仪,宝玉去吊祭。七日后便送殡掩埋了,别无记述。只有宝玉,日日感悼,思念不已,然亦无可如何了,又不知过了几时才罢。"
},
"address": {
"street": "科技大道123号",
"city": "西安",
"state": "陕西省",
"postalCode": "710000",
"country": "中国"
},
"contact": {
"phone": "+86 029 1234 5678",
"email": "info@cloudbase.com",
"website": "https://www.cloudbase.com",
"contract": "http://10.168.2.12:9000/nanshan/scenemanage/2025-03-06/b59ba1adbea3458383b683e12eb8e617.pdf"
},
"socialMediaLinks": {
"facebook": "https://facebook.com/cloudbase",
"twitter": "https://twitter.com/cloudbase",
"linkedin": "https://linkedin.com/company/cloudbase",
"instagram": "https://instagram.com/cloudbase"
},
"members": [{
"name": "张三",
"position": "首席执行官",
"phone": "+86 138 0013 8000",
"email": "zhangsan@cloudbase.com",
"bio": "拥有10年科技行业经验,专注于企业数字化转型。"
},
{
"name": "李四",
"position": "首席技术官",
"phone": "+86 138 0013 8001",
"email": "lisi@cloudbase.com",
"bio": "大数据和人工智能领域的专家,主导多个国家级项目。"
},
{
"name": "王五",
"position": "市场总监",
"phone": "+86 138 0013 8002",
"email": "wangwu@cloudbase.com",
"bio": "负责公司品牌推广和市场战略规划。"
}
],
"paymentRecords": [{
"payType": "购买套餐",
"payMoney": "10.00元",
"payStatus": "成功",
"payTime": "2025-03-06 16:05:17.055"
},
{
"payType": "购买资源",
"payMoney": "1000.00元",
"payStatus": "成功",
"payTime": "2025-03-06 16:05:17.055"
},
{
"payType": "发展会员",
"payMoney": "3000.00元",
"payStatus": "成功",
"payTime": "2025-03-06 16:05:17.055"
},
{
"payType": "商务开销",
"payMoney": "5000.00元",
"payStatus": "成功",
"payTime": "2025-03-06 16:05:17.055"
},
{
"payType": "员工团建",
"payMoney": "8000.00元",
"payStatus": "成功",
"payTime": "2025-03-06 16:05:17.055"
},
{
"payType": "工资发放",
"payMoney": "5000.00元",
"payStatus": "成功",
"payTime": "2025-03-06 16:05:17.055"
},
{
"payType": "生活开销",
"payMoney": "3000.00元",
"payStatus": "成功",
"payTime": "2025-03-06 16:05:17.055"
},
{
"payType": "日常采买",
"payMoney": "10000.00元",
"payStatus": "成功",
"payTime": "2025-03-06 16:05:17.055"
}
]
}
};
}
};
// 初始化加载模板和数据
onMounted(() => {
loadTemplateAndData(templateName.value);
// 初始化分页状态
if (data.value && data.value.organization) {
Object.keys(data.value.organization).forEach((key) => {
if (templateData.value[key]?.__type === "list") {
paginationState.value[key] = 1; // 默认选中第一页
}
});
}
});
</script>
<style scoped>
.container {
max-width: 1500px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.title {
font-size: 24px;
color: #333;
margin-bottom: 20px;
}
.details {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.section {
margin-bottom: 20px;
}
.section-title {
font-size: 20px;
color: #444;
margin-bottom: 15px;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.nested-fields {
padding-left: 20px;
display: flex;
flex-wrap: wrap;
}
.field {
width: 50%; /* 每行显示两个字段 */
margin-bottom: 10px;
display: flex;
align-items: flex-start; /* 对齐方式调整为顶部对齐 */
}
.field.full-width {
width: 100%; /* 文本域字段独占一行 */
}
.field-label {
color: #555;
font-weight: bold;
margin-right: 10px;
}
.field-value {
color: #333;
flex: 1; /* 占据剩余空间 */
}
.textarea {
width: 100%;
height: 100px; /* 文本域高度 */
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical; /* 允许垂直调整大小 */
background-color: #f9f9f9; /* 只读背景色 */
font-family: inherit;
font-size: 14px;
}
.image-container {
position: relative;
display: inline-block;
}
.image-preview {
position: absolute;
top: 100%;
left: 0;
z-index: 10;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.preview-image {
max-width: 200px;
max-height: 200px;
display: block;
}
.attachment-container {
display: flex;
align-items: center;
gap: 10px;
}
.attachment-link {
color: #007bff; /* 附件链接颜色 */
text-decoration: none;
cursor: pointer;
font-weight: bold;
}
.attachment-link:hover {
text-decoration: underline; /* 悬停时添加下划线 */
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.data-table th,
.data-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.data-table th {
background-color: #f4f4f4;
font-weight: bold;
}
.data-table tr:nth-child(even) {
background-color: #f9f9f9;
}
.data-table tr:hover {
background-color: #f1f1f1;
}
.loading {
text-align: center;
font-size: 18px;
color: #666;
margin-top: 50px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 5px;
}
.pagination button {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
transition: background-color 0.3s;
}
.pagination button:hover {
background-color: #f1f1f1;
}
.pagination button.active {
background-color: #007bff;
color: #fff;
border-color: #007bff;
}
</style>