vome-core 0.1.86 → 0.1.87

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.
Files changed (63) hide show
  1. package/dist/admin/components/ui/avatar/index.js +1 -1
  2. package/dist/admin/components/ui/checkbox/index.js +1 -1
  3. package/dist/admin/components/ui/dialog/index.js +1 -1
  4. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  5. package/dist/admin/components/ui/input/index.js +1 -1
  6. package/dist/admin/components/ui/label/index.js +1 -1
  7. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  8. package/dist/admin/components/ui/select/index.js +1 -1
  9. package/dist/admin/components/ui/separator/index.js +1 -1
  10. package/dist/admin/components/ui/sheet/index.js +1 -1
  11. package/dist/admin/components/ui/sonner/index.js +1 -1
  12. package/dist/admin/components/ui/table/index.js +1 -1
  13. package/dist/admin/components/ui/tabs/index.js +1 -1
  14. package/dist/admin/components/ui/tooltip/index.js +1 -1
  15. package/dist/admin/crud/components/vm-richtext-extensions.js +1 -1
  16. package/dist/admin/crud/config.js +1 -1
  17. package/dist/admin/crud/confirm.js +1 -1
  18. package/dist/admin/crud/crud-page-request.js +1 -1
  19. package/dist/admin/crud/data-i18n.d.ts +39 -0
  20. package/dist/admin/crud/data-i18n.js +1 -0
  21. package/dist/admin/crud/dict.js +1 -1
  22. package/dist/admin/crud/export-fetch.js +1 -1
  23. package/dist/admin/crud/key.js +1 -1
  24. package/dist/admin/crud/plugins.js +1 -1
  25. package/dist/admin/crud/span.js +1 -1
  26. package/dist/admin/crud/style.js +1 -1
  27. package/dist/admin/crud/validate.js +1 -1
  28. package/dist/admin/crud/vm-crud.vue +66 -0
  29. package/dist/admin/crud/vm-data-i18n-btn.vue +104 -42
  30. package/dist/admin/crud/vm-data-i18n-lang.vue +176 -0
  31. package/dist/admin/crud/vm-table.vue +188 -5
  32. package/dist/admin/crud/vm-toolbar.vue +80 -47
  33. package/dist/admin/directives/perm.js +1 -1
  34. package/dist/admin/hooks/useUpload.js +1 -1
  35. package/dist/admin/lib/browser.js +1 -1
  36. package/dist/admin/lib/cn.js +1 -1
  37. package/dist/admin/lib/dialog-float.js +1 -1
  38. package/dist/admin/lib/export-excel.js +1 -1
  39. package/dist/admin/lib/import-excel.js +1 -1
  40. package/dist/admin/lib/json.js +1 -1
  41. package/dist/admin/lib/menu.js +1 -1
  42. package/dist/admin/lib/micro-locale.js +1 -1
  43. package/dist/admin/lib/micro-theme.js +1 -1
  44. package/dist/admin/lib/setup-crud-defaults.js +1 -1
  45. package/dist/admin/lib/shell.js +1 -1
  46. package/dist/admin/lib/theme-color-options.js +1 -1
  47. package/dist/admin/lib/tree.js +1 -1
  48. package/dist/admin/lib/upload.js +1 -1
  49. package/dist/admin/lib/video-frame.js +1 -1
  50. package/dist/admin/router/create.js +1 -1
  51. package/dist/admin/router/guards.js +1 -1
  52. package/dist/admin/themes/dark.js +1 -1
  53. package/dist/admin/themes/index.js +1 -1
  54. package/dist/admin/themes/light.js +1 -1
  55. package/dist/index.js +1 -1
  56. package/dist/server/index.js +1 -1
  57. package/dist/shared/datetime-picker.js +1 -1
  58. package/dist/shared/excel.js +1 -1
  59. package/dist/shared/index.js +1 -1
  60. package/dist/shared/locale-json.js +1 -1
  61. package/dist/shared/tree.js +1 -1
  62. package/package.json +1 -1
  63. package/typings/admin/comm/crud.ts +8 -1
@@ -0,0 +1,176 @@
1
+ <template>
2
+ <DropdownMenu v-if="langs.length" :modal="false">
3
+ <DropdownMenuTrigger as-child>
4
+ <button
5
+ type="button"
6
+ class="vm-crud-toolbar__btn is-ghost vm-data-i18n-lang__btn"
7
+ :disabled="switching"
8
+ :title="current?.name || langCode"
9
+ >
10
+ <span class="vm-data-i18n-lang__flag" aria-hidden="true">
11
+ {{ current?.flag || '🏳️' }}
12
+ </span>
13
+ 切换
14
+ </button>
15
+ </DropdownMenuTrigger>
16
+ <DropdownMenuContent align="start" class="vm-data-i18n-lang__menu">
17
+ <DropdownMenuItem
18
+ v-for="lang in langs"
19
+ :key="lang.value"
20
+ class="vm-data-i18n-lang__item"
21
+ :class="{ 'is-active': langCode === lang.value }"
22
+ :disabled="switching"
23
+ @click="switchLang(lang.value)"
24
+ >
25
+ <span class="vm-data-i18n-lang__flag" aria-hidden="true">
26
+ {{ lang.flag || '🏳️' }}
27
+ </span>
28
+ <span class="vm-data-i18n-lang__label">{{ lang.name || lang.label }}</span>
29
+ </DropdownMenuItem>
30
+ </DropdownMenuContent>
31
+ </DropdownMenu>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { computed, onMounted, ref, watch } from 'vue'
36
+ import { useVome } from '../hooks/useVome'
37
+ import { useCrud } from './useCrud'
38
+ import {
39
+ DropdownMenu,
40
+ DropdownMenuContent,
41
+ DropdownMenuItem,
42
+ DropdownMenuTrigger,
43
+ } from '../components/ui/dropdown-menu'
44
+ import type { DataI18nLangOption } from './data-i18n'
45
+
46
+ defineOptions({ name: 'vm-data-i18n-lang' })
47
+
48
+ const crud = useCrud()
49
+ const { service } = useVome()
50
+ const langs = ref<DataI18nLangOption[]>([])
51
+ const switching = ref(false)
52
+
53
+ const langCode = computed({
54
+ get: () => crud.dataI18nLangCode?.value || '',
55
+ set: (v: string) => {
56
+ if (crud.dataI18nLangCode) crud.dataI18nLangCode.value = v
57
+ },
58
+ })
59
+
60
+ const current = computed(
61
+ () => langs.value.find((l) => l.value === langCode.value) || langs.value[0],
62
+ )
63
+
64
+ async function loadLangs() {
65
+ const langSvc = (service as { i18n?: { lang?: { list?: (q: unknown) => Promise<unknown> } } })
66
+ .i18n?.lang
67
+ if (!langSvc?.list) {
68
+ langs.value = [{ label: 'en-US', value: 'en-US', name: 'en-US', flag: '🇺🇸' }]
69
+ ensureLang()
70
+ return
71
+ }
72
+ try {
73
+ const rows = (await langSvc.list({ status: 1 })) as Array<{
74
+ code?: string
75
+ name?: string
76
+ flag?: string
77
+ }>
78
+ langs.value = (rows || [])
79
+ .filter((r) => r.code && r.code !== 'zh-CN')
80
+ .map((r) => ({
81
+ label: `${r.name || r.code} (${r.code})`,
82
+ value: String(r.code),
83
+ name: String(r.name || r.code),
84
+ flag: r.flag || '🏳️',
85
+ }))
86
+ } catch {
87
+ langs.value = [{ label: 'en-US', value: 'en-US', name: 'en-US', flag: '🇺🇸' }]
88
+ }
89
+ ensureLang()
90
+ if (crud.trashMode.value === 'dataI18n') {
91
+ await crud.refresh()
92
+ }
93
+ }
94
+
95
+ function ensureLang() {
96
+ if (!langs.value.length) return
97
+ if (!langCode.value || !langs.value.some((l) => l.value === langCode.value)) {
98
+ langCode.value = langs.value[0]!.value
99
+ }
100
+ }
101
+
102
+ async function switchLang(code: string) {
103
+ if (switching.value || !code || code === langCode.value) return
104
+ switching.value = true
105
+ try {
106
+ langCode.value = code
107
+ crud.pagination.value = { ...crud.pagination.value, page: 1 }
108
+ await crud.refresh()
109
+ } finally {
110
+ switching.value = false
111
+ }
112
+ }
113
+
114
+ watch(
115
+ () => crud.trashMode.value,
116
+ (mode) => {
117
+ if (mode === 'dataI18n') void loadLangs()
118
+ },
119
+ )
120
+
121
+ onMounted(() => {
122
+ if (crud.trashMode.value === 'dataI18n') void loadLangs()
123
+ })
124
+ </script>
125
+
126
+ <style lang="scss" scoped>
127
+ .vm-crud-toolbar__btn {
128
+ display: inline-flex;
129
+ height: 36px;
130
+ align-items: center;
131
+ gap: 4px;
132
+ padding: 0 14px;
133
+ border: none;
134
+ border-radius: 12px;
135
+ font-size: 13px;
136
+ font-weight: 600;
137
+ cursor: pointer;
138
+ background: var(--muted);
139
+ color: var(--foreground);
140
+ transition: background 0.15s ease, opacity 0.15s ease;
141
+
142
+ &:disabled {
143
+ cursor: not-allowed;
144
+ opacity: 0.45;
145
+ }
146
+
147
+ &:hover:not(:disabled) {
148
+ background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
149
+ }
150
+ }
151
+
152
+ .vm-data-i18n-lang__flag {
153
+ font-size: 16px;
154
+ line-height: 1;
155
+ }
156
+
157
+ .vm-data-i18n-lang__menu {
158
+ min-width: 140px;
159
+ }
160
+
161
+ .vm-data-i18n-lang__item {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 8px;
165
+ cursor: pointer;
166
+
167
+ &.is-active {
168
+ background: color-mix(in srgb, var(--brand) 12%, transparent);
169
+ color: var(--brand);
170
+ }
171
+ }
172
+
173
+ .vm-data-i18n-lang__label {
174
+ font-size: 13px;
175
+ }
176
+ </style>
@@ -109,6 +109,27 @@
109
109
  {{ crudLabel('forceDelete', crud.dict.label) }}
110
110
  </Button>
111
111
  </div>
112
+ <div
113
+ v-else-if="crud.trashMode.value === 'dataI18n'"
114
+ class="vm-crud-table__ops"
115
+ >
116
+ <Button
117
+ type="button"
118
+ size="xs"
119
+ variant="softPrimary"
120
+ @click.stop="openDataI18nEdit(row)"
121
+ >
122
+ {{ crudLabel('update', crud.dict.label) }}
123
+ </Button>
124
+ <Button
125
+ type="button"
126
+ size="xs"
127
+ variant="softDestructive"
128
+ @click.stop="deleteDataI18nEntry(row)"
129
+ >
130
+ {{ crudLabel('delete', crud.dict.label) }}
131
+ </Button>
132
+ </div>
112
133
  <div v-else class="vm-crud-table__ops">
113
134
  <template v-for="(btn, bi) in resolveButtons(col, row)" :key="bi">
114
135
  <Button
@@ -245,17 +266,60 @@
245
266
  </Table>
246
267
  </div>
247
268
  <vm-context-menu ref="ctxRef" :items="ctxItems" @select="onCtxSelect" />
269
+
270
+ <Dialog :open="dataI18nEditOpen" @update:open="onDataI18nEditOpenChange">
271
+ <DialogContent shell="crud" class="vm-data-i18n-edit__dialog">
272
+ <DialogHeader class="vm-crud-shell__head">
273
+ <DialogTitle class="vm-crud-shell__title">编辑译文</DialogTitle>
274
+ </DialogHeader>
275
+ <div class="vm-data-i18n-edit__body">
276
+ <p class="vm-data-i18n-edit__meta">
277
+ <span>ID:{{ dataI18nEditForm.id }}</span>
278
+ <span>key:{{ dataI18nEditForm.key }}</span>
279
+ </p>
280
+ <label class="vm-data-i18n-edit__field">
281
+ <span>value</span>
282
+ <Input v-model="dataI18nEditForm.value" />
283
+ </label>
284
+ </div>
285
+ <DialogFooter class="vm-crud-shell__foot">
286
+ <button
287
+ type="button"
288
+ class="vm-crud-shell__btn is-ghost"
289
+ :disabled="dataI18nEditSubmitting"
290
+ @click="dataI18nEditOpen = false"
291
+ >
292
+ 取消
293
+ </button>
294
+ <button
295
+ type="button"
296
+ class="vm-crud-shell__btn is-primary"
297
+ :disabled="dataI18nEditSubmitting"
298
+ @click="saveDataI18nEdit"
299
+ >
300
+ {{ dataI18nEditSubmitting ? '保存中…' : '保存' }}
301
+ </button>
302
+ </DialogFooter>
303
+ </DialogContent>
304
+ </Dialog>
248
305
  </div>
249
306
  </template>
250
307
 
251
308
  <script setup lang="ts">
252
309
  import { ref, computed, watch, onMounted, onBeforeUnmount, provide, useSlots, toValue } from 'vue'
310
+ import { toast } from 'vue-sonner'
253
311
  import { useCrud } from './useCrud'
254
312
  import { injectTableOptions, TABLE_API_KEY } from './key'
255
313
  import { flattenTree } from '../lib/tree'
256
314
  import { resolveCrudComponent } from './registry'
257
315
  import { getCrudStyle, crudLabel, crudT, crudLocaleRev } from './style'
258
316
  import { findEpsEntity } from '../lib/eps'
317
+ import { useVome } from '../hooks/useVome'
318
+ import { vmConfirm } from './confirm'
319
+ import {
320
+ canUseDataI18n,
321
+ dataPackFromService,
322
+ } from './data-i18n'
259
323
  import type {
260
324
  CrudColumn,
261
325
  CrudOpButton,
@@ -298,7 +362,17 @@ let emptyRo: ResizeObserver | null = null
298
362
 
299
363
  const overrideColumns = ref<CrudColumn[] | null>(null)
300
364
 
365
+ const DATA_I18N_COLUMNS: CrudColumn[] = [
366
+ { prop: 'id', label: 'ID', minWidth: 120 },
367
+ { prop: 'key', label: 'key', minWidth: 140 },
368
+ { prop: 'value', label: 'value', minWidth: 220 },
369
+ { type: 'op', buttons: ['edit', 'delete'], width: 140 },
370
+ ]
371
+
372
+ const isDataI18nView = computed(() => crud.trashMode.value === 'dataI18n')
373
+
301
374
  const columns = computed(() => {
375
+ if (isDataI18nView.value) return DATA_I18N_COLUMNS
302
376
  const raw =
303
377
  overrideColumns.value ||
304
378
  (props.columns?.length ? props.columns : tableOpts?.columns || [])
@@ -307,13 +381,16 @@ const columns = computed(() => {
307
381
 
308
382
  const hasSelectionCol = computed(() => columns.value.some((c) => c.type === 'selection'))
309
383
  const hasOpCol = computed(() => columns.value.some((c) => c.type === 'op'))
310
- const showSelection = computed(() =>
311
- props.selection !== undefined ? props.selection : hasSelectionCol.value,
312
- )
384
+ const showSelection = computed(() => {
385
+ if (isDataI18nView.value) return false
386
+ return props.selection !== undefined ? props.selection : hasSelectionCol.value
387
+ })
313
388
  const dataColumns = computed(() =>
314
389
  columns.value.filter((c) => c.type !== 'selection'),
315
390
  )
316
- const showSlotOp = computed(() => Boolean(slots.op) && !hasOpCol.value)
391
+ const showSlotOp = computed(
392
+ () => Boolean(slots.op) && !hasOpCol.value && !isDataI18nView.value,
393
+ )
317
394
 
318
395
  /** EPS 列 → field.{table}.{prop} */
319
396
  function epsFieldKey(prop: string): string | undefined {
@@ -357,7 +434,9 @@ const colSpan = computed(
357
434
  (showSlotOp.value ? 1 : 0),
358
435
  )
359
436
 
360
- const rowKey = computed(() => tableOpts?.rowKey || props.rowKey)
437
+ const rowKey = computed(() =>
438
+ isDataI18nView.value ? '__rowKey' : tableOpts?.rowKey || props.rowKey,
439
+ )
361
440
  const isTree = computed(() => Boolean(tableOpts?.tree))
362
441
  const border = computed(() => tableOpts?.border ?? style.border)
363
442
  const autoHeight = computed(() => tableOpts?.autoHeight ?? style.autoHeight)
@@ -498,6 +577,7 @@ function colStyle(col: CrudColumn, index?: number) {
498
577
  return base
499
578
  }
500
579
  const ctxEnabled = computed(() => {
580
+ if (isDataI18nView.value) return false
501
581
  const c = tableOpts?.contextMenu ?? style.contextMenu
502
582
  return c !== false && !(Array.isArray(c) && c.length === 0)
503
583
  })
@@ -604,6 +684,81 @@ function deleteRowTargets(row: Record<string, unknown>) {
604
684
  return isTree.value ? collectSubtree(row) : [row]
605
685
  }
606
686
 
687
+ const { service: hostService } = useVome()
688
+ const dataI18nEditOpen = ref(false)
689
+ const dataI18nEditSubmitting = ref(false)
690
+ const dataI18nEditForm = ref({ id: '', key: '', value: '' })
691
+
692
+ function onDataI18nEditOpenChange(v: boolean) {
693
+ dataI18nEditOpen.value = v
694
+ }
695
+
696
+ function openDataI18nEdit(row: Record<string, unknown>) {
697
+ dataI18nEditForm.value = {
698
+ id: String(row.id ?? ''),
699
+ key: String(row.key ?? ''),
700
+ value: String(row.value ?? ''),
701
+ }
702
+ dataI18nEditOpen.value = true
703
+ }
704
+
705
+ async function saveDataI18nEdit() {
706
+ if (dataI18nEditSubmitting.value) return
707
+ const meta = canUseDataI18n(crud.service, hostService)
708
+ const dataPack = dataPackFromService(hostService)
709
+ const lang = crud.dataI18nLangCode?.value
710
+ if (!meta.ok || !dataPack?.updateEntry || !lang) {
711
+ toast.error('未配置业务翻译服务')
712
+ return
713
+ }
714
+ dataI18nEditSubmitting.value = true
715
+ try {
716
+ await dataPack.updateEntry({
717
+ tableName: meta.tableName,
718
+ langCode: lang,
719
+ id: dataI18nEditForm.value.id,
720
+ key: dataI18nEditForm.value.key,
721
+ value: dataI18nEditForm.value.value,
722
+ })
723
+ toast.success(crudLabel('saveSuccess', crud.dict.label))
724
+ dataI18nEditOpen.value = false
725
+ await crud.refresh()
726
+ } catch (e) {
727
+ toast.error(e instanceof Error ? e.message : '保存失败')
728
+ } finally {
729
+ dataI18nEditSubmitting.value = false
730
+ }
731
+ }
732
+
733
+ async function deleteDataI18nEntry(row: Record<string, unknown>) {
734
+ const meta = canUseDataI18n(crud.service, hostService)
735
+ const dataPack = dataPackFromService(hostService)
736
+ const lang = crud.dataI18nLangCode?.value
737
+ if (!meta.ok || !dataPack?.deleteEntry || !lang) {
738
+ toast.error('未配置业务翻译服务')
739
+ return
740
+ }
741
+ const ok = await vmConfirm({
742
+ title: crudLabel('tips', crud.dict.label),
743
+ message: crudLabel('deleteConfirm', crud.dict.label),
744
+ confirmText: crudLabel('confirm', crud.dict.label),
745
+ cancelText: crudLabel('close', crud.dict.label),
746
+ })
747
+ if (!ok) return
748
+ try {
749
+ await dataPack.deleteEntry({
750
+ tableName: meta.tableName,
751
+ langCode: lang,
752
+ id: String(row.id ?? ''),
753
+ key: String(row.key ?? ''),
754
+ })
755
+ toast.success(crudLabel('deleteSuccess', crud.dict.label))
756
+ await crud.refresh()
757
+ } catch (e) {
758
+ toast.error(e instanceof Error ? e.message : '删除失败')
759
+ }
760
+ }
761
+
607
762
  function toggleAll(checked: boolean | 'indeterminate') {
608
763
  if (checked === true) {
609
764
  // 树表:含折叠未展示的子孙
@@ -1040,4 +1195,32 @@ defineExpose({
1040
1195
  color: var(--foreground);
1041
1196
  }
1042
1197
  }
1198
+
1199
+ .vm-data-i18n-edit__dialog {
1200
+ width: min(480px, calc(100vw - 32px));
1201
+ }
1202
+
1203
+ .vm-data-i18n-edit__body {
1204
+ display: flex;
1205
+ flex-direction: column;
1206
+ gap: 14px;
1207
+ padding: 8px 22px 16px;
1208
+ }
1209
+
1210
+ .vm-data-i18n-edit__meta {
1211
+ display: flex;
1212
+ flex-wrap: wrap;
1213
+ gap: 12px;
1214
+ margin: 0;
1215
+ font-size: 13px;
1216
+ color: var(--muted-foreground);
1217
+ }
1218
+
1219
+ .vm-data-i18n-edit__field {
1220
+ display: flex;
1221
+ flex-direction: column;
1222
+ gap: 6px;
1223
+ font-size: 12px;
1224
+ color: var(--muted-foreground);
1225
+ }
1043
1226
  </style>
@@ -1,42 +1,47 @@
1
1
  <template>
2
2
  <div class="vm-crud-toolbar">
3
3
  <div class="vm-crud-toolbar__left">
4
- <button
5
- v-if="showAddBtn"
6
- type="button"
7
- class="vm-crud-toolbar__btn is-primary"
8
- @click="onAdd"
9
- >
10
- <i class="ri-add-line" />
11
- {{ addText ?? crudLabel('add', crud.dict.label) }}
12
- </button>
13
- <button
14
- v-if="showDeleteBtn"
15
- type="button"
16
- class="vm-crud-toolbar__btn is-danger"
17
- :disabled="!crud.selection.value.length"
18
- @click="crud.rowDelete()"
19
- >
20
- <i class="ri-delete-bin-line" />
21
- {{ deleteText ?? crudLabel('multiDelete', crud.dict.label) }}
22
- </button>
23
- <vm-export-btn
24
- v-if="showExport"
25
- :text="exportText || crudLabel('export', crud.dict.label)"
26
- :filename="exportFilename"
27
- :max-export-limit="maxExportLimit"
28
- :columns="exportColumns"
29
- :data="exportData"
30
- />
31
- <vm-import-btn
32
- v-if="showImport"
33
- :text="importText || crudLabel('import', crud.dict.label)"
34
- :tip="importTip"
35
- :template-filename="importTemplateFilename"
36
- :max-size-mb="importMaxSizeMb"
37
- />
38
- <vm-data-i18n-btn v-if="showDataI18n" :text="dataI18nText" />
39
- <slot />
4
+ <template v-if="isDataI18nView">
5
+ <vm-data-i18n-btn v-if="showDataI18n" :text="dataI18nText" />
6
+ <vm-data-i18n-lang />
7
+ </template>
8
+ <template v-else>
9
+ <button
10
+ v-if="showAddBtn"
11
+ type="button"
12
+ class="vm-crud-toolbar__btn is-primary"
13
+ @click="onAdd"
14
+ >
15
+ <i class="ri-add-line" />
16
+ {{ addText ?? crudLabel('add', crud.dict.label) }}
17
+ </button>
18
+ <button
19
+ v-if="showDeleteBtn"
20
+ type="button"
21
+ class="vm-crud-toolbar__btn is-danger"
22
+ :disabled="!crud.selection.value.length"
23
+ @click="crud.rowDelete()"
24
+ >
25
+ <i class="ri-delete-bin-line" />
26
+ {{ deleteText ?? crudLabel('multiDelete', crud.dict.label) }}
27
+ </button>
28
+ <vm-export-btn
29
+ v-if="showExport"
30
+ :text="exportText || crudLabel('export', crud.dict.label)"
31
+ :filename="exportFilename"
32
+ :max-export-limit="maxExportLimit"
33
+ :columns="exportColumns"
34
+ :data="exportData"
35
+ />
36
+ <vm-import-btn
37
+ v-if="showImport"
38
+ :text="importText || crudLabel('import', crud.dict.label)"
39
+ :tip="importTip"
40
+ :template-filename="importTemplateFilename"
41
+ :max-size-mb="importMaxSizeMb"
42
+ />
43
+ <slot />
44
+ </template>
40
45
  </div>
41
46
  <div class="vm-crud-toolbar__right">
42
47
  <slot name="right" />
@@ -49,10 +54,10 @@
49
54
  </button>
50
55
  </div>
51
56
  <vm-tabs
52
- v-if="showTrash"
57
+ v-if="showViewTabs"
53
58
  :model-value="crud.trashMode.value"
54
- :items="trashTabs"
55
- @update:model-value="onTrash"
59
+ :items="viewTabs"
60
+ @update:model-value="onViewTab"
56
61
  />
57
62
  </div>
58
63
  </div>
@@ -60,11 +65,14 @@
60
65
 
61
66
  <script setup lang="ts">
62
67
  import { computed } from 'vue'
68
+ import { useVome } from '../hooks/useVome'
63
69
  import { useCrud } from './useCrud'
64
70
  import VmExportBtn from './vm-export-btn.vue'
65
71
  import VmImportBtn from './vm-import-btn.vue'
66
72
  import VmDataI18nBtn from './vm-data-i18n-btn.vue'
73
+ import VmDataI18nLang from './vm-data-i18n-lang.vue'
67
74
  import VmTabs from './vm-tabs.vue'
75
+ import { canUseDataI18n } from './data-i18n'
68
76
  import { crudLabel } from './style'
69
77
  import type {
70
78
  CrudColumn,
@@ -80,7 +88,7 @@ const props = withDefaults(
80
88
  showExport?: boolean
81
89
  /** 显示导入(默认开;需 add 权限) */
82
90
  showImport?: boolean
83
- /** 显示业务数据 AI 翻译(默认开;EPS dataI18n 时显示) */
91
+ /** 显示业务数据翻译(默认开;EPS dataI18n 且在语言 Tab) */
84
92
  showDataI18n?: boolean
85
93
  dataI18nText?: string
86
94
  /**
@@ -118,14 +126,32 @@ const props = withDefaults(
118
126
  )
119
127
 
120
128
  const crud = useCrud()
129
+ const { service } = useVome()
130
+
131
+ const dataI18nEnabled = computed(() => canUseDataI18n(crud.service, service).ok)
121
132
 
122
133
  /** 有 restore 权限(Controller api + 权限码)才显示列表/回收站 */
123
134
  const showTrash = computed(() => Boolean(crud.getPermission('restore')))
124
135
 
125
- const trashTabs = computed(() => [
126
- { label: crudLabel('list', crud.dict.label), value: 'default' as const },
127
- { label: crudLabel('trash', crud.dict.label), value: 'onlyTrashed' as const },
128
- ])
136
+ const showViewTabs = computed(() => showTrash.value || dataI18nEnabled.value)
137
+
138
+ const viewTabs = computed(() => {
139
+ const tabs: Array<{ label: string; value: CrudTrashMode }> = [
140
+ { label: crudLabel('list', crud.dict.label), value: 'default' },
141
+ ]
142
+ if (showTrash.value) {
143
+ tabs.push({
144
+ label: crudLabel('trash', crud.dict.label),
145
+ value: 'onlyTrashed',
146
+ })
147
+ }
148
+ if (dataI18nEnabled.value) {
149
+ tabs.push({ label: '语言', value: 'dataI18n' })
150
+ }
151
+ return tabs
152
+ })
153
+
154
+ const isDataI18nView = computed(() => crud.trashMode.value === 'dataI18n')
129
155
 
130
156
  /** 有 add 权限才显示;回收站内可再藏 */
131
157
  const showAddBtn = computed(() => {
@@ -138,7 +164,10 @@ const showAddBtn = computed(() => {
138
164
  const showDeleteBtn = computed(() => Boolean(crud.getPermission('delete')))
139
165
 
140
166
  const showSearchActions = computed(
141
- () => props.search && crud.hasSearchFilters.value,
167
+ () =>
168
+ props.search &&
169
+ crud.hasSearchFilters.value &&
170
+ crud.trashMode.value !== 'dataI18n',
142
171
  )
143
172
 
144
173
  async function onAdd() {
@@ -161,11 +190,15 @@ function onReset() {
161
190
  crud.searchActions.value?.reset()
162
191
  }
163
192
 
164
- function onTrash(mode: string | number) {
193
+ function onViewTab(mode: string | number) {
165
194
  const next = String(mode) as CrudTrashMode
166
- if (next !== 'default' && next !== 'onlyTrashed') return
195
+ if (next !== 'default' && next !== 'onlyTrashed' && next !== 'dataI18n') return
196
+ if (next === 'onlyTrashed' && !showTrash.value) return
197
+ if (next === 'dataI18n' && !dataI18nEnabled.value) return
167
198
  crud.trashMode.value = next
168
199
  crud.pagination.value = { ...crud.pagination.value, page: 1 }
200
+ // 语言 Tab:等 vm-data-i18n-lang 拉完语种再 refresh,避免默认 en-US 抢跑
201
+ if (next === 'dataI18n') return
169
202
  void crud.refresh()
170
203
  }
171
204
  </script>
@@ -1 +1 @@
1
- (function(_0x20284,_0x3e3b7c){const _0x14583c=_0x1785,_0x8712=_0x20284();while(!![]){try{const _0x377ff8=parseInt(_0x14583c(0x11e))/0x1*(parseInt(_0x14583c(0x11f))/0x2)+-parseInt(_0x14583c(0x113))/0x3+parseInt(_0x14583c(0x119))/0x4*(parseInt(_0x14583c(0x116))/0x5)+parseInt(_0x14583c(0x11b))/0x6*(-parseInt(_0x14583c(0x115))/0x7)+parseInt(_0x14583c(0x11c))/0x8+-parseInt(_0x14583c(0x118))/0x9+parseInt(_0x14583c(0x11a))/0xa*(parseInt(_0x14583c(0x117))/0xb);if(_0x377ff8===_0x3e3b7c)break;else _0x8712['push'](_0x8712['shift']());}catch(_0x4f8a19){_0x8712['push'](_0x8712['shift']());}}}(_0x1737,0x366d7));import{useUserStore}from'../stores/user';export const vPerm={'mounted'(_0x5c1fa3,_0x5bb40c){const _0x208218=_0x1785;apply(_0x5c1fa3,_0x5bb40c[_0x208218(0x11d)]);},'updated'(_0x532346,_0x16bb33){const _0x55c141=_0x1785;apply(_0x532346,_0x16bb33[_0x55c141(0x11d)]);}};function _0x1737(){const _0x42e6f1=['mJa2nda0og9nywnqAq','DMfSDwu','ntG1m0nxBuXbyW','mZHVCentChC','zgLZCgXHEq','mti3nZq4mwLAEMXhDa','C3r5Bgu','mJK4nJi3n2nsruHUBq','mtqWnvbUrwf3AG','mtfSzhvry1u','mJu1mZyZm0HYBfvHtq','mJKZnKHzsLz2vG','nZGZnJqZmejpCuDkAW','nM9XwvHXvW'];_0x1737=function(){return _0x42e6f1;};return _0x1737();}function _0x1785(_0x4af2b3,_0x1f8584){_0x4af2b3=_0x4af2b3-0x113;const _0x1737b9=_0x1737();let _0x17856e=_0x1737b9[_0x4af2b3];if(_0x1785['ETDSot']===undefined){var _0x515669=function(_0x5e0a7c){const _0x1e0536='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x5c1fa3='',_0x5bb40c='';for(let _0x532346=0x0,_0x16bb33,_0x144583,_0x167211=0x0;_0x144583=_0x5e0a7c['charAt'](_0x167211++);~_0x144583&&(_0x16bb33=_0x532346%0x4?_0x16bb33*0x40+_0x144583:_0x144583,_0x532346++%0x4)?_0x5c1fa3+=String['fromCharCode'](0xff&_0x16bb33>>(-0x2*_0x532346&0x6)):0x0){_0x144583=_0x1e0536['indexOf'](_0x144583);}for(let _0x1abb0=0x0,_0x5593f6=_0x5c1fa3['length'];_0x1abb0<_0x5593f6;_0x1abb0++){_0x5bb40c+='%'+('00'+_0x5c1fa3['charCodeAt'](_0x1abb0)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x5bb40c);};_0x1785['pIgQRB']=_0x515669,_0x1785['QgzRrD']={},_0x1785['ETDSot']=!![];}const _0x38ace9=_0x1737b9[0x0],_0x585f3c=_0x4af2b3+_0x38ace9,_0x1b4da2=_0x1785['QgzRrD'][_0x585f3c];return!_0x1b4da2?(_0x17856e=_0x1785['pIgQRB'](_0x17856e),_0x1785['QgzRrD'][_0x585f3c]=_0x17856e):_0x17856e=_0x1b4da2,_0x17856e;}function apply(_0x144583,_0x167211){const _0x35e89f=_0x1785,_0x1abb0=useUserStore(),_0x5593f6=_0x1abb0['hasPerm'](_0x167211);_0x144583[_0x35e89f(0x114)][_0x35e89f(0x120)]=_0x5593f6?'':'none';}
1
+ function _0x12cb(){const _0x2ac14d=['mtqZndC0uLvlBe5Q','DMfSDwu','ofD4r3Pcza','mZzxtMPRDu0','mtqWodu0AfftChbI','mtiXnJC4oeTWuhHcwq','nJiWotryyxzswfO','ndqYDvfZzhny','oxDyzeL5sW','mJaWnZaZwgfXAvfo','ntvevLPjBxa','zgLZCgXHEq','BM9Uzq','mZi1nJaYouTqtLjlvW','mteWzKPOs2np','AgfZugvYBq','ndG5nZq1mhffvwjPtq','mNbcvhDkza'];_0x12cb=function(){return _0x2ac14d;};return _0x12cb();}(function(_0x540b81,_0x22f599){const _0x54e094=_0x5541,_0x4a5856=_0x540b81();while(!![]){try{const _0x4ce157=-parseInt(_0x54e094(0x180))/0x1*(-parseInt(_0x54e094(0x16f))/0x2)+-parseInt(_0x54e094(0x178))/0x3*(-parseInt(_0x54e094(0x172))/0x4)+parseInt(_0x54e094(0x17d))/0x5*(-parseInt(_0x54e094(0x175))/0x6)+-parseInt(_0x54e094(0x17c))/0x7*(parseInt(_0x54e094(0x171))/0x8)+parseInt(_0x54e094(0x177))/0x9*(parseInt(_0x54e094(0x17f))/0xa)+-parseInt(_0x54e094(0x179))/0xb*(parseInt(_0x54e094(0x174))/0xc)+-parseInt(_0x54e094(0x176))/0xd*(-parseInt(_0x54e094(0x173))/0xe);if(_0x4ce157===_0x22f599)break;else _0x4a5856['push'](_0x4a5856['shift']());}catch(_0x3cb12e){_0x4a5856['push'](_0x4a5856['shift']());}}}(_0x12cb,0x5c2ee));import{useUserStore}from'../stores/user';export const vPerm={'mounted'(_0x126587,_0xb8063){const _0x4a00ef=_0x5541;apply(_0x126587,_0xb8063[_0x4a00ef(0x170)]);},'updated'(_0xd93563,_0x55edb8){const _0xaa8461=_0x5541;apply(_0xd93563,_0x55edb8[_0xaa8461(0x170)]);}};function _0x5541(_0x418c3f,_0x329ff6){_0x418c3f=_0x418c3f-0x16f;const _0x12cbcc=_0x12cb();let _0x5541c0=_0x12cbcc[_0x418c3f];if(_0x5541['kPPLdK']===undefined){var _0x260002=function(_0x4055f4){const _0x32cdc3='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x126587='',_0xb8063='';for(let _0xd93563=0x0,_0x55edb8,_0x1a8856,_0xe3c6e0=0x0;_0x1a8856=_0x4055f4['charAt'](_0xe3c6e0++);~_0x1a8856&&(_0x55edb8=_0xd93563%0x4?_0x55edb8*0x40+_0x1a8856:_0x1a8856,_0xd93563++%0x4)?_0x126587+=String['fromCharCode'](0xff&_0x55edb8>>(-0x2*_0xd93563&0x6)):0x0){_0x1a8856=_0x32cdc3['indexOf'](_0x1a8856);}for(let _0x9d9b33=0x0,_0x6479c2=_0x126587['length'];_0x9d9b33<_0x6479c2;_0x9d9b33++){_0xb8063+='%'+('00'+_0x126587['charCodeAt'](_0x9d9b33)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0xb8063);};_0x5541['OoemNK']=_0x260002,_0x5541['qtlEUF']={},_0x5541['kPPLdK']=!![];}const _0xdbc4bc=_0x12cbcc[0x0],_0x2f69de=_0x418c3f+_0xdbc4bc,_0x2754ff=_0x5541['qtlEUF'][_0x2f69de];return!_0x2754ff?(_0x5541c0=_0x5541['OoemNK'](_0x5541c0),_0x5541['qtlEUF'][_0x2f69de]=_0x5541c0):_0x5541c0=_0x2754ff,_0x5541c0;}function apply(_0x1a8856,_0xe3c6e0){const _0x4889a6=_0x5541,_0x9d9b33=useUserStore(),_0x6479c2=_0x9d9b33[_0x4889a6(0x17e)](_0xe3c6e0);_0x1a8856['style'][_0x4889a6(0x17a)]=_0x6479c2?'':_0x4889a6(0x17b);}