generated from template/astro-simple-template
feat: Enhance hotkeys app with customizable settings and modal support
- 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.
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { create } from 'zustand';
|
||||
import { query } from '../../modules/query'
|
||||
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;
|
||||
@@ -7,68 +16,221 @@ export interface CardItem {
|
||||
iconUrl?: string;
|
||||
description?: string;
|
||||
data?: any;
|
||||
sort?: number;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
items: CardItem[];
|
||||
isLoading: boolean;
|
||||
fetchItems: () => Promise<void>;
|
||||
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) => ({
|
||||
export const useStore = create<StoreState>((set, get) => ({
|
||||
items: [],
|
||||
isLoading: false,
|
||||
fetchItems: async () => {
|
||||
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 });
|
||||
// TODO: Replace with actual API call
|
||||
// const response = await fetch('/api/hotkeys');
|
||||
// const data = await response.json();
|
||||
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;
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockData: CardItem[] = Array.from({ length: 12 }).map((_, i) => ({
|
||||
id: `item-${i}`,
|
||||
title: i % 4 === 0 ? `工具 ${i + 1}` : `Application Long Name ${i + 1}`,
|
||||
iconUrl: i % 3 === 0 ? `https://api.dicebear.com/7.x/icons/svg?seed=${i}` : undefined,
|
||||
description: `Description for item ${i + 1}`
|
||||
}));
|
||||
mockData.unshift({
|
||||
id: 'item-search',
|
||||
title: 'win+d 显示桌面',
|
||||
data: { type: "hotkeys", hotkeys: "win+d" },
|
||||
iconUrl: 'https://api.dicebear.com/7.x/icons/svg?seed=search',
|
||||
description: '显示桌面'
|
||||
});
|
||||
mockData.unshift({
|
||||
id: 'item-recor',
|
||||
title: 'ctrl+alt+h 开始或停止语音转文字',
|
||||
data: { type: "hotkeys", hotkeys: "ctrl+alt+h" },
|
||||
iconUrl: 'https://api.dicebear.com/7.x/icons/svg?seed=record',
|
||||
description: '开始或停止语音转文字'
|
||||
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,文档)",
|
||||
})
|
||||
mockData.unshift({
|
||||
id: 'item-newtab',
|
||||
title: 'ctrl+t 打开浏览器新标签页',
|
||||
data: { type: "hotkeys", hotkeys: "ctrl+t" },
|
||||
iconUrl: 'https://api.dicebear.com/7.x/icons/svg?seed=newtab',
|
||||
description: '打开浏览器新标签页'
|
||||
})
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
set({ items: mockData, isLoading: false });
|
||||
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) => {
|
||||
// client/router?path=key-sender&keys=win+d
|
||||
console.log('Sending event for item:', item);
|
||||
const res = await query.post({
|
||||
path: 'key-sender',
|
||||
keys: item?.data?.hotkeys || 'win+d'
|
||||
});
|
||||
console.log('Event sent for item:', item, 'Response:', res);
|
||||
if (res.code !== 200) {
|
||||
alert('Failed to send event');
|
||||
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 : '导入失败'
|
||||
};
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user