openxiangda-cli 2.0.0-alpha.62 → 2.0.0-alpha.67

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.62",
3
+ "version": "2.0.0-alpha.67",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,9 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@oclif/core": "4.13.3",
26
- "openxiangda-contracts": "2.0.0-alpha.30",
27
- "openxiangda-devkit-core": "2.0.0-alpha.38",
28
- "openxiangda-mcp": "2.0.0-alpha.38"
26
+ "openxiangda-contracts": "2.0.0-alpha.31",
27
+ "openxiangda-devkit-core": "2.0.0-alpha.41",
28
+ "openxiangda-mcp": "2.0.0-alpha.41"
29
29
  },
30
30
  "devDependencies": {
31
31
  "tsx": "4.23.12",
@@ -4,7 +4,8 @@ RUN corepack enable
4
4
  COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
5
5
  COPY apps/server/package.json apps/server/package.json
6
6
  COPY packages/contracts/package.json packages/contracts/package.json
7
- RUN pnpm --filter @app/server... install --frozen-lockfile --ignore-scripts
7
+ RUN node -e "const fs=require('node:fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); delete p.devDependencies; fs.writeFileSync('package.json', JSON.stringify(p));"
8
+ RUN pnpm --filter @app/server --filter @app/contracts install --no-frozen-lockfile --ignore-scripts
8
9
  COPY apps/server apps/server
9
10
  COPY packages/contracts packages/contracts
10
11
  RUN pnpm --filter @app/server... build
@@ -15,7 +15,7 @@
15
15
  "@nestjs/common": "11.1.29",
16
16
  "@nestjs/core": "11.1.29",
17
17
  "@nestjs/platform-fastify": "11.1.29",
18
- "openxiangda-nest": "2.0.0-alpha.36",
18
+ "openxiangda-nest": "2.0.0-alpha.37",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -16,7 +16,7 @@
16
16
  "antd": "6.4.3",
17
17
  "dayjs": "1.11.18",
18
18
  "docx-preview": "0.3.7",
19
- "openxiangda-contracts": "2.0.0-alpha.30",
19
+ "openxiangda-contracts": "2.0.0-alpha.31",
20
20
  "react": "19.2.8",
21
21
  "react-dom": "19.2.8",
22
22
  "react-router-dom": "7.8.2",
@@ -107,6 +107,19 @@ function optionDescription(option: Option) {
107
107
  return option.description || '';
108
108
  }
109
109
 
110
+ export type AuthoritativeSelectorProps = {
111
+ value?: string | string[];
112
+ onChange?: (value: string | string[] | undefined) => void;
113
+ source: Source;
114
+ operation: 'create' | 'update';
115
+ multiple?: boolean;
116
+ disabled?: boolean;
117
+ id?: string;
118
+ placeholder: string;
119
+ resourceCode?: string;
120
+ labelField?: string;
121
+ };
122
+
110
123
  export function AuthoritativeSelector({
111
124
  value,
112
125
  onChange,
@@ -118,18 +131,7 @@ export function AuthoritativeSelector({
118
131
  placeholder,
119
132
  resourceCode,
120
133
  labelField,
121
- }: {
122
- value?: string | string[];
123
- onChange?: (value: string | string[] | undefined) => void;
124
- source: Source;
125
- operation: 'create' | 'update';
126
- multiple?: boolean;
127
- disabled?: boolean;
128
- id?: string;
129
- placeholder: string;
130
- resourceCode?: string;
131
- labelField?: string;
132
- }) {
134
+ }: AuthoritativeSelectorProps) {
133
135
  const [options, setOptions] = useState<Option[]>([]);
134
136
  const [keyword, setKeyword] = useState('');
135
137
  const [cursor, setCursor] = useState<string | null>(null);
@@ -199,6 +201,30 @@ export function AuthoritativeSelector({
199
201
  return () => window.clearTimeout(timer);
200
202
  }, [disabled, keyword, labelField, operation, resourceCode, source, values]);
201
203
 
204
+ useEffect(() => {
205
+ if (disabled || !open || keyword.trim().length >= 2) return;
206
+ const sequence = ++requestSequence.current;
207
+ setLoading(true);
208
+ setError('');
209
+ void searchSource(source, operation, '', undefined, resourceCode, labelField)
210
+ .then(page => {
211
+ if (sequence !== requestSequence.current) return;
212
+ setOptions(current => {
213
+ const selected = current.filter(option => values.includes(option.value));
214
+ return mergeOptions(selected, page.items);
215
+ });
216
+ setCursor(page.nextCursor);
217
+ })
218
+ .catch(reason => {
219
+ if (sequence === requestSequence.current) {
220
+ setError(reason instanceof Error ? reason.message : String(reason));
221
+ }
222
+ })
223
+ .finally(() => {
224
+ if (sequence === requestSequence.current) setLoading(false);
225
+ });
226
+ }, [disabled, keyword, labelField, open, operation, resourceCode, source, values]);
227
+
202
228
  const loadNext = () => {
203
229
  if (!cursor || loading) return;
204
230
  const nextCursor = cursor;
@@ -236,7 +262,7 @@ export function AuthoritativeSelector({
236
262
  ) : error ? (
237
263
  <Typography.Text type="danger">{error}</Typography.Text>
238
264
  ) : keyword.trim().length < 2 ? (
239
- '请输入至少 2 个字符'
265
+ '暂无可选数据,可输入至少 2 个字符搜索'
240
266
  ) : (
241
267
  '无可选数据'
242
268
  )
@@ -245,7 +271,10 @@ export function AuthoritativeSelector({
245
271
  onChange?.(next || undefined);
246
272
  if (!multiple) setOpen(false);
247
273
  }}
248
- onOpenChange={setOpen}
274
+ onOpenChange={next => {
275
+ setOpen(next);
276
+ if (!next) setKeyword('');
277
+ }}
249
278
  onPopupScroll={event => {
250
279
  const element = event.currentTarget;
251
280
  if (
@@ -319,6 +348,225 @@ export function AuthoritativeSelector({
319
348
  );
320
349
  }
321
350
 
351
+ /**
352
+ * Mobile reference picker. It deliberately avoids Ant Design Select so that
353
+ * resource references have a touch-friendly, full-width interaction on
354
+ * phones. The search and resolve calls are the same authoritative platform
355
+ * APIs used by the desktop selector.
356
+ */
357
+ export function MobileAuthoritativeSelector({
358
+ value,
359
+ onChange,
360
+ source,
361
+ operation,
362
+ multiple = false,
363
+ disabled = false,
364
+ id,
365
+ placeholder,
366
+ resourceCode,
367
+ labelField,
368
+ }: AuthoritativeSelectorProps) {
369
+ const [options, setOptions] = useState<Option[]>([]);
370
+ const [keyword, setKeyword] = useState('');
371
+ const [cursor, setCursor] = useState<string | null>(null);
372
+ const [loading, setLoading] = useState(false);
373
+ const [error, setError] = useState('');
374
+ const [open, setOpen] = useState(false);
375
+ const [draftValues, setDraftValues] = useState<string[]>([]);
376
+ const requestSequence = useRef(0);
377
+ const values = useMemo(
378
+ () => (Array.isArray(value) ? value : value ? [value] : []),
379
+ [value]
380
+ );
381
+
382
+ useEffect(() => {
383
+ setDraftValues(values);
384
+ }, [open, values]);
385
+
386
+ useEffect(() => {
387
+ const missing = values.filter(
388
+ selected => !options.some(option => option.value === selected)
389
+ );
390
+ if (missing.length === 0) return;
391
+ let active = true;
392
+ setLoading(true);
393
+ void resolveSource(source, operation, missing, resourceCode, labelField)
394
+ .then(items => {
395
+ if (active) setOptions(current => mergeOptions(current, items));
396
+ })
397
+ .catch(reason => {
398
+ if (active) setError(reason instanceof Error ? reason.message : String(reason));
399
+ })
400
+ .finally(() => {
401
+ if (active) setLoading(false);
402
+ });
403
+ return () => {
404
+ active = false;
405
+ };
406
+ }, [labelField, operation, options, resourceCode, source, values]);
407
+
408
+ useEffect(() => {
409
+ if (disabled || !open) return;
410
+ const trimmed = keyword.trim();
411
+ if (trimmed.length > 0 && trimmed.length < 2) return;
412
+ const sequence = ++requestSequence.current;
413
+ const timer = window.setTimeout(() => {
414
+ setLoading(true);
415
+ setError('');
416
+ void searchSource(source, operation, trimmed, undefined, resourceCode, labelField)
417
+ .then(page => {
418
+ if (sequence !== requestSequence.current) return;
419
+ setOptions(current => {
420
+ const selected = current.filter(option => values.includes(option.value));
421
+ return mergeOptions(selected, page.items);
422
+ });
423
+ setCursor(page.nextCursor);
424
+ })
425
+ .catch(reason => {
426
+ if (sequence === requestSequence.current) {
427
+ setError(reason instanceof Error ? reason.message : String(reason));
428
+ }
429
+ })
430
+ .finally(() => {
431
+ if (sequence === requestSequence.current) setLoading(false);
432
+ });
433
+ }, trimmed ? 300 : 0);
434
+ return () => window.clearTimeout(timer);
435
+ }, [disabled, keyword, labelField, open, operation, resourceCode, source, values]);
436
+
437
+ const loadNext = () => {
438
+ if (!cursor || loading) return;
439
+ const nextCursor = cursor;
440
+ setLoading(true);
441
+ setError('');
442
+ void searchSource(source, operation, keyword.trim(), nextCursor, resourceCode, labelField)
443
+ .then(page => {
444
+ setOptions(current => mergeOptions(current, page.items));
445
+ setCursor(page.nextCursor);
446
+ })
447
+ .catch(reason => setError(reason instanceof Error ? reason.message : String(reason)))
448
+ .finally(() => setLoading(false));
449
+ };
450
+
451
+ const selectedItems = values
452
+ .map(selected => options.find(option => option.value === selected))
453
+ .filter((item): item is Option => Boolean(item));
454
+ const selectedLabel = selectedItems.length
455
+ ? selectedItems.map(item => item.label).join('、')
456
+ : values.length
457
+ ? (loading ? '读取中…' : values.join('、'))
458
+ : `请选择${placeholder}`;
459
+
460
+ const toggleValue = (nextValue: string) => {
461
+ const item = options.find(option => option.value === nextValue);
462
+ if (!item?.selectable) return;
463
+ if (!multiple) {
464
+ onChange?.(nextValue);
465
+ setOpen(false);
466
+ return;
467
+ }
468
+ setDraftValues(current =>
469
+ current.includes(nextValue)
470
+ ? current.filter(itemValue => itemValue !== nextValue)
471
+ : [...current, nextValue]
472
+ );
473
+ };
474
+
475
+ return (
476
+ <>
477
+ <button
478
+ aria-haspopup="dialog"
479
+ className="oxa-mobile-reference-trigger"
480
+ disabled={disabled}
481
+ id={id}
482
+ onClick={() => {
483
+ setDraftValues(values);
484
+ setKeyword('');
485
+ setOpen(true);
486
+ }}
487
+ type="button"
488
+ >
489
+ <span className={values.length ? '' : 'oxa-mobile-reference-placeholder'}>
490
+ {selectedLabel}
491
+ </span>
492
+ <span aria-hidden="true">⌄</span>
493
+ </button>
494
+ {open && (
495
+ <div
496
+ aria-label={placeholder}
497
+ aria-modal="true"
498
+ className="oxa-mobile-reference-backdrop"
499
+ onClick={() => setOpen(false)}
500
+ role="dialog"
501
+ >
502
+ <div className="oxa-mobile-reference-sheet" onClick={event => event.stopPropagation()}>
503
+ <div className="oxa-mobile-reference-header">
504
+ <strong>{placeholder}</strong>
505
+ <button onClick={() => setOpen(false)} type="button">关闭</button>
506
+ </div>
507
+ <input
508
+ autoFocus
509
+ className="oxa-mobile-input oxa-mobile-reference-search"
510
+ onChange={event => setKeyword(event.target.value)}
511
+ placeholder={`搜索${placeholder}`}
512
+ type="search"
513
+ value={keyword}
514
+ />
515
+ {error && <div className="oxa-mobile-reference-error">{error}</div>}
516
+ {loading && <div className="oxa-mobile-reference-loading">加载中…</div>}
517
+ {!loading && !error && options.length === 0 && (
518
+ <div className="oxa-mobile-reference-empty">
519
+ {keyword.trim().length < 2 ? '请输入至少 2 个字符搜索' : '暂无可选数据'}
520
+ </div>
521
+ )}
522
+ <div aria-multiselectable={multiple} className="oxa-mobile-reference-options" role="listbox">
523
+ {options.map(option => {
524
+ const selected = (multiple ? draftValues : values).includes(option.value);
525
+ const description = optionDescription(option);
526
+ return (
527
+ <button
528
+ aria-selected={selected}
529
+ className={`oxa-mobile-reference-option${selected ? ' is-selected' : ''}`}
530
+ disabled={!option.selectable}
531
+ key={option.value}
532
+ onClick={() => toggleValue(option.value)}
533
+ role="option"
534
+ type="button"
535
+ >
536
+ {optionIcon(option)}
537
+ <span className="oxa-mobile-reference-option-copy">
538
+ <strong>{option.label}</strong>
539
+ {description && <small>{description}</small>}
540
+ </span>
541
+ {selected && <span aria-hidden="true">✓</span>}
542
+ </button>
543
+ );
544
+ })}
545
+ </div>
546
+ {cursor && (
547
+ <button className="oxa-mobile-reference-more" onClick={loadNext} type="button">
548
+ {loading ? '加载中…' : '加载更多'}
549
+ </button>
550
+ )}
551
+ {multiple && (
552
+ <button
553
+ className="oxa-mobile-reference-confirm"
554
+ onClick={() => {
555
+ onChange?.(draftValues.length ? draftValues : undefined);
556
+ setOpen(false);
557
+ }}
558
+ type="button"
559
+ >
560
+ 确定({draftValues.length})
561
+ </button>
562
+ )}
563
+ </div>
564
+ </div>
565
+ )}
566
+ </>
567
+ );
568
+ }
569
+
322
570
  export function ResolvedValueText({
323
571
  source,
324
572
  value,