@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.
@@ -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
+ }