+
setCategoryName(e.target.value)}
+ placeholder="请输入分类名称"
+ required
+ />
+
+
+
+
+ {colorOptions.map((option) => (
+
setCategoryColor(option.color)}
+ title={option.name}
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ {categories.map((category) => (
+
+
+
+
{category.name}
+
+ 使用次数: {getCategoryUsageCount(category._id)}
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+export default CategoriesPage;
\ No newline at end of file
diff --git a/src/pages/CreatePromptGroupPage.tsx b/src/pages/CreatePromptGroupPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d67153847e622f726608c44f84a0c9dba5477e00
--- /dev/null
+++ b/src/pages/CreatePromptGroupPage.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import Layout from '../components/Layout/Layout';
+import Card, { CardHeader, CardContent } from '../components/common/Card';
+import PromptGroupForm from '../components/PromptGroup/PromptGroupForm';
+import { useApp } from '../contexts/AppContext';
+
+const CreatePromptGroupPage: React.FC = () => {
+ const navigate = useNavigate();
+ const { addPromptGroup } = useApp();
+
+ const handleSubmit = (promptGroupData: { name: string; description: string; category: string }) => {
+ // Simply pass the form data directly to addPromptGroup
+ // The function itself will handle adding the additional properties
+ addPromptGroup(promptGroupData);
+ navigate('/');
+ };
+
+ return (
+
+
+
+
+ navigate('/')}
+ />
+
+
+
+ );
+};
+
+export default CreatePromptGroupPage;
\ No newline at end of file
diff --git a/src/pages/CreatePromptPage.tsx b/src/pages/CreatePromptPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6ceda78cdf25bfc943e63a58751f69f9bedb8ebe
--- /dev/null
+++ b/src/pages/CreatePromptPage.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import Layout from '../components/Layout/Layout';
+import Card, { CardHeader, CardContent } from '../components/common/Card';
+import PromptForm from '../components/Prompt/PromptForm';
+import { useApp } from '../contexts/AppContext';
+
+const CreatePromptPage: React.FC = () => {
+ const { id: groupId } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const { promptGroups, addPrompt } = useApp();
+
+ if (!groupId) {
+ return
提示词组ID无效
;
+ }
+
+ const promptGroup = promptGroups.find(group => group._id === groupId);
+
+ if (!promptGroup) {
+ return (
+
+
+
未找到提示词组
+
该提示词组可能已被删除
+
+
+ );
+ }
+
+ const handleSubmit = (promptData: Parameters
[1]) => {
+ addPrompt(groupId, promptData);
+ navigate(`/prompt-group/${groupId}`);
+ };
+
+ return (
+
+
+
+
+ navigate(`/prompt-group/${groupId}`)}
+ />
+
+
+
+ );
+};
+
+export default CreatePromptPage;
\ No newline at end of file
diff --git a/src/pages/EditPromptGroupPage.tsx b/src/pages/EditPromptGroupPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9d9996e543b9ace5e939f49576857ce66090722f
--- /dev/null
+++ b/src/pages/EditPromptGroupPage.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import Layout from '../components/Layout/Layout';
+import Card, { CardHeader, CardContent } from '../components/common/Card';
+import PromptGroupForm from '../components/PromptGroup/PromptGroupForm';
+import { useApp } from '../contexts/AppContext';
+
+const EditPromptGroupPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const { promptGroups, updatePromptGroup } = useApp();
+
+ if (!id) {
+ return 提示词组ID无效
;
+ }
+
+ const promptGroup = promptGroups.find(group => group._id === id);
+
+ if (!promptGroup) {
+ return (
+
+
+
未找到提示词组
+
该提示词组可能已被删除
+
+
+ );
+ }
+
+ const handleSubmit = (promptGroupData: { name: string; description: string; category: string }) => {
+ updatePromptGroup(promptGroup._id, promptGroupData);
+ navigate(`/prompt-group/${promptGroup._id}`);
+ };
+
+ return (
+
+
+
+
+ navigate(`/prompt-group/${promptGroup._id}`)}
+ />
+
+
+
+ );
+};
+
+export default EditPromptGroupPage;
\ No newline at end of file
diff --git a/src/pages/EditPromptPage.tsx b/src/pages/EditPromptPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4aa5d694828959445137763772e27c06bc1730f6
--- /dev/null
+++ b/src/pages/EditPromptPage.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import Layout from '../components/Layout/Layout';
+import Card, { CardHeader, CardContent } from '../components/common/Card';
+import PromptForm from '../components/Prompt/PromptForm';
+import { useApp } from '../contexts/AppContext';
+
+const EditPromptPage: React.FC = () => {
+ const { groupId, promptId } = useParams<{ groupId: string; promptId: string }>();
+ const navigate = useNavigate();
+ const { promptGroups, updatePrompt } = useApp();
+
+ if (!groupId || !promptId) {
+ return 参数无效
;
+ }
+
+ const promptGroup = promptGroups.find(group => group._id === groupId);
+
+ if (!promptGroup) {
+ return (
+
+
+
未找到提示词组
+
该提示词组可能已被删除
+
+
+ );
+ }
+
+ const prompt = promptGroup.prompts.find(p => p._id === promptId);
+
+ if (!prompt) {
+ return (
+
+
+
+ );
+ }
+
+ const handleSubmit = (promptData: { title: string; content: string; tags: string[] }) => {
+ updatePrompt(groupId, prompt._id, promptData);
+ navigate(`/prompt-group/${groupId}`);
+ };
+
+ return (
+
+
+
+
+ navigate(`/prompt-group/${groupId}`)}
+ />
+
+
+
+ );
+};
+
+export default EditPromptPage;
\ No newline at end of file
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eadb02233e7c0f018da6ab9789e90b27763eee76
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import Layout from '../components/Layout/Layout';
+import PromptGroupList from '../components/PromptGroup/PromptGroupList';
+import { useApp } from '../contexts/AppContext';
+
+const HomePage: React.FC = () => {
+ // eslint-disable-next-line
+ const { promptGroups } = useApp();
+
+ return (
+
+
+
+ );
+};
+
+export default HomePage;
\ No newline at end of file
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e12b9adbf7c4483e990e4d8669f8f1de3045455d
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,93 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import Button from '../components/common/Button';
+import Input from '../components/common/Input';
+
+const LoginPage: React.FC = () => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const { login, isAuthenticated, loading } = useAuth();
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ // Get the redirect path from location state or default to home
+ const from = location.state?.from?.pathname || '/';
+
+ // If already authenticated, redirect to the original page or home
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(from, { replace: true });
+ }
+ }, [isAuthenticated, navigate, from]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+
+ if (!username || !password) {
+ setError('请输入用户名和密码');
+ return;
+ }
+
+ try {
+ await login(username, password);
+ // Navigation is handled by the useEffect when isAuthenticated changes
+ } catch (err: any) {
+ console.error('Login error:', err);
+ setError(err.response?.data?.message || '登录失败,请检查凭据');
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
提示词管理系统
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ );
+};
+
+export default LoginPage;
\ No newline at end of file
diff --git a/src/pages/PromptGroupDetailPage.tsx b/src/pages/PromptGroupDetailPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b460ed883a6fad83a70a0e0143eafa353d9b59f1
--- /dev/null
+++ b/src/pages/PromptGroupDetailPage.tsx
@@ -0,0 +1,169 @@
+import React, { useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import Layout from '../components/Layout/Layout';
+import Card, { CardHeader, CardContent } from '../components/common/Card';
+import Button from '../components/common/Button';
+import PromptList from '../components/Prompt/PromptList';
+import DslFileList from '../components/DslFile/DslFileList';
+import DslFileUploader from '../components/DslFile/DslFileUploader';
+import CategoryBadge from '../components/Category/CategoryBadge';
+import { useApp } from '../contexts/AppContext';
+import { exportPromptGroupToZip } from '../utils/exportUtils';
+
+const PromptGroupDetailPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const {
+ promptGroups,
+ categories,
+ updatePromptGroup,
+ deletePromptGroup,
+ addPrompt,
+ updatePrompt,
+ deletePrompt
+ } = useApp();
+
+ const [activeTab, setActiveTab] = useState<'prompts' | 'dslFiles'>('prompts');
+
+ if (!id) {
+ return 提示词组ID无效
;
+ }
+
+ const promptGroup = promptGroups.find(group => group._id === id);
+
+ if (!promptGroup) {
+ return (
+
+
+
+
+
+
未找到提示词组
+
该提示词组可能已被删除
+
+
+
+ );
+ }
+
+ const category = categories.find(c => c._id === promptGroup.category);
+
+ const handleDeletePromptGroup = () => {
+ if (window.confirm('确定要删除此提示词组吗?此操作不可撤销,所有相关提示词和文件都将被删除。')) {
+ deletePromptGroup(promptGroup._id);
+ navigate('/');
+ }
+ };
+
+ const handleExportPromptGroup = () => {
+ exportPromptGroupToZip(promptGroup);
+ };
+
+ const formatDate = (date: string | Date) => {
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
+ return dateObj.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ };
+
+ return (
+
+ 导出
+
+ }
+ >
+
+
+
+
+
{promptGroup.name}
+ {category &&
}
+
{promptGroup.description || '无描述'}
+
+ 创建于 {formatDate(promptGroup.createdAt)}
+ •
+ 更新于 {formatDate(promptGroup.updatedAt)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {activeTab === 'prompts' && (
+
addPrompt(promptGroup._id, promptData)}
+ onUpdatePrompt={(promptId, promptData) => updatePrompt(promptGroup._id, promptId, promptData)}
+ onDeletePrompt={(promptId) => deletePrompt(promptGroup._id, promptId)}
+ />
+ )}
+
+ {activeTab === 'dslFiles' && (
+
+ )}
+
+ );
+};
+
+export default PromptGroupDetailPage;
\ No newline at end of file
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c995d42711df7d336f44a8dd428710f79568ab7a
--- /dev/null
+++ b/src/pages/SettingsPage.tsx
@@ -0,0 +1,157 @@
+import React, { useState } from 'react';
+import Layout from '../components/Layout/Layout';
+import Card, { CardHeader, CardContent } from '../components/common/Card';
+import Button from '../components/common/Button';
+import { useNavigate } from 'react-router-dom';
+
+const SettingsPage: React.FC = () => {
+ const navigate = useNavigate();
+ const [dataExported, setDataExported] = useState(false);
+
+ // 导出所有数据
+ const handleExportAllData = () => {
+ const promptGroups = localStorage.getItem('promptGroups');
+ const categories = localStorage.getItem('categories');
+
+ const allData = {
+ promptGroups: promptGroups ? JSON.parse(promptGroups) : [],
+ categories: categories ? JSON.parse(categories) : []
+ };
+
+ const jsonString = JSON.stringify(allData, null, 2);
+ const blob = new Blob([jsonString], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `prompt-manager-backup-${new Date().toISOString().split('T')[0]}.json`;
+ document.body.appendChild(a);
+ a.click();
+
+ // 清理
+ setTimeout(() => {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ setDataExported(true);
+
+ // 重置状态
+ setTimeout(() => setDataExported(false), 3000);
+ }, 0);
+ };
+
+ // 导入数据文件
+ const handleImportData = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ try {
+ const data = JSON.parse(e.target?.result as string);
+
+ if (data.promptGroups && Array.isArray(data.promptGroups)) {
+ localStorage.setItem('promptGroups', JSON.stringify(data.promptGroups));
+ }
+
+ if (data.categories && Array.isArray(data.categories)) {
+ localStorage.setItem('categories', JSON.stringify(data.categories));
+ }
+
+ alert('数据导入成功,应用将刷新以加载新数据。');
+ window.location.reload();
+ } catch (error) {
+ alert('导入失败,文件格式不正确。');
+ console.error('导入错误:', error);
+ }
+ };
+ reader.readAsText(file);
+ };
+
+ // 清除所有数据
+ const handleClearAllData = () => {
+ if (window.confirm('确定要清除所有数据吗?此操作不可撤销。')) {
+ localStorage.removeItem('promptGroups');
+ localStorage.removeItem('categories');
+ alert('所有数据已清除,应用将刷新。');
+ window.location.reload();
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
备份数据
+
+ 导出所有提示词组和分类数据为JSON文件,可用于备份或迁移。
+
+
+
+
+
+
+
+
导入数据
+
+ 从之前导出的JSON文件中恢复数据。将覆盖当前所有数据。
+
+
+
+
+
+
+
+
+
清除数据
+
+ 删除所有存储的提示词组和分类数据。此操作不可撤销。
+
+
+
+
+
+
+
+
+
+
+ 提示词管理应用
+
+ 版本 1.0.0
+
+
+ 这是一个用于管理提示词、工作流和DSL文件的本地应用。所有数据均存储在浏览器的本地存储中。
+
+
+
+
+
+ );
+};
+
+export default SettingsPage;
\ No newline at end of file
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7f40687c682a633b6157a9dee10d4b5c10a8b5c3
--- /dev/null
+++ b/src/reportWebVitals.ts
@@ -0,0 +1,15 @@
+import { ReportHandler } from 'web-vitals';
+
+const reportWebVitals = (onPerfEntry?: ReportHandler) => {
+ if (onPerfEntry && onPerfEntry instanceof Function) {
+ import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+ getCLS(onPerfEntry);
+ getFID(onPerfEntry);
+ getFCP(onPerfEntry);
+ getLCP(onPerfEntry);
+ getTTFB(onPerfEntry);
+ });
+ }
+};
+
+export default reportWebVitals;
\ No newline at end of file
diff --git a/src/services/api.ts b/src/services/api.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1aea9aed7bf33f567684e8da95a1955f84971d0c
--- /dev/null
+++ b/src/services/api.ts
@@ -0,0 +1,139 @@
+import axios from 'axios';
+
+// 创建 axios 实例
+const api = axios.create({
+ baseURL: 'https://samlax12-promptbackend.hf.space/api', // 使用实际后端地址
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+});
+
+// 请求拦截器,添加认证信息
+api.interceptors.request.use(config => {
+ const token = localStorage.getItem('authToken');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+}, error => {
+ return Promise.reject(error);
+});
+
+// 响应拦截器,处理错误
+api.interceptors.response.use(response => {
+ return response;
+}, error => {
+ if (error.response && error.response.status === 401) {
+ // 认证失败,清除登录信息并重定向到登录页
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('user');
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+});
+
+// 用户认证相关 API
+export const authAPI = {
+ login: async (username: string, password: string) => {
+ const response = await api.post('/auth/login', { username, password });
+ // eslint-disable-next-line
+ const { token, _id, username: user } = response.data;
+ localStorage.setItem('authToken', token);
+ localStorage.setItem('user', user);
+ return response.data;
+ },
+
+ getProfile: async () => {
+ return api.get('/auth/profile');
+ },
+
+ logout: () => {
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('user');
+ }
+};
+
+// 分类相关 API
+export const categoryAPI = {
+ getAll: async () => {
+ const response = await api.get('/categories');
+ return response.data;
+ },
+
+ create: async (data: { name: string; color: string }) => {
+ const response = await api.post('/categories', data);
+ return response.data;
+ },
+
+ update: async (id: string, data: { name: string; color: string }) => {
+ const response = await api.put(`/categories/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id: string) => {
+ const response = await api.delete(`/categories/${id}`);
+ return response.data;
+ }
+};
+
+// 提示词组相关 API
+export const promptGroupAPI = {
+ getAll: async () => {
+ const response = await api.get('/prompt-groups');
+ return response.data;
+ },
+
+ getById: async (id: string) => {
+ const response = await api.get(`/prompt-groups/${id}`);
+ return response.data;
+ },
+
+ create: async (data: { name: string; description: string; category: string }) => {
+ const response = await api.post('/prompt-groups', data);
+ return response.data;
+ },
+
+ update: async (id: string, data: { name: string; description: string; category: string }) => {
+ const response = await api.put(`/prompt-groups/${id}`, data);
+ return response.data;
+ },
+
+ delete: async (id: string) => {
+ const response = await api.delete(`/prompt-groups/${id}`);
+ return response.data;
+ },
+
+ // 提示词相关操作
+ addPrompt: async (groupId: string, data: { title: string; content: string; tags: string[] }) => {
+ const response = await api.post(`/prompt-groups/${groupId}/prompts`, data);
+ return response.data;
+ },
+
+ updatePrompt: async (groupId: string, promptId: string, data: { title: string; content: string; tags: string[] }) => {
+ const response = await api.put(`/prompt-groups/${groupId}/prompts/${promptId}`, data);
+ return response.data;
+ },
+
+ deletePrompt: async (groupId: string, promptId: string) => {
+ const response = await api.delete(`/prompt-groups/${groupId}/prompts/${promptId}`);
+ return response.data;
+ },
+
+ // DSL 文件相关操作 - 更新为支持内容上传
+ addDslFile: async (groupId: string, data: { name: string; content?: string; fileData?: string; mimeType?: string }) => {
+ const response = await api.post(`/prompt-groups/${groupId}/dsl-files`, data);
+ return response.data;
+ },
+
+ updateDslFile: async (groupId: string, fileId: string, data: { name?: string; content?: string }) => {
+ const response = await api.put(`/prompt-groups/${groupId}/dsl-files/${fileId}`, data);
+ return response.data;
+ },
+
+ deleteDslFile: async (groupId: string, fileId: string) => {
+ const response = await api.delete(`/prompt-groups/${groupId}/dsl-files/${fileId}`);
+ return response.data;
+ }
+};
+
+export default api;
\ No newline at end of file
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/styles/global.css b/src/styles/global.css
new file mode 100644
index 0000000000000000000000000000000000000000..d1b8a46cf2ba9c048e40caf1d14584c75275a155
--- /dev/null
+++ b/src/styles/global.css
@@ -0,0 +1,71 @@
+/* Tailwind基础样式 */
+@import 'tailwindcss/base';
+@import 'tailwindcss/components';
+@import 'tailwindcss/utilities';
+
+/* 全局样式 */
+html, body {
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ color: #000000;
+ height: 100%;
+}
+
+#root {
+ min-height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+/* 滚动条样式 */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #d1d1d6;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #c7c7cc;
+}
+
+/* 文本溢出处理 */
+.line-clamp-1 {
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.line-clamp-2 {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.line-clamp-3 {
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+/* 修复安全区域问题 */
+@supports (padding: max(0px)) {
+ .ios-safe-padding-bottom {
+ padding-bottom: max(16px, env(safe-area-inset-bottom));
+ }
+
+ .ios-toolbar {
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+}
\ No newline at end of file
diff --git a/src/styles/iosStyles.css b/src/styles/iosStyles.css
new file mode 100644
index 0000000000000000000000000000000000000000..6ca282177e4de7caf67df6f42e285b1a892e4b77
--- /dev/null
+++ b/src/styles/iosStyles.css
@@ -0,0 +1,481 @@
+/* iOS风格全局样式 */
+:root {
+ /* iOS 风格颜色 */
+ --ios-blue: #007AFF;
+ --ios-green: #4CD964;
+ --ios-red: #FF3B30;
+ --ios-orange: #FF9500;
+ --ios-yellow: #FFCC00;
+ --ios-purple: #5856D6;
+ --ios-pink: #FF2D55;
+ --ios-teal: #5AC8FA;
+ --ios-indigo: #5E5CE6;
+
+ /* 系统灰色 */
+ --ios-gray: #8E8E93;
+ --ios-gray2: #AEAEB2;
+ --ios-gray3: #C7C7CC;
+ --ios-gray4: #D1D1D6;
+ --ios-gray5: #E5E5EA;
+ --ios-gray6: #F2F2F7;
+
+ /* 背景色 */
+ --ios-background: #F2F2F7;
+ --ios-card-background: #FFFFFF;
+
+ /* 字体大小 */
+ --ios-font-small: 13px;
+ --ios-font-medium: 15px;
+ --ios-font-large: 17px;
+ --ios-font-xlarge: 20px;
+ --ios-font-xxlarge: 22px;
+ --ios-font-title: 28px;
+
+ /* 圆角 */
+ --ios-radius-small: 6px;
+ --ios-radius-medium: 10px;
+ --ios-radius-large: 14px;
+
+ /* 内边距 */
+ --ios-padding-small: 8px;
+ --ios-padding-medium: 16px;
+ --ios-padding-large: 24px;
+
+ /* 动画 */
+ --ios-transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ }
+
+ /* 全局样式重置 */
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
+ }
+
+ body {
+ background-color: var(--ios-background);
+ color: #000000;
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+
+ /* iOS风格容器 */
+ .ios-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: var(--ios-padding-medium);
+ }
+
+ /* iOS风格页面标题 */
+ .ios-page-title {
+ font-size: var(--ios-font-title);
+ font-weight: 700;
+ margin-bottom: var(--ios-padding-medium);
+ }
+
+ /* iOS风格卡片 */
+ .ios-card {
+ background-color: var(--ios-card-background);
+ border-radius: var(--ios-radius-medium);
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
+ overflow: hidden;
+ margin-bottom: var(--ios-padding-medium);
+ transition: var(--ios-transition);
+ }
+
+ .ios-card:hover {
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+ transform: translateY(-2px);
+ }
+
+ /* iOS风格按钮 */
+ .ios-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 44px;
+ padding: 0 var(--ios-padding-medium);
+ border-radius: 22px;
+ font-size: var(--ios-font-medium);
+ font-weight: 600;
+ text-align: center;
+ transition: var(--ios-transition);
+ cursor: pointer;
+ user-select: none;
+ border: none;
+ outline: none;
+ }
+
+ .ios-button-primary {
+ background-color: var(--ios-blue);
+ color: white;
+ }
+
+ .ios-button-primary:hover {
+ background-color: #0071EB;
+ }
+
+ .ios-button-secondary {
+ background-color: var(--ios-gray5);
+ color: #000000;
+ }
+
+ .ios-button-secondary:hover {
+ background-color: var(--ios-gray4);
+ }
+
+ .ios-button-danger {
+ background-color: var(--ios-red);
+ color: white;
+ }
+
+ .ios-button-danger:hover {
+ background-color: #FF2D30;
+ }
+
+ /* iOS风格输入框 */
+ .ios-input {
+ width: 100%;
+ height: 44px;
+ padding: 0 var(--ios-padding-medium);
+ font-size: var(--ios-font-medium);
+ background-color: var(--ios-gray6);
+ border-radius: var(--ios-radius-medium);
+ border: none;
+ transition: var(--ios-transition);
+ }
+
+ .ios-input:focus {
+ background-color: white;
+ box-shadow: 0 0 0 2px var(--ios-blue);
+ outline: none;
+ }
+
+ /* iOS风格文本域 */
+ .ios-textarea {
+ width: 100%;
+ min-height: 100px;
+ padding: var(--ios-padding-medium);
+ font-size: var(--ios-font-medium);
+ background-color: var(--ios-gray6);
+ border-radius: var(--ios-radius-medium);
+ border: none;
+ resize: vertical;
+ transition: var(--ios-transition);
+ }
+
+ .ios-textarea:focus {
+ background-color: white;
+ box-shadow: 0 0 0 2px var(--ios-blue);
+ outline: none;
+ }
+
+ /* iOS风格标签 */
+ .ios-tag {
+ display: inline-flex;
+ align-items: center;
+ height: 24px;
+ padding: 0 10px;
+ font-size: var(--ios-font-small);
+ font-weight: 500;
+ border-radius: 12px;
+ margin-right: 8px;
+ margin-bottom: 8px;
+ }
+
+ /* iOS风格导航栏 */
+ .ios-navbar {
+ display: flex;
+ align-items: center;
+ height: 44px;
+ padding: 0 var(--ios-padding-medium);
+ background-color: rgba(255, 255, 255, 0.8);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ border-bottom: 1px solid var(--ios-gray5);
+ }
+
+ .ios-navbar-title {
+ font-size: var(--ios-font-large);
+ font-weight: 600;
+ flex: 1;
+ text-align: center;
+ }
+
+ .ios-navbar-button {
+ font-size: var(--ios-font-medium);
+ font-weight: 500;
+ color: var(--ios-blue);
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0 10px;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ }
+
+ /* iOS风格底部工具栏 */
+ .ios-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ height: 50px;
+ background-color: rgba(255, 255, 255, 0.8);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-top: 1px solid var(--ios-gray5);
+ padding-bottom: env(safe-area-inset-bottom);
+ z-index: 100;
+ }
+
+ .ios-toolbar-item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ font-size: 10px;
+ color: var(--ios-gray);
+ }
+
+ .ios-toolbar-item.active {
+ color: var(--ios-blue);
+ }
+
+ /* iOS风格列表 */
+ .ios-list {
+ background-color: white;
+ border-radius: var(--ios-radius-medium);
+ overflow: hidden;
+ }
+
+ .ios-list-item {
+ display: flex;
+ align-items: center;
+ min-height: 44px;
+ padding: 0 var(--ios-padding-medium);
+ border-bottom: 1px solid var(--ios-gray5);
+ }
+
+ .ios-list-item:last-child {
+ border-bottom: none;
+ }
+
+ .ios-list-item-content {
+ flex: 1;
+ }
+
+ .ios-list-item-title {
+ font-size: var(--ios-font-medium);
+ font-weight: 400;
+ }
+
+ .ios-list-item-subtitle {
+ font-size: var(--ios-font-small);
+ color: var(--ios-gray);
+ margin-top: 2px;
+ }
+
+ .ios-list-item-chevron {
+ color: var(--ios-gray3);
+ margin-left: var(--ios-padding-small);
+ }
+
+ /* iOS风格分割线 */
+ .ios-divider {
+ height: 1px;
+ background-color: var(--ios-gray5);
+ margin: var(--ios-padding-medium) 0;
+ }
+
+ /* iOS风格开关 */
+ .ios-switch {
+ position: relative;
+ display: inline-block;
+ width: 51px;
+ height: 31px;
+ }
+
+ .ios-switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ .ios-switch-slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: var(--ios-gray4);
+ border-radius: 34px;
+ transition: var(--ios-transition);
+ }
+
+ .ios-switch-slider:before {
+ position: absolute;
+ content: "";
+ height: 27px;
+ width: 27px;
+ left: 2px;
+ bottom: 2px;
+ background-color: white;
+ border-radius: 50%;
+ transition: var(--ios-transition);
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+ }
+
+ .ios-switch input:checked + .ios-switch-slider {
+ background-color: var(--ios-green);
+ }
+
+ .ios-switch input:checked + .ios-switch-slider:before {
+ transform: translateX(20px);
+ }
+
+ /* iOS风格模态框 */
+ .ios-modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ opacity: 0;
+ pointer-events: none;
+ transition: var(--ios-transition);
+ }
+
+ .ios-modal-backdrop.open {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .ios-modal {
+ width: 90%;
+ max-width: 500px;
+ background-color: white;
+ border-radius: var(--ios-radius-large);
+ overflow: hidden;
+ transform: scale(0.9);
+ transition: var(--ios-transition);
+ }
+
+ .ios-modal-backdrop.open .ios-modal {
+ transform: scale(1);
+ }
+
+ .ios-modal-header {
+ padding: var(--ios-padding-medium);
+ text-align: center;
+ border-bottom: 1px solid var(--ios-gray5);
+ }
+
+ .ios-modal-title {
+ font-size: var(--ios-font-large);
+ font-weight: 600;
+ }
+
+ .ios-modal-body {
+ padding: var(--ios-padding-medium);
+ max-height: 50vh;
+ overflow-y: auto;
+ }
+
+ .ios-modal-footer {
+ display: flex;
+ border-top: 1px solid var(--ios-gray5);
+ }
+
+ .ios-modal-button {
+ flex: 1;
+ height: 44px;
+ border: none;
+ background: none;
+ font-size: var(--ios-font-medium);
+ font-weight: 500;
+ cursor: pointer;
+ transition: var(--ios-transition);
+ }
+
+ .ios-modal-button:not(:last-child) {
+ border-right: 1px solid var(--ios-gray5);
+ }
+
+ .ios-modal-button-primary {
+ color: var(--ios-blue);
+ }
+
+ .ios-modal-button-primary:hover {
+ background-color: rgba(0, 122, 255, 0.1);
+ }
+
+ .ios-modal-button-danger {
+ color: var(--ios-red);
+ }
+
+ .ios-modal-button-danger:hover {
+ background-color: rgba(255, 59, 48, 0.1);
+ }
+
+ /* iOS风格加载指示器 */
+ .ios-loader {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 2px solid rgba(0, 122, 255, 0.3);
+ border-radius: 50%;
+ border-top-color: var(--ios-blue);
+ animation: ios-spin 1s linear infinite;
+ }
+
+ @keyframes ios-spin {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+
+ /* iOS风格空状态 */
+ .ios-empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+ padding: var(--ios-padding-large);
+ text-align: center;
+ }
+
+ .ios-empty-state-icon {
+ font-size: 48px;
+ color: var(--ios-gray3);
+ margin-bottom: var(--ios-padding-medium);
+ }
+
+ .ios-empty-state-title {
+ font-size: var(--ios-font-large);
+ font-weight: 600;
+ margin-bottom: 8px;
+ }
+
+ .ios-empty-state-text {
+ font-size: var(--ios-font-medium);
+ color: var(--ios-gray);
+ max-width: 250px;
+ }
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b28853e110705bc91209f46e1b0256d247c3c70d
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,41 @@
+// 提示词项目组
+export interface PromptGroup {
+ _id: string; // 后端使用 _id 而不是 id
+ name: string;
+ description: string;
+ category: string | Category;
+ createdAt: string; // 后端返回的是字符串而不是 Date 对象
+ updatedAt: string;
+ prompts: Prompt[];
+ workflows: string[];
+ dslFiles: DslFile[];
+ createdBy?: string;
+}
+
+// 单个提示词
+export interface Prompt {
+ _id: string; // 后端使用 _id 而不是 id
+ title: string;
+ content: string;
+ tags: string[];
+ createdAt: string; // 字符串格式的日期
+ updatedAt: string;
+}
+
+// DSL文件 - 修改后的接口
+export interface DslFile {
+ _id: string; // 后端使用 _id 而不是 id
+ name: string;
+ content: string; // 修改: 用于存储 YAML 文本内容
+ mimeType: string;
+ uploadedAt: string; // 字符串格式的日期
+}
+
+// 分类
+export interface Category {
+ _id: string; // 后端使用 _id 而不是 id
+ name: string;
+ color: string;
+ createdAt?: string;
+ updatedAt?: string;
+}
\ No newline at end of file
diff --git a/src/utils/exportUtils.ts b/src/utils/exportUtils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..36952414e6793554e746c64392c3dcfe59a3dd50
--- /dev/null
+++ b/src/utils/exportUtils.ts
@@ -0,0 +1,110 @@
+import { PromptGroup, Prompt, DslFile } from '../types';
+import JSZip from 'jszip';
+import { saveAs } from 'file-saver';
+
+// 将提示词转换为 Markdown 格式
+const formatPromptToMarkdown = (prompt: Prompt): string => {
+ return `# ${prompt.title}
+
+${prompt.content}
+
+## 标签
+${prompt.tags.map(tag => `- ${tag}`).join('\n')}
+
+## 元数据
+- 创建时间: ${new Date(prompt.createdAt).toLocaleString()}
+- 更新时间: ${new Date(prompt.updatedAt).toLocaleString()}
+`;
+};
+
+// 导出提示词组为 ZIP
+export const exportPromptGroupToZip = async (group: PromptGroup) => {
+ const zip = new JSZip();
+
+ // 创建提示词文件夹
+ const promptsFolder = zip.folder('prompts');
+ if (!promptsFolder) throw new Error('创建提示词文件夹失败');
+
+ // 添加提示词为 Markdown 文件
+ group.prompts.forEach(prompt => {
+ const content = formatPromptToMarkdown(prompt);
+ promptsFolder.file(`${prompt.title.replace(/[\\/:*?"<>|]/g, '_')}.md`, content);
+ });
+
+ // 创建 DSL 文件文件夹
+ const dslFolder = zip.folder('dsl_files');
+ if (!dslFolder) throw new Error('创建 DSL 文件夹失败');
+
+ // 添加 DSL 文件 - 直接使用文本内容
+ group.dslFiles.forEach(file => {
+ try {
+ dslFolder.file(file.name, file.content);
+ } catch (error) {
+ console.error(`处理文件 ${file.name} 时出错:`, error);
+ }
+ });
+
+ // 添加元数据文件
+ const metadata = JSON.stringify({
+ name: group.name,
+ description: group.description,
+ category: typeof group.category === 'object' ? group.category.name : group.category,
+ createdAt: group.createdAt,
+ updatedAt: group.updatedAt,
+ promptCount: group.prompts.length,
+ dslFileCount: group.dslFiles.length
+ }, null, 2);
+
+ zip.file('metadata.json', metadata);
+
+ // 创建并下载 ZIP 文件
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
+ const fileName = `${group.name.replace(/[\\/:*?"<>|]/g, '_')}_${new Date().toISOString().split('T')[0]}.zip`;
+ saveAs(zipBlob, fileName);
+};
+
+// 导出单个提示词为 Markdown
+export const exportPrompt = (prompt: Prompt) => {
+ const content = formatPromptToMarkdown(prompt);
+ const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
+ saveAs(blob, `${prompt.title.replace(/[\\/:*?"<>|]/g, '_')}.md`);
+};
+
+// 导出多个提示词为 ZIP
+export const exportMultiplePrompts = async (prompts: Prompt[]) => {
+ const zip = new JSZip();
+
+ prompts.forEach(prompt => {
+ const content = formatPromptToMarkdown(prompt);
+ zip.file(`${prompt.title.replace(/[\\/:*?"<>|]/g, '_')}.md`, content);
+ });
+
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
+ saveAs(zipBlob, `prompts_${new Date().toISOString().split('T')[0]}.zip`);
+};
+
+// 导出单个 DSL 文件
+export const exportDslFile = (file: DslFile) => {
+ const blob = new Blob([file.content], { type: 'text/yaml;charset=utf-8' });
+ saveAs(blob, file.name);
+};
+
+// 导出多个 DSL 文件为 ZIP
+export const exportMultipleDslFiles = async (files: DslFile[]) => {
+ const zip = new JSZip();
+
+ files.forEach(file => {
+ zip.file(file.name, file.content);
+ });
+
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
+ saveAs(zipBlob, `dsl_files_${new Date().toISOString().split('T')[0]}.zip`);
+};
+// eslint-disable-next-line
+export default {
+ exportPrompt,
+ exportMultiplePrompts,
+ exportPromptGroupToZip,
+ exportDslFile,
+ exportMultipleDslFiles
+};
\ No newline at end of file
diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2df7ebaa873b16b554f3721175639b4348e039b5
--- /dev/null
+++ b/src/utils/fileUtils.ts
@@ -0,0 +1,143 @@
+/**
+ * 文件工具函数,提供文件处理相关的实用方法
+ */
+
+/**
+ * 将文件转换为Base64编码字符串
+ * @param file 文件对象
+ * @returns Promise,解析为Base64编码的字符串
+ */
+export const fileToBase64 = (file: File): Promise => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onload = () => {
+ if (typeof reader.result === 'string') {
+ // 从data URI中提取Base64部分
+ const base64String = reader.result.split(',')[1];
+ resolve(base64String);
+ } else {
+ reject(new Error('读取文件失败'));
+ }
+ };
+
+ reader.onerror = () => {
+ reject(new Error('读取文件时发生错误'));
+ };
+
+ reader.readAsDataURL(file);
+ });
+ };
+
+ /**
+ * 将Base64编码字符串转换回Blob对象
+ * @param base64 Base64编码的字符串
+ * @param mimeType MIME类型
+ * @returns Blob对象
+ */
+ export const base64ToBlob = (base64: string, mimeType: string): Blob => {
+ const byteCharacters = atob(base64);
+ const byteArrays = [];
+
+ for (let offset = 0; offset < byteCharacters.length; offset += 512) {
+ const slice = byteCharacters.slice(offset, offset + 512);
+
+ const byteNumbers = new Array(slice.length);
+ for (let i = 0; i < slice.length; i++) {
+ byteNumbers[i] = slice.charCodeAt(i);
+ }
+
+ const byteArray = new Uint8Array(byteNumbers);
+ byteArrays.push(byteArray);
+ }
+
+ return new Blob(byteArrays, { type: mimeType });
+ };
+
+ /**
+ * 获取文件扩展名
+ * @param fileName 文件名
+ * @returns 文件扩展名(小写,不包含点号)
+ */
+ export const getFileExtension = (fileName: string): string => {
+ return fileName.split('.').pop()?.toLowerCase() || '';
+ };
+
+ /**
+ * 根据文件扩展名猜测MIME类型
+ * @param fileName 文件名
+ * @returns MIME类型
+ */
+ export const guessMimeType = (fileName: string): string => {
+ const extension = getFileExtension(fileName);
+
+ const mimeTypes: Record = {
+ 'json': 'application/json',
+ 'txt': 'text/plain',
+ 'md': 'text/markdown',
+ 'dsl': 'application/octet-stream',
+ 'js': 'application/javascript',
+ 'ts': 'application/typescript',
+ 'html': 'text/html',
+ 'css': 'text/css',
+ 'csv': 'text/csv',
+ 'xml': 'application/xml',
+ 'pdf': 'application/pdf',
+ 'doc': 'application/msword',
+ 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'xls': 'application/vnd.ms-excel',
+ 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'ppt': 'application/vnd.ms-powerpoint',
+ 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'png': 'image/png',
+ 'gif': 'image/gif',
+ 'svg': 'image/svg+xml',
+ 'zip': 'application/zip',
+ 'rar': 'application/x-rar-compressed',
+ '7z': 'application/x-7z-compressed',
+ };
+
+ return mimeTypes[extension] || 'application/octet-stream';
+ };
+
+ /**
+ * 格式化文件大小为人类可读格式
+ * @param bytes 文件大小(字节)
+ * @param decimals 小数位数
+ * @returns 格式化后的文件大小字符串
+ */
+ export const formatFileSize = (bytes: number, decimals: number = 2): string => {
+ if (bytes === 0) return '0 Bytes';
+
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+ };
+
+ /**
+ * 检查文件是否为有效的DSL文件类型
+ * @param file 文件对象
+ * @returns 布尔值,指示文件是否为有效的DSL文件类型
+ */
+ export const isValidDslFile = (file: File): boolean => {
+ const validExtensions = ['dsl', 'json', 'txt'];
+ const extension = getFileExtension(file.name);
+
+ return validExtensions.includes(extension);
+ };
+
+ /**
+ * 安全的文件名生成(移除不安全字符)
+ * @param fileName 原始文件名
+ * @returns 安全的文件名
+ */
+ export const sanitizeFileName = (fileName: string): string => {
+ // 移除文件名中的不安全字符
+ return fileName.replace(/[\\/:*?"<>|]/g, '_');
+ };
\ No newline at end of file
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f6c3ae6fb5b762d7b36c5decc650c2a51da7521a
--- /dev/null
+++ b/src/utils/helpers.ts
@@ -0,0 +1,237 @@
+/**
+ * 通用辅助工具函数
+ */
+
+/**
+ * 生成唯一ID
+ * @returns 唯一ID字符串
+ */
+export const generateId = (): string => {
+ return Date.now().toString(36) + Math.random().toString(36).substring(2);
+ };
+
+ /**
+ * 格式化日期为本地字符串
+ * @param date 日期对象或日期字符串
+ * @param options 格式化选项
+ * @returns 格式化后的日期字符串
+ */
+ export const formatDate = (
+ date: Date | string,
+ options: Intl.DateTimeFormatOptions = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ }
+ ): string => {
+ const dateObj = date instanceof Date ? date : new Date(date);
+ return dateObj.toLocaleDateString('zh-CN', options);
+ };
+
+ /**
+ * 格式化日期和时间为本地字符串
+ * @param date 日期对象或日期字符串
+ * @returns 格式化后的日期和时间字符串
+ */
+ export const formatDateTime = (date: Date | string): string => {
+ const dateObj = date instanceof Date ? date : new Date(date);
+ return dateObj.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ /**
+ * 截断文本到指定长度
+ * @param text 需要截断的文本
+ * @param maxLength 最大长度
+ * @param suffix 省略号后缀,默认为"..."
+ * @returns 截断后的文本
+ */
+ export const truncateText = (
+ text: string,
+ maxLength: number,
+ suffix: string = '...'
+ ): string => {
+ if (text.length <= maxLength) return text;
+ return text.substring(0, maxLength) + suffix;
+ };
+
+ /**
+ * 深度克隆对象
+ * @param obj 需要克隆的对象
+ * @returns 克隆后的对象
+ */
+ export const deepClone = (obj: T): T => {
+ return JSON.parse(JSON.stringify(obj));
+ };
+
+ /**
+ * 按属性对对象数组进行排序
+ * @param array 对象数组
+ * @param key 排序键
+ * @param ascending 是否升序,默认为true
+ * @returns 排序后的数组
+ */
+ export const sortByProperty = (
+ array: T[],
+ key: keyof T,
+ ascending: boolean = true
+ ): T[] => {
+ return [...array].sort((a, b) => {
+ const valueA = a[key];
+ const valueB = b[key];
+
+ if (valueA === valueB) return 0;
+
+ // 处理日期对象
+ if (valueA instanceof Date && valueB instanceof Date) {
+ return ascending ? valueA.getTime() - valueB.getTime() : valueB.getTime() - valueA.getTime();
+ }
+
+ // 处理字符串
+ if (typeof valueA === 'string' && typeof valueB === 'string') {
+ return ascending ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
+ }
+
+ // 处理数字
+ if (typeof valueA === 'number' && typeof valueB === 'number') {
+ return ascending ? valueA - valueB : valueB - valueA;
+ }
+
+ // 默认使用字符串比较
+ return ascending
+ ? String(valueA).localeCompare(String(valueB))
+ : String(valueB).localeCompare(String(valueA));
+ });
+ };
+
+ /**
+ * 延迟执行(Promise形式)
+ * @param ms 延迟毫秒数
+ * @returns Promise
+ */
+ export const delay = (ms: number): Promise => {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ };
+
+ /**
+ * 防抖函数
+ * @param func 要执行的函数
+ * @param wait 等待时间(毫秒)
+ * @returns 防抖处理后的函数
+ */
+ export function debounce any>(
+ func: T,
+ wait: number
+ ): (...args: Parameters) => void {
+ let timeout: NodeJS.Timeout | null = null;
+
+ return function (...args: Parameters) {
+ const later = () => {
+ timeout = null;
+ func(...args);
+ };
+
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+ }
+
+ /**
+ * 节流函数
+ * @param func 要执行的函数
+ * @param limit 时间限制(毫秒)
+ * @returns 节流处理后的函数
+ */
+ export function throttle any>(
+ func: T,
+ limit: number
+ ): (...args: Parameters) => void {
+ let inThrottle = false;
+
+ return function (...args: Parameters) {
+ if (!inThrottle) {
+ func(...args);
+ inThrottle = true;
+ setTimeout(() => {
+ inThrottle = false;
+ }, limit);
+ }
+ };
+ }
+
+ /**
+ * 过滤对象数组
+ * @param array 对象数组
+ * @param searchTerm 搜索关键词
+ * @param keys 要搜索的属性键数组
+ * @returns 过滤后的数组
+ */
+ export const filterArrayBySearchTerm = (
+ array: T[],
+ searchTerm: string,
+ keys: (keyof T)[]
+ ): T[] => {
+ if (!searchTerm) return array;
+
+ const lowercasedTerm = searchTerm.toLowerCase();
+
+ return array.filter(item => {
+ return keys.some(key => {
+ const value = item[key];
+ if (value == null) return false;
+ return String(value).toLowerCase().includes(lowercasedTerm);
+ });
+ });
+ };
+
+ /**
+ * 分组对象数组
+ * @param array 对象数组
+ * @param key 分组键
+ * @returns 分组后的对象
+ */
+ export const groupBy = (array: T[], key: keyof T): Record => {
+ return array.reduce((result, item) => {
+ const groupKey = String(item[key]);
+ if (!result[groupKey]) {
+ result[groupKey] = [];
+ }
+ result[groupKey].push(item);
+ return result;
+ }, {} as Record);
+ };
+
+ /**
+ * 检查对象是否为空
+ * @param obj 要检查的对象
+ * @returns 布尔值,指示对象是否为空
+ */
+ export const isEmptyObject = (obj: Record): boolean => {
+ return Object.keys(obj).length === 0;
+ };
+
+ /**
+ * 从数组中移除重复项
+ * @param array 数组
+ * @returns 去重后的数组
+ */
+ export const removeDuplicates = (array: T[]): T[] => {
+ return [...new Set(array)];
+ };
+
+ /**
+ * 从日期对象获取YYYY-MM-DD格式的日期字符串
+ * @param date 日期对象
+ * @returns 格式化后的日期字符串
+ */
+ export const getFormattedDate = (date: Date): string => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+ };
\ No newline at end of file