在上一个工程中我们实现了一个基于next的索引页,现在需求是配置文件要持久化,这里我们使用IndexedDB存储我们的数据,并实现对配置文件的编辑。
1. 安装必要的依赖
我们需要一个工具来简化 IndexedDB 的操作,这里推荐使用 idb(一个轻量级的 IndexedDB 封装库)。
在项目根目录运行:
npm install idb
2. 创建 IndexedDB 工具函数
新建 utils/db.ts 文件,用于初始化和管理 IndexedDB:
// utils/db.ts
import { openDB, DBSchema, IDBPDatabase } from 'idb';
import { CardItem } from '../types/card';
interface MyDB extends DBSchema {
cardConfig: {
key: string;
value: CardItem[];
};
}
let dbPromise: Promise<IDBPDatabase<MyDB>> | null = null;
export const getDB = async () => {
if (!dbPromise) {
dbPromise = openDB<MyDB>('directoryDB', 1, {
upgrade(db) {
db.createObjectStore('cardConfig', { keyPath: 'key' });
},
});
}
return dbPromise;
};
// 初始化默认配置
export const initCardConfig = async (defaultConfig: CardItem[]) => {
const db = await getDB();
const tx = db.transaction('cardConfig', 'readwrite');
const store = tx.objectStore('cardConfig');
const existingConfig = await store.get('cards');
if (!existingConfig) {
await store.put({ key: 'cards', value: defaultConfig });
}
await tx.done;
};
// 获取配置
export const getCardConfig = async (): Promise<CardItem[]> => {
const db = await getDB();
const tx = db.transaction('cardConfig', 'readonly');
const store = tx.objectStore('cardConfig');
const config = await store.get('cards');
return config?.value || [];
};
// 更新配置
export const updateCardConfig = async (config: CardItem[]) => {
const db = await getDB();
const tx = db.transaction('cardConfig', 'readwrite');
const store = tx.objectStore('cardConfig');
await store.put({ key: 'cards', value: config });
await tx.done;
};
3. 修改 cardConfig.ts
将默认配置作为初始数据,不直接使用,而是通过 IndexedDB 获取:
// config/cardConfig.ts
import { CardItem } from '../types/card';
export const defaultCardConfig: CardItem[] = [
{
id: '1',
title: '首页',
description: '网站首页导航',
url: '/',
apiUrl: '/api/home',
icon: 'home',
urls: [
{ label: '管理后台', href: '/admin', port: 8080 },
{ label: '用户中心', href: '/user', port: 8081 },
],
},
{
id: '2',
title: '产品页面',
description: '查看所有产品信息',
url: '/products',
apiUrl: '/api/products',
icon: 'appstore',
urls: [
{ label: '产品详情', href: '/products/detail', port: 8080 },
{ label: '产品API', href: '/api/products', port: 8082 },
],
},
];
4. 修改主页面 index.tsx
在 pages/index.tsx 中使用 IndexedDB 获取数据,并添加编辑功能:
// pages/index.tsx
import { useState, useEffect } from 'react';
import { ConfigProvider, Button, Modal, Input, Space } from '@arco-design/web-react';
import CardList from '../components/CardList';
import { CardItem } from '../types/card';
import { getCardConfig, initCardConfig, updateCardConfig } from '../utils/db';
import { defaultCardConfig } from '../config/cardConfig';
export default function Home() {
const [cards, setCards] = useState<CardItem[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editingCard, setEditingCard] = useState<CardItem | null>(null);
// 初始化和加载数据
useEffect(() => {
const loadData = async () => {
await initCardConfig(defaultCardConfig); // 初始化默认数据
const config = await getCardConfig();
setCards(config);
};
loadData();
}, []);
// 编辑卡片
const handleEdit = (card: CardItem) => {
setEditingCard({ ...card });
setIsModalVisible(true);
};
// 保存编辑
const handleSave = async () => {
if (editingCard) {
const updatedCards = cards.map((card) =>
card.id === editingCard.id ? editingCard : card
);
await updateCardConfig(updatedCards);
setCards(updatedCards);
setIsModalVisible(false);
setEditingCard(null);
}
};
return (
<ConfigProvider>
<div style={{ padding: '20px' }}>
<h1 style={{ marginBottom: '20px' }}>页面目录</h1>
<CardList
items={cards}
onEdit={handleEdit} // 传递编辑回调
/>
<Modal
title="编辑卡片"
visible={isModalVisible}
onOk={handleSave}
onCancel={() => setIsModalVisible(false)}
style={{ width: 600 }}
>
{editingCard && (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Input
addonBefore="标题"
value={editingCard.title}
onChange={(value) => setEditingCard({ ...editingCard, title: value })}
/>
<Input
addonBefore="描述"
value={editingCard.description}
onChange={(value) => setEditingCard({ ...editingCard, description: value })}
/>
<Input
addonBefore="主URL"
value={editingCard.url}
onChange={(value) => setEditingCard({ ...editingCard, url: value })}
/>
{/* 可以继续添加其他字段的编辑 */}
</Space>
)}
</Modal>
</div>
</ConfigProvider>
);
}
5. 修改 CardList 和 CardItem 组件
调整 CardList 和 CardItem,添加编辑按钮:
CardList.tsx
// components/CardList.tsx
import { Grid } from '@arco-design/web-react';
import CardItem from './CardItem';
import { CardItem as CardItemType } from '../types/card';
interface CardListProps {
items: CardItemType[];
onEdit?: (card: CardItemType) => void; // 新增编辑回调
}
const Row = Grid.Row;
const Col = Grid.Col;
const CardList: React.FC<CardListProps> = ({ items, onEdit }) => {
return (
<Row gutter={[20, 20]}>
{items.map((item) => (
<Col key={item.id} span={8}>
<CardItem item={item} onEdit={onEdit} />
</Col>
))}
</Row>
);
};
export default CardList;
CardItem.tsx
// components/CardItem.tsx
import { Card, Button, Space } from '@arco-design/web-react';
import { CardItem } from '../types/card';
interface CardItemProps {
item: CardItem;
onEdit?: (card: CardItem) => void; // 新增编辑回调
}
const CardItem: React.FC<CardItemProps> = ({ item, onEdit }) => {
const getMainUrl = () => {
if (typeof window === 'undefined') {
return `http://localhost:3000${item.url}`;
}
const host = window.location.hostname;
const protocol = window.location.protocol;
const port = window.location.port || '3000';
return `${protocol}//${host}:${port}${item.url}`;
};
const getUrl = (urlItem: { href: string; port?: number }) => {
if (typeof window === 'undefined') {
return `http://localhost:${urlItem.port || 3000}${urlItem.href}`;
}
const host = window.location.hostname;
const protocol = window.location.protocol;
const port = urlItem.port || window.location.port || 3000;
return `${protocol}//${host}:${port}${urlItem.href}`;
};
return (
<Card
hoverable
style={{ width: 300, margin: '0 20px 20px 0' }}
title={
<a href={getMainUrl()} target="_blank" rel="noopener noreferrer">
{item.title}
</a>
}
extra={
onEdit && (
<Button type="text" onClick={() => onEdit(item)}>
编辑
</Button>
)
}
>
<Card.Meta description={item.description} />
{item.apiUrl && (
<div style={{ marginTop: 10 }}>
API: <span style={{ color: '#666' }}>{item.apiUrl}</span>
</div>
)}
{item.urls && item.urls.length > 0 && (
<Space direction="vertical" style={{ marginTop: 10 }}>
{item.urls.map((urlItem, index) => (
<Button
key={index}
type="primary"
size="small"
href={getUrl(urlItem)}
target="_blank"
rel="noopener noreferrer"
>
{urlItem.label}
</Button>
))}
</Space>
)}
</Card>
);
};
export default CardItem;
6. 功能说明
- IndexedDB 存储:
- 项目启动时,
initCardConfig检查 IndexedDB 中是否已有数据,没有则初始化默认配置。 getCardConfig获取存储的数据,updateCardConfig更新数据。
- 项目启动时,
- 编辑功能:
- 点击卡片右上角的“编辑”按钮,弹出模态框。
- 在模态框中修改
title、description和url,点击“确定”保存到 IndexedDB。 - 当前仅实现了基本字段的编辑,你可以根据需要扩展其他字段(如
urls)。
7. 运行和测试
- 运行项目:
npm run dev - 打开浏览器开发者工具,检查 Application -> IndexedDB,确认
directoryDB已创建。
以上就完成了对IndexedDB的基本使用。