vome-core 0.1.95 → 0.1.97

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 (68) hide show
  1. package/dist/admin/api/client.js +33 -4
  2. package/dist/admin/components/ui/avatar/index.js +1 -1
  3. package/dist/admin/components/ui/checkbox/index.js +1 -1
  4. package/dist/admin/components/ui/dialog/index.js +1 -1
  5. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  6. package/dist/admin/components/ui/input/index.js +1 -1
  7. package/dist/admin/components/ui/label/index.js +1 -1
  8. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  9. package/dist/admin/components/ui/select/index.js +1 -1
  10. package/dist/admin/components/ui/separator/index.js +1 -1
  11. package/dist/admin/components/ui/sheet/index.js +1 -1
  12. package/dist/admin/components/ui/sonner/index.js +1 -1
  13. package/dist/admin/components/ui/table/index.js +1 -1
  14. package/dist/admin/components/ui/tabs/index.js +1 -1
  15. package/dist/admin/components/ui/tooltip/index.js +1 -1
  16. package/dist/admin/crud/components/vm-richtext-extensions.js +1 -1
  17. package/dist/admin/crud/config.js +1 -1
  18. package/dist/admin/crud/confirm.js +1 -1
  19. package/dist/admin/crud/crud-page-request.js +1 -1
  20. package/dist/admin/crud/data-i18n.d.ts +16 -6
  21. package/dist/admin/crud/data-i18n.js +1 -1
  22. package/dist/admin/crud/dict.js +1 -1
  23. package/dist/admin/crud/export-fetch.js +1 -1
  24. package/dist/admin/crud/key.js +1 -1
  25. package/dist/admin/crud/plugins.js +1 -1
  26. package/dist/admin/crud/span.js +1 -1
  27. package/dist/admin/crud/style.js +1 -1
  28. package/dist/admin/crud/validate.js +1 -1
  29. package/dist/admin/crud/vm-crud.vue +45 -9
  30. package/dist/admin/crud/vm-data-i18n-btn.vue +45 -22
  31. package/dist/admin/crud/vm-data-i18n-lang.vue +147 -44
  32. package/dist/admin/crud/vm-table.vue +108 -27
  33. package/dist/admin/crud/vm-toolbar.vue +1 -1
  34. package/dist/admin/directives/perm.js +1 -1
  35. package/dist/admin/hooks/useUpload.js +1 -1
  36. package/dist/admin/lib/browser.js +1 -1
  37. package/dist/admin/lib/cn.js +1 -1
  38. package/dist/admin/lib/dialog-float.js +1 -1
  39. package/dist/admin/lib/export-excel.js +1 -1
  40. package/dist/admin/lib/import-excel.js +1 -1
  41. package/dist/admin/lib/json.js +1 -1
  42. package/dist/admin/lib/menu.js +1 -1
  43. package/dist/admin/lib/micro-locale.js +1 -1
  44. package/dist/admin/lib/micro-theme.js +1 -1
  45. package/dist/admin/lib/setup-crud-defaults.js +1 -1
  46. package/dist/admin/lib/shell.js +1 -1
  47. package/dist/admin/lib/theme-color-options.js +1 -1
  48. package/dist/admin/lib/tree.js +1 -1
  49. package/dist/admin/lib/upload.js +1 -1
  50. package/dist/admin/lib/video-frame.js +1 -1
  51. package/dist/admin/router/create.js +1 -1
  52. package/dist/admin/router/guards.js +1 -1
  53. package/dist/admin/themes/dark.js +1 -1
  54. package/dist/admin/themes/index.js +1 -1
  55. package/dist/admin/themes/light.js +1 -1
  56. package/dist/client/base.js +7 -8
  57. package/dist/index.js +1 -1
  58. package/dist/server/index.js +1 -1
  59. package/dist/shared/datetime-picker.js +1 -1
  60. package/dist/shared/excel.js +1 -1
  61. package/dist/shared/index.js +1 -1
  62. package/dist/shared/locale-json.js +1 -1
  63. package/dist/shared/tree.js +1 -1
  64. package/dist/typings/routing/eps.d.ts +2 -0
  65. package/package.json +1 -1
  66. package/typings/admin/comm/crud.ts +4 -0
  67. package/typings/admin/comm/eps.ts +2 -0
  68. package/typings/routing/eps.ts +2 -0
@@ -7,7 +7,7 @@
7
7
  @click="openDialog"
8
8
  >
9
9
  <i class="ri-translate-2" />
10
- {{ text || '翻译' }}
10
+ {{ text || crudLabel('dataI18nTranslate', crud.dict.label) }}
11
11
  </button>
12
12
 
13
13
  <Dialog :open="open" @update:open="onOpenChange">
@@ -20,7 +20,9 @@
20
20
  @interact-outside="preventDialogOutsideClose"
21
21
  >
22
22
  <DialogHeader class="vm-crud-shell__head">
23
- <DialogTitle class="vm-crud-shell__title">业务数据翻译</DialogTitle>
23
+ <DialogTitle class="vm-crud-shell__title">
24
+ {{ crudLabel('dataI18nTranslateTitle', crud.dict.label) }}
25
+ </DialogTitle>
24
26
  <DialogClose class="vm-crud-shell__close">
25
27
  <i class="ri-close-line" />
26
28
  <span class="sr-only">Close</span>
@@ -33,7 +35,9 @@
33
35
  class="vm-data-i18n-btn__field"
34
36
  :style="{ gridColumn: `span ${fieldSpan}` }"
35
37
  >
36
- <Label class="vm-data-i18n-btn__label">业务表</Label>
38
+ <Label class="vm-data-i18n-btn__label">
39
+ {{ crudLabel('dataI18nTable', crud.dict.label) }}
40
+ </Label>
37
41
  <Input
38
42
  class="vm-data-i18n-btn__control is-readonly"
39
43
  :model-value="tableName"
@@ -46,7 +50,7 @@
46
50
  :style="{ gridColumn: `span ${fieldSpan}` }"
47
51
  >
48
52
  <Label class="vm-data-i18n-btn__label">
49
- 目标语种
53
+ {{ crudLabel('dataI18nLang', crud.dict.label) }}
50
54
  <span class="vm-data-i18n-btn__req">*</span>
51
55
  </Label>
52
56
  <vm-select
@@ -55,7 +59,7 @@
55
59
  :options="langOptions"
56
60
  :refresh-on-change="false"
57
61
  width="100%"
58
- placeholder="选择语种"
62
+ :placeholder="crudLabel('dataI18nPickLang', crud.dict.label)"
59
63
  />
60
64
  </div>
61
65
 
@@ -64,7 +68,7 @@
64
68
  :style="{ gridColumn: `span ${fieldSpan}` }"
65
69
  >
66
70
  <Label class="vm-data-i18n-btn__label">
67
- AI 模型
71
+ {{ crudLabel('dataI18nModel', crud.dict.label) }}
68
72
  <span class="vm-data-i18n-btn__req">*</span>
69
73
  </Label>
70
74
  <vm-select
@@ -73,7 +77,7 @@
73
77
  :options="modelOptions"
74
78
  :refresh-on-change="false"
75
79
  width="100%"
76
- placeholder="请选择模型"
80
+ :placeholder="crudLabel('dataI18nPickModel', crud.dict.label)"
77
81
  />
78
82
  </div>
79
83
 
@@ -82,7 +86,7 @@
82
86
  :style="{ gridColumn: `span ${fieldSpan}` }"
83
87
  >
84
88
  <Label class="vm-data-i18n-btn__label">
85
- 模式
89
+ {{ crudLabel('dataI18nMode', crud.dict.label) }}
86
90
  <span class="vm-data-i18n-btn__req">*</span>
87
91
  </Label>
88
92
  <vm-select
@@ -102,7 +106,7 @@
102
106
  :disabled="submitting"
103
107
  @click="open = false"
104
108
  >
105
- 取消
109
+ {{ crudLabel('close', crud.dict.label) }}
106
110
  </button>
107
111
  <button
108
112
  type="button"
@@ -110,7 +114,11 @@
110
114
  :disabled="submitting || !form.model || !form.langCode"
111
115
  @click="runTranslate"
112
116
  >
113
- {{ submitting ? '翻译中…' : '翻译' }}
117
+ {{
118
+ submitting
119
+ ? crudLabel('dataI18nTranslating', crud.dict.label)
120
+ : crudLabel('dataI18nTranslate', crud.dict.label)
121
+ }}
114
122
  </button>
115
123
  </DialogFooter>
116
124
  </DialogContent>
@@ -127,6 +135,7 @@ import { useVome } from '../hooks/useVome'
127
135
  import { useCrud } from './useCrud'
128
136
  import VmSelect from './components/vm-select.vue'
129
137
  import { resolveFormSpan, useFormCols } from './span'
138
+ import { crudFormat, crudLabel } from './style'
130
139
  import {
131
140
  canUseDataI18n,
132
141
  dataPackFromService,
@@ -160,10 +169,16 @@ const form = reactive({
160
169
  mode: 'incremental' as 'incremental' | 'full',
161
170
  })
162
171
 
163
- const modeOptions = [
164
- { label: '增量翻译', value: 'incremental' },
165
- { label: '全量覆盖', value: 'full' },
166
- ]
172
+ const modeOptions = computed(() => [
173
+ {
174
+ label: crudLabel('dataI18nModeIncremental', crud.dict.label),
175
+ value: 'incremental',
176
+ },
177
+ {
178
+ label: crudLabel('dataI18nModeFull', crud.dict.label),
179
+ value: 'full',
180
+ },
181
+ ])
167
182
 
168
183
  const meta = computed(() =>
169
184
  canUseDataI18n(crud.service, service, props.tableName),
@@ -245,20 +260,20 @@ async function runTranslate() {
245
260
  if (submitting.value || props.disabled) return
246
261
  const table = tableName.value
247
262
  if (!table) {
248
- toast.error('未解析到业务表名')
263
+ toast.error(crudLabel('dataI18nNoTable', crud.dict.label))
249
264
  return
250
265
  }
251
266
  if (!form.langCode || form.langCode === 'zh-CN') {
252
- toast.error('请选择非中文目标语种')
267
+ toast.error(crudLabel('dataI18nNeedLang', crud.dict.label))
253
268
  return
254
269
  }
255
270
  if (!form.model) {
256
- toast.error('请选择 AI 模型')
271
+ toast.error(crudLabel('dataI18nNeedModel', crud.dict.label))
257
272
  return
258
273
  }
259
274
  const dataPack = dataPackFromService(service)
260
275
  if (!dataPack?.translateTable) {
261
- toast.error('未配置业务翻译服务')
276
+ toast.error(crudLabel('dataI18nNoService', crud.dict.label))
262
277
  return
263
278
  }
264
279
  submitting.value = true
@@ -279,7 +294,9 @@ async function runTranslate() {
279
294
  if (isAiStreamResult(out)) {
280
295
  for await (const chunk of out.stream) {
281
296
  if (chunk.type === 'error') {
282
- throw new Error(chunk.error?.message || 'AI 翻译失败')
297
+ throw new Error(
298
+ chunk.error?.message || crudLabel('dataI18nFail', crud.dict.label),
299
+ )
283
300
  }
284
301
  if (chunk.type === 'done') {
285
302
  skipped = Boolean(chunk.data?.skipped)
@@ -298,9 +315,11 @@ async function runTranslate() {
298
315
  rowCount = Number(res?.rowCount ?? 0)
299
316
  }
300
317
  if (skipped) {
301
- toast.message(message || '已跳过')
318
+ toast.message(message || crudLabel('dataI18nSkipped', crud.dict.label))
302
319
  } else {
303
- toast.success(`翻译完成,共 ${rowCount} 条`)
320
+ toast.success(
321
+ crudFormat(crudLabel('dataI18nDone', crud.dict.label), rowCount),
322
+ )
304
323
  }
305
324
  open.value = false
306
325
  if (crud.dataI18nLangCode) {
@@ -311,7 +330,11 @@ async function runTranslate() {
311
330
  }
312
331
  await crud.refresh()
313
332
  } catch (e) {
314
- toast.error(e instanceof Error ? e.message : '翻译失败')
333
+ toast.error(
334
+ e instanceof Error
335
+ ? e.message
336
+ : crudLabel('dataI18nFail', crud.dict.label),
337
+ )
315
338
  } finally {
316
339
  submitting.value = false
317
340
  }
@@ -1,40 +1,84 @@
1
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>
2
+ <div v-if="targetLangs.length" class="vm-data-i18n-lang">
3
+ <DropdownMenu :modal="false">
4
+ <DropdownMenuTrigger as-child>
5
+ <button
6
+ type="button"
7
+ class="vm-crud-toolbar__btn is-ghost vm-data-i18n-lang__btn"
8
+ :disabled="switching"
9
+ :title="
10
+ crudFormat(
11
+ crudLabel('dataI18nTargetLang', crud.dict.label),
12
+ targetCurrent?.name || targetCode,
13
+ )
14
+ "
15
+ >
16
+ <span class="vm-data-i18n-lang__flag" aria-hidden="true">
17
+ {{ targetCurrent?.flag || '🏳️' }}
18
+ </span>
19
+ {{ crudLabel('dataI18nSwitch', crud.dict.label) }}
20
+ </button>
21
+ </DropdownMenuTrigger>
22
+ <DropdownMenuContent align="start" class="vm-data-i18n-lang__menu">
23
+ <DropdownMenuItem
24
+ v-for="lang in targetLangs"
25
+ :key="lang.value"
26
+ class="vm-data-i18n-lang__item"
27
+ :class="{ 'is-active': targetCode === lang.value }"
28
+ :disabled="switching"
29
+ @click="switchTarget(lang.value)"
30
+ >
31
+ <span class="vm-data-i18n-lang__flag" aria-hidden="true">
32
+ {{ lang.flag || '🏳️' }}
33
+ </span>
34
+ <span class="vm-data-i18n-lang__label">{{ lang.name || lang.label }}</span>
35
+ </DropdownMenuItem>
36
+ </DropdownMenuContent>
37
+ </DropdownMenu>
38
+
39
+ <DropdownMenu :modal="false">
40
+ <DropdownMenuTrigger as-child>
41
+ <button
42
+ type="button"
43
+ class="vm-crud-toolbar__btn is-ghost vm-data-i18n-lang__btn"
44
+ :disabled="switching"
45
+ :title="
46
+ crudFormat(
47
+ crudLabel('dataI18nSourceLang', crud.dict.label),
48
+ sourceCurrent?.name || sourceCode,
49
+ )
50
+ "
51
+ >
52
+ <span class="vm-data-i18n-lang__flag" aria-hidden="true">
53
+ {{ sourceCurrent?.flag || '🇨🇳' }}
54
+ </span>
55
+ {{ crudLabel('dataI18nSource', crud.dict.label) }}
56
+ </button>
57
+ </DropdownMenuTrigger>
58
+ <DropdownMenuContent align="start" class="vm-data-i18n-lang__menu">
59
+ <DropdownMenuItem
60
+ v-for="lang in sourceLangs"
61
+ :key="lang.value"
62
+ class="vm-data-i18n-lang__item"
63
+ :class="{ 'is-active': sourceCode === lang.value }"
64
+ :disabled="switching"
65
+ @click="switchSource(lang.value)"
66
+ >
67
+ <span class="vm-data-i18n-lang__flag" aria-hidden="true">
68
+ {{ lang.flag || '🏳️' }}
69
+ </span>
70
+ <span class="vm-data-i18n-lang__label">{{ lang.name || lang.label }}</span>
71
+ </DropdownMenuItem>
72
+ </DropdownMenuContent>
73
+ </DropdownMenu>
74
+ </div>
32
75
  </template>
33
76
 
34
77
  <script setup lang="ts">
35
78
  import { computed, onMounted, ref, watch } from 'vue'
36
79
  import { useVome } from '../hooks/useVome'
37
80
  import { useCrud } from './useCrud'
81
+ import { crudFormat, crudLabel } from './style'
38
82
  import {
39
83
  DropdownMenu,
40
84
  DropdownMenuContent,
@@ -50,15 +94,49 @@ const { service } = useVome()
50
94
  const langs = ref<DataI18nLangOption[]>([])
51
95
  const switching = ref(false)
52
96
 
53
- const langCode = computed({
97
+ const targetCode = computed({
54
98
  get: () => crud.dataI18nLangCode?.value || '',
55
99
  set: (v: string) => {
56
100
  if (crud.dataI18nLangCode) crud.dataI18nLangCode.value = v
57
101
  },
58
102
  })
59
103
 
60
- const current = computed(
61
- () => langs.value.find((l) => l.value === langCode.value) || langs.value[0],
104
+ const sourceCode = computed({
105
+ get: () => crud.dataI18nSourceLangCode?.value || 'zh-CN',
106
+ set: (v: string) => {
107
+ if (crud.dataI18nSourceLangCode) crud.dataI18nSourceLangCode.value = v
108
+ },
109
+ })
110
+
111
+ const targetLangs = computed(() =>
112
+ langs.value.filter((l) => l.value !== 'zh-CN'),
113
+ )
114
+
115
+ const sourceLangs = computed(() => {
116
+ const zhName = crudLabel('dataI18nZh', crud.dict.label)
117
+ const zh: DataI18nLangOption = {
118
+ label: `${zhName} (zh-CN)`,
119
+ value: 'zh-CN',
120
+ name: zhName,
121
+ flag: '🇨🇳',
122
+ }
123
+ // 对照可选中文 + 其它语种(不含当前目标,避免无意义对照)
124
+ const rest = langs.value.filter(
125
+ (l) => l.value !== 'zh-CN' && l.value !== targetCode.value,
126
+ )
127
+ return [zh, ...rest]
128
+ })
129
+
130
+ const targetCurrent = computed(
131
+ () =>
132
+ targetLangs.value.find((l) => l.value === targetCode.value) ||
133
+ targetLangs.value[0],
134
+ )
135
+
136
+ const sourceCurrent = computed(
137
+ () =>
138
+ sourceLangs.value.find((l) => l.value === sourceCode.value) ||
139
+ sourceLangs.value[0],
62
140
  )
63
141
 
64
142
  async function loadLangs() {
@@ -66,7 +144,7 @@ async function loadLangs() {
66
144
  .i18n?.lang
67
145
  if (!langSvc?.list) {
68
146
  langs.value = [{ label: 'en-US', value: 'en-US', name: 'en-US', flag: '🇺🇸' }]
69
- ensureLang()
147
+ ensureLangs()
70
148
  return
71
149
  }
72
150
  try {
@@ -76,34 +154,53 @@ async function loadLangs() {
76
154
  flag?: string
77
155
  }>
78
156
  langs.value = (rows || [])
79
- .filter((r) => r.code && r.code !== 'zh-CN')
157
+ .filter((r) => r.code)
80
158
  .map((r) => ({
81
159
  label: `${r.name || r.code} (${r.code})`,
82
160
  value: String(r.code),
83
161
  name: String(r.name || r.code),
84
- flag: r.flag || '🏳️',
162
+ flag: r.flag || (r.code === 'zh-CN' ? '🇨🇳' : '🏳️'),
85
163
  }))
86
164
  } catch {
87
165
  langs.value = [{ label: 'en-US', value: 'en-US', name: 'en-US', flag: '🇺🇸' }]
88
166
  }
89
- ensureLang()
167
+ ensureLangs()
90
168
  if (crud.trashMode.value === 'dataI18n') {
91
169
  await crud.refresh()
92
170
  }
93
171
  }
94
172
 
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
173
+ function ensureLangs() {
174
+ if (!targetLangs.value.length) return
175
+ if (
176
+ !targetCode.value ||
177
+ !targetLangs.value.some((o) => o.value === targetCode.value)
178
+ ) {
179
+ targetCode.value = targetLangs.value[0]!.value
180
+ }
181
+ if (!sourceCode.value) sourceCode.value = 'zh-CN'
182
+ if (sourceCode.value === targetCode.value) sourceCode.value = 'zh-CN'
183
+ }
184
+
185
+ async function switchTarget(code: string) {
186
+ if (switching.value || !code || code === targetCode.value) return
187
+ switching.value = true
188
+ try {
189
+ targetCode.value = code
190
+ if (sourceCode.value === code) sourceCode.value = 'zh-CN'
191
+ crud.pagination.value = { ...crud.pagination.value, page: 1 }
192
+ await crud.refresh()
193
+ } finally {
194
+ switching.value = false
99
195
  }
100
196
  }
101
197
 
102
- async function switchLang(code: string) {
103
- if (switching.value || !code || code === langCode.value) return
198
+ async function switchSource(code: string) {
199
+ if (switching.value || !code || code === sourceCode.value) return
200
+ if (code === targetCode.value) return
104
201
  switching.value = true
105
202
  try {
106
- langCode.value = code
203
+ sourceCode.value = code
107
204
  crud.pagination.value = { ...crud.pagination.value, page: 1 }
108
205
  await crud.refresh()
109
206
  } finally {
@@ -124,6 +221,12 @@ onMounted(() => {
124
221
  </script>
125
222
 
126
223
  <style lang="scss" scoped>
224
+ .vm-data-i18n-lang {
225
+ display: inline-flex;
226
+ align-items: center;
227
+ gap: 8px;
228
+ }
229
+
127
230
  .vm-crud-toolbar__btn {
128
231
  display: inline-flex;
129
232
  height: 36px;
@@ -277,7 +277,9 @@
277
277
  @interact-outside="preventDialogOutsideClose"
278
278
  >
279
279
  <DialogHeader class="vm-crud-shell__head">
280
- <DialogTitle class="vm-crud-shell__title">编辑译文</DialogTitle>
280
+ <DialogTitle class="vm-crud-shell__title">
281
+ {{ crudLabel('dataI18nEdit', crud.dict.label) }}
282
+ </DialogTitle>
281
283
  <DialogClose class="vm-crud-shell__close">
282
284
  <i class="ri-close-line" />
283
285
  <span class="sr-only">Close</span>
@@ -296,17 +298,27 @@
296
298
  />
297
299
  </div>
298
300
  <div
301
+ v-for="f in dataI18nEditFields"
302
+ :key="f.name"
299
303
  class="vm-data-i18n-edit__field"
300
304
  :style="{ gridColumn: `span ${dataI18nEditSpan}` }"
301
305
  >
302
- <Label class="vm-data-i18n-edit__label">
303
- value
304
- <span class="vm-data-i18n-edit__req">*</span>
305
- </Label>
306
+ <Label class="vm-data-i18n-edit__label">{{ f.label || f.name }}</Label>
306
307
  <Input
307
308
  class="vm-data-i18n-edit__control"
308
- v-model="dataI18nEditForm.value"
309
+ v-model="dataI18nEditForm.values[f.name]"
309
310
  />
311
+ <div
312
+ v-if="dataI18nEditForm.sources[f.name]"
313
+ class="vm-data-i18n-edit__source"
314
+ >
315
+ {{
316
+ crudFormat(
317
+ crudLabel('dataI18nCompare', crud.dict.label),
318
+ dataI18nEditForm.sources[f.name],
319
+ )
320
+ }}
321
+ </div>
310
322
  </div>
311
323
  </div>
312
324
  <DialogFooter class="vm-crud-shell__foot">
@@ -316,7 +328,7 @@
316
328
  :disabled="dataI18nEditSubmitting"
317
329
  @click="dataI18nEditOpen = false"
318
330
  >
319
- 取消
331
+ {{ crudLabel('close', crud.dict.label) }}
320
332
  </button>
321
333
  <button
322
334
  type="button"
@@ -324,7 +336,11 @@
324
336
  :disabled="dataI18nEditSubmitting"
325
337
  @click="saveDataI18nEdit"
326
338
  >
327
- {{ dataI18nEditSubmitting ? '保存中…' : '保存' }}
339
+ {{
340
+ dataI18nEditSubmitting
341
+ ? crudLabel('saving', crud.dict.label)
342
+ : crudLabel('save', crud.dict.label)
343
+ }}
328
344
  </button>
329
345
  </DialogFooter>
330
346
  </DialogContent>
@@ -339,7 +355,7 @@ import { useCrud } from './useCrud'
339
355
  import { injectTableOptions, TABLE_API_KEY } from './key'
340
356
  import { flattenTree } from '../lib/tree'
341
357
  import { resolveCrudComponent } from './registry'
342
- import { getCrudStyle, crudLabel, crudT, crudLocaleRev } from './style'
358
+ import { getCrudStyle, crudLabel, crudT, crudFormat, crudLocaleRev } from './style'
343
359
  import { findEpsEntity } from '../lib/eps'
344
360
  import { useVome } from '../hooks/useVome'
345
361
  import { vmConfirm } from './confirm'
@@ -391,17 +407,56 @@ let emptyRo: ResizeObserver | null = null
391
407
 
392
408
  const overrideColumns = ref<CrudColumn[] | null>(null)
393
409
 
394
- const DATA_I18N_COLUMNS: CrudColumn[] = [
395
- { prop: 'id', label: 'ID', minWidth: 100 },
396
- { prop: 'value', label: 'value', minWidth: 180 },
397
- { prop: 'source', label: '原数据', minWidth: 180 },
398
- { type: 'op', buttons: ['edit', 'delete'], width: 140 },
399
- ]
410
+ const DATA_I18N_ID_COL: CrudColumn = {
411
+ prop: 'id',
412
+ label: 'ID',
413
+ minWidth: 100,
414
+ }
415
+
416
+ const DATA_I18N_OP_COL: CrudColumn = {
417
+ type: 'op',
418
+ buttons: ['edit', 'delete'],
419
+ width: 140,
420
+ }
400
421
 
401
422
  const isDataI18nView = computed(() => crud.trashMode.value === 'dataI18n')
402
423
 
403
- const columns = computed(() => {
404
- if (isDataI18nView.value) return DATA_I18N_COLUMNS
424
+ /** 语言 Tab:按表头/当前页内容估 minWidth(限幅) */
425
+ function dataI18nColMinWidth(header: string, samples: string[]) {
426
+ let maxChars = [...header].length
427
+ for (const s of samples) {
428
+ const n = [...String(s || '')].length
429
+ if (n > maxChars) maxChars = n
430
+ }
431
+ return Math.min(360, Math.max(108, maxChars * 14 + 28))
432
+ }
433
+
434
+ const columns = computed((): CrudColumn[] => {
435
+ if (isDataI18nView.value) {
436
+ void crudLocaleRev.value
437
+ const fields = crud.dataI18nFields?.value || []
438
+ const rows = crud.list.value || []
439
+ const origSuffix = crudLabel('dataI18nOrigSuffix', crud.dict.label)
440
+ const fieldCols: CrudColumn[] = []
441
+ for (const f of fields) {
442
+ const base = f.label || f.name
443
+ const srcProp = `__src_${f.name}`
444
+ const srcHeader = `${base}${origSuffix}`
445
+ const srcSamples = rows.map((r) => String(r[srcProp] ?? ''))
446
+ const valSamples = rows.map((r) => String(r[f.name] ?? ''))
447
+ fieldCols.push({
448
+ prop: srcProp,
449
+ label: srcHeader,
450
+ minWidth: dataI18nColMinWidth(srcHeader, srcSamples),
451
+ })
452
+ fieldCols.push({
453
+ prop: f.name,
454
+ label: base,
455
+ minWidth: dataI18nColMinWidth(base, valSamples),
456
+ })
457
+ }
458
+ return [DATA_I18N_ID_COL, ...fieldCols, DATA_I18N_OP_COL]
459
+ }
405
460
  const raw =
406
461
  overrideColumns.value ||
407
462
  (props.columns?.length ? props.columns : tableOpts?.columns || [])
@@ -724,21 +779,36 @@ function deleteRowTargets(row: Record<string, unknown>) {
724
779
  const { service: hostService } = useVome()
725
780
  const dataI18nEditOpen = ref(false)
726
781
  const dataI18nEditSubmitting = ref(false)
727
- const dataI18nEditForm = ref({ id: '', key: '', value: '' })
782
+ const dataI18nEditForm = ref<{
783
+ id: string
784
+ values: Record<string, string>
785
+ sources: Record<string, string>
786
+ }>({ id: '', values: {}, sources: {} })
728
787
  const dataI18nEditFormCols = useFormCols()
729
788
  const dataI18nEditSpan = computed(() =>
730
789
  resolveFormSpan(12, dataI18nEditFormCols.value),
731
790
  )
791
+ const dataI18nEditFields = computed(
792
+ () => crud.dataI18nFields?.value || [],
793
+ )
732
794
 
733
795
  function onDataI18nEditOpenChange(v: boolean) {
734
796
  dataI18nEditOpen.value = v
735
797
  }
736
798
 
737
799
  function openDataI18nEdit(row: Record<string, unknown>) {
800
+ const fields = crud.dataI18nFields?.value || []
801
+ const sources = (row.__sources as Record<string, string>) || {}
802
+ const values: Record<string, string> = {}
803
+ const src: Record<string, string> = {}
804
+ for (const f of fields) {
805
+ values[f.name] = String(row[f.name] ?? '')
806
+ src[f.name] = String(sources[f.name] ?? '')
807
+ }
738
808
  dataI18nEditForm.value = {
739
809
  id: String(row.id ?? ''),
740
- key: String(row.key ?? ''),
741
- value: String(row.value ?? ''),
810
+ values,
811
+ sources: src,
742
812
  }
743
813
  dataI18nEditOpen.value = true
744
814
  }
@@ -749,7 +819,7 @@ async function saveDataI18nEdit() {
749
819
  const dataPack = dataPackFromService(hostService)
750
820
  const lang = crud.dataI18nLangCode?.value
751
821
  if (!meta.ok || !dataPack?.updateEntry || !lang) {
752
- toast.error('未配置业务翻译服务')
822
+ toast.error(crudLabel('dataI18nNoService', crud.dict.label))
753
823
  return
754
824
  }
755
825
  dataI18nEditSubmitting.value = true
@@ -758,8 +828,7 @@ async function saveDataI18nEdit() {
758
828
  tableName: meta.tableName,
759
829
  langCode: lang,
760
830
  id: dataI18nEditForm.value.id,
761
- key: dataI18nEditForm.value.key,
762
- value: dataI18nEditForm.value.value,
831
+ values: { ...dataI18nEditForm.value.values },
763
832
  })
764
833
  toast.success(crudLabel('saveSuccess', crud.dict.label))
765
834
  dataI18nEditOpen.value = false
@@ -776,12 +845,17 @@ async function deleteDataI18nEntry(row: Record<string, unknown>) {
776
845
  const dataPack = dataPackFromService(hostService)
777
846
  const lang = crud.dataI18nLangCode?.value
778
847
  if (!meta.ok || !dataPack?.deleteEntry || !lang) {
779
- toast.error('未配置业务翻译服务')
848
+ toast.error(crudLabel('dataI18nNoService', crud.dict.label))
780
849
  return
781
850
  }
851
+ const id = String(row.id ?? '')
852
+ if (!id) return
782
853
  const ok = await vmConfirm({
783
854
  title: crudLabel('tips', crud.dict.label),
784
- message: crudLabel('deleteConfirm', crud.dict.label),
855
+ message: crudFormat(
856
+ crudLabel('dataI18nDeleteConfirm', crud.dict.label),
857
+ id,
858
+ ),
785
859
  confirmText: crudLabel('confirm', crud.dict.label),
786
860
  cancelText: crudLabel('close', crud.dict.label),
787
861
  })
@@ -790,8 +864,7 @@ async function deleteDataI18nEntry(row: Record<string, unknown>) {
790
864
  await dataPack.deleteEntry({
791
865
  tableName: meta.tableName,
792
866
  langCode: lang,
793
- id: String(row.id ?? ''),
794
- key: String(row.key ?? ''),
867
+ id,
795
868
  })
796
869
  toast.success(crudLabel('deleteSuccess', crud.dict.label))
797
870
  await crud.refresh()
@@ -1283,4 +1356,12 @@ defineExpose({
1283
1356
  cursor: default;
1284
1357
  }
1285
1358
  }
1359
+
1360
+ .vm-data-i18n-edit__source {
1361
+ margin-top: 6px;
1362
+ font-size: 12px;
1363
+ line-height: 1.4;
1364
+ color: var(--muted-foreground);
1365
+ word-break: break-word;
1366
+ }
1286
1367
  </style>
@@ -146,7 +146,7 @@ const viewTabs = computed(() => {
146
146
  })
147
147
  }
148
148
  if (dataI18nEnabled.value) {
149
- tabs.push({ label: '语言', value: 'dataI18n' })
149
+ tabs.push({ label: crudLabel('dataI18n', crud.dict.label), value: 'dataI18n' })
150
150
  }
151
151
  return tabs
152
152
  })