samlax12 commited on
Commit
4fc8eae
·
verified ·
1 Parent(s): 8f4172c

Upload 54 files

Browse files
src/components/PromptGroup/PromptGroupCard.tsx CHANGED
@@ -17,7 +17,12 @@ const PromptGroupCard: React.FC<PromptGroupCardProps> = ({
17
  const navigate = useNavigate();
18
  const { categories } = useApp();
19
 
20
- const category = categories.find(c => c._id === promptGroup.category);
 
 
 
 
 
21
 
22
  const handleClick = () => {
23
  navigate(`/prompt-group/${promptGroup._id}`);
 
17
  const navigate = useNavigate();
18
  const { categories } = useApp();
19
 
20
+ // 查找分类对象 - 支持两种可能的数据结构
21
+ const categoryId = typeof promptGroup.category === 'object'
22
+ ? promptGroup.category._id
23
+ : promptGroup.category;
24
+
25
+ const category = categories.find(c => c._id === categoryId);
26
 
27
  const handleClick = () => {
28
  navigate(`/prompt-group/${promptGroup._id}`);
src/components/PromptGroup/PromptGroupDetail.tsx CHANGED
@@ -23,7 +23,12 @@ const PromptGroupDetail: React.FC<PromptGroupDetailProps> = ({
23
  const { categories } = useApp();
24
  const [showDeleteModal, setShowDeleteModal] = useState(false);
25
 
26
- const category = categories.find(c => c._id === promptGroup.category);
 
 
 
 
 
27
 
28
  const formatDate = (date: string | Date) => {
29
  const dateObj = typeof date === 'string' ? new Date(date) : date;
 
23
  const { categories } = useApp();
24
  const [showDeleteModal, setShowDeleteModal] = useState(false);
25
 
26
+ // 查找分类对象 - 支持两种可能的数据结构
27
+ const categoryId = typeof promptGroup.category === 'object'
28
+ ? promptGroup.category._id
29
+ : promptGroup.category;
30
+
31
+ const category = categories.find(c => c._id === categoryId);
32
 
33
  const formatDate = (date: string | Date) => {
34
  const dateObj = typeof date === 'string' ? new Date(date) : date;
src/components/PromptGroup/PromptGroupList.tsx CHANGED
@@ -1,101 +1,106 @@
1
- import React, { useState } from 'react';
2
- // eslint-disable-next-line
3
- import { PromptGroup } from '../../types';
4
- import PromptGroupCard from './PromptGroupCard';
5
- import { useApp } from '../../contexts/AppContext';
6
- import CategorySelector from '../Category/CategorySelector';
7
- import Input from '../common/Input';
8
-
9
- const PromptGroupList: React.FC = () => {
10
- // eslint-disable-next-line
11
- const { promptGroups, categories } = useApp();
12
- const [searchTerm, setSearchTerm] = useState('');
13
- const [selectedCategory, setSelectedCategory] = useState<string>('');
14
-
15
- // 搜索和过滤提示词组
16
- const filteredPromptGroups = promptGroups.filter((group) => {
17
- const matchesSearch =
18
- group.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
19
- group.description.toLowerCase().includes(searchTerm.toLowerCase());
20
-
21
- const matchesCategory =
22
- selectedCategory === '' || group.category === selectedCategory;
23
-
24
- return matchesSearch && matchesCategory;
25
- });
26
-
27
- const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
28
- setSearchTerm(e.target.value);
29
- };
30
-
31
- const handleCategoryChange = (categoryId: string) => {
32
- setSelectedCategory(categoryId);
33
- };
34
-
35
- const resetFilters = () => {
36
- setSearchTerm('');
37
- setSelectedCategory('');
38
- };
39
-
40
- return (
41
- <div>
42
- <div className="mb-4 flex flex-col sm:flex-row gap-3">
43
- <Input
44
- placeholder="搜索提示词组..."
45
- value={searchTerm}
46
- onChange={handleSearch}
47
- className="flex-1"
48
- style={{ paddingLeft: "40px" }} /* Add explicit inline padding */
49
- icon={
50
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
51
- <circle cx="11" cy="11" r="8"></circle>
52
- <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
53
- </svg>
54
- }
55
- />
56
- <div className="flex gap-2">
57
- <CategorySelector
58
- selectedCategory={selectedCategory}
59
- onChange={handleCategoryChange}
60
- />
61
- {(searchTerm || selectedCategory) && (
62
- <button
63
- className="ios-navbar-button"
64
- onClick={resetFilters}
65
- >
66
- 清除筛选
67
- </button>
68
- )}
69
- </div>
70
- </div>
71
-
72
- {filteredPromptGroups.length === 0 ? (
73
- <div className="ios-empty-state">
74
- <div className="ios-empty-state-icon">
75
- <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
76
- <circle cx="11" cy="11" r="8"></circle>
77
- <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
78
- </svg>
79
- </div>
80
- <h3 className="ios-empty-state-title">未找到提示词组</h3>
81
- <p className="ios-empty-state-text">
82
- {searchTerm || selectedCategory
83
- ? '请尝试调整筛选条件'
84
- : '点击底部的"新建"按钮创建您的第一个提示词组'}
85
- </p>
86
- </div>
87
- ) : (
88
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
89
- {filteredPromptGroups.map((group) => (
90
- <PromptGroupCard
91
- key={group._id}
92
- promptGroup={group}
93
- />
94
- ))}
95
- </div>
96
- )}
97
- </div>
98
- );
99
- };
100
-
 
 
 
 
 
101
  export default PromptGroupList;
 
1
+ import React, { useState } from 'react';
2
+ // eslint-disable-next-line
3
+ import { PromptGroup } from '../../types';
4
+ import PromptGroupCard from './PromptGroupCard';
5
+ import { useApp } from '../../contexts/AppContext';
6
+ import CategorySelector from '../Category/CategorySelector';
7
+ import Input from '../common/Input';
8
+
9
+ const PromptGroupList: React.FC = () => {
10
+ // eslint-disable-next-line
11
+ const { promptGroups, categories } = useApp();
12
+ const [searchTerm, setSearchTerm] = useState('');
13
+ const [selectedCategory, setSelectedCategory] = useState<string>('');
14
+
15
+ // 搜索和过滤提示词组
16
+ const filteredPromptGroups = promptGroups.filter((group) => {
17
+ const matchesSearch =
18
+ group.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
19
+ (group.description?.toLowerCase() || '').includes(searchTerm.toLowerCase());
20
+
21
+ // 处理 category 可能是对象或字符串的情况
22
+ const groupCategoryId = typeof group.category === 'object'
23
+ ? group.category._id
24
+ : group.category;
25
+
26
+ const matchesCategory =
27
+ selectedCategory === '' || groupCategoryId === selectedCategory;
28
+
29
+ return matchesSearch && matchesCategory;
30
+ });
31
+
32
+ const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
33
+ setSearchTerm(e.target.value);
34
+ };
35
+
36
+ const handleCategoryChange = (categoryId: string) => {
37
+ setSelectedCategory(categoryId);
38
+ };
39
+
40
+ const resetFilters = () => {
41
+ setSearchTerm('');
42
+ setSelectedCategory('');
43
+ };
44
+
45
+ return (
46
+ <div>
47
+ <div className="mb-4 flex flex-col sm:flex-row gap-3">
48
+ <Input
49
+ placeholder="搜索提示词组..."
50
+ value={searchTerm}
51
+ onChange={handleSearch}
52
+ className="flex-1"
53
+ style={{ paddingLeft: "40px" }} /* Add explicit inline padding */
54
+ icon={
55
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
56
+ <circle cx="11" cy="11" r="8"></circle>
57
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
58
+ </svg>
59
+ }
60
+ />
61
+ <div className="flex gap-2">
62
+ <CategorySelector
63
+ selectedCategory={selectedCategory}
64
+ onChange={handleCategoryChange}
65
+ />
66
+ {(searchTerm || selectedCategory) && (
67
+ <button
68
+ className="ios-navbar-button"
69
+ onClick={resetFilters}
70
+ >
71
+ 清除筛选
72
+ </button>
73
+ )}
74
+ </div>
75
+ </div>
76
+
77
+ {filteredPromptGroups.length === 0 ? (
78
+ <div className="ios-empty-state">
79
+ <div className="ios-empty-state-icon">
80
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
81
+ <circle cx="11" cy="11" r="8"></circle>
82
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
83
+ </svg>
84
+ </div>
85
+ <h3 className="ios-empty-state-title">未找到提示词组</h3>
86
+ <p className="ios-empty-state-text">
87
+ {searchTerm || selectedCategory
88
+ ? '请尝试调整筛选条件'
89
+ : '点击底部的"新建"按钮创建您的第一个提示词组'}
90
+ </p>
91
+ </div>
92
+ ) : (
93
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
94
+ {filteredPromptGroups.map((group) => (
95
+ <PromptGroupCard
96
+ key={group._id}
97
+ promptGroup={group}
98
+ />
99
+ ))}
100
+ </div>
101
+ )}
102
+ </div>
103
+ );
104
+ };
105
+
106
  export default PromptGroupList;
src/pages/SettingsPage.tsx CHANGED
@@ -1,184 +1,184 @@
1
- import React, { useState } from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
- import Layout from '../components/Layout/Layout';
4
- import Card, { CardHeader, CardContent } from '../components/common/Card';
5
- import Button from '../components/common/Button';
6
- import { useAuth } from '../contexts/AuthContext';
7
-
8
- const SettingsPage: React.FC = () => {
9
- const navigate = useNavigate();
10
- const { logout } = useAuth();
11
- const [dataExported, setDataExported] = useState(false);
12
-
13
- // 导出所有数据
14
- const handleExportAllData = () => {
15
- const promptGroups = localStorage.getItem('promptGroups');
16
- const categories = localStorage.getItem('categories');
17
-
18
- const allData = {
19
- promptGroups: promptGroups ? JSON.parse(promptGroups) : [],
20
- categories: categories ? JSON.parse(categories) : []
21
- };
22
-
23
- const jsonString = JSON.stringify(allData, null, 2);
24
- const blob = new Blob([jsonString], { type: 'application/json' });
25
- const url = URL.createObjectURL(blob);
26
-
27
- const a = document.createElement('a');
28
- a.href = url;
29
- a.download = `prompt-manager-backup-${new Date().toISOString().split('T')[0]}.json`;
30
- document.body.appendChild(a);
31
- a.click();
32
-
33
- // 清理
34
- setTimeout(() => {
35
- document.body.removeChild(a);
36
- URL.revokeObjectURL(url);
37
- setDataExported(true);
38
-
39
- // 重置状态
40
- setTimeout(() => setDataExported(false), 3000);
41
- }, 0);
42
- };
43
-
44
- // 导入数据文件
45
- const handleImportData = (event: React.ChangeEvent<HTMLInputElement>) => {
46
- const file = event.target.files?.[0];
47
- if (!file) return;
48
-
49
- const reader = new FileReader();
50
- reader.onload = (e) => {
51
- try {
52
- const data = JSON.parse(e.target?.result as string);
53
-
54
- if (data.promptGroups && Array.isArray(data.promptGroups)) {
55
- localStorage.setItem('promptGroups', JSON.stringify(data.promptGroups));
56
- }
57
-
58
- if (data.categories && Array.isArray(data.categories)) {
59
- localStorage.setItem('categories', JSON.stringify(data.categories));
60
- }
61
-
62
- alert('数据导入成功,应用将刷新以加载新数据。');
63
- window.location.reload();
64
- } catch (error) {
65
- alert('导入失败,文件格式不正确。');
66
- console.error('导入错误:', error);
67
- }
68
- };
69
- reader.readAsText(file);
70
- };
71
-
72
- // 清除所有数据
73
- const handleClearAllData = () => {
74
- if (window.confirm('确定要清除所有数据吗?此操作不可撤销。')) {
75
- localStorage.removeItem('promptGroups');
76
- localStorage.removeItem('categories');
77
- alert('所有数据已清除,应用将刷新。');
78
- window.location.reload();
79
- }
80
- };
81
-
82
- // 退出登录
83
- const handleLogout = () => {
84
- if (window.confirm('确定要退出登录吗?')) {
85
- logout();
86
- navigate('/login');
87
- }
88
- };
89
-
90
- return (
91
- <Layout title="设置">
92
- <div className="space-y-4">
93
- {/* 账户管理卡片 */}
94
- <Card>
95
- <CardHeader title="账户管理" />
96
- <CardContent>
97
- <div className="space-y-4">
98
- <Button
99
- variant="danger"
100
- fullWidth
101
- onClick={handleLogout}
102
- >
103
- 退出登录
104
- </Button>
105
- </div>
106
- </CardContent>
107
- </Card>
108
-
109
- {/* 数据管理卡片 */}
110
- <Card>
111
- <CardHeader title="数据管理" />
112
- <CardContent>
113
- <div className="space-y-4">
114
- <div>
115
- <h3 className="font-medium mb-2">备份数据</h3>
116
- <p className="text-gray-600 text-sm mb-2">
117
- 导出所有提示词组和分类数据为JSON文件,可用于备份或迁移。
118
- </p>
119
- <Button
120
- variant="primary"
121
- onClick={handleExportAllData}
122
- >
123
- {dataExported ? '✓ 导出成功' : '导出所有数据'}
124
- </Button>
125
- </div>
126
-
127
- <div className="ios-divider"></div>
128
-
129
- <div>
130
- <h3 className="font-medium mb-2">导入数据</h3>
131
- <p className="text-gray-600 text-sm mb-2">
132
- 从之前导出的JSON文件中恢复数据。将覆盖当前所有数据。
133
- </p>
134
- <input
135
- type="file"
136
- id="import-file"
137
- accept=".json"
138
- style={{ display: 'none' }}
139
- onChange={handleImportData}
140
- />
141
- <Button
142
- variant="secondary"
143
- onClick={() => document.getElementById('import-file')?.click()}
144
- >
145
- 导入数据
146
- </Button>
147
- </div>
148
-
149
- <div className="ios-divider"></div>
150
-
151
- <div>
152
- <h3 className="font-medium mb-2">清除数据</h3>
153
- <p className="text-gray-600 text-sm mb-2">
154
- 删除所有存储的提示词组和分类数据。此操作不可撤销。
155
- </p>
156
- <Button
157
- variant="danger"
158
- onClick={handleClearAllData}
159
- >
160
- 清除所有数据
161
- </Button>
162
- </div>
163
- </div>
164
- </CardContent>
165
- </Card>
166
-
167
- <Card>
168
- <CardHeader title="关于" />
169
- <CardContent>
170
- <h3 className="font-medium mb-2">提示词管理应用</h3>
171
- <p className="text-gray-600 text-sm mb-4">
172
- 版本 1.0.0
173
- </p>
174
- <p className="text-gray-600 text-sm">
175
- 这是一个用于管理提示词、工作流和DSL文件的本地应用。所有数据均存储在浏览器的本地存储中。
176
- </p>
177
- </CardContent>
178
- </Card>
179
- </div>
180
- </Layout>
181
- );
182
- };
183
-
184
  export default SettingsPage;
 
1
+ import React, { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import Layout from '../components/Layout/Layout';
4
+ import Card, { CardHeader, CardContent } from '../components/common/Card';
5
+ import Button from '../components/common/Button';
6
+ import { useAuth } from '../contexts/AuthContext';
7
+
8
+ const SettingsPage: React.FC = () => {
9
+ const navigate = useNavigate();
10
+ const { logout } = useAuth();
11
+ const [dataExported, setDataExported] = useState(false);
12
+
13
+ // 导出所有数据
14
+ const handleExportAllData = () => {
15
+ const promptGroups = localStorage.getItem('promptGroups');
16
+ const categories = localStorage.getItem('categories');
17
+
18
+ const allData = {
19
+ promptGroups: promptGroups ? JSON.parse(promptGroups) : [],
20
+ categories: categories ? JSON.parse(categories) : []
21
+ };
22
+
23
+ const jsonString = JSON.stringify(allData, null, 2);
24
+ const blob = new Blob([jsonString], { type: 'application/json' });
25
+ const url = URL.createObjectURL(blob);
26
+
27
+ const a = document.createElement('a');
28
+ a.href = url;
29
+ a.download = `prompt-manager-backup-${new Date().toISOString().split('T')[0]}.json`;
30
+ document.body.appendChild(a);
31
+ a.click();
32
+
33
+ // 清理
34
+ setTimeout(() => {
35
+ document.body.removeChild(a);
36
+ URL.revokeObjectURL(url);
37
+ setDataExported(true);
38
+
39
+ // 重置状态
40
+ setTimeout(() => setDataExported(false), 3000);
41
+ }, 0);
42
+ };
43
+
44
+ // 导入数据文件
45
+ const handleImportData = (event: React.ChangeEvent<HTMLInputElement>) => {
46
+ const file = event.target.files?.[0];
47
+ if (!file) return;
48
+
49
+ const reader = new FileReader();
50
+ reader.onload = (e) => {
51
+ try {
52
+ const data = JSON.parse(e.target?.result as string);
53
+
54
+ if (data.promptGroups && Array.isArray(data.promptGroups)) {
55
+ localStorage.setItem('promptGroups', JSON.stringify(data.promptGroups));
56
+ }
57
+
58
+ if (data.categories && Array.isArray(data.categories)) {
59
+ localStorage.setItem('categories', JSON.stringify(data.categories));
60
+ }
61
+
62
+ alert('数据导入成功,应用将刷新以加载新数据。');
63
+ window.location.reload();
64
+ } catch (error) {
65
+ alert('导入失败,文件格式不正确。');
66
+ console.error('导入错误:', error);
67
+ }
68
+ };
69
+ reader.readAsText(file);
70
+ };
71
+
72
+ // 清除所有数据
73
+ const handleClearAllData = () => {
74
+ if (window.confirm('确定要清除所有数据吗?此操作不可撤销。')) {
75
+ localStorage.removeItem('promptGroups');
76
+ localStorage.removeItem('categories');
77
+ alert('所有数据已清除,应用将刷新。');
78
+ window.location.reload();
79
+ }
80
+ };
81
+
82
+ // 退出登录
83
+ const handleLogout = () => {
84
+ if (window.confirm('确定要退出登录吗?')) {
85
+ logout();
86
+ navigate('/login');
87
+ }
88
+ };
89
+
90
+ return (
91
+ <Layout title="设置">
92
+ <div className="space-y-4">
93
+ {/* 账户管理卡片 */}
94
+ <Card>
95
+ <CardHeader title="账户管理" />
96
+ <CardContent>
97
+ <div className="space-y-4">
98
+ <Button
99
+ variant="danger"
100
+ fullWidth
101
+ onClick={handleLogout}
102
+ >
103
+ 退出登录
104
+ </Button>
105
+ </div>
106
+ </CardContent>
107
+ </Card>
108
+
109
+ {/* 数据管理卡片 */}
110
+ <Card>
111
+ <CardHeader title="数据管理" />
112
+ <CardContent>
113
+ <div className="space-y-4">
114
+ <div>
115
+ <h3 className="font-medium mb-2">备份数据</h3>
116
+ <p className="text-gray-600 text-sm mb-2">
117
+ 导出所有提示词组和分类数据为JSON文件,可用于备份或迁移。
118
+ </p>
119
+ <Button
120
+ variant="primary"
121
+ onClick={handleExportAllData}
122
+ >
123
+ {dataExported ? '✓ 导出成功' : '导出所有数据'}
124
+ </Button>
125
+ </div>
126
+
127
+ <div className="ios-divider"></div>
128
+
129
+ <div>
130
+ <h3 className="font-medium mb-2">导入数据</h3>
131
+ <p className="text-gray-600 text-sm mb-2">
132
+ 从之前导出的JSON文件中恢复数据。将覆盖当前所有数据。
133
+ </p>
134
+ <input
135
+ type="file"
136
+ id="import-file"
137
+ accept=".json"
138
+ style={{ display: 'none' }}
139
+ onChange={handleImportData}
140
+ />
141
+ <Button
142
+ variant="secondary"
143
+ onClick={() => document.getElementById('import-file')?.click()}
144
+ >
145
+ 导入数据
146
+ </Button>
147
+ </div>
148
+
149
+ <div className="ios-divider"></div>
150
+
151
+ <div>
152
+ <h3 className="font-medium mb-2">清除数据</h3>
153
+ <p className="text-gray-600 text-sm mb-2">
154
+ 删除所有存储的提示词组和分类数据。此操作不可撤销。
155
+ </p>
156
+ <Button
157
+ variant="danger"
158
+ onClick={handleClearAllData}
159
+ >
160
+ 清除所有数据
161
+ </Button>
162
+ </div>
163
+ </div>
164
+ </CardContent>
165
+ </Card>
166
+
167
+ <Card>
168
+ <CardHeader title="关于" />
169
+ <CardContent>
170
+ <h3 className="font-medium mb-2">提示词管理应用</h3>
171
+ <p className="text-gray-600 text-sm mb-4">
172
+ 版本 1.0.0
173
+ </p>
174
+ <p className="text-gray-600 text-sm">
175
+ 这是一个用于管理提示词、工作流和DSL文件的本地应用。所有数据均存储在浏览器的本地存储中。
176
+ </p>
177
+ </CardContent>
178
+ </Card>
179
+ </div>
180
+ </Layout>
181
+ );
182
+ };
183
+
184
  export default SettingsPage;