vome-core 0.1.86 → 0.1.88
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/dist/admin/components/ui/avatar/index.js +1 -1
- package/dist/admin/components/ui/checkbox/index.js +1 -1
- package/dist/admin/components/ui/dialog/index.js +1 -1
- package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
- package/dist/admin/components/ui/input/index.js +1 -1
- package/dist/admin/components/ui/label/index.js +1 -1
- package/dist/admin/components/ui/scroll-area/index.js +1 -1
- package/dist/admin/components/ui/select/index.js +1 -1
- package/dist/admin/components/ui/separator/index.js +1 -1
- package/dist/admin/components/ui/sheet/index.js +1 -1
- package/dist/admin/components/ui/sonner/index.js +1 -1
- package/dist/admin/components/ui/table/index.js +1 -1
- package/dist/admin/components/ui/tabs/index.js +1 -1
- package/dist/admin/components/ui/tooltip/index.js +1 -1
- package/dist/admin/crud/components/vm-richtext-extensions.js +1 -1
- package/dist/admin/crud/config.js +1 -1
- package/dist/admin/crud/confirm.js +1 -1
- package/dist/admin/crud/crud-page-request.js +1 -1
- package/dist/admin/crud/data-i18n.d.ts +39 -0
- package/dist/admin/crud/data-i18n.js +1 -0
- package/dist/admin/crud/dict.js +1 -1
- package/dist/admin/crud/export-fetch.js +1 -1
- package/dist/admin/crud/key.js +1 -1
- package/dist/admin/crud/plugins.js +1 -1
- package/dist/admin/crud/span.js +1 -1
- package/dist/admin/crud/style.js +1 -1
- package/dist/admin/crud/validate.js +1 -1
- package/dist/admin/crud/vm-crud.vue +66 -0
- package/dist/admin/crud/vm-data-i18n-btn.vue +209 -72
- package/dist/admin/crud/vm-data-i18n-lang.vue +176 -0
- package/dist/admin/crud/vm-table.vue +188 -5
- package/dist/admin/crud/vm-toolbar.vue +80 -47
- package/dist/admin/directives/perm.js +1 -1
- package/dist/admin/hooks/useUpload.js +1 -1
- package/dist/admin/lib/browser.js +1 -1
- package/dist/admin/lib/cn.js +1 -1
- package/dist/admin/lib/dialog-float.js +1 -1
- package/dist/admin/lib/export-excel.js +1 -1
- package/dist/admin/lib/import-excel.js +1 -1
- package/dist/admin/lib/json.js +1 -1
- package/dist/admin/lib/menu.js +1 -1
- package/dist/admin/lib/micro-locale.js +1 -1
- package/dist/admin/lib/micro-theme.js +1 -1
- package/dist/admin/lib/setup-crud-defaults.js +1 -1
- package/dist/admin/lib/shell.js +1 -1
- package/dist/admin/lib/theme-color-options.js +1 -1
- package/dist/admin/lib/tree.js +1 -1
- package/dist/admin/lib/upload.js +1 -1
- package/dist/admin/lib/video-frame.js +1 -1
- package/dist/admin/router/create.js +1 -1
- package/dist/admin/router/guards.js +1 -1
- package/dist/admin/themes/dark.js +1 -1
- package/dist/admin/themes/index.js +1 -1
- package/dist/admin/themes/light.js +1 -1
- package/dist/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/shared/datetime-picker.js +1 -1
- package/dist/shared/excel.js +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/locale-json.js +1 -1
- package/dist/shared/tree.js +1 -1
- package/package.json +1 -1
- package/typings/admin/comm/crud.ts +8 -1
|
@@ -1,55 +1,116 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="visible">
|
|
2
|
+
<div v-if="visible" class="vm-data-i18n-btn">
|
|
3
3
|
<button
|
|
4
4
|
type="button"
|
|
5
5
|
class="vm-crud-toolbar__btn is-ghost"
|
|
6
6
|
:disabled="disabled || submitting"
|
|
7
|
-
@click="
|
|
7
|
+
@click="openDialog"
|
|
8
8
|
>
|
|
9
9
|
<i class="ri-translate-2" />
|
|
10
|
-
{{ text || '
|
|
10
|
+
{{ text || '翻译' }}
|
|
11
11
|
</button>
|
|
12
12
|
|
|
13
|
+
<!-- 与 vm-upsert 同壳:24 栅格、字段默认 span 12 -->
|
|
13
14
|
<Dialog :open="open" @update:open="onOpenChange">
|
|
14
|
-
<DialogContent
|
|
15
|
+
<DialogContent
|
|
16
|
+
shell="crud"
|
|
17
|
+
:show-close-button="false"
|
|
18
|
+
class="vm-crud-upsert vm-data-i18n-btn__dialog"
|
|
19
|
+
@pointer-down-outside="preventDialogOutsideClose"
|
|
20
|
+
@focus-outside="preventDialogOutsideClose"
|
|
21
|
+
@interact-outside="preventDialogOutsideClose"
|
|
22
|
+
>
|
|
15
23
|
<DialogHeader class="vm-crud-shell__head">
|
|
16
|
-
<DialogTitle class="vm-crud-shell__title"
|
|
24
|
+
<DialogTitle class="vm-crud-shell__title">业务数据翻译</DialogTitle>
|
|
25
|
+
<DialogClose class="vm-crud-shell__close">
|
|
26
|
+
<i class="ri-close-line" />
|
|
27
|
+
<span class="sr-only">Close</span>
|
|
28
|
+
</DialogClose>
|
|
17
29
|
</DialogHeader>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
|
|
31
|
+
<div class="vm-crud-upsert__body">
|
|
32
|
+
<div
|
|
33
|
+
class="vm-crud-upsert__field"
|
|
34
|
+
:style="{ gridColumn: `span ${fieldSpan}` }"
|
|
35
|
+
>
|
|
36
|
+
<Label class="vm-crud-upsert__label">业务表</Label>
|
|
37
|
+
<Input
|
|
38
|
+
class="vm-crud-upsert__control"
|
|
39
|
+
:model-value="tableName"
|
|
40
|
+
disabled
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div
|
|
45
|
+
class="vm-crud-upsert__field"
|
|
46
|
+
:style="{ gridColumn: `span ${fieldSpan}` }"
|
|
47
|
+
>
|
|
48
|
+
<Label class="vm-crud-upsert__label">
|
|
49
|
+
目标语种
|
|
50
|
+
<span class="vm-crud-upsert__req">*</span>
|
|
51
|
+
</Label>
|
|
24
52
|
<vm-select
|
|
25
53
|
v-model="form.langCode"
|
|
54
|
+
class="vm-crud-upsert__control"
|
|
26
55
|
:options="langOptions"
|
|
27
56
|
:refresh-on-change="false"
|
|
28
57
|
width="100%"
|
|
29
58
|
placeholder="选择语种"
|
|
30
59
|
/>
|
|
31
|
-
</
|
|
32
|
-
|
|
33
|
-
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div
|
|
63
|
+
class="vm-crud-upsert__field"
|
|
64
|
+
:style="{ gridColumn: `span ${fieldSpan}` }"
|
|
65
|
+
>
|
|
66
|
+
<Label class="vm-crud-upsert__label">
|
|
67
|
+
AI 模型
|
|
68
|
+
<span class="vm-crud-upsert__req">*</span>
|
|
69
|
+
</Label>
|
|
70
|
+
<vm-select
|
|
71
|
+
v-model="form.model"
|
|
72
|
+
class="vm-crud-upsert__control"
|
|
73
|
+
:options="modelOptions"
|
|
74
|
+
:refresh-on-change="false"
|
|
75
|
+
width="100%"
|
|
76
|
+
placeholder="请选择模型"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
class="vm-crud-upsert__field"
|
|
82
|
+
:style="{ gridColumn: `span ${fieldSpan}` }"
|
|
83
|
+
>
|
|
84
|
+
<Label class="vm-crud-upsert__label">
|
|
85
|
+
模式
|
|
86
|
+
<span class="vm-crud-upsert__req">*</span>
|
|
87
|
+
</Label>
|
|
34
88
|
<vm-select
|
|
35
89
|
v-model="form.mode"
|
|
90
|
+
class="vm-crud-upsert__control"
|
|
36
91
|
:options="modeOptions"
|
|
37
92
|
:refresh-on-change="false"
|
|
38
93
|
width="100%"
|
|
39
94
|
/>
|
|
40
|
-
</
|
|
95
|
+
</div>
|
|
41
96
|
</div>
|
|
97
|
+
|
|
42
98
|
<DialogFooter class="vm-crud-shell__foot">
|
|
43
|
-
<button
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
class="vm-crud-shell__btn is-ghost"
|
|
102
|
+
:disabled="submitting"
|
|
103
|
+
@click="open = false"
|
|
104
|
+
>
|
|
44
105
|
取消
|
|
45
106
|
</button>
|
|
46
107
|
<button
|
|
47
108
|
type="button"
|
|
48
|
-
class="vm-crud-
|
|
49
|
-
:disabled="submitting"
|
|
109
|
+
class="vm-crud-shell__btn is-primary"
|
|
110
|
+
:disabled="submitting || !form.model || !form.langCode"
|
|
50
111
|
@click="runTranslate"
|
|
51
112
|
>
|
|
52
|
-
{{ submitting ? '翻译中…' : '
|
|
113
|
+
{{ submitting ? '翻译中…' : '翻译' }}
|
|
53
114
|
</button>
|
|
54
115
|
</DialogFooter>
|
|
55
116
|
</DialogContent>
|
|
@@ -60,11 +121,15 @@
|
|
|
60
121
|
<script setup lang="ts">
|
|
61
122
|
import { computed, reactive, ref, watch } from 'vue'
|
|
62
123
|
import { toast } from 'vue-sonner'
|
|
63
|
-
import {
|
|
124
|
+
import { preventDialogOutsideClose } from '../lib/dialog-float'
|
|
64
125
|
import { useVome } from '../hooks/useVome'
|
|
65
126
|
import { useCrud } from './useCrud'
|
|
66
127
|
import VmSelect from './components/vm-select.vue'
|
|
67
|
-
import
|
|
128
|
+
import { resolveFormSpan, useFormCols } from './span'
|
|
129
|
+
import {
|
|
130
|
+
canUseDataI18n,
|
|
131
|
+
dataPackFromService,
|
|
132
|
+
} from './data-i18n'
|
|
68
133
|
|
|
69
134
|
defineOptions({ name: 'vm-data-i18n-btn' })
|
|
70
135
|
|
|
@@ -85,9 +150,14 @@ const { service } = useVome()
|
|
|
85
150
|
const open = ref(false)
|
|
86
151
|
const submitting = ref(false)
|
|
87
152
|
const langOptions = ref<Array<{ label: string; value: string }>>([])
|
|
153
|
+
const modelOptions = ref<Array<{ label: string; value: string }>>([])
|
|
154
|
+
const formCols = useFormCols()
|
|
155
|
+
/** 与 skill 约定一致:表单字段默认 span 12(半宽两列) */
|
|
156
|
+
const fieldSpan = computed(() => resolveFormSpan(12, formCols.value))
|
|
88
157
|
|
|
89
158
|
const form = reactive({
|
|
90
159
|
langCode: 'en-US',
|
|
160
|
+
model: '',
|
|
91
161
|
mode: 'incremental' as 'incremental' | 'full',
|
|
92
162
|
})
|
|
93
163
|
|
|
@@ -96,41 +166,34 @@ const modeOptions = [
|
|
|
96
166
|
{ label: '全量覆盖', value: 'full' },
|
|
97
167
|
]
|
|
98
168
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return findEpsEntity(`/${ns}`) as (EpsEntity & { dataI18n?: boolean }) | undefined
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
function resolveTableName(entity?: EpsEntity): string {
|
|
106
|
-
if (props.tableName?.trim()) return props.tableName.trim()
|
|
107
|
-
if (!entity) return ''
|
|
108
|
-
const cols = [...(entity.columns || []), ...(entity.pageColumns || [])]
|
|
109
|
-
for (const col of cols) {
|
|
110
|
-
const table = String(col.table || '').trim()
|
|
111
|
-
if (table) return table
|
|
112
|
-
}
|
|
113
|
-
return ''
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const tableName = computed(() => resolveTableName(epsEntity.value))
|
|
169
|
+
const meta = computed(() =>
|
|
170
|
+
canUseDataI18n(crud.service, service, props.tableName),
|
|
171
|
+
)
|
|
117
172
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
if (!tableName.value) return false
|
|
121
|
-
const dataPack = (service as { i18n?: { dataPack?: { translateTable?: unknown } } })
|
|
122
|
-
.i18n?.dataPack
|
|
123
|
-
return Boolean(dataPack?.translateTable)
|
|
124
|
-
})
|
|
173
|
+
const tableName = computed(() => meta.value.tableName)
|
|
174
|
+
const visible = computed(() => meta.value.ok)
|
|
125
175
|
|
|
126
176
|
watch(open, (v) => {
|
|
127
|
-
if (v) void
|
|
177
|
+
if (v) void loadDialogData()
|
|
128
178
|
})
|
|
129
179
|
|
|
130
180
|
function onOpenChange(v: boolean) {
|
|
131
181
|
open.value = v
|
|
132
182
|
}
|
|
133
183
|
|
|
184
|
+
function openDialog() {
|
|
185
|
+
if (props.disabled || submitting.value) return
|
|
186
|
+
open.value = true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function loadDialogData() {
|
|
190
|
+
await Promise.all([loadLangs(), loadModels()])
|
|
191
|
+
const viewing = crud.dataI18nLangCode?.value
|
|
192
|
+
if (viewing && langOptions.value.some((o) => o.value === viewing)) {
|
|
193
|
+
form.langCode = viewing
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
134
197
|
async function loadLangs() {
|
|
135
198
|
const langSvc = (service as { i18n?: { lang?: { list?: (q: unknown) => Promise<unknown> } } })
|
|
136
199
|
.i18n?.lang
|
|
@@ -157,6 +220,27 @@ async function loadLangs() {
|
|
|
157
220
|
}
|
|
158
221
|
}
|
|
159
222
|
|
|
223
|
+
async function loadModels() {
|
|
224
|
+
const dataPack = dataPackFromService(service)
|
|
225
|
+
if (!dataPack?.chatModels) {
|
|
226
|
+
modelOptions.value = []
|
|
227
|
+
form.model = ''
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const list = (await dataPack.chatModels()) as Array<{ code?: string }>
|
|
232
|
+
modelOptions.value = (list || [])
|
|
233
|
+
.filter((m) => m.code)
|
|
234
|
+
.map((m) => ({ label: String(m.code), value: String(m.code) }))
|
|
235
|
+
if (!modelOptions.value.some((o) => o.value === form.model)) {
|
|
236
|
+
form.model = modelOptions.value[0]?.value || ''
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
modelOptions.value = []
|
|
240
|
+
form.model = ''
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
160
244
|
async function runTranslate() {
|
|
161
245
|
if (submitting.value || props.disabled) return
|
|
162
246
|
const table = tableName.value
|
|
@@ -168,13 +252,11 @@ async function runTranslate() {
|
|
|
168
252
|
toast.error('请选择非中文目标语种')
|
|
169
253
|
return
|
|
170
254
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
}).i18n?.dataPack
|
|
255
|
+
if (!form.model) {
|
|
256
|
+
toast.error('请选择 AI 模型')
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
const dataPack = dataPackFromService(service)
|
|
178
260
|
if (!dataPack?.translateTable) {
|
|
179
261
|
toast.error('未配置业务翻译服务')
|
|
180
262
|
return
|
|
@@ -185,6 +267,7 @@ async function runTranslate() {
|
|
|
185
267
|
tableName: table,
|
|
186
268
|
langCode: form.langCode,
|
|
187
269
|
mode: form.mode,
|
|
270
|
+
model: form.model,
|
|
188
271
|
})) as { skipped?: boolean; message?: string; rowCount?: number }
|
|
189
272
|
if (res?.skipped) {
|
|
190
273
|
toast.message(res.message || '已跳过')
|
|
@@ -192,6 +275,12 @@ async function runTranslate() {
|
|
|
192
275
|
toast.success(`翻译完成,共 ${res?.rowCount ?? 0} 条`)
|
|
193
276
|
}
|
|
194
277
|
open.value = false
|
|
278
|
+
if (crud.dataI18nLangCode) {
|
|
279
|
+
crud.dataI18nLangCode.value = form.langCode
|
|
280
|
+
}
|
|
281
|
+
if (crud.trashMode.value !== 'dataI18n') {
|
|
282
|
+
crud.trashMode.value = 'dataI18n'
|
|
283
|
+
}
|
|
195
284
|
await crud.refresh()
|
|
196
285
|
} catch (e) {
|
|
197
286
|
toast.error(e instanceof Error ? e.message : '翻译失败')
|
|
@@ -202,33 +291,81 @@ async function runTranslate() {
|
|
|
202
291
|
</script>
|
|
203
292
|
|
|
204
293
|
<style lang="scss" scoped>
|
|
205
|
-
.vm-data-i18n-
|
|
206
|
-
|
|
294
|
+
.vm-data-i18n-btn {
|
|
295
|
+
display: inline-flex;
|
|
207
296
|
}
|
|
208
297
|
|
|
209
|
-
.vm-
|
|
210
|
-
display: flex;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
298
|
+
.vm-crud-toolbar__btn {
|
|
299
|
+
display: inline-flex;
|
|
300
|
+
height: 36px;
|
|
301
|
+
align-items: center;
|
|
302
|
+
gap: 4px;
|
|
303
|
+
padding: 0 14px;
|
|
304
|
+
border: none;
|
|
305
|
+
border-radius: 12px;
|
|
306
|
+
font-size: 13px;
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
background: var(--muted);
|
|
310
|
+
color: var(--foreground);
|
|
311
|
+
transition: background 0.15s ease, opacity 0.15s ease;
|
|
312
|
+
|
|
313
|
+
&:disabled {
|
|
314
|
+
cursor: not-allowed;
|
|
315
|
+
opacity: 0.45;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
&:hover:not(:disabled) {
|
|
319
|
+
background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
|
|
320
|
+
}
|
|
214
321
|
}
|
|
215
322
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
323
|
+
/* 复用 vm-upsert 栅格与控件视觉(与业务编辑弹窗一致) */
|
|
324
|
+
.vm-crud-upsert__body {
|
|
325
|
+
display: grid;
|
|
326
|
+
grid-template-columns: repeat(24, minmax(0, 1fr));
|
|
327
|
+
gap: 12px 16px;
|
|
328
|
+
padding: 8px 22px 16px;
|
|
220
329
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
330
|
+
@media (max-width: 768px) {
|
|
331
|
+
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
332
|
+
gap: 10px;
|
|
333
|
+
padding: 10px 12px;
|
|
224
334
|
}
|
|
225
335
|
}
|
|
226
336
|
|
|
227
|
-
.vm-
|
|
228
|
-
display: flex;
|
|
229
|
-
|
|
230
|
-
gap:
|
|
337
|
+
.vm-crud-upsert__label {
|
|
338
|
+
display: inline-flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 2px;
|
|
341
|
+
margin-bottom: 6px;
|
|
231
342
|
font-size: 12px;
|
|
232
343
|
color: var(--muted-foreground);
|
|
233
344
|
}
|
|
345
|
+
|
|
346
|
+
.vm-crud-upsert__req {
|
|
347
|
+
color: var(--destructive);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.vm-crud-upsert__field {
|
|
351
|
+
min-width: 0;
|
|
352
|
+
|
|
353
|
+
:deep(.vm-crud-upsert__control),
|
|
354
|
+
:deep(.vm-crud-select.vm-crud-upsert__control) {
|
|
355
|
+
width: 100%;
|
|
356
|
+
height: var(--vm-control-height) !important;
|
|
357
|
+
min-height: var(--vm-control-height);
|
|
358
|
+
border-radius: var(--vm-control-radius) !important;
|
|
359
|
+
font-size: var(--vm-control-font-size) !important;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
:deep(input.vm-crud-upsert__control),
|
|
363
|
+
:deep(.vm-crud-upsert__control[data-slot='input']) {
|
|
364
|
+
background: var(--card) !important;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
:deep(.vm-crud-select.vm-crud-upsert__control) {
|
|
368
|
+
background: var(--muted) !important;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
234
371
|
</style>
|
|
@@ -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>
|