@lobehub/chat 1.104.5 → 1.105.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/profile/apikey/Client.tsx +209 -0
- package/src/app/[variants]/(main)/profile/apikey/features/ApiKeyDatePicker/index.tsx +39 -0
- package/src/app/[variants]/(main)/profile/apikey/features/ApiKeyDisplay/index.tsx +60 -0
- package/src/app/[variants]/(main)/profile/apikey/features/ApiKeyModal/index.tsx +58 -0
- package/src/app/[variants]/(main)/profile/apikey/features/EditableCell/index.tsx +223 -0
- package/src/app/[variants]/(main)/profile/apikey/features/index.ts +3 -0
- package/src/app/[variants]/(main)/profile/apikey/page.tsx +32 -0
- package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +12 -1
- package/src/config/aiModels/qwen.ts +207 -2
- package/src/config/featureFlags/schema.ts +7 -0
- package/src/database/migrations/0029_add_apikey_manage.sql +16 -0
- package/src/database/migrations/meta/0029_snapshot.json +6166 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/models/apiKey.ts +116 -0
- package/src/database/schemas/apiKey.ts +25 -0
- package/src/database/schemas/index.ts +1 -0
- package/src/database/schemas/rbac.ts +11 -1
- package/src/libs/model-runtime/qwen/createImage.ts +29 -9
- package/src/locales/default/auth.ts +54 -0
- package/src/server/routers/lambda/apiKey.ts +80 -0
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/store/serverConfig/selectors.test.ts +1 -0
- package/src/types/apiKey.ts +12 -0
- package/src/utils/apiKey.ts +60 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.105.1](https://github.com/lobehub/lobe-chat/compare/v1.105.0...v1.105.1)
|
6
|
+
|
7
|
+
<sup>Released on **2025-07-29**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Support more Text2Image from Qwen.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Support more Text2Image from Qwen, closes [#8574](https://github.com/lobehub/lobe-chat/issues/8574) ([b8c0e2d](https://github.com/lobehub/lobe-chat/commit/b8c0e2d))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
## [Version 1.105.0](https://github.com/lobehub/lobe-chat/compare/v1.104.5...v1.105.0)
|
31
|
+
|
32
|
+
<sup>Released on **2025-07-28**</sup>
|
33
|
+
|
34
|
+
#### ✨ Features
|
35
|
+
|
36
|
+
- **misc**: Implement API Key management functionality.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### What's improved
|
44
|
+
|
45
|
+
- **misc**: Implement API Key management functionality, closes [#8535](https://github.com/lobehub/lobe-chat/issues/8535) ([fdaa725](https://github.com/lobehub/lobe-chat/commit/fdaa725))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.104.5](https://github.com/lobehub/lobe-chat/compare/v1.104.4...v1.104.5)
|
6
56
|
|
7
57
|
<sup>Released on **2025-07-28**</sup>
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Support more Text2Image from Qwen."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-07-29",
|
9
|
+
"version": "1.105.1"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"features": [
|
14
|
+
"Implement API Key management functionality."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-07-28",
|
18
|
+
"version": "1.105.0"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"improvements": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.105.1",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -0,0 +1,209 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
4
|
+
import { Button } from '@lobehub/ui';
|
5
|
+
import { useMutation } from '@tanstack/react-query';
|
6
|
+
import { Popconfirm, Switch } from 'antd';
|
7
|
+
import { createStyles } from 'antd-style';
|
8
|
+
import { Trash } from 'lucide-react';
|
9
|
+
import { FC, useRef, useState } from 'react';
|
10
|
+
import { useTranslation } from 'react-i18next';
|
11
|
+
|
12
|
+
import { lambdaClient } from '@/libs/trpc/client';
|
13
|
+
import { ApiKeyItem, CreateApiKeyParams, UpdateApiKeyParams } from '@/types/apiKey';
|
14
|
+
|
15
|
+
import { ApiKeyDisplay, ApiKeyModal, EditableCell } from './features';
|
16
|
+
|
17
|
+
const useStyles = createStyles(({ css, token }) => ({
|
18
|
+
container: css`
|
19
|
+
.ant-pro-card-body {
|
20
|
+
padding-inline: 0;
|
21
|
+
|
22
|
+
.ant-pro-table-list-toolbar-container {
|
23
|
+
padding-block-start: 0;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
`,
|
27
|
+
header: css`
|
28
|
+
display: flex;
|
29
|
+
justify-content: flex-end;
|
30
|
+
margin-block-end: ${token.margin}px;
|
31
|
+
`,
|
32
|
+
table: css`
|
33
|
+
border-radius: ${token.borderRadius}px;
|
34
|
+
background: ${token.colorBgContainer};
|
35
|
+
`,
|
36
|
+
}));
|
37
|
+
|
38
|
+
const Client: FC = () => {
|
39
|
+
const { styles } = useStyles();
|
40
|
+
const { t } = useTranslation('auth');
|
41
|
+
const [modalOpen, setModalOpen] = useState(false);
|
42
|
+
|
43
|
+
const actionRef = useRef<ActionType>(null);
|
44
|
+
|
45
|
+
const createMutation = useMutation({
|
46
|
+
mutationFn: (params: CreateApiKeyParams) => lambdaClient.apiKey.createApiKey.mutate(params),
|
47
|
+
onSuccess: () => {
|
48
|
+
actionRef.current?.reload();
|
49
|
+
setModalOpen(false);
|
50
|
+
},
|
51
|
+
});
|
52
|
+
|
53
|
+
const updateMutation = useMutation({
|
54
|
+
mutationFn: ({ id, params }: { id: number; params: UpdateApiKeyParams }) =>
|
55
|
+
lambdaClient.apiKey.updateApiKey.mutate({ id, value: params }),
|
56
|
+
onSuccess: () => {
|
57
|
+
actionRef.current?.reload();
|
58
|
+
},
|
59
|
+
});
|
60
|
+
|
61
|
+
const deleteMutation = useMutation({
|
62
|
+
mutationFn: (id: number) => lambdaClient.apiKey.deleteApiKey.mutate({ id }),
|
63
|
+
onSuccess: () => {
|
64
|
+
actionRef.current?.reload();
|
65
|
+
},
|
66
|
+
});
|
67
|
+
|
68
|
+
const handleCreate = () => {
|
69
|
+
setModalOpen(true);
|
70
|
+
};
|
71
|
+
|
72
|
+
const handleModalOk = (values: CreateApiKeyParams) => {
|
73
|
+
createMutation.mutate(values);
|
74
|
+
};
|
75
|
+
|
76
|
+
const columns: ProColumns<ApiKeyItem>[] = [
|
77
|
+
{
|
78
|
+
dataIndex: 'name',
|
79
|
+
key: 'name',
|
80
|
+
render: (_, apiKey) => (
|
81
|
+
<EditableCell
|
82
|
+
onSubmit={(name) => {
|
83
|
+
if (!name || name === apiKey.name) {
|
84
|
+
return;
|
85
|
+
}
|
86
|
+
|
87
|
+
updateMutation.mutate({ id: apiKey.id!, params: { name: name as string } });
|
88
|
+
}}
|
89
|
+
placeholder={t('apikey.display.enterPlaceholder')}
|
90
|
+
type="text"
|
91
|
+
value={apiKey.name}
|
92
|
+
/>
|
93
|
+
),
|
94
|
+
title: t('apikey.list.columns.name'),
|
95
|
+
},
|
96
|
+
{
|
97
|
+
dataIndex: 'key',
|
98
|
+
ellipsis: true,
|
99
|
+
key: 'key',
|
100
|
+
render: (_, apiKey) => <ApiKeyDisplay apiKey={apiKey.key} />,
|
101
|
+
title: t('apikey.list.columns.key'),
|
102
|
+
width: 230,
|
103
|
+
},
|
104
|
+
{
|
105
|
+
dataIndex: 'enabled',
|
106
|
+
key: 'enabled',
|
107
|
+
render: (_, apiKey: ApiKeyItem) => (
|
108
|
+
<Switch
|
109
|
+
checked={!!apiKey.enabled}
|
110
|
+
onChange={(checked) => {
|
111
|
+
updateMutation.mutate({ id: apiKey.id!, params: { enabled: checked } });
|
112
|
+
}}
|
113
|
+
/>
|
114
|
+
),
|
115
|
+
title: t('apikey.list.columns.status'),
|
116
|
+
width: 100,
|
117
|
+
},
|
118
|
+
{
|
119
|
+
dataIndex: 'expiresAt',
|
120
|
+
key: 'expiresAt',
|
121
|
+
render: (_, apiKey) => (
|
122
|
+
<EditableCell
|
123
|
+
onSubmit={(expiresAt) => {
|
124
|
+
if (expiresAt === apiKey.expiresAt) {
|
125
|
+
return;
|
126
|
+
}
|
127
|
+
|
128
|
+
updateMutation.mutate({
|
129
|
+
id: apiKey.id!,
|
130
|
+
params: { expiresAt: expiresAt ? new Date(expiresAt as string) : null },
|
131
|
+
});
|
132
|
+
}}
|
133
|
+
placeholder={t('apikey.display.neverExpires')}
|
134
|
+
type="date"
|
135
|
+
value={apiKey.expiresAt?.toLocaleString() || t('apikey.display.neverExpires')}
|
136
|
+
/>
|
137
|
+
),
|
138
|
+
title: t('apikey.list.columns.expiresAt'),
|
139
|
+
width: 170,
|
140
|
+
},
|
141
|
+
{
|
142
|
+
dataIndex: 'lastUsedAt',
|
143
|
+
key: 'lastUsedAt',
|
144
|
+
renderText: (_, apiKey: ApiKeyItem) =>
|
145
|
+
apiKey.lastUsedAt?.toLocaleString() || t('apikey.display.neverUsed'),
|
146
|
+
title: t('apikey.list.columns.lastUsedAt'),
|
147
|
+
},
|
148
|
+
{
|
149
|
+
key: 'action',
|
150
|
+
render: (_: any, apiKey: ApiKeyItem) => (
|
151
|
+
<Popconfirm
|
152
|
+
cancelText={t('apikey.list.actions.deleteConfirm.actions.cancel')}
|
153
|
+
description={t('apikey.list.actions.deleteConfirm.content')}
|
154
|
+
okText={t('apikey.list.actions.deleteConfirm.actions.ok')}
|
155
|
+
onConfirm={() => deleteMutation.mutate(apiKey.id!)}
|
156
|
+
title={t('apikey.list.actions.deleteConfirm.title')}
|
157
|
+
>
|
158
|
+
<Button
|
159
|
+
icon={Trash}
|
160
|
+
size="small"
|
161
|
+
style={{ verticalAlign: 'middle' }}
|
162
|
+
title={t('apikey.list.actions.delete')}
|
163
|
+
type="text"
|
164
|
+
/>
|
165
|
+
</Popconfirm>
|
166
|
+
),
|
167
|
+
title: t('apikey.list.columns.actions'),
|
168
|
+
width: 100,
|
169
|
+
},
|
170
|
+
];
|
171
|
+
|
172
|
+
return (
|
173
|
+
<div className={styles.container}>
|
174
|
+
<ProTable
|
175
|
+
actionRef={actionRef}
|
176
|
+
className={styles.table}
|
177
|
+
columns={columns}
|
178
|
+
headerTitle={t('apikey.list.title')}
|
179
|
+
options={false}
|
180
|
+
pagination={false}
|
181
|
+
request={async () => {
|
182
|
+
const apiKeys = await lambdaClient.apiKey.getApiKeys.query();
|
183
|
+
|
184
|
+
return {
|
185
|
+
data: apiKeys,
|
186
|
+
success: true,
|
187
|
+
};
|
188
|
+
}}
|
189
|
+
rowKey="id"
|
190
|
+
search={false}
|
191
|
+
toolbar={{
|
192
|
+
actions: [
|
193
|
+
<Button key="create" onClick={handleCreate} type="primary">
|
194
|
+
{t('apikey.list.actions.create')}
|
195
|
+
</Button>,
|
196
|
+
],
|
197
|
+
}}
|
198
|
+
/>
|
199
|
+
<ApiKeyModal
|
200
|
+
onCancel={() => setModalOpen(false)}
|
201
|
+
onOk={handleModalOk}
|
202
|
+
open={modalOpen}
|
203
|
+
submitLoading={createMutation.isPending}
|
204
|
+
/>
|
205
|
+
</div>
|
206
|
+
);
|
207
|
+
};
|
208
|
+
|
209
|
+
export default Client;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { DatePicker } from '@lobehub/ui';
|
2
|
+
import { DatePickerProps, Flex } from 'antd';
|
3
|
+
import dayjs, { Dayjs } from 'dayjs';
|
4
|
+
import { FC } from 'react';
|
5
|
+
import { useTranslation } from 'react-i18next';
|
6
|
+
|
7
|
+
interface ApiKeyDatePickerProps extends Omit<DatePickerProps, 'onChange'> {
|
8
|
+
onChange?: (date: Dayjs | null) => void;
|
9
|
+
}
|
10
|
+
|
11
|
+
const ApiKeyDatePicker: FC<ApiKeyDatePickerProps> = ({ value, onChange, ...props }) => {
|
12
|
+
const { t } = useTranslation('auth');
|
13
|
+
|
14
|
+
const handleOnChange = (date: Dayjs | null) => {
|
15
|
+
// 如果选择了日期,设置为当天的 23:59:59
|
16
|
+
const submitData = date ? date.hour(23).minute(59).second(59).millisecond(999) : null;
|
17
|
+
|
18
|
+
onChange?.(submitData);
|
19
|
+
};
|
20
|
+
|
21
|
+
return (
|
22
|
+
<DatePicker
|
23
|
+
key={value?.valueOf() || 'EMPTY'}
|
24
|
+
value={value}
|
25
|
+
{...props}
|
26
|
+
minDate={dayjs()}
|
27
|
+
onChange={handleOnChange}
|
28
|
+
placeholder={t('apikey.form.fields.expiresAt.placeholder')}
|
29
|
+
renderExtraFooter={() => (
|
30
|
+
<Flex justify="center">
|
31
|
+
<a onClick={() => handleOnChange(null)}>{t('apikey.display.neverExpires')}</a>
|
32
|
+
</Flex>
|
33
|
+
)}
|
34
|
+
showNow={false}
|
35
|
+
/>
|
36
|
+
);
|
37
|
+
};
|
38
|
+
|
39
|
+
export default ApiKeyDatePicker;
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
|
2
|
+
import { Button } from '@lobehub/ui';
|
3
|
+
import { App, Flex } from 'antd';
|
4
|
+
import { FC, useState } from 'react';
|
5
|
+
import { useTranslation } from 'react-i18next';
|
6
|
+
|
7
|
+
interface ApiKeyDisplayProps {
|
8
|
+
apiKey?: string;
|
9
|
+
}
|
10
|
+
|
11
|
+
const ApiKeyDisplay: FC<ApiKeyDisplayProps> = ({ apiKey }) => {
|
12
|
+
const { t } = useTranslation('auth');
|
13
|
+
const [isVisible, setIsVisible] = useState(false);
|
14
|
+
const { message } = App.useApp();
|
15
|
+
|
16
|
+
const toggleVisibility = () => {
|
17
|
+
setIsVisible(!isVisible);
|
18
|
+
};
|
19
|
+
|
20
|
+
const handleCopy = async () => {
|
21
|
+
if (!apiKey) return;
|
22
|
+
|
23
|
+
try {
|
24
|
+
await navigator.clipboard.writeText(apiKey);
|
25
|
+
message.success(t('apikey.display.copySuccess'));
|
26
|
+
} catch {
|
27
|
+
message.error(t('apikey.display.copyError'));
|
28
|
+
}
|
29
|
+
};
|
30
|
+
|
31
|
+
const displayValue = apiKey && (isVisible ? apiKey : `lb-${'*'.repeat(apiKey.length - 2)}`);
|
32
|
+
|
33
|
+
if (!apiKey) {
|
34
|
+
return t('apikey.display.autoGenerated');
|
35
|
+
}
|
36
|
+
|
37
|
+
return (
|
38
|
+
<Flex align="center" gap={8}>
|
39
|
+
<span style={{ fontSize: '14px' }}>{displayValue}</span>
|
40
|
+
<Flex>
|
41
|
+
<Button
|
42
|
+
icon={isVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
43
|
+
onClick={toggleVisibility}
|
44
|
+
size="small"
|
45
|
+
title={isVisible ? t('apikey.display.hide') : t('apikey.display.show')}
|
46
|
+
type="text"
|
47
|
+
/>
|
48
|
+
<Button
|
49
|
+
icon={<CopyOutlined />}
|
50
|
+
onClick={handleCopy}
|
51
|
+
size="small"
|
52
|
+
title={t('apikey.display.copy')}
|
53
|
+
type="text"
|
54
|
+
/>
|
55
|
+
</Flex>
|
56
|
+
</Flex>
|
57
|
+
);
|
58
|
+
};
|
59
|
+
|
60
|
+
export default ApiKeyDisplay;
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import { FormModal, Input } from '@lobehub/ui';
|
2
|
+
import { Dayjs } from 'dayjs';
|
3
|
+
import { FC } from 'react';
|
4
|
+
import { useTranslation } from 'react-i18next';
|
5
|
+
|
6
|
+
import { CreateApiKeyParams } from '@/types/apiKey';
|
7
|
+
|
8
|
+
import ApiKeyDatePicker from '../ApiKeyDatePicker';
|
9
|
+
|
10
|
+
interface ApiKeyModalProps {
|
11
|
+
onCancel: () => void;
|
12
|
+
onOk: (values: CreateApiKeyParams) => void;
|
13
|
+
open: boolean;
|
14
|
+
submitLoading?: boolean;
|
15
|
+
}
|
16
|
+
|
17
|
+
type FormValues = Omit<CreateApiKeyParams, 'expiresAt'> & {
|
18
|
+
expiresAt: Dayjs | null;
|
19
|
+
};
|
20
|
+
|
21
|
+
const ApiKeyModal: FC<ApiKeyModalProps> = ({ open, onCancel, onOk, submitLoading }) => {
|
22
|
+
const { t } = useTranslation('auth');
|
23
|
+
|
24
|
+
return (
|
25
|
+
<FormModal
|
26
|
+
destroyOnHidden
|
27
|
+
height={'90%'}
|
28
|
+
itemMinWidth={'max(30%,240px)'}
|
29
|
+
items={[
|
30
|
+
{
|
31
|
+
children: <Input placeholder={t('apikey.form.fields.name.placeholder')} />,
|
32
|
+
label: t('apikey.form.fields.name.label'),
|
33
|
+
name: 'name',
|
34
|
+
rules: [{ required: true }],
|
35
|
+
},
|
36
|
+
{
|
37
|
+
children: <ApiKeyDatePicker style={{ width: '100%' }} />,
|
38
|
+
label: t('apikey.form.fields.expiresAt.label'),
|
39
|
+
name: 'expiresAt',
|
40
|
+
},
|
41
|
+
]}
|
42
|
+
itemsType={'flat'}
|
43
|
+
onCancel={onCancel}
|
44
|
+
onFinish={(values: FormValues) => {
|
45
|
+
onOk({
|
46
|
+
...values,
|
47
|
+
expiresAt: values.expiresAt ? values.expiresAt.toDate() : null,
|
48
|
+
} satisfies CreateApiKeyParams);
|
49
|
+
}}
|
50
|
+
open={open}
|
51
|
+
submitLoading={submitLoading}
|
52
|
+
submitText={t('apikey.form.submit')}
|
53
|
+
title={t('apikey.form.title')}
|
54
|
+
/>
|
55
|
+
);
|
56
|
+
};
|
57
|
+
|
58
|
+
export default ApiKeyModal;
|
@@ -0,0 +1,223 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { ActionIcon, Input } from '@lobehub/ui';
|
4
|
+
import { App, InputRef } from 'antd';
|
5
|
+
import { createStyles } from 'antd-style';
|
6
|
+
import dayjs, { Dayjs } from 'dayjs';
|
7
|
+
import { Check, Edit, X } from 'lucide-react';
|
8
|
+
import React, { memo, useRef, useState } from 'react';
|
9
|
+
import { useTranslation } from 'react-i18next';
|
10
|
+
|
11
|
+
import ApiKeyDatePicker from '../ApiKeyDatePicker';
|
12
|
+
|
13
|
+
// 内容类型定义
|
14
|
+
export type ContentType = 'text' | 'date';
|
15
|
+
|
16
|
+
// 组件Props接口定义
|
17
|
+
export interface EditableCellProps {
|
18
|
+
/** 是否禁用编辑 */
|
19
|
+
disabled?: boolean;
|
20
|
+
/** 提交回调函数 */
|
21
|
+
onSubmit: (value: string | Date | null) => void;
|
22
|
+
/** 占位符文本 */
|
23
|
+
placeholder?: string;
|
24
|
+
/** 内容类型 */
|
25
|
+
type: ContentType;
|
26
|
+
/** 从数据库中查出的值,不管是什么类型,存进去的都是字符串 */
|
27
|
+
value: string | null;
|
28
|
+
}
|
29
|
+
|
30
|
+
// 样式定义
|
31
|
+
const useStyles = createStyles(({ css, token }) => ({
|
32
|
+
actionButtons: css`
|
33
|
+
display: flex;
|
34
|
+
flex-shrink: 0;
|
35
|
+
gap: 4px;
|
36
|
+
`,
|
37
|
+
container: css`
|
38
|
+
position: relative;
|
39
|
+
|
40
|
+
display: flex;
|
41
|
+
gap: 8px;
|
42
|
+
align-items: center;
|
43
|
+
|
44
|
+
min-height: 32px;
|
45
|
+
|
46
|
+
&:hover .edit-button {
|
47
|
+
opacity: 1;
|
48
|
+
}
|
49
|
+
`,
|
50
|
+
content: css`
|
51
|
+
min-width: 0;
|
52
|
+
line-height: 1.5;
|
53
|
+
color: ${token.colorText};
|
54
|
+
word-break: break-all;
|
55
|
+
`,
|
56
|
+
editButton: css`
|
57
|
+
opacity: 0;
|
58
|
+
transition: opacity 0.2s ease;
|
59
|
+
|
60
|
+
&.edit-button {
|
61
|
+
opacity: 0;
|
62
|
+
}
|
63
|
+
`,
|
64
|
+
editingContainer: css`
|
65
|
+
display: flex;
|
66
|
+
gap: 8px;
|
67
|
+
align-items: center;
|
68
|
+
width: 100%;
|
69
|
+
`,
|
70
|
+
inputWrapper: css`
|
71
|
+
flex: 1;
|
72
|
+
`,
|
73
|
+
textareaWrapper: css`
|
74
|
+
flex: 1;
|
75
|
+
`,
|
76
|
+
}));
|
77
|
+
|
78
|
+
// 主组件实现
|
79
|
+
const EditableCell = memo<EditableCellProps>(
|
80
|
+
({ value, type, onSubmit, placeholder, disabled = false }) => {
|
81
|
+
const { styles, cx } = useStyles();
|
82
|
+
const { t } = useTranslation('auth');
|
83
|
+
const { message } = App.useApp();
|
84
|
+
|
85
|
+
// 编辑状态管理
|
86
|
+
const [isEditing, setIsEditing] = useState(false);
|
87
|
+
|
88
|
+
// 用于Input的ref
|
89
|
+
const inputRef = useRef<InputRef>(null);
|
90
|
+
|
91
|
+
// 格式化显示值
|
92
|
+
const formatDisplayValue = (val: string | null) => {
|
93
|
+
if (type === 'date' && val) {
|
94
|
+
const date = dayjs(val);
|
95
|
+
|
96
|
+
return date.isValid() ? date.format('YYYY-MM-DD') : val || placeholder || '';
|
97
|
+
}
|
98
|
+
|
99
|
+
return val || placeholder || '';
|
100
|
+
};
|
101
|
+
|
102
|
+
// 开始编辑
|
103
|
+
const handleEdit = () => {
|
104
|
+
if (disabled) return;
|
105
|
+
|
106
|
+
setIsEditing(true);
|
107
|
+
};
|
108
|
+
|
109
|
+
// 提交编辑
|
110
|
+
const handleSubmit = () => {
|
111
|
+
if (type === 'text') {
|
112
|
+
const inputValue = inputRef.current?.input?.value;
|
113
|
+
|
114
|
+
if (!inputValue) {
|
115
|
+
message.warning(t('apikey.validation.required'));
|
116
|
+
return;
|
117
|
+
}
|
118
|
+
|
119
|
+
onSubmit(inputValue);
|
120
|
+
}
|
121
|
+
|
122
|
+
setIsEditing(false);
|
123
|
+
};
|
124
|
+
|
125
|
+
// 取消编辑
|
126
|
+
const handleCancel = () => {
|
127
|
+
setIsEditing(false);
|
128
|
+
};
|
129
|
+
|
130
|
+
// 输入框组件的键盘事件处理
|
131
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
132
|
+
if (e.key === 'Enter') {
|
133
|
+
e.preventDefault();
|
134
|
+
handleSubmit();
|
135
|
+
} else if (e.key === 'Escape') {
|
136
|
+
e.preventDefault();
|
137
|
+
handleCancel();
|
138
|
+
}
|
139
|
+
};
|
140
|
+
|
141
|
+
// 日期选择器提交
|
142
|
+
const handleDatePickerSubmit = (date: Dayjs | null) => {
|
143
|
+
onSubmit(date && dayjs(date).toISOString());
|
144
|
+
|
145
|
+
setIsEditing(false);
|
146
|
+
};
|
147
|
+
|
148
|
+
// 渲染编辑模式
|
149
|
+
const renderEditMode = () => {
|
150
|
+
switch (type) {
|
151
|
+
case 'text': {
|
152
|
+
return (
|
153
|
+
<div className={styles.inputWrapper}>
|
154
|
+
<Input
|
155
|
+
autoFocus
|
156
|
+
defaultValue={value as string}
|
157
|
+
onKeyDown={handleKeyDown}
|
158
|
+
placeholder={placeholder}
|
159
|
+
ref={inputRef}
|
160
|
+
/>
|
161
|
+
</div>
|
162
|
+
);
|
163
|
+
}
|
164
|
+
|
165
|
+
case 'date': {
|
166
|
+
const dateValue = value && dayjs(value).isValid() ? dayjs(value) : null;
|
167
|
+
|
168
|
+
return (
|
169
|
+
<ApiKeyDatePicker
|
170
|
+
defaultValue={dateValue}
|
171
|
+
onChange={handleDatePickerSubmit}
|
172
|
+
onOpenChange={() => {
|
173
|
+
if (isEditing) {
|
174
|
+
setIsEditing(false);
|
175
|
+
}
|
176
|
+
}}
|
177
|
+
open={true}
|
178
|
+
/>
|
179
|
+
);
|
180
|
+
}
|
181
|
+
|
182
|
+
default: {
|
183
|
+
return null;
|
184
|
+
}
|
185
|
+
}
|
186
|
+
};
|
187
|
+
|
188
|
+
// 文本类型的编辑模式,展示保存和取消按钮
|
189
|
+
if (type === 'text' && isEditing) {
|
190
|
+
return (
|
191
|
+
<div className={styles.editingContainer}>
|
192
|
+
{renderEditMode()}
|
193
|
+
<div className={styles.actionButtons}>
|
194
|
+
<ActionIcon icon={Check} onClick={handleSubmit} size="small" />
|
195
|
+
<ActionIcon icon={X} onClick={handleCancel} size="small" />
|
196
|
+
</div>
|
197
|
+
</div>
|
198
|
+
);
|
199
|
+
}
|
200
|
+
|
201
|
+
// 日期类型的编辑模式,展示日期选择器
|
202
|
+
if (type === 'date' && isEditing) {
|
203
|
+
return renderEditMode();
|
204
|
+
}
|
205
|
+
|
206
|
+
// 展示模式
|
207
|
+
return (
|
208
|
+
<div className={styles.container}>
|
209
|
+
<div className={styles.content}>{formatDisplayValue(value)}</div>
|
210
|
+
<ActionIcon
|
211
|
+
className={cx(styles.editButton, 'edit-button')}
|
212
|
+
icon={Edit}
|
213
|
+
onClick={handleEdit}
|
214
|
+
size="small"
|
215
|
+
/>
|
216
|
+
</div>
|
217
|
+
);
|
218
|
+
},
|
219
|
+
);
|
220
|
+
|
221
|
+
EditableCell.displayName = 'EditableCell';
|
222
|
+
|
223
|
+
export default EditableCell;
|