Sfoglia il codice sorgente

同步多语言(仅菜单相关,不含自动翻译)

夜鹰 7 mesi fa
parent
commit
d73cbda491

+ 16 - 0
Web/src/api-services/models/list-sys-lang-text-input.ts

@@ -51,4 +51,20 @@ export interface ListSysLangTextInput {
      * @memberof ListSysLangTextInput
      */
     langCode?: string | null;
+
+    /**
+     * 当前页码
+     *
+     * @type {number}
+     * @memberof PageNoticeInput
+     */
+    page?: number;
+
+    /**
+     * 页码容量
+     *
+     * @type {number}
+     * @memberof PageNoticeInput
+     */
+    pageSize?: number;
 }

+ 1 - 1
Web/src/api-services/models/update-menu-input.ts

@@ -139,7 +139,7 @@ export interface UpdateMenuInput {
      * @type {string}
      * @memberof UpdateMenuInput
      */
-    icon?: string | null;
+    icon?: string;
 
     /**
      * 是否内嵌

+ 180 - 0
Web/src/components/multiLangInput/index.vue

@@ -0,0 +1,180 @@
+<template>
+    <div class="multi-lang-input">
+    <el-input v-model="inputModelValue" :placeholder="`请输入 ${currentLangLabel}`" clearable @update:model-value="(val: string) => { emit('update:modelValue', val); }">
+            <template #append>
+                <el-button @click="openDialog" circle>
+                    <template #icon>
+                        <i class="iconfont icon-diqiu1"></i>
+                    </template>
+                </el-button>
+            </template>
+        </el-input>
+
+        <el-dialog v-model="dialogVisible" title="多语言设置" width="600px">
+            <el-form ref="ruleFormRef" label-width="auto">
+                <el-row :gutter="35">
+                    <el-col v-for="lang in languages" :key="lang.code" :span="24">
+                        <el-form-item :label="lang.label">
+                            <el-input v-model="multiLangValue[lang.code]" :placeholder="`请输入: ${lang.label}`" clearable />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </el-form>
+
+            <template #footer>
+                <el-button @click="aiTranslation">AI翻译</el-button>
+                <el-button @click="closeDialog">关闭</el-button>
+                <el-button type="primary" @click="confirmDialog">确认修改</el-button>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { useLangStore } from '/@/stores/useLangStore';
+import { Local } from '/@/utils/storage';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysLangTextApi } from '/@/api-services/api';
+import { ElMessage } from 'element-plus';
+const emit = defineEmits<{ (e: 'update:modelValue', value: string): void; }>();
+const ruleFormRef = ref();
+
+const fetchMultiLang = async () => {    
+    const result = await getAPI(SysLangTextApi).apiSysLangTextListPost({ entityName: props.entityName, entityId: props.entityId, fieldName: props.fieldName, pageSize: 200 }).then(res => res.data.result)
+    return result ?? [];
+};
+
+const inputModelValue = computed({
+    get: () => props.modelValue,
+    set: (val) => emit('update:modelValue', val),
+});
+
+const props = defineProps<{
+    modelValue: string;
+    entityName: string;
+    entityId: number;
+    fieldName: string;
+}>();
+
+// 全局语言
+const langStore = useLangStore();
+const languages = ref<any>([] as any);
+
+// 当前语言(可根据用户设置或浏览器设置)
+const currentLang = ref('zh_CN');
+const activeLang = ref('zh_CN');
+
+// 是否弹框
+const dialogVisible = ref(false);
+
+// 多语言对象
+const multiLangValue = ref<Record<string, string>>({});
+
+// 当前语言显示 Label
+const currentLangLabel = computed(() => {
+    return (
+        languages.value.find((l: { code: string; }) => l.code === currentLang.value)?.Label || currentLang.value
+    );
+});
+
+// 初始化语言
+onMounted(async () => {
+    if (langStore.languages.length === 0) {
+        await langStore.loadLanguages();
+    }
+    const themeConfig = Local.get('themeConfig');
+    const globalI18n = themeConfig?.globalI18n;
+    if (globalI18n) {
+        const matched = langStore.languages.find(l => l.code === globalI18n);
+        const langCode = matched?.code || 'zh_CN';
+        currentLang.value = langCode;
+        activeLang.value = langCode;
+    }
+    languages.value = langStore.languages;
+
+    if (languages.value.length > 0) {
+        currentLang.value = languages.value[0].code;
+        activeLang.value = languages.value[0].code;
+    }
+});
+const aiTranslation = async () => {
+    languages.value.forEach(async (element: { code: string; value: string | null; }) => {
+        if (element.code == currentLang.value) {
+            return;
+        }
+        multiLangValue.value[element.code] = '正在翻译...';
+        try {
+            const text = await getAPI(SysLangTextApi).apiSysLangTextAiTranslateTextPost({ originalText: props.modelValue, targetLang: element.value }).then(res => res.data.result);
+            if (text) {
+                multiLangValue.value[element.code] = text;
+            } else {
+                multiLangValue.value[element.code] = '';
+            }
+        } catch (e: any) {
+            multiLangValue.value[element.code] = '';
+            ElMessage.warning(e.message);
+        }
+    });
+}
+
+// 打开对话框(点击按钮)
+const openDialog = async () => {
+    if (!props.entityId) {
+        ElMessage.warning('请先保存数据!');
+        return;
+    }
+    multiLangValue.value = {};
+    const res = await fetchMultiLang();
+    multiLangValue.value[currentLang.value] = props.modelValue;
+    res.forEach((element: { langCode?: string | null; content?: string | null; }) => {
+        multiLangValue.value[element.langCode ?? 0] = element.content ?? '';
+    });
+    dialogVisible.value = true;
+    ruleFormRef.value?.resetFields();
+};
+
+// 关闭对话框(只是关闭)
+const closeDialog = () => {
+    dialogVisible.value = false;
+    multiLangValue.value = {};
+    ruleFormRef.value?.resetFields();
+};
+
+// 确认按钮(更新 + 关闭)
+const confirmDialog = async () => {
+    const langItems = Object.entries(multiLangValue.value)
+        .filter(([_, content]) => content && content.trim() !== '')
+        .map(([code, content]) => ({
+            entityName: props.entityName,
+            entityId: props.entityId,
+            fieldName: props.fieldName,
+            langCode: code,
+            content: content,
+        }));
+
+    if (langItems.length === 0) {
+        ElMessage.warning('请输入至少一条多语言内容!');
+        return;
+    }
+
+    try {
+        await getAPI(SysLangTextApi).apiSysLangTextBatchSavePost(langItems);
+        ElMessage.success('保存成功!');
+        // 同步当前语言内容到父组件 input
+        emit('update:modelValue', multiLangValue.value[currentLang.value]);
+        dialogVisible.value = false;
+    } catch (err) {
+        console.error(err);
+        ElMessage.error('保存失败!');
+    }
+    dialogVisible.value = false;
+    ruleFormRef.value?.resetFields();
+};
+</script>
+
+<style scoped>
+.multi-lang-input {
+    width: 100%;
+}
+</style>

+ 3 - 0
Web/src/main.ts

@@ -22,6 +22,7 @@ import 'vform3-builds/dist/designer.style.css';
 // 关闭自动打印
 import { disAutoConnect } from 'vue-plugin-hiprint';
 import sysDict from "/@/components/sysDict/sysDict.vue";
+import multiLangInput from "/@/components/multiLangInput/index.vue";
 disAutoConnect();
 
 const app = createApp(App);
@@ -31,6 +32,8 @@ other.elSvg(app);
 
 // 注册全局字典组件
 app.component('GSysDict', sysDict);
+// 注册全局多语言组件
+app.component('GMultiLangInput', multiLangInput);
 
 const TooltipProps = ElTooltip.props
 TooltipProps.showAfter = { type: Number, default: 800 }; // 设置全局tooltip延时显示时间为800毫秒

+ 17 - 0
Web/src/stores/useLangStore.ts

@@ -0,0 +1,17 @@
+import { defineStore } from 'pinia';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysLangApi } from '/@/api-services/api';
+
+export const useLangStore = defineStore('lang', {
+	state: () => ({
+		languages: [] as any[],
+	}),
+	actions: {
+		async loadLanguages() {
+			if (this.languages.length === 0) {
+				const res = await getAPI(SysLangApi).apiSysLangDropdownDataPost();
+				this.languages = res.data.result ?? [];
+			}
+		},
+	},
+});

+ 3 - 2
Web/src/views/system/menu/component/editMenu.vue

@@ -21,12 +21,13 @@
 					</el-col>
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 						<el-form-item label="菜单类型" prop="type" :rules="[{ required: true, message: '菜单类型不能为空', trigger: 'blur' }]">
-              <g-sys-dict v-model="state.ruleForm.type" code="MenuTypeEnum" render-as="radio" />
+							<g-sys-dict v-model="state.ruleForm.type" code="MenuTypeEnum" render-as="radio" />
 						</el-form-item>
 					</el-col>
 					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 						<el-form-item label="菜单名称" prop="title" :rules="[{ required: true, message: '菜单名称不能为空', trigger: 'blur' }]">
-							<el-input v-model="state.ruleForm.title" placeholder="菜单名称" clearable />
+							<!-- <el-input v-model="state.ruleForm.title" placeholder="菜单名称" clearable /> -->
+                            <g-multi-lang-Input entityName="SysMenu" fieldName="Title" :entityId="Number(state.ruleForm.id)" v-model="state.ruleForm.title" placeholder="菜单名称" clearable />
 						</el-form-item>
 					</el-col>
 					<template v-if="state.ruleForm.type === 1 || state.ruleForm.type === 2">

+ 14 - 0
Web/src/views/system/user/component/editUser.vue

@@ -46,6 +46,13 @@
 									<el-input v-model="state.ruleForm.email" placeholder="邮箱" clearable />
 								</el-form-item>
 							</el-col>
+							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+								<el-form-item label="语言" prop="langCode" :rules="[{ required: true, message: '语言不能为空', trigger: 'blur' }]">
+									<el-select clearable filterable v-model="state.ruleForm.langCode" placeholder="请选择语言">
+										<el-option v-for="(item, index) in state.languages" :key="index" :value="item.code" :label="item.label" />
+									</el-select>
+								</el-form-item>
+							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb5">
 								<el-form-item label="排序">
 									<el-input-number v-model="state.ruleForm.orderNo" placeholder="排序" class="w100" />
@@ -226,6 +233,8 @@ import { useUserInfo } from '/@/stores/userInfo';
 import { getAPI } from '/@/utils/axios-utils';
 import { SysPosApi, SysRoleApi, SysUserApi } from '/@/api-services/api';
 import {AccountTypeEnum, RoleOutput, OrgTreeOutput, SysPos, UpdateUserInput} from '/@/api-services/models';
+import { useLangStore } from '/@/stores/useLangStore';
+const langStore = useLangStore();
 
 const props = defineProps({
 	title: String,
@@ -243,6 +252,7 @@ const state = reactive({
 	ruleForm: {} as UpdateUserInput,
 	posData: [] as Array<SysPos>, // 职位数据
 	roleData: [] as Array<RoleOutput>, // 角色数据
+	languages: [] as any[], // 语言数据
 });
 // 级联选择器配置选项
 const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'name', expandTrigger: 'hover' };
@@ -253,6 +263,10 @@ onMounted(async () => {
 	state.posData = res.data.result ?? [];
 	var res1 = await getAPI(SysRoleApi).apiSysRoleListGet();
 	state.roleData = res1.data.result ?? [];
+	if (langStore.languages.length === 0) {
+        await langStore.loadLanguages();
+    }
+	state.languages = langStore.languages;
 	state.loading = false;
 });
 

+ 17 - 3
Web/src/views/system/user/component/userCenter.vue

@@ -89,6 +89,13 @@
 											</el-radio-group>
 										</el-form-item>
 									</el-col>
+									<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+										<el-form-item label="语言" prop="langCode" :rules="[{ required: true, message: '语言不能为空', trigger: 'blur' }]">
+											<el-select clearable filterable v-model="state.ruleFormBase.langCode" placeholder="请选择语言">
+												<el-option v-for="(item, index) in state.languages" :key="index" :value="item.code" :label="item.label" />
+											</el-select>
+										</el-form-item>
+									</el-col>
 									<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 										<el-form-item label="地址">
 											<el-input v-model="state.ruleFormBase.address" placeholder="地址" clearable type="textarea" />
@@ -170,10 +177,12 @@ import OrgTree from '/@/views/system/user/component/orgTree.vue';
 import CropperDialog from '/@/components/cropper/index.vue';
 import VueGridLayout from 'vue-grid-layout';
 import { sm2 } from 'sm-crypto-v2';
-import { clearAccessAfterReload, getAPI } from '/@/utils/axios-utils';
-import { SysFileApi, SysUserApi } from '/@/api-services/api';
+import { accessTokenKey, clearAccessAfterReload, getAPI } from '/@/utils/axios-utils';
+import { SysAuthApi, SysFileApi, SysUserApi } from '/@/api-services/api';
 import { ChangePwdInput, SysUser, SysFile } from '/@/api-services/models';
-
+import { useLangStore } from '/@/stores/useLangStore';
+import { Local } from '/@/utils/storage';
+const langStore = useLangStore();
 const stores = useUserInfo();
 const { userInfos } = storeToRefs(stores);
 const uploadSignRef = ref<UploadInstance>();
@@ -198,12 +207,17 @@ const state = reactive({
 	signFileList: [] as any,
 	passwordNew2: '',
 	cropperTitle: '',
+	languages: [] as any[], // 语言数据
 });
 
 onMounted(async () => {
 	state.loading = true;
 	var res = await getAPI(SysUserApi).apiSysUserBaseInfoGet();
 	state.ruleFormBase = res.data.result ?? { account: '' };
+	if (langStore.languages.length === 0) {
+        await langStore.loadLanguages();
+    }
+	state.languages = langStore.languages;
 	state.loading = false;
 });