@liujitcn/kratos-admin-system 0.0.21 → 0.0.24

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 (106) hide show
  1. package/dist/package/declarations/src/api/system/base_language.d.ts +5 -4
  2. package/dist/package/declarations/src/api/system/base_translation.d.ts +7 -4
  3. package/dist/package/declarations/src/api/system/ops_monitoring.d.ts +19 -0
  4. package/dist/package/declarations/src/api/system/ops_monitoring_sse.d.ts +12 -0
  5. package/dist/package/declarations/src/components/DynamicTranslationCell.vue.d.ts +9 -32
  6. package/dist/package/declarations/src/components/DynamicTranslationEditor.vue.d.ts +3 -6
  7. package/dist/package/declarations/src/components/dynamicTranslation.d.ts +11 -17
  8. package/dist/package/declarations/src/locales/generated.d.ts +6536 -0
  9. package/dist/package/declarations/src/rpc/base/v1/ai_session.d.ts +12 -2
  10. package/dist/package/declarations/src/rpc/base/v1/ai_tool.d.ts +9 -1
  11. package/dist/package/declarations/src/rpc/base/v1/config.d.ts +35 -0
  12. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +1 -1
  13. package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +23 -7
  14. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +7 -77
  15. package/dist/package/declarations/src/rpc/system/admin/v1/base_job.d.ts +9 -1
  16. package/dist/package/declarations/src/rpc/system/admin/v1/base_language.d.ts +7 -2
  17. package/dist/package/declarations/src/rpc/system/admin/v1/base_menu.d.ts +5 -40
  18. package/dist/package/declarations/src/rpc/system/admin/v1/base_migration.d.ts +6 -6
  19. package/dist/package/declarations/src/rpc/system/admin/v1/base_role.d.ts +13 -1
  20. package/dist/package/declarations/src/rpc/system/admin/v1/base_translation.d.ts +54 -81
  21. package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +11 -1
  22. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen.d.ts +11 -11
  23. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_table.d.ts +11 -1
  24. package/dist/package/declarations/src/rpc/system/admin/v1/common.d.ts +13 -0
  25. package/dist/package/declarations/src/rpc/system/admin/v1/ops_monitoring.d.ts +222 -0
  26. package/dist/package/declarations/src/views/tool/ops-monitoring/index.vue.d.ts +3 -0
  27. package/dist/package/src/api/base/ai_message.ts +2 -2
  28. package/dist/package/src/api/system/base_language.ts +13 -15
  29. package/dist/package/src/api/system/base_translation.ts +17 -6
  30. package/dist/package/src/api/system/ops_monitoring.ts +88 -0
  31. package/dist/package/src/api/system/ops_monitoring_sse.ts +49 -0
  32. package/dist/package/src/components/DynamicTranslationCell.vue +199 -126
  33. package/dist/package/src/components/DynamicTranslationEditor.vue +109 -78
  34. package/dist/package/src/components/dynamicTranslation.ts +11 -31
  35. package/dist/package/src/locales/README.md +7 -5
  36. package/dist/package/src/locales/en-US.json +876 -872
  37. package/dist/package/src/locales/es-ES.json +900 -895
  38. package/dist/package/src/locales/fr-FR.json +896 -891
  39. package/dist/package/src/locales/generated.ts +22 -0
  40. package/dist/package/src/locales/ja-JP.json +876 -872
  41. package/dist/package/src/locales/ko-KR.json +881 -876
  42. package/dist/package/src/locales/zh-CN.json +876 -872
  43. package/dist/package/src/locales/zh-TW.json +876 -871
  44. package/dist/package/src/module.ts +3 -17
  45. package/dist/package/src/rpc/base/v1/ai_session.ts +13 -2
  46. package/dist/package/src/rpc/base/v1/ai_tool.ts +10 -1
  47. package/dist/package/src/rpc/base/v1/config.ts +47 -0
  48. package/dist/package/src/rpc/system/admin/v1/auth.ts +1 -1
  49. package/dist/package/src/rpc/system/admin/v1/base_config.ts +24 -7
  50. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +7 -79
  51. package/dist/package/src/rpc/system/admin/v1/base_job.ts +10 -1
  52. package/dist/package/src/rpc/system/admin/v1/base_language.ts +8 -2
  53. package/dist/package/src/rpc/system/admin/v1/base_menu.ts +5 -41
  54. package/dist/package/src/rpc/system/admin/v1/base_migration.ts +8 -8
  55. package/dist/package/src/rpc/system/admin/v1/base_role.ts +14 -1
  56. package/dist/package/src/rpc/system/admin/v1/base_translation.ts +54 -81
  57. package/dist/package/src/rpc/system/admin/v1/base_user.ts +12 -1
  58. package/dist/package/src/rpc/system/admin/v1/code_gen.ts +11 -11
  59. package/dist/package/src/rpc/system/admin/v1/code_gen_table.ts +12 -1
  60. package/dist/package/src/rpc/system/admin/v1/common.ts +21 -0
  61. package/dist/package/src/rpc/system/admin/v1/ops_monitoring.ts +255 -0
  62. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +36 -36
  63. package/dist/package/src/views/ai/chat/components/SessionPanel.vue +6 -6
  64. package/dist/package/src/views/ai/chat/components/XSender.vue +20 -20
  65. package/dist/package/src/views/ai/chat/index.vue +33 -32
  66. package/dist/package/src/views/ai/chat/message.ts +16 -15
  67. package/dist/package/src/views/base/api/index.vue +55 -56
  68. package/dist/package/src/views/base/area/index.vue +18 -19
  69. package/dist/package/src/views/base/config/index.vue +143 -133
  70. package/dist/package/src/views/base/dept/index.vue +42 -43
  71. package/dist/package/src/views/base/dict/index.vue +49 -45
  72. package/dist/package/src/views/base/dict/item.vue +54 -49
  73. package/dist/package/src/views/base/job/index.vue +51 -52
  74. package/dist/package/src/views/base/job/log.vue +18 -18
  75. package/dist/package/src/views/{tool → base}/language/index.vue +51 -47
  76. package/dist/package/src/views/base/log/index.vue +40 -40
  77. package/dist/package/src/views/base/menu/index.vue +220 -177
  78. package/dist/package/src/views/base/migration/index.vue +24 -24
  79. package/dist/package/src/views/base/post/index.vue +42 -43
  80. package/dist/package/src/views/base/role/index.vue +54 -55
  81. package/dist/package/src/views/base/tenant/index.vue +42 -43
  82. package/dist/package/src/views/base/user/components/dept-tree.vue +1 -1
  83. package/dist/package/src/views/base/user/index.vue +69 -71
  84. package/dist/package/src/views/profile/components/base.vue +9 -9
  85. package/dist/package/src/views/profile/components/password.vue +19 -20
  86. package/dist/package/src/views/profile/components/security.vue +20 -20
  87. package/dist/package/src/views/profile/index.vue +3 -3
  88. package/dist/package/src/views/tool/api-doc/index.vue +1 -1
  89. package/dist/package/src/views/tool/code-gen/code-preview/index.vue +13 -13
  90. package/dist/package/src/views/tool/code-gen/columns/index.vue +89 -89
  91. package/dist/package/src/views/tool/code-gen/components/CodeGenLocaleEditor.vue +5 -5
  92. package/dist/package/src/views/tool/code-gen/components/CodeGenProgressDialog.vue +21 -21
  93. package/dist/package/src/views/tool/code-gen/components/CodePreviewPane.vue +5 -5
  94. package/dist/package/src/views/tool/code-gen/config.ts +60 -60
  95. package/dist/package/src/views/tool/code-gen/preview/data.ts +2 -2
  96. package/dist/package/src/views/tool/code-gen/preview/index.vue +30 -31
  97. package/dist/package/src/views/tool/code-gen/proto/index.vue +43 -43
  98. package/dist/package/src/views/tool/code-gen/table/index.vue +95 -95
  99. package/dist/package/src/views/tool/ops-monitoring/index.vue +1175 -0
  100. package/dist/package/src/views/tool/project-doc/index.vue +7 -7
  101. package/package.json +2 -2
  102. package/dist/package/declarations/src/rpc/base/v1/enum.d.ts +0 -43
  103. package/dist/package/declarations/src/rpc/system/common/v1/enum.d.ts +0 -67
  104. package/dist/package/src/rpc/base/v1/enum.ts +0 -54
  105. package/dist/package/src/rpc/system/common/v1/enum.ts +0 -80
  106. /package/dist/package/declarations/src/views/{tool → base}/language/index.vue.d.ts +0 -0
@@ -1,129 +1,230 @@
1
1
  <template>
2
- <el-popover
3
- placement="top-start"
4
- trigger="hover"
5
- :show-after="120"
6
- :width="360"
7
- :enterable="true"
8
- :teleported="true"
9
- popper-class="dynamic-translation-cell__popover"
2
+ <el-link class="dynamic-translation-cell" type="primary" underline="never" @click="openDialog">
3
+ <span class="dynamic-translation-cell__source">{{ displaySource || "--" }}</span>
4
+ </el-link>
5
+
6
+ <ProDialog
7
+ v-model="dialogVisible"
8
+ :title="displaySource"
9
+ width="760px"
10
+ append-to-body
11
+ :destroy-on-close="true"
12
+ @closed="handleDialogClosed"
10
13
  >
11
- <template #reference>
12
- <span class="dynamic-translation-cell">
13
- <span class="dynamic-translation-cell__source">{{ displaySource || "--" }}</span>
14
- </span>
15
- </template>
16
-
17
- <div class="dynamic-translation-cell__rows">
18
- <div v-for="item in translationRows" :key="item.locale" class="dynamic-translation-cell__row">
19
- <span class="dynamic-translation-cell__language">{{ getLanguageLabel(item.locale) }}</span>
20
- <span class="dynamic-translation-cell__text">{{ item.text || t('system.common.value.none') }}</span>
21
- <el-tag v-if="showStatus && !item.isSource" :type="statusTagType(item)" effect="light" size="small">
22
- {{ statusLabel(item) }}
23
- </el-tag>
24
- </div>
25
- </div>
26
- </el-popover>
14
+ <el-table v-loading="loading" :data="translationRows" border>
15
+ <el-table-column :label="t('common.field.language')" width="150">
16
+ <template #default="{ row }">
17
+ {{ getLanguageLabel(row.locale) }}
18
+ </template>
19
+ </el-table-column>
20
+ <el-table-column :label="t('system.base.translation.field.translations')" min-width="360">
21
+ <template #default="{ row }">
22
+ <el-input
23
+ v-if="row.editing"
24
+ v-model="row.text"
25
+ :placeholder="t('system.base.translation.placeholder.text', { language: getLanguageLabel(row.locale) })"
26
+ />
27
+ <span v-else class="dynamic-translation-dialog__text">
28
+ {{ row.text || t("common.value.none") }}
29
+ </span>
30
+ </template>
31
+ </el-table-column>
32
+ <el-table-column :label="t('common.field.operation')" width="150" fixed="right">
33
+ <template #default="{ row }">
34
+ <el-button v-if="!row.editing" link type="primary" :icon="EditPen" @click="startEdit(row)">
35
+ {{ t("common.action.edit") }}
36
+ </el-button>
37
+ <template v-else>
38
+ <el-button link type="primary" :icon="Check" :loading="row.saving" @click="saveTranslation(row)">
39
+ {{ t("common.action.save") }}
40
+ </el-button>
41
+ <el-button link :icon="Close" :disabled="row.saving" @click="cancelEdit(row)">
42
+ {{ t("common.action.cancel") }}
43
+ </el-button>
44
+ </template>
45
+ </template>
46
+ </el-table-column>
47
+ </el-table>
48
+ </ProDialog>
27
49
  </template>
28
50
 
29
51
  <script setup lang="ts">
30
- import { computed } from "vue";
52
+ import { computed, ref, watch } from "vue";
53
+ import { ElMessage } from "element-plus";
54
+ import { Check, Close, EditPen } from "@element-plus/icons-vue";
55
+ import ProDialog from "@liujitcn/kratos-admin-core/components/Dialog/ProDialog.vue";
31
56
  import { t, useLocaleStore } from "@liujitcn/kratos-admin-core";
32
- import { useEnabledBaseLanguages } from "@liujitcn/kratos-admin-system/api/system/base_language";
33
- import { TranslationStatus } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_translation";
57
+ import { loadEnabledBaseLanguages, useEnabledBaseLanguages } from "@liujitcn/kratos-admin-system/api/system/base_language";
58
+ import { defBaseTranslationService } from "@liujitcn/kratos-admin-system/api/system/base_translation";
59
+ import type { BaseTranslation } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_translation";
60
+ import { TranslationTargetType } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_translation";
34
61
  import { getLanguageLabel } from "./dynamicTranslation";
35
62
 
36
- /** DynamicTranslationCellRecord 描述列表翻译预览所需的最小记录结构。 */
37
- export interface DynamicTranslationCellRecord {
38
- /** 语言区域。 */
39
- locale: string;
40
- /** 通用翻译文本,供系统配置使用。 */
41
- text?: string;
42
- /** 字典名称翻译。 */
43
- name?: string;
44
- /** 字典项标签翻译。 */
45
- label?: string;
46
- /** 菜单标题翻译。 */
47
- title?: string;
48
- /** 翻译审核状态。 */
49
- translation_status?: TranslationStatus;
50
- /** 中文源文是否已变化。 */
51
- source_changed?: boolean;
52
- }
53
-
54
63
  /** DynamicTranslationCellProps 动态翻译列表单元格属性。 */
55
64
  interface DynamicTranslationCellProps {
56
65
  /** 资源源文。 */
57
66
  source: string;
58
- /** 非主语言翻译记录。 */
59
- translations?: DynamicTranslationCellRecord[];
60
- /** 源文所属语言,仅用于语言配置尚未加载时的回退。 */
61
- sourceLocale?: string;
62
- /** 从翻译记录中读取的文本字段。 */
63
- textField?: "text" | "name" | "label" | "title";
64
- /** 是否显示翻译状态标签。 */
65
- showStatus?: boolean;
67
+ /** 统一翻译表目标类型。 */
68
+ targetType?: TranslationTargetType;
69
+ /** 目标资源编号。 */
70
+ targetId?: number;
71
+ /** 资源列表返回的非主语言翻译。 */
72
+ translations?: BaseTranslation[];
66
73
  }
67
74
 
68
- const props = withDefaults(defineProps<DynamicTranslationCellProps>(), {
69
- translations: () => [],
70
- sourceLocale: "zh-CN",
71
- textField: "text",
72
- showStatus: true
73
- });
75
+ const props = defineProps<DynamicTranslationCellProps>();
74
76
 
75
- const { locale } = useLocaleStore();
76
77
  const { languages } = useEnabledBaseLanguages();
77
- const primaryLocale = computed(() => languages.value.find(item => item.is_primary)?.language_code ?? props.sourceLocale ?? "zh-CN");
78
+ const { locale } = useLocaleStore();
79
+ const dialogVisible = ref(false);
80
+ const loading = ref(false);
81
+ const translationRows = ref<DynamicTranslationRow[]>([]);
82
+ const translationOverrides = ref(new Map<string, string>());
78
83
 
79
84
  const displaySource = computed(() => {
80
- if (locale.value === primaryLocale.value) return props.source;
81
- const record = props.translations.find(item => item.locale === locale.value);
82
- if (record?.translation_status !== TranslationStatus.TRANSLATION_STATUS_REVIEWED) return props.source;
83
- return getTranslationText(record) || props.source;
85
+ const override = translationOverrides.value.get(locale.value);
86
+ if (override) return override;
87
+ const translation = props.translations?.find(item => item.locale === locale.value && item.name);
88
+ return translation?.name || props.source;
84
89
  });
85
90
 
86
- const translationRows = computed(() =>
87
- languages.value
88
- .filter(item => item.language_code !== locale.value)
91
+ watch(
92
+ () => [props.targetType, props.targetId],
93
+ () => {
94
+ translationRows.value = [];
95
+ translationOverrides.value.clear();
96
+ dialogVisible.value = false;
97
+ }
98
+ );
99
+
100
+ /** DynamicTranslationRow 描述弹窗内可编辑的单语言翻译行。 */
101
+ interface DynamicTranslationRow {
102
+ /** 翻译记录主键,新增翻译时为空。 */
103
+ id: number;
104
+ /** 语言区域。 */
105
+ locale: string;
106
+ /** 翻译文本。 */
107
+ text: string;
108
+ /** 是否处于编辑状态。 */
109
+ editing: boolean;
110
+ /** 是否正在保存。 */
111
+ saving: boolean;
112
+ /** 编辑前的文本,用于取消编辑。 */
113
+ originalText: string;
114
+ }
115
+
116
+ /** openDialog 打开翻译编辑弹窗并加载当前资源译文。 */
117
+ async function openDialog() {
118
+ dialogVisible.value = true;
119
+ await loadTranslations();
120
+ }
121
+
122
+ /** loadTranslations 查询当前资源的翻译并补齐启用语言行。 */
123
+ async function loadTranslations() {
124
+ const targetType = props.targetType;
125
+ const targetId = props.targetId;
126
+ if (!targetType || !targetId) return;
127
+ loading.value = true;
128
+ try {
129
+ await loadEnabledBaseLanguages();
130
+ const rows = await queryTranslationRows();
131
+ translationRows.value = rows;
132
+ const missingRows = rows.filter(row => !row.text);
133
+ if (missingRows.length === 0) return;
134
+ if (!props.source) return;
135
+ const draft = await defBaseTranslationService.DraftBaseTranslation({ source: props.source });
136
+ const translations = new Map(draft.translations.map(item => [item.locale, item.translation]));
137
+ const results = await Promise.allSettled(
138
+ missingRows.map(async row => {
139
+ const translation = translations.get(row.locale);
140
+ if (!translation) throw new Error("translation not found");
141
+ await defBaseTranslationService.UpdateBaseTranslation({
142
+ id: row.id,
143
+ target_type: targetType,
144
+ target_id: targetId,
145
+ locale: row.locale,
146
+ name: translation
147
+ });
148
+ return { row, text: translation };
149
+ })
150
+ );
151
+ for (const result of results) {
152
+ if (result.status !== "fulfilled") continue;
153
+ result.value.row.text = result.value.text;
154
+ result.value.row.originalText = result.value.text;
155
+ translationOverrides.value.set(result.value.row.locale, result.value.text);
156
+ }
157
+ } catch {
158
+ ElMessage.error(t("common.message.system_error"));
159
+ } finally {
160
+ loading.value = false;
161
+ }
162
+ }
163
+
164
+ /** queryTranslationRows 将列表返回的翻译记录补齐为可编辑语言行。 */
165
+ async function queryTranslationRows(): Promise<DynamicTranslationRow[]> {
166
+ const records = new Map((props.translations ?? []).map(item => [item.locale, item]));
167
+ return languages.value
168
+ .filter(item => !item.is_primary)
89
169
  .map(item => {
90
- const itemLocale = item.language_code;
91
- const isSource = itemLocale === primaryLocale.value;
92
- const record = props.translations.find(item => item.locale === itemLocale);
93
- const text = isSource ? props.source : record ? getTranslationText(record) : "";
170
+ const record = records.get(item.language_code);
171
+ const text = translationOverrides.value.get(item.language_code) ?? record?.name ?? "";
94
172
  return {
95
- locale: itemLocale,
173
+ id: record?.id ?? 0,
174
+ locale: item.language_code,
96
175
  text,
97
- isSource,
98
- translation_status: record?.translation_status,
99
- source_changed: record?.source_changed ?? false
176
+ editing: false,
177
+ saving: false,
178
+ originalText: text
100
179
  };
101
- })
102
- );
180
+ });
181
+ }
182
+
183
+ /** startEdit 开启指定语言的编辑状态。 */
184
+ function startEdit(row: DynamicTranslationRow) {
185
+ row.originalText = row.text;
186
+ row.editing = true;
187
+ }
103
188
 
104
- /** getTranslationText 读取当前资源对应的翻译字段。 */
105
- function getTranslationText(record: DynamicTranslationCellRecord) {
106
- return record[props.textField] ?? "";
189
+ /** cancelEdit 恢复编辑前的内容。 */
190
+ function cancelEdit(row: DynamicTranslationRow) {
191
+ row.text = row.originalText;
192
+ row.editing = false;
107
193
  }
108
194
 
109
- /** statusLabel 返回当前翻译审核状态文案。 */
110
- function statusLabel(item: (typeof translationRows.value)[number]) {
111
- if (item.source_changed) return t("system.translation.status.sourceChanged");
112
- const statusKey =
113
- {
114
- [TranslationStatus.TRANSLATION_STATUS_PENDING]: "pending",
115
- [TranslationStatus.TRANSLATION_STATUS_MACHINE]: "machine",
116
- [TranslationStatus.TRANSLATION_STATUS_REVIEWED]: "reviewed"
117
- }[item.translation_status ?? TranslationStatus.TRANSLATION_STATUS_PENDING] ?? "pending";
118
- return t(`system.translation.status.${statusKey}`);
195
+ /** saveTranslation 使用统一更新接口保存指定语言译文。 */
196
+ async function saveTranslation(row: DynamicTranslationRow) {
197
+ const targetType = props.targetType;
198
+ const targetId = props.targetId;
199
+ if (row.saving || !targetType || !targetId) return;
200
+ if (row.id === 0 && !row.text) {
201
+ row.editing = false;
202
+ return;
203
+ }
204
+ row.saving = true;
205
+ try {
206
+ await defBaseTranslationService.UpdateBaseTranslation({
207
+ id: row.id,
208
+ target_type: targetType,
209
+ target_id: targetId,
210
+ locale: row.locale,
211
+ name: row.text
212
+ });
213
+ ElMessage.success(t("common.message.operation_success"));
214
+ row.editing = false;
215
+ row.originalText = row.text;
216
+ translationOverrides.value.set(row.locale, row.text);
217
+ } catch {
218
+ ElMessage.error(t("common.message.system_error"));
219
+ } finally {
220
+ row.saving = false;
221
+ }
119
222
  }
120
223
 
121
- /** statusTagType 返回当前翻译审核状态对应的标签样式。 */
122
- function statusTagType(item: (typeof translationRows.value)[number]) {
123
- if (item.source_changed) return "warning";
124
- if (item.translation_status === TranslationStatus.TRANSLATION_STATUS_REVIEWED) return "success";
125
- if (item.translation_status === TranslationStatus.TRANSLATION_STATUS_MACHINE) return "primary";
126
- return "info";
224
+ /** handleDialogClosed 清理弹窗状态,避免复用旧资源译文。 */
225
+ function handleDialogClosed() {
226
+ translationRows.value = [];
227
+ loading.value = false;
127
228
  }
128
229
  </script>
129
230
 
@@ -141,36 +242,8 @@ function statusTagType(item: (typeof translationRows.value)[number]) {
141
242
  text-overflow: ellipsis;
142
243
  white-space: nowrap;
143
244
  }
144
-
145
- </style>
146
-
147
- <style>
148
- .dynamic-translation-cell__popover .dynamic-translation-cell__rows {
149
- display: flex;
150
- flex-direction: column;
151
- gap: 10px;
152
- }
153
-
154
- .dynamic-translation-cell__popover {
155
- max-width: calc(100vw - 24px);
156
- box-sizing: border-box;
157
- }
158
-
159
- .dynamic-translation-cell__popover .dynamic-translation-cell__row {
160
- display: grid;
161
- grid-template-columns: 76px minmax(0, 1fr) auto;
162
- gap: 10px;
163
- align-items: center;
164
- }
165
-
166
- .dynamic-translation-cell__popover .dynamic-translation-cell__language {
167
- color: var(--el-text-color-regular);
168
- font-weight: 600;
169
- }
170
-
171
- .dynamic-translation-cell__popover .dynamic-translation-cell__text {
245
+ .dynamic-translation-dialog__text {
172
246
  min-width: 0;
173
- color: var(--el-text-color-primary);
174
247
  overflow-wrap: anywhere;
175
248
  }
176
249
  </style>
@@ -1,75 +1,78 @@
1
1
  <template>
2
2
  <div class="translation-editor">
3
+ <div class="translation-editor__toolbar">
4
+ <el-button
5
+ type="primary"
6
+ size="small"
7
+ :icon="Promotion"
8
+ :loading="translating"
9
+ :disabled="!canTranslate"
10
+ @click="handleBatchTranslate"
11
+ >
12
+ {{ t("system.base.translation.action.batch_translate") }}
13
+ </el-button>
14
+ </div>
3
15
  <div v-for="item in localValues" :key="item.locale" class="translation-editor__row">
4
16
  <div class="translation-editor__heading">
5
17
  <span>{{ getLanguageLabel(item.locale) }}</span>
6
- <el-tag :type="statusTagType(item)" effect="light" size="small">{{ statusLabel(item) }}</el-tag>
7
18
  </div>
8
- <div class="translation-editor__control">
19
+ <div class="translation-editor__control" :class="{ 'translation-editor__control--multiline': multiline }">
9
20
  <el-input
10
21
  :model-value="item.text"
11
22
  :maxlength="maxlength"
12
23
  :type="multiline ? 'textarea' : 'text'"
13
24
  :rows="multiline ? 6 : undefined"
25
+ :disabled="translating || translatingLocale === item.locale"
14
26
  show-word-limit
15
- :placeholder="t('system.translation.placeholder.text', { language: getLanguageLabel(item.locale) })"
27
+ :placeholder="t('system.base.translation.placeholder.text', { language: getLanguageLabel(item.locale) })"
16
28
  @update:model-value="value => updateText(item.locale, value)"
17
29
  />
18
- <el-tooltip
19
- v-if="draftEnabled && resourceId > 0"
20
- :content="t('system.translation.action.generateDraft', { language: getLanguageLabel(item.locale) })"
30
+ <el-button
31
+ class="translation-editor__translate"
32
+ link
33
+ type="primary"
34
+ size="small"
35
+ :icon="Promotion"
36
+ :loading="translatingLocale === item.locale"
37
+ :disabled="!props.source || Boolean(item.text) || translating || Boolean(translatingLocale)"
38
+ @click="handleTranslate(item.locale)"
21
39
  >
22
- <el-button
23
- :icon="MagicStick"
24
- :loading="loadingLocale === item.locale"
25
- :disabled="item.translation_status === TranslationStatus.TRANSLATION_STATUS_REVIEWED && !item.source_changed"
26
- @click="generateDraft(item.locale)"
27
- />
28
- </el-tooltip>
40
+ {{ t("system.base.translation.action.translate") }}
41
+ </el-button>
29
42
  </div>
30
43
  </div>
31
- <el-alert
32
- v-if="resourceId === 0 && draftEnabled"
33
- :title="t('system.translation.message.saveBeforeDraft')"
34
- type="info"
35
- :closable="false"
36
- show-icon
37
- />
38
44
  </div>
39
45
  </template>
40
46
 
41
47
  <script setup lang="ts">
42
- import { ref, watch } from "vue";
43
- import { MagicStick } from "@element-plus/icons-vue";
44
- import { t } from "@liujitcn/kratos-admin-core";
48
+ import { computed, ref, watch } from "vue";
49
+ import { ElMessage } from "element-plus";
50
+ import { Promotion } from "@element-plus/icons-vue";
51
+ import { t, useLocaleStore } from "@liujitcn/kratos-admin-core";
45
52
  import { defBaseTranslationService } from "@liujitcn/kratos-admin-system/api/system/base_translation";
46
- import {
47
- BaseConfigTranslationField,
48
- TranslationResourceType,
49
- TranslationStatus
50
- } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_translation";
51
53
  import { getLanguageLabel, type DynamicTranslationValue } from "./dynamicTranslation";
52
54
 
53
55
  /** DynamicTranslationEditorProps 动态翻译编辑器属性。 */
54
56
  interface DynamicTranslationEditorProps {
55
57
  modelValue: DynamicTranslationValue[];
56
- resourceId: number;
57
- resourceType: TranslationResourceType;
58
- field?: BaseConfigTranslationField;
59
- draftEnabled?: boolean;
58
+ /** 当前语言源文本。 */
59
+ source?: string;
60
60
  maxlength?: number;
61
61
  multiline?: boolean;
62
62
  }
63
63
 
64
64
  const props = withDefaults(defineProps<DynamicTranslationEditorProps>(), {
65
- draftEnabled: false,
65
+ source: "",
66
66
  maxlength: 100,
67
67
  multiline: false
68
68
  });
69
69
 
70
+ const { locale: currentLocale } = useLocaleStore();
70
71
  const emit = defineEmits<{ "update:modelValue": [value: DynamicTranslationValue[]] }>();
71
72
  const localValues = ref<DynamicTranslationValue[]>([]);
72
- const loadingLocale = ref("");
73
+ const translating = ref(false);
74
+ const translatingLocale = ref("");
75
+ const canTranslate = computed(() => Boolean(props.source) && localValues.value.some(item => !item.text && item.locale !== currentLocale.value));
73
76
 
74
77
  watch(
75
78
  () => props.modelValue,
@@ -86,56 +89,60 @@ function updateText(locale: DynamicTranslationValue["locale"], text: string) {
86
89
  emit("update:modelValue", values);
87
90
  }
88
91
 
89
- /** generateDraft 为指定目标语言生成机器翻译草稿。 */
90
- async function generateDraft(locale: DynamicTranslationValue["locale"]) {
91
- loadingLocale.value = locale;
92
+ /** handleBatchTranslate 批量翻译当前仍为空的语言输入框。 */
93
+ async function handleBatchTranslate() {
94
+ if (!canTranslate.value || translating.value) return;
95
+ translating.value = true;
92
96
  try {
93
- const response = await defBaseTranslationService.GenerateTranslationDraft({
94
- resource_type: props.resourceType,
95
- resource_id: props.resourceId,
96
- target_locale: locale,
97
- field: props.field ?? BaseConfigTranslationField.BASE_CONFIG_TRANSLATION_FIELD_UNSPECIFIED
98
- });
99
- const values = localValues.value.map(item =>
100
- item.locale === locale
101
- ? {
102
- ...item,
103
- text: response.translation,
104
- translation_status: response.translation_status,
105
- source_changed: false,
106
- source_hash: response.source_hash,
107
- translation_provider: response.translation_provider,
108
- translated_at: response.translated_at
109
- }
110
- : item
111
- );
112
- localValues.value = values;
113
- emit("update:modelValue", values);
114
- ElMessage.success(t("system.translation.message.draftSuccess", { language: getLanguageLabel(locale) }));
97
+ const pending = localValues.value.filter(item => !item.text && item.locale !== currentLocale.value);
98
+ const response = await defBaseTranslationService.DraftBaseTranslation({ source: props.source });
99
+ const translations = new Map(response.translations.map(item => [item.locale, item.translation]));
100
+ const failed = pending.filter(item => !translations.has(item.locale)).length;
101
+ if (translations.size) {
102
+ const values = localValues.value.map(item => {
103
+ const translation = translations.get(item.locale);
104
+ return translation === undefined ? item : { ...item, text: translation };
105
+ });
106
+ localValues.value = values;
107
+ emit("update:modelValue", values);
108
+ }
109
+ if (failed) {
110
+ ElMessage.warning(
111
+ t("system.base.translation.message.batch_translate_partial", {
112
+ success: translations.size,
113
+ failed
114
+ })
115
+ );
116
+ } else {
117
+ ElMessage.success(t("system.base.translation.message.batch_translate_success", { count: translations.size }));
118
+ }
119
+ } catch {
120
+ ElMessage.error(t("system.base.translation.message.batch_translate_failed"));
115
121
  } finally {
116
- loadingLocale.value = "";
122
+ translating.value = false;
117
123
  }
118
124
  }
119
125
 
120
- /** statusLabel 返回当前翻译审核状态文案。 */
121
- function statusLabel(item: DynamicTranslationValue) {
122
- if (item.source_changed) return t("system.translation.status.sourceChanged");
123
- const statusKey =
124
- {
125
- [TranslationStatus.TRANSLATION_STATUS_PENDING]: "pending",
126
- [TranslationStatus.TRANSLATION_STATUS_MACHINE]: "machine",
127
- [TranslationStatus.TRANSLATION_STATUS_REVIEWED]: "reviewed"
128
- }[item.translation_status] ?? "pending";
129
- return t(`system.translation.status.${statusKey}`);
126
+ /** handleTranslate 翻译指定语言的单个输入框。 */
127
+ async function handleTranslate(locale: string) {
128
+ const item = localValues.value.find(value => value.locale === locale);
129
+ if (!item || item.text || item.locale === currentLocale.value || !props.source || translating.value || translatingLocale.value) return;
130
+ translatingLocale.value = locale;
131
+ try {
132
+ const response = await defBaseTranslationService.DraftBaseTranslation({ source: props.source, locale });
133
+ const translation = response.translations.find(item => item.locale === locale)?.translation;
134
+ if (!translation) throw new Error("translation not found");
135
+ const values = localValues.value.map(value => (value.locale === locale ? { ...value, text: translation } : value));
136
+ localValues.value = values;
137
+ emit("update:modelValue", values);
138
+ ElMessage.success(t("system.base.translation.message.translate_success", { language: getLanguageLabel(locale) }));
139
+ } catch {
140
+ ElMessage.error(t("system.base.translation.message.translate_failed", { language: getLanguageLabel(locale) }));
141
+ } finally {
142
+ translatingLocale.value = "";
143
+ }
130
144
  }
131
145
 
132
- /** statusTagType 返回当前翻译审核状态对应的标签样式。 */
133
- function statusTagType(item: DynamicTranslationValue) {
134
- if (item.source_changed) return "warning";
135
- if (item.translation_status === TranslationStatus.TRANSLATION_STATUS_REVIEWED) return "success";
136
- if (item.translation_status === TranslationStatus.TRANSLATION_STATUS_MACHINE) return "primary";
137
- return "info";
138
- }
139
146
  </script>
140
147
 
141
148
  <style scoped>
@@ -145,6 +152,10 @@ function statusTagType(item: DynamicTranslationValue) {
145
152
  gap: 14px;
146
153
  width: 100%;
147
154
  }
155
+ .translation-editor__toolbar {
156
+ display: flex;
157
+ justify-content: flex-end;
158
+ }
148
159
  .translation-editor__row {
149
160
  display: flex;
150
161
  flex-direction: column;
@@ -157,10 +168,30 @@ function statusTagType(item: DynamicTranslationValue) {
157
168
  align-items: center;
158
169
  }
159
170
  .translation-editor__heading {
160
- justify-content: space-between;
161
171
  color: var(--el-text-color-regular);
162
172
  }
173
+ .translation-editor__control {
174
+ position: relative;
175
+ width: 100%;
176
+ }
163
177
  .translation-editor__control :deep(.el-input) {
164
178
  flex: 1;
165
179
  }
180
+ .translation-editor__control--multiline :deep(.el-textarea__inner) {
181
+ padding-right: 78px;
182
+ }
183
+ .translation-editor__control:not(.translation-editor__control--multiline) :deep(.el-input__suffix) {
184
+ padding-right: 78px;
185
+ }
186
+ .translation-editor__translate {
187
+ position: absolute;
188
+ z-index: 1;
189
+ top: 50%;
190
+ right: 8px;
191
+ transform: translateY(-50%);
192
+ }
193
+ .translation-editor__control--multiline .translation-editor__translate {
194
+ top: 8px;
195
+ transform: none;
196
+ }
166
197
  </style>