one-design-next 0.0.40 → 0.0.42

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.
@@ -35,6 +35,14 @@ export interface Attachment {
35
35
  status?: 'uploading' | 'error' | 'expired' | 'done';
36
36
  /** status='error' 时展示的失败原因文案。 */
37
37
  errorMessage?: string;
38
+ /**
39
+ * 图片自然像素宽 / 高(image/* 附件可选携带)。
40
+ * 主要用途:图片过期后 url 已失效、无法再测量,用存下来的原始尺寸
41
+ * 让「已过期」占位复刻原图 footprint(单图场景),消息流不因过期而跳版。
42
+ * 拿不到时占位退化成 88×88(同多图)。
43
+ */
44
+ width?: number;
45
+ height?: number;
38
46
  /**
39
47
  * 特殊变体:
40
48
  * - 'report':AI 生成报告的大卡片(含创建时间 + 右侧预览缩略图)。
@@ -1,13 +1,20 @@
1
1
  function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
2
2
  import HoverFill from "../hover-fill";
3
3
  import Icon from "../icon";
4
+ import Image from "../image";
5
+ import Tooltip from "../tooltip";
4
6
  import "./style";
5
7
  var categoryMap = {
6
- image: {
7
- icon: 'image',
8
+ sheet: {
9
+ icon: 'table',
8
10
  color: 'var(--odn-color-green-6)',
9
11
  bg: 'var(--odn-color-green-2)'
10
12
  },
13
+ slides: {
14
+ icon: 'presentation',
15
+ color: 'var(--odn-color-orange-6)',
16
+ bg: 'var(--odn-color-orange-2)'
17
+ },
11
18
  code: {
12
19
  icon: 'file-code',
13
20
  color: 'var(--odn-color-blue-8)',
@@ -40,17 +47,17 @@ var defaultMeta = {
40
47
  bg: 'var(--odn-color-solid-black-3)'
41
48
  };
42
49
  var mimeCategory = {
43
- 'application/pdf': 'doc',
50
+ 'application/pdf': 'text',
44
51
  'application/msword': 'doc',
45
52
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'doc',
46
- 'application/vnd.ms-excel': 'doc',
47
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'doc',
48
- 'application/vnd.ms-powerpoint': 'doc',
49
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'doc',
53
+ 'application/vnd.ms-excel': 'sheet',
54
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'sheet',
55
+ 'application/vnd.ms-powerpoint': 'slides',
56
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'slides',
50
57
  'application/json': 'data',
51
58
  'application/xml': 'data',
52
59
  'text/xml': 'data',
53
- 'text/csv': 'data',
60
+ 'text/csv': 'sheet',
54
61
  'text/html': 'code',
55
62
  'text/css': 'code',
56
63
  'text/javascript': 'code',
@@ -71,7 +78,6 @@ var mimeCategory = {
71
78
  function getFileMeta(mime) {
72
79
  if (mimeCategory[mime]) return categoryMap[mimeCategory[mime]];
73
80
  var prefix = mime.split('/')[0];
74
- if (prefix === 'image') return categoryMap.image;
75
81
  if (prefix === 'text') return categoryMap.text;
76
82
  return defaultMeta;
77
83
  }
@@ -123,18 +129,24 @@ function formatSize(bytes) {
123
129
  if (bytes < 1024 * 1024) return "".concat((bytes / 1024).toFixed(1), " KB");
124
130
  return "".concat((bytes / (1024 * 1024)).toFixed(1), " MB");
125
131
  }
126
- function truncateMessage(text, maxChars) {
127
- if (text.length <= maxChars) return text;
128
- return "".concat(text.slice(0, maxChars), "...");
132
+
133
+ /** image/* 附件缩略:统一走 Image leaf,不再内联裸 `<img>`。 */
134
+ function AttachmentImage(_ref) {
135
+ var attachment = _ref.attachment;
136
+ if (!attachment.url) return null;
137
+ return /*#__PURE__*/React.createElement(Image, {
138
+ url: attachment.url,
139
+ alt: attachment.name
140
+ });
129
141
  }
130
- export function Attachments(_ref) {
131
- var attachments = _ref.attachments,
132
- onRemove = _ref.onRemove,
133
- onFileClick = _ref.onFileClick,
134
- onRetry = _ref.onRetry,
135
- onDownload = _ref.onDownload,
136
- _ref$compact = _ref.compact,
137
- compact = _ref$compact === void 0 ? false : _ref$compact;
142
+ export function Attachments(_ref2) {
143
+ var attachments = _ref2.attachments,
144
+ onRemove = _ref2.onRemove,
145
+ onFileClick = _ref2.onFileClick,
146
+ onRetry = _ref2.onRetry,
147
+ onDownload = _ref2.onDownload,
148
+ _ref2$compact = _ref2.compact,
149
+ compact = _ref2$compact === void 0 ? false : _ref2$compact;
138
150
  if (attachments.length === 0) return null;
139
151
 
140
152
  /**
@@ -153,8 +165,7 @@ export function Attachments(_ref) {
153
165
  var isUploading = status === 'uploading';
154
166
  var isError = status === 'error';
155
167
  var isExpired = status === 'expired';
156
- var clickable = !!onFileClick && !isUploading && !isExpired;
157
- var retryable = !!onRetry && isError;
168
+ var clickable = !!onFileClick && !isUploading && !isExpired && !isError;
158
169
  var thumbCardInner = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
159
170
  "data-odn-attachments-thumb-card-thumb": true,
160
171
  style: isImage && !isUploading && !isError ? undefined : isUploading || isError ? undefined : {
@@ -173,9 +184,8 @@ export function Attachments(_ref) {
173
184
  style: {
174
185
  color: 'var(--odn-color-error)'
175
186
  }
176
- }) : isImage ? /*#__PURE__*/React.createElement("img", {
177
- src: a.url,
178
- alt: ""
187
+ }) : isImage ? /*#__PURE__*/React.createElement(AttachmentImage, {
188
+ attachment: a
179
189
  }) : /*#__PURE__*/React.createElement(Icon, {
180
190
  name: meta.icon,
181
191
  size: 16,
@@ -188,8 +198,8 @@ export function Attachments(_ref) {
188
198
  "data-odn-attachments-thumb-card-name": true
189
199
  }, a.name), /*#__PURE__*/React.createElement("div", {
190
200
  "data-odn-attachments-thumb-card-type": true
191
- }, isError ? truncateMessage(a.errorMessage || '上传失败,请重试', 18) : isExpired ? '已过期' : formatType(a.type, a.name))));
192
- return /*#__PURE__*/React.createElement("div", {
201
+ }, isExpired ? '已过期' : formatType(a.type, a.name))));
202
+ var cardEl = /*#__PURE__*/React.createElement("div", {
193
203
  key: i,
194
204
  "data-odn-attachments-item": true,
195
205
  "data-odn-attachments-state": status,
@@ -202,17 +212,10 @@ export function Attachments(_ref) {
202
212
  "data-odn-attachments-thumb-card": true,
203
213
  "data-odn-attachments-thumb-card-image": isImage || undefined,
204
214
  "data-odn-attachments-clickable": ""
205
- }, thumbCardInner) : retryable ? /*#__PURE__*/React.createElement("button", {
206
- type: "button",
207
- onClick: function onClick() {
208
- return onRetry(i);
209
- },
210
- "data-odn-attachments-thumb-card": true,
211
- "data-odn-attachments-thumb-card-image": isImage || undefined,
212
- "data-odn-attachments-retryable": ""
213
215
  }, thumbCardInner) : /*#__PURE__*/React.createElement("div", {
214
216
  "data-odn-attachments-thumb-card": true,
215
- "data-odn-attachments-thumb-card-image": isImage || undefined
217
+ "data-odn-attachments-thumb-card-image": isImage || undefined,
218
+ "data-odn-attachments-error": isError || undefined
216
219
  }, thumbCardInner), onRemove && /*#__PURE__*/React.createElement("button", {
217
220
  onClick: function onClick() {
218
221
  return onRemove(i);
@@ -222,10 +225,18 @@ export function Attachments(_ref) {
222
225
  name: "x",
223
226
  size: 10
224
227
  })));
228
+ if (isError) {
229
+ return /*#__PURE__*/React.createElement(Tooltip, {
230
+ key: i,
231
+ placement: "top",
232
+ popup: a.errorMessage || '上传失败'
233
+ }, cardEl);
234
+ }
235
+ return cardEl;
225
236
  }
226
237
 
227
238
  /**
228
- * image-thumb 渲染:纯图大缩略方块(56×56 cover、无边框)。
239
+ * image-thumb 渲染:纯图缩略方块(48×48 cover,与 file-card 等高)。
229
240
  *
230
241
  * 与文件信息卡(data-odn-attachments-card)严格同源——共享
231
242
  * data-odn-attachments-state 三态机、remove 角标。composer 待发送区把 image/*
@@ -238,10 +249,9 @@ export function Attachments(_ref) {
238
249
  var isError = status === 'error';
239
250
  var isExpired = status === 'expired';
240
251
  var clickable = !!onFileClick && !isUploading && !isError && !isExpired;
241
- var thumb = /*#__PURE__*/React.createElement("img", {
242
- src: a.url,
243
- alt: a.name
244
- });
252
+ var thumb = a.url ? /*#__PURE__*/React.createElement(AttachmentImage, {
253
+ attachment: a
254
+ }) : null;
245
255
  return /*#__PURE__*/React.createElement("div", {
246
256
  key: i,
247
257
  "data-odn-attachments-item": true,
@@ -262,25 +272,21 @@ export function Attachments(_ref) {
262
272
  name: "loading",
263
273
  size: 16,
264
274
  spin: true
265
- })), isError && (onRetry ? /*#__PURE__*/React.createElement("button", {
266
- type: "button",
267
- "data-odn-attachments-image-overlay": true,
268
- "data-odn-attachments-image-overlay-error": "",
269
- onClick: function onClick() {
270
- return onRetry(i);
271
- },
272
- title: a.errorMessage || '上传失败,点击重试'
273
- }, /*#__PURE__*/React.createElement(Icon, {
274
- name: "alert-triangle",
275
- size: 16
276
- }), /*#__PURE__*/React.createElement("span", null, "\u91CD\u8BD5")) : /*#__PURE__*/React.createElement("div", {
275
+ })), isError &&
276
+ /*#__PURE__*/
277
+ // 图片缩略方块太小塞不下错误文案:错误态只留居中的告警图标,
278
+ // 具体原因(errorMessage,来自业务层回写)走 hover 向上的 Tooltip。
279
+ // 图片上传不支持重试——overlay 不承载点击。
280
+ React.createElement(Tooltip, {
281
+ placement: "top",
282
+ popup: a.errorMessage || '上传失败'
283
+ }, /*#__PURE__*/React.createElement("div", {
277
284
  "data-odn-attachments-image-overlay": true,
278
- "data-odn-attachments-image-overlay-error": "",
279
- title: a.errorMessage || '上传失败'
285
+ "data-odn-attachments-image-overlay-error": ""
280
286
  }, /*#__PURE__*/React.createElement(Icon, {
281
287
  name: "alert-triangle",
282
288
  size: 16
283
- }), /*#__PURE__*/React.createElement("span", null, "\u5931\u8D25"))), onRemove && /*#__PURE__*/React.createElement("button", {
289
+ }))), onRemove && /*#__PURE__*/React.createElement("button", {
284
290
  onClick: function onClick() {
285
291
  return onRemove(i);
286
292
  },
@@ -423,8 +429,7 @@ export function Attachments(_ref) {
423
429
  var isUploading = status === 'uploading';
424
430
  var isError = status === 'error';
425
431
  var isExpired = status === 'expired';
426
- var clickable = !!onFileClick && !isUploading && !isExpired;
427
- var retryable = !!onRetry && isError;
432
+ var clickable = !!onFileClick && !isUploading && !isExpired && !isError;
428
433
  var fileCardInner = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
429
434
  "data-odn-attachments-file-icon": true,
430
435
  style: isUploading || isError ? undefined : {
@@ -455,8 +460,8 @@ export function Attachments(_ref) {
455
460
  "data-odn-attachments-name": true
456
461
  }, a.name), /*#__PURE__*/React.createElement("div", {
457
462
  "data-odn-attachments-meta": true
458
- }, isError ? truncateMessage(a.errorMessage || '上传失败,请重试', 18) : isExpired ? '已过期' : "".concat(formatType(a.type, a.name)).concat(a.size ? " \xB7 ".concat(formatSize(a.size)) : ''))));
459
- return /*#__PURE__*/React.createElement("div", _extends({
463
+ }, isExpired ? '已过期' : "".concat(formatType(a.type, a.name)).concat(a.size ? " \xB7 ".concat(formatSize(a.size)) : ''))));
464
+ var cardEl = /*#__PURE__*/React.createElement("div", _extends({
460
465
  key: i,
461
466
  "data-odn-attachments-item": true,
462
467
  "data-odn-attachments-state": status
@@ -469,15 +474,9 @@ export function Attachments(_ref) {
469
474
  },
470
475
  "data-odn-attachments-card": true,
471
476
  "data-odn-attachments-clickable": ""
472
- }, fileCardInner) : retryable ? /*#__PURE__*/React.createElement("button", {
473
- type: "button",
474
- onClick: function onClick() {
475
- return onRetry(i);
476
- },
477
- "data-odn-attachments-card": true,
478
- "data-odn-attachments-retryable": ""
479
477
  }, fileCardInner) : /*#__PURE__*/React.createElement("div", {
480
- "data-odn-attachments-card": true
478
+ "data-odn-attachments-card": true,
479
+ "data-odn-attachments-error": isError || undefined
481
480
  }, fileCardInner), onRemove && /*#__PURE__*/React.createElement("button", {
482
481
  onClick: function onClick() {
483
482
  return onRemove(i);
@@ -487,6 +486,14 @@ export function Attachments(_ref) {
487
486
  name: "x",
488
487
  size: 10
489
488
  })));
489
+ if (isError) {
490
+ return /*#__PURE__*/React.createElement(Tooltip, {
491
+ key: i,
492
+ placement: "top",
493
+ popup: a.errorMessage || '上传失败'
494
+ }, cardEl);
495
+ }
496
+ return cardEl;
490
497
  }));
491
498
  }
492
499
  Attachments.displayName = 'Attachments';
@@ -83,15 +83,8 @@
83
83
  [data-odn-attachments-card][data-odn-attachments-clickable]:hover {
84
84
  border-color: color-mix(in srgb, var(--odn-color-primary) 50%, transparent);
85
85
  }
86
- [data-odn-attachments-card][data-odn-attachments-retryable] {
87
- cursor: pointer;
88
- border: 1px solid color-mix(in srgb, var(--odn-color-error) 35%, var(--odn-color-black-6));
89
- font: inherit;
90
- text-align: left;
91
- appearance: none;
92
- }
93
- [data-odn-attachments-card][data-odn-attachments-retryable]:hover {
94
- border-color: color-mix(in srgb, var(--odn-color-error) 55%, transparent);
86
+ [data-odn-attachments-card][data-odn-attachments-error] {
87
+ border-color: color-mix(in srgb, var(--odn-color-error) 35%, var(--odn-color-black-6));
95
88
  }
96
89
 
97
90
  [data-odn-attachments-file-icon] {
@@ -133,10 +126,6 @@
133
126
  color: var(--odn-color-black-8);
134
127
  }
135
128
 
136
- [data-odn-attachments-item][data-odn-attachments-state=error] [data-odn-attachments-meta] {
137
- color: var(--odn-color-error);
138
- }
139
-
140
129
  [data-odn-attachments-item][data-odn-attachments-state=expired] {
141
130
  opacity: 0.55;
142
131
  }
@@ -190,7 +179,7 @@
190
179
  grid-template-columns: auto 1fr;
191
180
  gap: 8px;
192
181
  padding: 6px 10px 6px 6px;
193
- min-height: 42px;
182
+ min-height: 48px;
194
183
  border-radius: 6px;
195
184
  border: 1px solid var(--odn-color-black-6);
196
185
  background: var(--odn-color-solid-black-1);
@@ -207,12 +196,8 @@
207
196
  [data-odn-attachments-thumb-card][data-odn-attachments-clickable]:hover {
208
197
  border-color: color-mix(in srgb, var(--odn-color-primary) 50%, transparent);
209
198
  }
210
- [data-odn-attachments-thumb-card][data-odn-attachments-retryable] {
211
- cursor: pointer;
212
- border: 1px solid color-mix(in srgb, var(--odn-color-error) 35%, var(--odn-color-black-6));
213
- }
214
- [data-odn-attachments-thumb-card][data-odn-attachments-retryable]:hover {
215
- border-color: color-mix(in srgb, var(--odn-color-error) 55%, transparent);
199
+ [data-odn-attachments-thumb-card][data-odn-attachments-error] {
200
+ border-color: color-mix(in srgb, var(--odn-color-error) 35%, var(--odn-color-black-6));
216
201
  }
217
202
 
218
203
  /* 图片保持原先「无边框」视觉,仅与文件卡片对齐高度。 */
@@ -220,7 +205,7 @@
220
205
  border: none;
221
206
  background: transparent;
222
207
  }
223
- [data-odn-attachments-thumb-card][data-odn-attachments-thumb-card-image][data-odn-attachments-clickable]:hover, [data-odn-attachments-thumb-card][data-odn-attachments-thumb-card-image][data-odn-attachments-retryable]:hover {
208
+ [data-odn-attachments-thumb-card][data-odn-attachments-thumb-card-image][data-odn-attachments-clickable]:hover {
224
209
  border: none;
225
210
  }
226
211
 
@@ -234,6 +219,11 @@
234
219
  border-radius: 3px;
235
220
  overflow: hidden;
236
221
  }
222
+ [data-odn-attachments-thumb-card-thumb] [data-odn-image] {
223
+ display: block;
224
+ width: 100%;
225
+ height: 100%;
226
+ }
237
227
  [data-odn-attachments-thumb-card-thumb] img {
238
228
  width: 100%;
239
229
  height: 100%;
@@ -275,12 +265,12 @@
275
265
  [data-odn-attachments-card]:not([data-odn-attachments-image-card]),
276
266
  [data-odn-attachments-thumb-card] {
277
267
  width: 192px;
278
- min-height: 42px;
268
+ min-height: 48px;
279
269
  }
280
270
 
281
271
  /* ==========================================================================
282
- * image-thumb 变体(纯图大缩略方块)
283
- * 56×56 cover、无边框,承载 image/* + url。与文件信息卡(data-odn-attachments-card)
272
+ * image-thumb 变体(纯图缩略方块)
273
+ * 48×48 cover(与 file-card 等高),承载 image/* + url。缩略走 Image leaf(不再裸 `<img>`)。
284
274
  * 同源:共享 data-odn-attachments-state 三态机、remove 角标、fresh-done。
285
275
  * composer 待发送区即以此变体让"图片大预览 + 文件信息卡"并列同行、等高同源。
286
276
  * ========================================================================== */
@@ -290,8 +280,8 @@
290
280
 
291
281
  [data-odn-attachments-image-thumb] {
292
282
  position: relative;
293
- width: 56px;
294
- height: 56px;
283
+ width: 48px;
284
+ height: 48px;
295
285
  border-radius: 6px;
296
286
  overflow: hidden;
297
287
  padding: 0;
@@ -299,6 +289,11 @@
299
289
  background: var(--odn-color-black-2);
300
290
  box-sizing: border-box;
301
291
  }
292
+ [data-odn-attachments-image-thumb] [data-odn-image] {
293
+ display: block;
294
+ width: 100%;
295
+ height: 100%;
296
+ }
302
297
  [data-odn-attachments-image-thumb] img {
303
298
  width: 100%;
304
299
  height: 100%;
@@ -342,20 +337,10 @@
342
337
  }
343
338
  [data-odn-attachments-image-overlay][data-odn-attachments-image-overlay-error] {
344
339
  background: var(--odn-color-solid-black-1);
345
- flex-direction: column;
346
340
  border: 1px solid color-mix(in srgb, var(--odn-color-error) 35%, var(--odn-color-black-6));
347
341
  color: var(--odn-color-error);
348
- }
349
- [data-odn-attachments-image-overlay][data-odn-attachments-image-overlay-error]:is(button) {
350
- cursor: pointer;
351
- appearance: none;
352
- font: inherit;
353
- font-size: 10px;
354
342
  pointer-events: auto;
355
343
  }
356
- [data-odn-attachments-image-overlay][data-odn-attachments-image-overlay-error]:is(button):hover {
357
- border-color: color-mix(in srgb, var(--odn-color-error) 55%, transparent);
358
- }
359
344
 
360
345
  /* ==========================================================================
361
346
  * Report 变体(AI 生成报告入口卡片)
@@ -490,7 +490,6 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
490
490
  var composerBoxFileDragAttrs = fileDragOver ? {
491
491
  'data-odn-composer-file-drag': ''
492
492
  } : {};
493
- var showFileDragOverlay = fileDragOver && !disabled && !attachmentsDisabled;
494
493
  var handleComposerDragEnter = function handleComposerDragEnter(e) {
495
494
  e.preventDefault();
496
495
  if (disabled || attachmentsDisabled) return;
@@ -889,7 +888,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
889
888
  ref: editorRef,
890
889
  autoSize: {
891
890
  minRows: 1,
892
- maxRows: 5
891
+ maxRows: 10
893
892
  },
894
893
  placeholder: placeholder,
895
894
  value: text,
@@ -997,13 +996,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
997
996
  "data-odn-composer-lite-tools": true
998
997
  }, toolNodes), isLite && /*#__PURE__*/React.createElement("div", {
999
998
  "data-odn-composer-lite-send": true
1000
- }, sendOrStopButton), showFileDragOverlay && /*#__PURE__*/React.createElement("div", {
1001
- "data-odn-composer-file-drag-overlay": true,
1002
- "aria-hidden": true
1003
- }, /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement(Icon, {
1004
- name: "paperclip",
1005
- size: 14
1006
- }), "\u62D6\u653E\u6587\u4EF6\u5230\u6B64\u5904\u4E0A\u4F20"))), hint && /*#__PURE__*/React.createElement("p", {
999
+ }, sendOrStopButton)), hint && /*#__PURE__*/React.createElement("p", {
1007
1000
  "data-odn-composer-hint": true
1008
1001
  }, submitType === 'enter' ? 'Enter 发送 · Shift+Enter 换行 · 可拖入或粘贴文件' : 'Shift+Enter 发送 · Enter 换行 · 可拖入或粘贴文件')));
1009
1002
  });
@@ -26,6 +26,8 @@
26
26
  border: 1px solid var(--odn-color-black-6);
27
27
  background: var(--odn-color-bg-elevated);
28
28
  transition: border-color 0.15s;
29
+ max-height: 50vh;
30
+ min-height: 120px;
29
31
  }
30
32
  [data-odn-composer-box]:focus-within {
31
33
  border-color: color-mix(in srgb, var(--odn-color-brand-6) 50%, transparent);
@@ -35,42 +37,6 @@
35
37
  background: color-mix(in srgb, var(--odn-color-brand-6) 6%, var(--odn-color-bg-elevated));
36
38
  }
37
39
 
38
- [data-odn-composer-file-drag-overlay] {
39
- position: absolute;
40
- inset: 0;
41
- border-radius: inherit;
42
- background: color-mix(in srgb, var(--odn-color-brand-6) 8%, transparent);
43
- backdrop-filter: blur(4px);
44
- -webkit-backdrop-filter: blur(4px);
45
- display: flex;
46
- align-items: center;
47
- justify-content: center;
48
- pointer-events: none;
49
- z-index: 4;
50
- }
51
- [data-odn-composer-file-drag-overlay] span {
52
- display: inline-flex;
53
- align-items: center;
54
- gap: 6px;
55
- color: var(--odn-color-brand-6);
56
- font-size: 14px;
57
- line-height: 24px;
58
- font-weight: 600;
59
- }
60
-
61
- /* 拖放蒙层出现时,冻结内部交互样式事件;保留 box 自身 drop 能力。 */
62
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-editor],
63
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-toolbar],
64
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-lite-tools],
65
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-lite-send],
66
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-attachments] {
67
- pointer-events: none !important;
68
- }
69
- [data-odn-composer-box][data-odn-composer-file-drag] [data-odn-composer-editor] {
70
- caret-color: transparent;
71
- user-select: none;
72
- }
73
-
74
40
  /* ------------------------------------------------------------------
75
41
  * lite variant:
76
42
  * tools 和 send 按钮 absolute 在 box 的左下/右下;
@@ -211,6 +177,12 @@
211
177
  * ScrollArea root 的 height transition 按"目标态"分别定义在 lite 单行 /
212
178
  * lite 升格 selector 上,做出「先上再左 / 先右再下」的对称两段动效。
213
179
  * ----------------------------------------------------------------- */
180
+ [data-odn-composer] .odn-composer-editor-scroll {
181
+ flex: 1 1 auto;
182
+ min-height: 0;
183
+ overflow: hidden;
184
+ }
185
+
214
186
  [data-odn-composer] [data-odn-composer-editor] {
215
187
  position: relative;
216
188
  width: 100%;
@@ -471,7 +443,7 @@ p[data-odn-composer-hint] {
471
443
  * 三态机 / remove 角标 / fresh-done 全部下沉到 Attachments 组件。
472
444
  * ----------------------------------------------------------------- */
473
445
  [data-odn-composer-attachments] {
474
- --odn-composer-attachment-h: 42px;
446
+ --odn-composer-attachment-h: 48px;
475
447
  display: flex;
476
448
  flex-wrap: wrap;
477
449
  align-items: flex-start;
@@ -8,7 +8,7 @@ export interface ImageProps {
8
8
  alt?: string;
9
9
  /**
10
10
  * 整图点击回调。传此 prop 即把图片包一层 button,业务方按需对接
11
- * `PreviewPanel` 等 lightbox 组件。
11
+ * `PreviewPanel` 等 lightbox 组件。失败态(unavailable / 加载出错)下自动屏蔽。
12
12
  */
13
13
  onPreview?: () => void;
14
14
  /**
@@ -16,6 +16,20 @@ export interface ImageProps {
16
16
  * trash icon,用于 Composer 待发送态等需要删除的场景。
17
17
  */
18
18
  onRemove?: () => void;
19
+ /**
20
+ * 主动标记图片不可用(如上层已知 url 失效)。为 true 时**不加载 url**、
21
+ * 直接渲染 `fallback`,并屏蔽 onPreview。
22
+ *
23
+ * 这是渲染层的中性概念——leaf 不认识「过期 / 已删除」等业务原因,只知道
24
+ * 「这张图画不出来」。业务原因与文案由上层(如 `MessageImage`)翻译后
25
+ * 通过 `unavailable` + `fallback` 传入。
26
+ */
27
+ unavailable?: boolean;
28
+ /**
29
+ * 画不出图时(`unavailable` 或 `<img>` 运行时 onError)渲染的兜底内容。
30
+ * 不传则用内置中性占位(image-off 图标)。
31
+ */
32
+ fallback?: React.ReactNode;
19
33
  /** 透传到容器(非 img 本体)的 className,用于上层套尺寸约束。 */
20
34
  className?: string;
21
35
  /** 透传到容器(非 img 本体)的 style,用于上层套尺寸约束。 */
@@ -32,12 +46,13 @@ export interface ImageProps {
32
46
  * 不职责:
33
47
  * - 不带尺寸 opinion(max-width / max-height / object-fit 全由上层套)
34
48
  * - 不带消息态 / 输入态等业务语义
49
+ * - 不认识「过期 / 已删除」等业务原因,只认「画得出 / 画不出」(unavailable / fallback)
35
50
  *
36
51
  * 业务态包装在更上层的组件里实现:
37
52
  * - 消息态:`<MessageImage>` 套一层 max 280×360 contain(单图)或 88×88 cover(多图)容器
38
53
  * - Composer 待发送态:composer 内部直接套一层 72×72 cover 容器
39
54
  */
40
- export declare function Image({ url, alt, onPreview, onRemove, className, style }: ImageProps): import("react").JSX.Element;
55
+ export declare function Image({ url, alt, onPreview, onRemove, unavailable, fallback, className, style, }: ImageProps): import("react").JSX.Element;
41
56
  export declare namespace Image {
42
57
  var displayName: string;
43
58
  }
@@ -1,3 +1,10 @@
1
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
2
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
3
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
4
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
5
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
+ import { useState } from 'react';
1
8
  import Icon from "../icon";
2
9
  import "./style";
3
10
  /**
@@ -11,6 +18,7 @@ import "./style";
11
18
  * 不职责:
12
19
  * - 不带尺寸 opinion(max-width / max-height / object-fit 全由上层套)
13
20
  * - 不带消息态 / 输入态等业务语义
21
+ * - 不认识「过期 / 已删除」等业务原因,只认「画得出 / 画不出」(unavailable / fallback)
14
22
  *
15
23
  * 业务态包装在更上层的组件里实现:
16
24
  * - 消息态:`<MessageImage>` 套一层 max 280×360 contain(单图)或 88×88 cover(多图)容器
@@ -21,18 +29,41 @@ export function Image(_ref) {
21
29
  alt = _ref.alt,
22
30
  onPreview = _ref.onPreview,
23
31
  onRemove = _ref.onRemove,
32
+ unavailable = _ref.unavailable,
33
+ fallback = _ref.fallback,
24
34
  className = _ref.className,
25
35
  style = _ref.style;
26
- var imgEl = /*#__PURE__*/React.createElement("img", {
36
+ // 运行时加载失败:url 存活性不由上层保证(签名过期 / 404 / 断网都可能),
37
+ // onError 兜住让「画不出」自动降级,不出碎图。
38
+ var _useState = useState(false),
39
+ _useState2 = _slicedToArray(_useState, 2),
40
+ loadError = _useState2[0],
41
+ setLoadError = _useState2[1];
42
+ var showFallback = !!unavailable || loadError;
43
+
44
+ // 默认占位只有图标、无文案——leaf 不知道「为什么画不出」,任何原因文字
45
+ // 都是业务语义,由上层通过 fallback 传入(如 MessageImage 传「已过期」)。
46
+ var fallbackEl = /*#__PURE__*/React.createElement("div", {
47
+ "data-odn-image-fallback": true
48
+ }, fallback !== null && fallback !== void 0 ? fallback : /*#__PURE__*/React.createElement(Icon, {
49
+ name: "image-off",
50
+ size: 20
51
+ }));
52
+ var imgEl = showFallback ? fallbackEl : /*#__PURE__*/React.createElement("img", {
27
53
  src: url,
28
54
  alt: alt !== null && alt !== void 0 ? alt : '',
29
- loading: "lazy"
55
+ loading: "lazy",
56
+ onError: function onError() {
57
+ return setLoadError(true);
58
+ }
30
59
  });
60
+ var canPreview = !!onPreview && !showFallback;
31
61
  return /*#__PURE__*/React.createElement("div", {
32
62
  "data-odn-image": true,
63
+ "data-odn-image-unavailable": showFallback || undefined,
33
64
  className: className,
34
65
  style: style
35
- }, onPreview ? /*#__PURE__*/React.createElement("button", {
66
+ }, canPreview ? /*#__PURE__*/React.createElement("button", {
36
67
  type: "button",
37
68
  onClick: onPreview,
38
69
  "data-odn-image-trigger": true,
@@ -7,6 +7,30 @@
7
7
  display: block;
8
8
  }
9
9
 
10
+ /* 不可用态(unavailable 或运行时加载失败):渲染中性占位。
11
+ * 弱化感由占位块自身的纯色灰底 + 纯色图标承担,不用容器 opacity——
12
+ * 整体降透明会让图标 / 文字变成半透明合成,视觉上「层叠」发虚。 */
13
+ /* 占位块:和 <img> 一样填满上层套的尺寸盒(width/height 100%),leaf 不带
14
+ * 尺寸 opinion;上层没套尺寸时退化成按内容(图标 / 文字)撑开——不硬编码 min。
15
+ * 纯色 solid-black:底 solid-black-2、图标 / 文字 solid-black-8,
16
+ * 全程不透明,避免 alpha token 叠底产生的透视感。 */
17
+ [data-odn-image-fallback] {
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ justify-content: center;
22
+ gap: 8px;
23
+ width: 100%;
24
+ height: 100%;
25
+ padding: 8px;
26
+ box-sizing: border-box;
27
+ background: var(--odn-color-solid-black-2);
28
+ color: var(--odn-color-solid-black-8);
29
+ font-size: 12px;
30
+ line-height: 1.25;
31
+ text-align: center;
32
+ }
33
+
10
34
  /** 整图作为 onPreview 触发区时的 button 包装;样式上完全融入容器(无边框、无 padding)。 */
11
35
  [data-odn-image-trigger] {
12
36
  display: block;
@@ -0,0 +1,24 @@
1
+ /// <reference types="react" />
2
+ /**
3
+ * 大图查看器的最小数据形态。刻意不复用 `MessageImageItem`——查看器只认
4
+ * url / alt,与「消息态」无关,保持解耦(MessageImageItem 结构上可直接传入)。
5
+ */
6
+ export interface ImageViewerItem {
7
+ url: string;
8
+ alt?: string;
9
+ }
10
+ interface ImageViewerProps {
11
+ images: ImageViewerItem[];
12
+ initialIndex: number;
13
+ onClose: () => void;
14
+ }
15
+ /**
16
+ * MessageImage 的**私有内置**大图查看器——不写进 `components/index.ts`,
17
+ * 外部 import 不到。多个图片组件(未来含 Attachments)可共用这一份实现。
18
+ *
19
+ * 交互:ESC / 点遮罩 关闭;←/→ 切换;点图本身不关。
20
+ * 用 portal 挂到 body,规避祖先 `transform` / `overflow` 对 `position: fixed`
21
+ * 的截断(聊天流容器常带这些)。mounted guard 兜 SSR。
22
+ */
23
+ export declare function ImageViewer({ images, initialIndex, onClose }: ImageViewerProps): import("react").ReactPortal | null;
24
+ export default ImageViewer;
@@ -0,0 +1,119 @@
1
+ 'use client';
2
+
3
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
4
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
5
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
6
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
7
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
8
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
9
+ import { useCallback, useEffect, useState } from 'react';
10
+ import { createPortal } from 'react-dom';
11
+ import Icon from "../icon";
12
+
13
+ /**
14
+ * 大图查看器的最小数据形态。刻意不复用 `MessageImageItem`——查看器只认
15
+ * url / alt,与「消息态」无关,保持解耦(MessageImageItem 结构上可直接传入)。
16
+ */
17
+
18
+ /**
19
+ * MessageImage 的**私有内置**大图查看器——不写进 `components/index.ts`,
20
+ * 外部 import 不到。多个图片组件(未来含 Attachments)可共用这一份实现。
21
+ *
22
+ * 交互:ESC / 点遮罩 关闭;←/→ 切换;点图本身不关。
23
+ * 用 portal 挂到 body,规避祖先 `transform` / `overflow` 对 `position: fixed`
24
+ * 的截断(聊天流容器常带这些)。mounted guard 兜 SSR。
25
+ */
26
+ export function ImageViewer(_ref) {
27
+ var _it$alt;
28
+ var images = _ref.images,
29
+ initialIndex = _ref.initialIndex,
30
+ onClose = _ref.onClose;
31
+ var _useState = useState(initialIndex),
32
+ _useState2 = _slicedToArray(_useState, 2),
33
+ index = _useState2[0],
34
+ setIndex = _useState2[1];
35
+ var _useState3 = useState(false),
36
+ _useState4 = _slicedToArray(_useState3, 2),
37
+ mounted = _useState4[0],
38
+ setMounted = _useState4[1];
39
+ var total = images.length;
40
+ var prev = useCallback(function () {
41
+ return setIndex(function (i) {
42
+ return (i - 1 + total) % total;
43
+ });
44
+ }, [total]);
45
+ var next = useCallback(function () {
46
+ return setIndex(function (i) {
47
+ return (i + 1) % total;
48
+ });
49
+ }, [total]);
50
+ useEffect(function () {
51
+ return setMounted(true);
52
+ }, []);
53
+ useEffect(function () {
54
+ var onKey = function onKey(e) {
55
+ if (e.key === 'Escape') onClose();
56
+ if (e.key === 'ArrowLeft') prev();
57
+ if (e.key === 'ArrowRight') next();
58
+ };
59
+ document.addEventListener('keydown', onKey);
60
+ return function () {
61
+ return document.removeEventListener('keydown', onKey);
62
+ };
63
+ }, [onClose, prev, next]);
64
+ var it = images[index];
65
+ if (!mounted || !it) return null;
66
+ var overlay = /*#__PURE__*/React.createElement("div", {
67
+ "data-odn-image-viewer": true,
68
+ role: "dialog",
69
+ "aria-modal": "true",
70
+ onClick: onClose
71
+ }, /*#__PURE__*/React.createElement("img", {
72
+ src: it.url,
73
+ alt: (_it$alt = it.alt) !== null && _it$alt !== void 0 ? _it$alt : '',
74
+ onClick: function onClick(e) {
75
+ return e.stopPropagation();
76
+ },
77
+ "data-odn-image-viewer-img": true
78
+ }), /*#__PURE__*/React.createElement("button", {
79
+ type: "button",
80
+ onClick: function onClick(e) {
81
+ e.stopPropagation();
82
+ onClose();
83
+ },
84
+ "data-odn-image-viewer-btn": "close",
85
+ "aria-label": "\u5173\u95ED\uFF08ESC\uFF09",
86
+ title: "\u5173\u95ED\uFF08ESC\uFF09"
87
+ }, /*#__PURE__*/React.createElement(Icon, {
88
+ name: "x",
89
+ size: 20
90
+ })), total > 1 && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
91
+ type: "button",
92
+ onClick: function onClick(e) {
93
+ e.stopPropagation();
94
+ prev();
95
+ },
96
+ "data-odn-image-viewer-btn": "prev",
97
+ "aria-label": "\u4E0A\u4E00\u5F20\uFF08\u2190\uFF09",
98
+ title: "\u4E0A\u4E00\u5F20\uFF08\u2190\uFF09"
99
+ }, /*#__PURE__*/React.createElement(Icon, {
100
+ name: "chevron-left",
101
+ size: 24
102
+ })), /*#__PURE__*/React.createElement("button", {
103
+ type: "button",
104
+ onClick: function onClick(e) {
105
+ e.stopPropagation();
106
+ next();
107
+ },
108
+ "data-odn-image-viewer-btn": "next",
109
+ "aria-label": "\u4E0B\u4E00\u5F20\uFF08\u2192\uFF09",
110
+ title: "\u4E0B\u4E00\u5F20\uFF08\u2192\uFF09"
111
+ }, /*#__PURE__*/React.createElement(Icon, {
112
+ name: "chevron-right",
113
+ size: 24
114
+ })), /*#__PURE__*/React.createElement("div", {
115
+ "data-odn-image-viewer-counter": true
116
+ }, index + 1, " / ", total)));
117
+ return /*#__PURE__*/createPortal(overlay, document.body);
118
+ }
119
+ export default ImageViewer;
@@ -8,15 +8,35 @@ import './style';
8
8
  export interface MessageImageItem {
9
9
  url: string;
10
10
  alt?: string;
11
+ /**
12
+ * 图片已过期(消息流只读态)——历史消息里的图 url 随时间失效。
13
+ * 这是「消息态」的业务语义,由 MessageImage 翻译成 `Image` leaf 的
14
+ * `unavailable` + 「已过期」占位(leaf 本身不认识「过期」这个词)。
15
+ */
16
+ expired?: boolean;
17
+ /**
18
+ * 图片自然像素宽 / 高(可选)。仅用于单图过期时让占位复刻原图尺寸;
19
+ * 拿不到则占位退化成 88×88。
20
+ */
21
+ width?: number;
22
+ height?: number;
11
23
  }
12
24
  export interface MessageImageProps {
13
25
  /** 待渲染的图片列表。 */
14
26
  images: MessageImageItem[];
15
27
  /**
16
- * 点击图片回调,传入图片 index。组件不内置大图查看器,业务方按需对接
17
- * `PreviewPanel` / 自己实现的 lightbox。
28
+ * 是否可点开看大图(**内置**查看器,opt-out)。默认 `true`——「点开看大图」是
29
+ * 消息图片的标配语义。纯展示 / 由业务自己接预览的场景传 `false` 关掉。
30
+ *
31
+ * 过期 / 不可用的图始终不可点开,与此开关无关(`Image` leaf 自动屏蔽)。
32
+ */
33
+ preview?: boolean;
34
+ /**
35
+ * 图片点击**通知**(埋点 / 统计用),传入被点图片 index。这**不是**「自定义
36
+ * 查看器」的挂载点——大图预览由内置查看器(`preview`)负责,此回调只是知会
37
+ * 上层「第 i 张被点了」,不阻止内置行为。过期图不触发。
18
38
  */
19
- onPreview?: (index: number) => void;
39
+ onImageClick?: (index: number) => void;
20
40
  /**
21
41
  * 删除按钮点击回调,传入图片 index。本期 MessageImage 主要承载消息态展示,
22
42
  * 该回调主要为未来扩展(如撤回未发送消息中的图)保留。
@@ -35,7 +55,7 @@ export interface MessageImageProps {
35
55
  * Composer 待发送态等其他场景**不应消费此组件**——那是输入态语义,
36
56
  * 应直接用 `Image` leaf + 自己的尺寸 wrap。
37
57
  */
38
- export declare function MessageImage({ images, onPreview, onRemove }: MessageImageProps): import("react").JSX.Element | null;
58
+ export declare function MessageImage({ images, preview, onImageClick, onRemove, }: MessageImageProps): import("react").JSX.Element | null;
39
59
  export declare namespace MessageImage {
40
60
  var displayName: string;
41
61
  }
@@ -1,6 +1,55 @@
1
+ 'use client';
2
+
3
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
4
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
5
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
6
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
7
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
8
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
9
+ import { useCallback, useMemo, useState } from 'react';
1
10
  import Image from "../image";
11
+ import Icon from "../icon";
12
+ import ImageViewer from "./image-viewer";
2
13
  import "./style";
3
14
 
15
+ /**
16
+ * 单图过期占位的尺寸边界,镜像 style/index.scss 的尺寸 token:
17
+ * - SINGLE_MAX_* 对齐 --odn-message-image-max-width / -max-height
18
+ * - THUMB 对齐 --odn-message-image-thumb-size(多图缩略边长)
19
+ * 放在 wrapper(MessageImage)而非 leaf——尺寸始终归 wrapper。
20
+ */
21
+ var SINGLE_MAX_W = 280;
22
+ var SINGLE_MAX_H = 360;
23
+ var THUMB = 88;
24
+
25
+ /** 过期图片占位:文案由 MessageImage(消息态)传入 leaf——leaf 不认识「过期」。 */
26
+ function ExpiredFallback() {
27
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
28
+ name: "image-off",
29
+ size: 20
30
+ }), /*#__PURE__*/React.createElement("span", null, "\u56FE\u7247\u5DF2\u8FC7\u671F"));
31
+ }
32
+
33
+ /**
34
+ * 单图过期时占位盒的尺寸:
35
+ * - 有原图尺寸 → 按 contain 缩放到 max 280×360 内,复刻原图 footprint,不跳版
36
+ * - 没有原图尺寸 → 退化成 88×88(同多图缩略)
37
+ * 多图不走这里——多图恒为 88×88,由 scss 的 multi 选择器兜住。
38
+ */
39
+ function expiredSingleSize(item) {
40
+ if (item.width && item.height) {
41
+ var scale = Math.min(SINGLE_MAX_W / item.width, SINGLE_MAX_H / item.height, 1);
42
+ return {
43
+ width: Math.round(item.width * scale),
44
+ height: Math.round(item.height * scale)
45
+ };
46
+ }
47
+ return {
48
+ width: THUMB,
49
+ height: THUMB
50
+ };
51
+ }
52
+
4
53
  /**
5
54
  * MessageImage 渲染的图片项。
6
55
  * 与 `Attachment` 不同:图片消息的「主体」就是图本身,不需要 name / type / size 等
@@ -21,25 +70,61 @@ import "./style";
21
70
  */
22
71
  export function MessageImage(_ref) {
23
72
  var images = _ref.images,
24
- onPreview = _ref.onPreview,
73
+ _ref$preview = _ref.preview,
74
+ preview = _ref$preview === void 0 ? true : _ref$preview,
75
+ onImageClick = _ref.onImageClick,
25
76
  onRemove = _ref.onRemove;
77
+ var _useState = useState(null),
78
+ _useState2 = _slicedToArray(_useState, 2),
79
+ viewerIndex = _useState2[0],
80
+ setViewerIndex = _useState2[1];
81
+
82
+ // 查看器只收「画得出」的图——过期 / 不可用的排除,避免上下张切到碎图。
83
+ var viewable = useMemo(function () {
84
+ return images.filter(function (img) {
85
+ return !img.expired;
86
+ });
87
+ }, [images]);
88
+
89
+ // 点第 i 张(原始 index):先知会上层,再按需打开内置查看器。
90
+ var handleClick = useCallback(function (i) {
91
+ onImageClick === null || onImageClick === void 0 || onImageClick(i);
92
+ if (!preview) return;
93
+ var pos = viewable.indexOf(images[i]);
94
+ if (pos >= 0) setViewerIndex(pos);
95
+ }, [onImageClick, preview, images, viewable]);
26
96
  if (images.length === 0) return null;
27
97
  var isMulti = images.length > 1;
28
- return /*#__PURE__*/React.createElement("div", {
98
+ // 只要「可预览」或「要通知」,就把图包成可点区(过期图由 leaf 内部再屏蔽)。
99
+ var clickable = preview || !!onImageClick;
100
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
29
101
  "data-odn-message-image": true,
30
102
  "data-odn-message-image-multi": isMulti || undefined
31
103
  }, images.map(function (img, i) {
32
104
  return /*#__PURE__*/React.createElement(Image, {
33
105
  key: i,
34
106
  url: img.url,
35
- alt: img.alt,
36
- onPreview: onPreview ? function () {
37
- return onPreview(i);
107
+ alt: img.alt
108
+ // 「过期」是消息态业务语义,翻译成 leaf 的渲染态:不加载失效 url + 已过期占位。
109
+ ,
110
+ unavailable: img.expired || undefined,
111
+ fallback: img.expired ? /*#__PURE__*/React.createElement(ExpiredFallback, null) : undefined
112
+ // 单图过期时用原图尺寸复刻 footprint;多图恒 88×88 由 scss 兜。
113
+ ,
114
+ style: !isMulti && img.expired ? expiredSingleSize(img) : undefined,
115
+ onPreview: clickable ? function () {
116
+ return handleClick(i);
38
117
  } : undefined,
39
118
  onRemove: onRemove ? function () {
40
119
  return onRemove(i);
41
120
  } : undefined
42
121
  });
122
+ })), viewerIndex !== null && viewable.length > 0 && /*#__PURE__*/React.createElement(ImageViewer, {
123
+ images: viewable,
124
+ initialIndex: viewerIndex,
125
+ onClose: function onClose() {
126
+ return setViewerIndex(null);
127
+ }
43
128
  }));
44
129
  }
45
130
  MessageImage.displayName = 'MessageImage';
@@ -57,4 +57,102 @@ html {
57
57
  height: 100%;
58
58
  max-height: none;
59
59
  object-fit: cover;
60
+ }
61
+
62
+ /* 内置大图查看器(私有 image-viewer.tsx 渲染,portal 到 body):
63
+ * ESC / 点遮罩 关闭;←/→ 切换;点图不关。样式从 message-image-verify 的
64
+ * SimpleLightbox 收编而来——不再散落在 demo 里各写一份。 */
65
+ [data-odn-image-viewer] {
66
+ position: fixed;
67
+ inset: 0;
68
+ z-index: 9999;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ padding: 64px;
73
+ background: rgba(0, 0, 0, 0.78);
74
+ cursor: zoom-out;
75
+ animation: odn-image-viewer-fade 0.15s ease-out;
76
+ }
77
+
78
+ @keyframes odn-image-viewer-fade {
79
+ from {
80
+ opacity: 0;
81
+ }
82
+ to {
83
+ opacity: 1;
84
+ }
85
+ }
86
+ [data-odn-image-viewer-img] {
87
+ max-width: 90vw;
88
+ max-height: 90vh;
89
+ object-fit: contain;
90
+ border-radius: 8px;
91
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
92
+ cursor: default;
93
+ }
94
+
95
+ [data-odn-image-viewer-btn] {
96
+ position: absolute;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ border: none;
101
+ border-radius: 50%;
102
+ background: rgba(255, 255, 255, 0.12);
103
+ color: rgba(255, 255, 255, 0.95);
104
+ cursor: pointer;
105
+ backdrop-filter: blur(8px);
106
+ transition: background 0.15s, transform 0.1s;
107
+ }
108
+ [data-odn-image-viewer-btn]:hover {
109
+ background: rgba(255, 255, 255, 0.22);
110
+ }
111
+ [data-odn-image-viewer-btn]:active {
112
+ transform: scale(0.94);
113
+ }
114
+ [data-odn-image-viewer-btn]:focus-visible {
115
+ outline: 2px solid color-mix(in srgb, var(--odn-color-primary) 60%, transparent);
116
+ outline-offset: 2px;
117
+ }
118
+
119
+ [data-odn-image-viewer-btn=close] {
120
+ top: 20px;
121
+ right: 24px;
122
+ width: 40px;
123
+ height: 40px;
124
+ }
125
+
126
+ [data-odn-image-viewer-btn=prev],
127
+ [data-odn-image-viewer-btn=next] {
128
+ top: 50%;
129
+ width: 48px;
130
+ height: 48px;
131
+ transform: translateY(-50%);
132
+ }
133
+ [data-odn-image-viewer-btn=prev]:active,
134
+ [data-odn-image-viewer-btn=next]:active {
135
+ transform: translateY(-50%) scale(0.94);
136
+ }
137
+
138
+ [data-odn-image-viewer-btn=prev] {
139
+ left: 24px;
140
+ }
141
+
142
+ [data-odn-image-viewer-btn=next] {
143
+ right: 24px;
144
+ }
145
+
146
+ [data-odn-image-viewer-counter] {
147
+ position: absolute;
148
+ bottom: 24px;
149
+ left: 50%;
150
+ padding: 6px 14px;
151
+ border-radius: 999px;
152
+ background: rgba(0, 0, 0, 0.4);
153
+ color: rgba(255, 255, 255, 0.85);
154
+ font-size: 13px;
155
+ font-variant-numeric: tabular-nums;
156
+ backdrop-filter: blur(8px);
157
+ transform: translateX(-50%);
60
158
  }
@@ -6,16 +6,15 @@ export interface UserBubbleProps {
6
6
  message: ChatMessage;
7
7
  onFileClick?: (attachment: Attachment) => void;
8
8
  /**
9
- * 点击图片附件回调,业务方按此对接自己的 lightbox / 大图查看器。
9
+ * 图片点击**通知**(埋点 / 统计用)。大图预览由 `MessageImage` 内置查看器
10
+ * 负责——用户点图默认即可看大图,**业务方无需对接任何 lightbox**。此回调只
11
+ * 知会「第 i 张被点了」,不影响内置预览;过期图不触发。
10
12
  * @param index 被点击的图片在「当前消息内已分流的图片子集」中的位置(0-based)
11
- * @param images 当前消息内所有 image attachments(已转成 `MessageImageItem`
12
- * 的形态,包含 `url` 与 `alt`),方便直接 push 到 lightbox
13
- * 实现上下张切换
14
- * 不传 → 图片不可点击预览。
13
+ * @param images 当前消息内所有 image attachments(已转成 `MessageImageItem` 形态)
15
14
  */
16
- onImagePreview?: (index: number, images: MessageImageItem[]) => void;
15
+ onImageClick?: (index: number, images: MessageImageItem[]) => void;
17
16
  }
18
- export declare function UserBubble({ message, onFileClick, onImagePreview }: UserBubbleProps): import("react").JSX.Element;
17
+ export declare function UserBubble({ message, onFileClick, onImageClick }: UserBubbleProps): import("react").JSX.Element;
19
18
  export declare namespace UserBubble {
20
19
  var displayName: string;
21
20
  }
@@ -6,7 +6,7 @@ export function UserBubble(_ref) {
6
6
  var _message$content, _message$attachments;
7
7
  var message = _ref.message,
8
8
  onFileClick = _ref.onFileClick,
9
- onImagePreview = _ref.onImagePreview;
9
+ onImageClick = _ref.onImageClick;
10
10
  var segments = message.segments;
11
11
  var hasText = segments !== null && segments !== void 0 && segments.length ? segmentsHaveVisibleContent(segments) : !!((_message$content = message.content) !== null && _message$content !== void 0 && _message$content.trim());
12
12
  var textBody = segments !== null && segments !== void 0 && segments.length ? renderSegmentsContent(segments) : message.content;
@@ -20,7 +20,10 @@ export function UserBubble(_ref) {
20
20
  }).map(function (a) {
21
21
  return {
22
22
  url: a.url,
23
- alt: a.name
23
+ alt: a.name,
24
+ expired: a.status === 'expired',
25
+ width: a.width,
26
+ height: a.height
24
27
  };
25
28
  });
26
29
  var files = all.filter(function (a) {
@@ -34,8 +37,8 @@ export function UserBubble(_ref) {
34
37
  "data-odn-user-bubble-images": true
35
38
  }, /*#__PURE__*/React.createElement(MessageImage, {
36
39
  images: images,
37
- onPreview: onImagePreview ? function (i) {
38
- return onImagePreview(i, images);
40
+ onImageClick: onImageClick ? function (i) {
41
+ return onImageClick(i, images);
39
42
  } : undefined
40
43
  })), hasText && textBody != null && /*#__PURE__*/React.createElement("div", {
41
44
  "data-odn-user-bubble-text": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "packageManager": "pnpm@11.8.0",
6
6
  "module": "dist/index.js",