@dsh-plus/image-studio 0.1.0 → 0.1.2
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/lib/client.js +3588 -60
- package/lib/index.d.ts +2 -1
- package/lib/index.js +3 -2
- package/package.json +4 -2
- package/src/client/card.tsx +579 -9
- package/src/client/client.ts +69 -36
- package/src/client/i18n.ts +230 -9
- package/src/client/panel/controller.ts +75 -0
- package/src/client/panel/entry.tsx +35 -0
- package/src/client/panel/gallery.tsx +217 -0
- package/src/client/panel/generator.tsx +521 -0
- package/src/client/panel/panel.tsx +219 -0
- package/src/client/panel/param-state.ts +144 -0
- package/src/client/panel/params-form.tsx +136 -0
- package/src/client/panel/picker.tsx +111 -0
- package/src/client/panel/tasks.tsx +72 -0
- package/src/client/styles.ts +571 -17
- package/src/service.ts +7 -5
- package/src/client/section.tsx +0 -14
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 画廊视图:条目网格(首图缩略)+ 详情模态(全部图片、提示词/改写、
|
|
3
|
+
* 请求参数回放、衍生链源图、下载/删除/二次编辑)。
|
|
4
|
+
* @module image-studio/client/panel/gallery
|
|
5
|
+
*/
|
|
6
|
+
import { Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
7
|
+
import { IconDownload, IconRefreshOutline16, IconTrashOutline16 } from '@dsh-plus/shared/client'
|
|
8
|
+
import { type ReactElement, useState } from 'react'
|
|
9
|
+
import type { GalleryItem } from '../../gallery/store.ts'
|
|
10
|
+
import { deleteGalleryItem, imageUrl } from '../api.ts'
|
|
11
|
+
import type { Translate } from '../i18n.ts'
|
|
12
|
+
|
|
13
|
+
interface GalleryViewProps {
|
|
14
|
+
t: Translate
|
|
15
|
+
items: GalleryItem[]
|
|
16
|
+
loading: boolean
|
|
17
|
+
failed: boolean
|
|
18
|
+
onRefresh(): void
|
|
19
|
+
onEdit(item: GalleryItem): void
|
|
20
|
+
/** 外部请求打开的条目(任务条「查看」);打开后由 onDetailOpened 消费。 */
|
|
21
|
+
detailId: string | null
|
|
22
|
+
onDetailChange(id: string | null): void
|
|
23
|
+
hidden: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fmtTime(iso: string): string {
|
|
27
|
+
const date = new Date(iso)
|
|
28
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleString()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 详情模态。 */
|
|
32
|
+
function GalleryDetail(props: {
|
|
33
|
+
t: Translate
|
|
34
|
+
item: GalleryItem
|
|
35
|
+
onClose(): void
|
|
36
|
+
onEdit(item: GalleryItem): void
|
|
37
|
+
onDeleted(id: string): void
|
|
38
|
+
}): ReactElement {
|
|
39
|
+
const { t, item, onClose, onEdit, onDeleted } = props
|
|
40
|
+
const [confirming, setConfirming] = useState(false)
|
|
41
|
+
const [deleting, setDeleting] = useState(false)
|
|
42
|
+
const [error, setError] = useState<string | null>(null)
|
|
43
|
+
|
|
44
|
+
const doDelete = (): void => {
|
|
45
|
+
setDeleting(true)
|
|
46
|
+
deleteGalleryItem(item.id)
|
|
47
|
+
.then(() => onDeleted(item.id))
|
|
48
|
+
.catch((err: unknown) => {
|
|
49
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
50
|
+
setDeleting(false)
|
|
51
|
+
setConfirming(false)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const paramRows = Object.entries(item.params)
|
|
56
|
+
// 官方 Modal 承载(嵌套 portal:层级高于工作台,遮罩/Escape 由 Modal 处理)。
|
|
57
|
+
return (
|
|
58
|
+
<Modal
|
|
59
|
+
open
|
|
60
|
+
onClose={onClose}
|
|
61
|
+
title={item.prompt.slice(0, 40)}
|
|
62
|
+
headless
|
|
63
|
+
className="ims-detailModal"
|
|
64
|
+
>
|
|
65
|
+
<div className="ims-sub">
|
|
66
|
+
<div className="ims-subHead">
|
|
67
|
+
<span className="ims-detailMeta">
|
|
68
|
+
{t('gallery.detail.model')}:{item.model} · {t('gallery.detail.endpoint')}:
|
|
69
|
+
{item.endpoint} · {t('gallery.detail.time')}:{fmtTime(item.createdAt)}
|
|
70
|
+
</span>
|
|
71
|
+
<button type="button" className="ims-btn ims-btnGhost" onClick={onClose}>
|
|
72
|
+
{t('common.close')}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="ims-detailBody">
|
|
76
|
+
<div className="ims-detailImages">
|
|
77
|
+
{item.imageIds.map((imageId, index) => (
|
|
78
|
+
<figure key={imageId} className="ims-detailFigure">
|
|
79
|
+
<img src={imageUrl(imageId)} alt={item.prompt} />
|
|
80
|
+
<figcaption>
|
|
81
|
+
{item.revisedPrompts[index] !== null &&
|
|
82
|
+
item.revisedPrompts[index] !== undefined ? (
|
|
83
|
+
<span className="ims-revised" title={item.revisedPrompts[index] ?? ''}>
|
|
84
|
+
{t('gallery.detail.revised')}:{item.revisedPrompts[index]}
|
|
85
|
+
</span>
|
|
86
|
+
) : null}
|
|
87
|
+
<a
|
|
88
|
+
className="ims-btn ims-btnGhost ims-btnSmall"
|
|
89
|
+
href={imageUrl(imageId)}
|
|
90
|
+
download
|
|
91
|
+
>
|
|
92
|
+
<IconDownload size={14} /> {t('common.download')}
|
|
93
|
+
</a>
|
|
94
|
+
</figcaption>
|
|
95
|
+
</figure>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
<div className="ims-detailInfo">
|
|
99
|
+
<h4 className="ims-detailLabel">{t('gallery.detail.prompt')}</h4>
|
|
100
|
+
<p className="ims-detailPrompt">{item.prompt}</p>
|
|
101
|
+
{item.sourceIds.length > 0 ? (
|
|
102
|
+
<>
|
|
103
|
+
<h4 className="ims-detailLabel">{t('gallery.detail.sources')}</h4>
|
|
104
|
+
<div className="ims-sources">
|
|
105
|
+
{item.sourceIds.map((imageId) => (
|
|
106
|
+
<span key={imageId} className="ims-thumb">
|
|
107
|
+
<img src={imageUrl(imageId)} alt="" />
|
|
108
|
+
</span>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
</>
|
|
112
|
+
) : null}
|
|
113
|
+
<h4 className="ims-detailLabel">{t('gallery.detail.params')}</h4>
|
|
114
|
+
{paramRows.length === 0 ? (
|
|
115
|
+
<p className="ims-hint">—</p>
|
|
116
|
+
) : (
|
|
117
|
+
<table className="ims-paramTable">
|
|
118
|
+
<tbody>
|
|
119
|
+
{paramRows.map(([key, value]) => (
|
|
120
|
+
<tr key={key}>
|
|
121
|
+
<td className="ims-paramTableKey">{key}</td>
|
|
122
|
+
<td>{JSON.stringify(value)}</td>
|
|
123
|
+
</tr>
|
|
124
|
+
))}
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="ims-subFoot">
|
|
131
|
+
{error !== null ? (
|
|
132
|
+
<p className="ims-status ims-statusError" role="alert">
|
|
133
|
+
{error}
|
|
134
|
+
</p>
|
|
135
|
+
) : null}
|
|
136
|
+
<button type="button" className="ims-btn ims-btnPrimary" onClick={() => onEdit(item)}>
|
|
137
|
+
{t('gallery.edit')}
|
|
138
|
+
</button>
|
|
139
|
+
{confirming ? (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
className="ims-btn ims-btnDanger"
|
|
143
|
+
disabled={deleting}
|
|
144
|
+
onClick={doDelete}
|
|
145
|
+
>
|
|
146
|
+
{t('common.confirmDelete')}
|
|
147
|
+
</button>
|
|
148
|
+
) : (
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
className="ims-btn ims-btnGhost"
|
|
152
|
+
onClick={() => setConfirming(true)}
|
|
153
|
+
>
|
|
154
|
+
<IconTrashOutline16 size={14} /> {t('common.delete')}
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</Modal>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function GalleryView(props: GalleryViewProps): ReactElement {
|
|
164
|
+
const { t, items, loading, failed, onRefresh, onEdit, detailId, onDetailChange } = props
|
|
165
|
+
const detail = items.find((item) => item.id === detailId) ?? null
|
|
166
|
+
return (
|
|
167
|
+
<div className="ims-gallery" hidden={props.hidden}>
|
|
168
|
+
<div className="ims-galleryBar">
|
|
169
|
+
<span className="ims-hint">{t('gallery.total').replace('{n}', String(items.length))}</span>
|
|
170
|
+
<button type="button" className="ims-btn ims-btnGhost ims-btnSmall" onClick={onRefresh}>
|
|
171
|
+
<IconRefreshOutline16 size={14} /> {t('common.refresh')}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
{failed ? (
|
|
175
|
+
<p className="ims-empty">
|
|
176
|
+
{t('common.loadFailed')}
|
|
177
|
+
<button type="button" className="ims-btn ims-btnGhost ims-btnSmall" onClick={onRefresh}>
|
|
178
|
+
{t('common.retry')}
|
|
179
|
+
</button>
|
|
180
|
+
</p>
|
|
181
|
+
) : loading ? (
|
|
182
|
+
<p className="ims-empty">{t('common.loading')}</p>
|
|
183
|
+
) : items.length === 0 ? (
|
|
184
|
+
<p className="ims-empty">{t('gallery.empty')}</p>
|
|
185
|
+
) : (
|
|
186
|
+
<div className="ims-grid">
|
|
187
|
+
{[...items].reverse().map((item) => (
|
|
188
|
+
<button
|
|
189
|
+
key={item.id}
|
|
190
|
+
type="button"
|
|
191
|
+
className="ims-cell"
|
|
192
|
+
onClick={() => onDetailChange(item.id)}
|
|
193
|
+
>
|
|
194
|
+
<img src={imageUrl(item.imageIds[0] ?? '')} alt={item.prompt} loading="lazy" />
|
|
195
|
+
<span className="ims-cellPrompt">{item.prompt}</span>
|
|
196
|
+
</button>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
{detail !== null ? (
|
|
201
|
+
<GalleryDetail
|
|
202
|
+
t={t}
|
|
203
|
+
item={detail}
|
|
204
|
+
onClose={() => onDetailChange(null)}
|
|
205
|
+
onEdit={(item) => {
|
|
206
|
+
onDetailChange(null)
|
|
207
|
+
onEdit(item)
|
|
208
|
+
}}
|
|
209
|
+
onDeleted={() => {
|
|
210
|
+
onDetailChange(null)
|
|
211
|
+
onRefresh()
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
) : null}
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
}
|