generated from template/astro-simple-template
- Added a modal component for user input in settings. - Implemented drag-and-drop functionality for customizing hotkeys. - Integrated toast notifications for user feedback on actions. - Updated store to manage customizable items and namespaces. - Enhanced the refresh button to fetch items with a refresh option. - Improved settings button to open settings in a new tab. - Added caching mechanisms for hotkeys data. - Created a settings page to manage hotkeys and namespaces. - Updated query module to handle configuration retrieval.
237 lines
7.4 KiB
TypeScript
237 lines
7.4 KiB
TypeScript
import { create } from 'zustand';
|
||
import { queryClient, config } from '../../modules/query'
|
||
import { NocoApi } from '@kevisual/noco'
|
||
import { toast } from 'react-toastify'
|
||
import { MyCache } from '@kevisual/cache'
|
||
const cache = new MyCache<CardItem[]>('hot_api')
|
||
type CacheCustom = Array<{
|
||
namespace: string;
|
||
items: CardItem[];
|
||
}>
|
||
const cacheCustom = new MyCache<CacheCustom>('hot_api_customize');
|
||
// --- Types ---
|
||
export interface CardItem {
|
||
id: string;
|
||
title: string;
|
||
iconUrl?: string;
|
||
description?: string;
|
||
data?: any;
|
||
sort?: number;
|
||
namespace?: string;
|
||
}
|
||
|
||
export interface StoreState {
|
||
items: CardItem[];
|
||
isLoading: boolean;
|
||
machineId?: string;
|
||
fetchItems: (refresh?: boolean) => Promise<void>;
|
||
sendEvent: (item: CardItem) => Promise<void>;
|
||
namespace?: string;
|
||
setNamespace?: (namespace: string) => void;
|
||
customizeItems?: CardItem[];
|
||
setCustomizeItems?: (items: CardItem[]) => void;
|
||
getCustomizeItems: (namespace?: string) => Promise<CardItem[]>;
|
||
initCustomizeItems?: () => Promise<void>;
|
||
getMachineId?: () => Promise<string>;
|
||
setCacheData?: (cacheCustom: CacheCustom) => void;
|
||
getCacheData?: () => Promise<CacheCustom | null>;
|
||
exportCacheData?: () => Promise<string | null>;
|
||
importCacheData?: (jsonData: string) => Promise<{
|
||
success: boolean;
|
||
importedNamespaces?: string[];
|
||
message: string;
|
||
}>;
|
||
nocoApi?: NocoApi;
|
||
}
|
||
|
||
// --- Store ---
|
||
export const useStore = create<StoreState>((set, get) => ({
|
||
items: [],
|
||
isLoading: false,
|
||
getMachineId: async () => {
|
||
let machineId = get().machineId;
|
||
if (machineId) {
|
||
return machineId;
|
||
}
|
||
const res = await queryClient.get({ path: 'config', key: 'getId' });
|
||
if (res.code === 200) {
|
||
machineId = res.data;
|
||
set({ machineId });
|
||
return machineId!;
|
||
}
|
||
return '';
|
||
},
|
||
fetchItems: async (refresh?: boolean) => {
|
||
set({ isLoading: true });
|
||
get().initCustomizeItems?.();
|
||
get().getMachineId?.();
|
||
const chacheData = await cache.getData().catch(() => null);
|
||
if (!refresh && chacheData && Array.isArray(chacheData) && chacheData.length > 0) {
|
||
set({ items: chacheData, isLoading: false });
|
||
return;
|
||
}
|
||
const life = await config.getConfig('life.json').catch(() => null);
|
||
if (!life) return;
|
||
|
||
const baseId = life?.baseId || '';
|
||
const nocoToken = life?.token || '';
|
||
const baseURL = life?.baseURL || '';
|
||
if (!baseId || !nocoToken || !baseURL) {
|
||
toast.error('未配置 nocodb 的 baseId 或 token,请前往设置页面配置');
|
||
return
|
||
};
|
||
const nocoApi = new NocoApi({ baseURL, token: nocoToken });
|
||
const table = await nocoApi.getTableByName('控制中枢', baseId)
|
||
if (!table) {
|
||
toast.error('未找到 nocodb 中的 控制中枢 表,请检查配置');
|
||
return;
|
||
}
|
||
nocoApi.record.table = table.id
|
||
set({ nocoApi });
|
||
const res = await nocoApi.record.list({
|
||
page: 1,
|
||
limit: 10000,
|
||
where: "(类型,neq,文档)",
|
||
})
|
||
console.log('Fetched records:', res);
|
||
if (res.code === 200) {
|
||
let items: CardItem[] = res.data.list.map((record: any) => ({
|
||
id: record.Id,
|
||
title: record['标题'] || '未命名',
|
||
iconUrl: record['图标'] || `https://api.dicebear.com/9.x/bottts/svg?seed=${record.Id}`,
|
||
description: record['总结'] || '',
|
||
data: record['数据'] || {},
|
||
sort: 0,
|
||
}));
|
||
cache.setData(items, { expireTime: 30 * 24 * 60 * 60 * 1000 }); // Cache for 10 days
|
||
set({ items, isLoading: false });
|
||
} else {
|
||
toast.error('获取控制中枢数据失败,请检查配置');
|
||
set({ isLoading: false });
|
||
return;
|
||
}
|
||
},
|
||
sendEvent: async (item: CardItem) => {
|
||
console.log('Sending event for item:', item);
|
||
if (item.data?.type === 'hotkeys') {
|
||
const res = await queryClient.post({
|
||
path: 'key-sender',
|
||
keys: item?.data?.hotkeys
|
||
});
|
||
console.log('Event sent for item:', item, 'Response:', res);
|
||
if (res.code !== 200) {
|
||
toast.error('事件发送失败');
|
||
}
|
||
return;
|
||
}
|
||
|
||
toast.info('暂不支持该类型的控制中枢操作');
|
||
},
|
||
namespace: localStorage.getItem('hotapi-namespace') || 'default',
|
||
customizeItems: [],
|
||
setCustomizeItems: (items: CardItem[]) => {
|
||
set({ customizeItems: items });
|
||
// Save to cache asynchronously without blocking
|
||
const ns = useStore.getState().namespace || 'default';
|
||
(async () => {
|
||
const chacheData = await cacheCustom.getData().catch(() => null);
|
||
let allData: CacheCustom = chacheData || [];
|
||
|
||
// Update or add the namespace data
|
||
const existingIndex = allData.findIndex(c => c.namespace === ns);
|
||
if (existingIndex >= 0) {
|
||
allData[existingIndex].items = items;
|
||
} else {
|
||
allData.push({ namespace: ns, items });
|
||
}
|
||
|
||
await cacheCustom.setData(allData, { expireTime: 365 * 24 * 60 * 60 * 1000 }); // Cache for 1 year
|
||
})();
|
||
},
|
||
setNamespace: (namespace: string) => {
|
||
set({ namespace })
|
||
get().getCustomizeItems(namespace);
|
||
localStorage.setItem('hotapi-namespace', namespace);
|
||
},
|
||
getAllNamespaces: async () => {
|
||
const chacheData = await cacheCustom.getData().catch(() => null);
|
||
if (chacheData) {
|
||
return chacheData.map(c => c.namespace);
|
||
}
|
||
return ['default'];
|
||
},
|
||
getCustomizeItems: async (namespace?: string) => {
|
||
const ns = namespace || useStore.getState().namespace || 'default';
|
||
const chacheData = await cacheCustom.getData().catch(() => null);
|
||
if (chacheData) {
|
||
const nsData = chacheData.find(c => c.namespace === ns);
|
||
if (nsData) {
|
||
set({ customizeItems: nsData.items });
|
||
return nsData.items;
|
||
} else {
|
||
set({ customizeItems: [] });
|
||
return [];
|
||
}
|
||
}
|
||
return [];
|
||
},
|
||
initCustomizeItems: async () => {
|
||
const ns = useStore.getState().namespace || 'default';
|
||
await get().getCustomizeItems(ns);
|
||
},
|
||
getCacheData: async () => {
|
||
const chacheData = await cacheCustom.getData().catch(() => null);
|
||
return chacheData;
|
||
},
|
||
setCacheData: (cacheCustomData: CacheCustom) => {
|
||
cacheCustom.setData(cacheCustomData, { expireTime: 365 * 24 * 60 * 60 * 1000 }); // Cache for 1 year
|
||
},
|
||
exportCacheData: async () => {
|
||
const cacheData = await get().getCacheData?.();
|
||
if (cacheData) {
|
||
const exportData = {
|
||
version: '1.0.0',
|
||
exportTime: new Date().toISOString(),
|
||
data: cacheData
|
||
};
|
||
return JSON.stringify(exportData, null, 2);
|
||
}
|
||
return null;
|
||
},
|
||
importCacheData: async (jsonData: string) => {
|
||
try {
|
||
const importData = JSON.parse(jsonData);
|
||
|
||
// 验证导入的数据格式
|
||
if (!importData.data || !Array.isArray(importData.data)) {
|
||
throw new Error('导入数据格式不正确');
|
||
}
|
||
|
||
// 验证每个命名空间的数据格式
|
||
for (const nsData of importData.data) {
|
||
if (!nsData.namespace || !Array.isArray(nsData.items)) {
|
||
throw new Error('命名空间数据格式不正确');
|
||
}
|
||
}
|
||
|
||
// 导入数据
|
||
get().setCacheData?.(importData.data);
|
||
|
||
// 刷新当前命名空间的数据
|
||
const currentNs = get().namespace || 'default';
|
||
get().getCustomizeItems?.(currentNs);
|
||
|
||
return {
|
||
success: true,
|
||
importedNamespaces: importData.data.map((ns: any) => ns.namespace),
|
||
message: '数据导入成功'
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : '导入失败'
|
||
};
|
||
}
|
||
},
|
||
}));
|