通用ID转名称组件:表格字段显示与缓存策略

通用ID转名称组件:表格字段显示与缓存策略

引言 📜

在Vue项目中,我们经常需要将后端返回的ID值转换为对应的名称进行展示。本文介绍一个高效的解决方案:通用ID转名称组件及其缓存机制。

核心思路 🎯

使用Pinia存储管理不同表(如分类表、平台表)的ID到名称的映射,并实现多级缓存策略:

  1. 内存缓存 - 快速访问
  2. localStorage缓存 - 持久化存储
  3. API调用 - 最终数据源

存储设计(useTableInfoStore)📦

数据结构

javascript 复制代码
interface TableInfo {
  [key: number]: string; // ID到名称的映射
}

interface TableInfoState {
  [tableName: string]: TableInfo; // 不同表的数据存储
}

表名与API的映射

javascript 复制代码
const tableApiMap = {
  category: {
    api: getAllDsCategoryList,
    responseType: 'dsCategoryList',
    nameField: 'categoryName'
  },
  platform: {
    api: getAllDsPlatformList,
    responseType: 'dsPlatformList',
    nameField: 'platformName'
  }
} as const;

数据获取流程

graph TD A[调用getTableInfo] --> B{内存中是否有数据?} B -->|是| C[返回内存数据] B -->|否| D{localStorage中是否有缓存?} D -->|是| E[返回缓存数据并存入内存] D -->|否| F[调用API] F --> G{API调用成功?} G -->|是| H[处理数据并存入缓存] G -->|否| I[返回空对象] H --> J[返回新数据]

核心方法实现

javascript 复制代码
async getTableInfo(tableName: string) {
  // 1. 检查内存缓存
  if (this.tableInfo[tableName]?.length) return this.tableInfo[tableName];
  
  // 2. 检查localStorage缓存
  const cachedData = localStorage.getItem(`tableInfo:${tableName}`);
  if (cachedData) {
    this.tableInfo[tableName] = JSON.parse(cachedData);
    return this.tableInfo[tableName];
  }
  
  // 3. 调用API
  try {
    const tableConfig = tableApiMap[tableName];
    const res = await tableConfig.api();
    
    if (res.code === 0) {
      // 转换数组为ID-名称映射
      const tableInfo = res.data[tableConfig.responseType].reduce((acc, item) => {
        acc[item.id] = item[tableConfig.nameField];
        return acc;
      }, {});
      
      // 更新缓存
      this.tableInfo[tableName] = tableInfo;
      localStorage.setItem(`tableInfo:${tableName}`, JSON.stringify(tableInfo));
      
      return tableInfo;
    }
  } catch (error) {
    console.error(`获取${tableName}数据失败:`, error);
    return {};
  }
}

组件设计(TableText) 🧩

组件功能

  • 支持单个ID转换
  • 支持多个ID转换(数组或逗号分隔字符串)
  • 自动处理缓存更新

组件实现

vue 复制代码
<template>
  {{ text }}
</template>

<script lang="ts" setup>
// 省略导入语句

interface ITableText {
  table: string;
  id?: number;
  ids?: string | number[];
}

const props = defineProps<{ props: ITableText }>();
const text = ref('');

const updateText = async () => {
  const { table, id, ids } = props.props;
  
  // 获取表信息
  const result = await tableInfoStore.getTableInfo(table);
  
  if (ids !== undefined) {
    // 处理多个ID
    const idArray = typeof ids === 'string' 
      ? ids.split(',').map(i => parseInt(i.trim())) 
      : ids;
    
    text.value = idArray
      .map(i => result[i] || '')
      .filter(name => name !== '')
      .join(',');
  } else if (id !== undefined) {
    // 处理单个ID
    text.value = result[id] || '';
  }
};

// 初始化和监听变化
onMounted(updateText);
watch(() => props, updateText, { deep: true });
</script>

使用示例 🚀

单个ID展示

vue 复制代码
<el-table-column prop="categoryId" label="分类">
  <template #default="scope">
    <TableText :props="{ table: 'category', id: scope.row.categoryId }" />
  </template>
</el-table-column>

多个ID展示

vue 复制代码
<el-table-column prop="platformIds" label="平台">
  <template #default="scope">
    <TableText :props="{ table: 'platform', ids: scope.row.platformIds }" />
  </template>
</el-table-column>

数据流转示例 🔄

单个ID场景

TableText组件 TableInfoStore 后端API localStorage 获取分类信息('category') 返回内存中的映射 读取缓存 返回缓存数据 存入内存 返回缓存数据 调用getAllDsCategoryList() 返回分类列表 转换为{id: name}映射 存入缓存 存入内存 返回新数据 alt [localStorage中有缓存] [无缓存] alt [内存中有数据] [内存中无数据] 根据ID查找名称并显示 TableText组件 TableInfoStore 后端API localStorage

多个ID场景

组件接收IDs参数 拆分IDs为数组 获取表映射数据 遍历IDs查找名称 过滤空值 用逗号连接名称 显示结果

方案优势 ✨

  1. 高性能:三级缓存策略减少API调用
  2. 通用性:通过配置支持不同表类型
  3. 易用性:组件化设计简化使用
  4. 灵活性:支持单个和多个ID转换
  5. 容错性:自动处理异常情况

这个通用ID转名称解决方案通过精心设计的缓存策略和组件化实现,显著提高了表格数据显示的效率,同时保持了代码的简洁性和可维护性。

附录 🔗

pinia完整代码

web\admin\src\stores\tableInfo.ts

js 复制代码
import { defineStore } from 'pinia';
import { getAllDsCategoryList } from '/@/api/ds/dsCategory';
import { getAllDsPlatformList } from '/@/api/ds/dsPlatform';

interface TableInfo {
	[key: number]: string;
}

interface TableInfoState {
	[tableName: string]: TableInfo;
}

// 通用响应类型
interface BaseApiResponse {
	code: number;
	data: any;
}

// 分类响应类型
interface CategoryApiResponse extends BaseApiResponse {
	data: {
		dsCategoryList: Array<{
			id: number;
			categoryName: string;
		}>;
	};
}

// 平台响应类型
interface PlatformApiResponse extends BaseApiResponse {
	data: {
		dsPlatformList: Array<{
			id: number;
			platformName: string;
		}>;
	};
}

// 表名与接口的映射
const tableApiMap = {
	category: {
		api: getAllDsCategoryList,
		responseType: 'dsCategoryList',
		nameField: 'categoryName'
	},
	platform: {
		api: getAllDsPlatformList,
		responseType: 'dsPlatformList',
		nameField: 'platformName'
	}
} as const;

export const useTableInfoStore = defineStore('tableInfo', {
	state: () => ({
		tableInfo: {} as TableInfoState,
	}),

	actions: {
		async getTableInfo(tableName: string) {
			let key = 'tableInfo:' + tableName;
			console.log('开始获取表信息:', tableName);
			
			// 初始化tableInfo
			if (!this.tableInfo) {
				this.tableInfo = {} as TableInfoState;
			}

			// 如果内存中已有数据且不为空,直接返回
			if (this.tableInfo[tableName] && Object.keys(this.tableInfo[tableName]).length > 0) {
				console.log('从内存中获取数据:', this.tableInfo[tableName]);
				return this.tableInfo[tableName];
			}

			// 查询localStorage缓存
			const cachedData = localStorage.getItem(key);
			if (cachedData) {
				const parsedData = JSON.parse(cachedData) as TableInfo;
				// 只有当缓存数据不为空时才使用
				if (Object.keys(parsedData).length > 0) {
					this.tableInfo[tableName] = parsedData;
					console.log('从localStorage获取数据:', parsedData);
					return parsedData;
				} else {
					// 如果缓存是空对象,删除它
					localStorage.removeItem(key);
				}
			}

			// 缓存不存在或无效,根据表名请求对应接口
			try {
				let tableInfo: TableInfo = {};
				
				const tableConfig = tableApiMap[tableName as keyof typeof tableApiMap];
				if (tableConfig) {
					console.log('开始调用API:', tableName);
					const res = await tableConfig.api() as unknown as BaseApiResponse;
					console.log('API响应:', res);
					
					if (res.code === 0 && res.data[tableConfig.responseType]) {
						const list = res.data[tableConfig.responseType];
						console.log('获取到的列表数据:', list);
						
						// 将数组转换为id-name映射
						tableInfo = list.reduce((acc: TableInfo, item: any) => {
							acc[item.id] = item[tableConfig.nameField];
							return acc;
						}, {});
						
						console.log('转换后的映射数据:', tableInfo);
						
						// 保存到内存和localStorage
						if (Object.keys(tableInfo).length > 0) {
							this.tableInfo[tableName] = tableInfo;
							localStorage.setItem(key, JSON.stringify(tableInfo));
							console.log('数据已保存到缓存');
						}
					} else {
						console.warn('API响应格式不正确:', res);
					}
				} else {
					console.warn('未找到表配置:', tableName);
				}
				
				return tableInfo;
			} catch (error) {
				console.error(`获取${tableName}数据失败:`, error);
				return {};
			}
		},
	},
});

组件完整实现

web\admin\src\components\TableText\index.vue

vue 复制代码
<template>
	{{ text }}
</template>

<script lang="ts" setup name="TableText">
import { ref, onMounted, watch } from 'vue';
import { useTableInfoStore } from '/@/stores/tableInfo';

const tableInfoStore = useTableInfoStore();

interface ITableText {
	table: string;
	id?: number;
	ids?: string | number[];
}

const props = defineProps<{ props: ITableText }>();
const text = ref('');

const updateText = async () => {
	const { table, id, ids } = props.props;
	try {
		console.log('获取表信息:', table, 'ID:', id, 'IDs:', ids);
		const result = await tableInfoStore.getTableInfo(table);
		console.log('获取到的结果:', result);

		if (ids !== undefined) {
			// 处理多个ID的情况
			const idArray = typeof ids === 'string' ? ids.split(',').map(i => parseInt(i.trim())) : ids;
			const names = idArray
				.map(i => result[i] || '')
				.filter(name => name !== '');
			text.value = names.join(',');
		} else if (id !== undefined) {
			// 处理单个ID的情况
			text.value = result[id] || '';
		}
		
		console.log('设置的文本值:', text.value);
	} catch (error) {
		console.error('获取文本失败:', error);
		text.value = '';
	}
};

onMounted(() => {
	updateText();
});

watch(
	() => props,
	() => {
		updateText();
	},
	{ deep: true }
);
</script>

<style scoped></style>