feat(国际化): 去除冗余代码,增加国际化配置页面

This commit is contained in:
Zhunianya
2026-03-20 11:20:50 +08:00
parent 69d58763ee
commit edeaa75337
36 changed files with 1304 additions and 3893 deletions

View File

@@ -0,0 +1,548 @@
<template>
<div class="system-app-lang">
<el-card v-show="showSearch" class="search-card">
<el-form
@submit.native.prevent
:model="queryParams"
ref="queryForm"
:inline="true"
label-width="46px"
class="search-form"
>
<el-form-item prop="langName">
<el-input
v-model="queryParams.langName"
:placeholder="$t('app.lang.755172-14')"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item prop="country">
<el-input
v-model="queryParams.country"
:placeholder="$t('app.lang.755172-12')"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<div style="float: right">
<el-button type="primary" icon="el-icon-search" @click="handleQuery">{{ $t('search') }}</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">{{ $t('reset') }}</el-button>
</div>
</el-form>
</el-card>
<el-card>
<el-row :gutter="10" style="margin-bottom: 16px">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="small"
@click="handleAdd"
v-hasPermi="['app:language:add']"
>
{{ $t('add') }}
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
plain
icon="el-icon-delete"
size="small"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['app:language:remove']"
>
{{ $t('del') }}
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-hasPermi="['app:language:export']"
plain
size="small"
:loading="exportLoading"
@click="handleExport"
>
{{ $t('app.start.891644-43') }}
</el-button>
</el-col>
<el-col :span="1.5">
<el-upload v-hasRole="['admin']" :show-file-list="false" ref="upload" action="" :http-request="handleImport">
<el-button slot="trigger" :loading="importLoading" size="small" plain>
{{ $t('app.start.891644-44') }}
</el-button>
</el-upload>
</el-col>
<el-col :span="1.5">
<el-dropdown v-hasRole="['admin']" @command="handleExportBackendMenu($event, true)">
<el-button plain size="small">
{{ $t('app.start.891644-47') }}
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="dict in dict.type.international_configuration_template"
:key="dict.value"
:command="dict.value"
>
{{ dict.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
<el-col :span="1.5">
<el-dropdown v-hasRole="['admin']" @command="handleExportBackendMenu($event, false)">
<el-button plain size="small">
{{ $t('app.start.891644-45') }}
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="dict in dict.type.international_configuration_template"
:key="dict.value"
:command="dict.value"
>
{{ dict.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
<el-col :span="1.5">
<el-dropdown v-hasRole="['admin']" @command="handleImportTranslate">
<el-upload
v-hasRole="['admin']"
:show-file-list="false"
ref="upload"
action=""
:http-request="handleImportBackendMenu"
:disabled="true"
>
<el-button slot="trigger" size="small" plain>
{{ $t('app.start.891644-46') }}
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
</el-upload>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="dict in dict.type.international_configuration_template"
:key="dict.value"
:command="dict.value"
>
{{ dict.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="languageList" :border="false" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column :label="$t('app.lang.755172-4')" align="center" prop="id" min-width="80" />
<el-table-column :label="$t('app.lang.755172-8')" align="left" prop="langName" min-width="180" />
<el-table-column :label="$t('app.lang.755172-5')" align="center" prop="language" min-width="100" />
<el-table-column :label="$t('app.lang.755172-6')" align="center" prop="country" min-width="120" />
<el-table-column :label="$t('app.lang.755172-7')" align="center" prop="timeZone" min-width="100" />
<el-table-column fixed="right" :label="$t('opation')" align="center" width="130">
<template slot-scope="scope">
<el-button
size="small"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['app:language:edit']"
>
{{ $t('update') }}
</el-button>
<el-button
size="small"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['app:language:remove']"
>
{{ $t('del') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
style="margin-bottom: 20px"
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<!-- 添加或修改app语言对话框 -->
<el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="75px">
<el-form-item :label="$t('app.lang.755172-8')" prop="langName">
<el-input v-model="form.langName" :placeholder="$t('app.lang.755172-14')" style="width: 390px" />
</el-form-item>
<el-form-item :label="$t('app.lang.755172-5')" prop="language">
<el-input v-model="form.language" :placeholder="$t('app.lang.755172-11')" style="width: 390px" />
</el-form-item>
<el-form-item :label="$t('app.lang.755172-6')" prop="country">
<el-input v-model="form.country" :placeholder="$t('app.lang.755172-12')" style="width: 390px" />
</el-form-item>
<el-form-item :label="$t('app.lang.755172-7')" prop="timeZone">
<el-input v-model="form.timeZone" :placeholder="$t('app.lang.755172-13')" style="width: 390px" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">
{{ $t('confirm') }}
</el-button>
<el-button @click="cancel">
{{ $t('cancel') }}
</el-button>
</div>
</el-dialog>
<el-dialog :title="$t('app.lang.755172-22')" :visible.sync="productModelVisible" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item :label="$t('app.lang.755172-23')" prop="productId">
<el-select v-model="productId" :placeholder="$t('pleaseSelect')">
<el-option v-for="item in prodcutModels" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitProdcutModel">
{{ $t('confirm') }}
</el-button>
<el-button @click="closeProductModelDialog">
{{ $t('cancel') }}
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listLanguage,
getLanguage,
delLanguage,
addLanguage,
updateLanguage,
exportTranslate,
importTranslate,
} from '@/api/system/language';
import * as langTransformer from './script/langTransformer';
import * as xlsxHandler from './script/xlsx';
import * as jszip from './script/jszip';
import { downFileByBlob } from '@/utils/common.js';
import { listShortProduct } from '@/api/iot/product';
export default {
name: 'AppLang',
dicts: ['international_configuration_template'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// app语言表格数据
languageList: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
langName: '',
country: '',
pageNum: 1,
pageSize: 10,
},
// 表单参数
form: {},
// 表单校验
rules: {
langName: [
{
required: true,
message: this.$t('app.lang.755172-14'),
trigger: 'blur',
},
],
language: [
{
required: true,
message: this.$t('app.lang.755172-11'),
trigger: 'blur',
},
],
country: [
{
required: true,
message: this.$t('app.lang.755172-12'),
trigger: 'blur',
},
],
},
// 导出语言包loading
exportLoading: false,
// 导入excel生成语言包loading
importLoading: false,
currentLanguage: '',
currentTranslateModule: '',
productModelVisible: false,
prodcutModels: [],
productId: '',
};
},
created() {
this.getList();
this.getProductModels();
},
methods: {
/** 查询app语言列表 */
getList() {
this.loading = true;
listLanguage(this.queryParams).then((response) => {
this.languageList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
id: null,
language: null,
country: null,
timeZone: null,
createBy: null,
createTime: null,
langName: null,
};
this.resetForm('form');
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm');
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = this.$t('app.lang.755172-17');
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id || this.ids;
getLanguage(id).then((response) => {
this.form = response.data;
this.open = true;
this.title = this.$t('app.lang.755172-18');
});
},
/** 提交按钮 */
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {
if (this.form.id != null) {
updateLanguage(this.form).then((response) => {
this.$modal.msgSuccess(this.$t('updateSuccess'));
this.open = false;
this.getList();
});
} else {
addLanguage(this.form).then((response) => {
this.$modal.msgSuccess(this.$t('addSuccess'));
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal
.confirm(this.$t('app.lang.755172-21', [ids]))
.then(function () {
return delLanguage(ids);
})
.then(() => {
this.getList();
this.$modal.msgSuccess(this.$t('delSuccess'));
})
.catch(() => {});
},
// 导出语言包
handleExport() {
if (this.languageList.length === 0) {
return;
}
try {
this.exportLoading = true;
// 中英文映射
const langs = this.languageList.reduce((obj, item) => {
obj[item.language] = item.country;
return obj;
}, {});
// 获取json数据
const jsonMap = langTransformer.getLangJson();
// 转换为excel导出需要的数据格式
const excelData = langTransformer.transoformToExcel(jsonMap, langs);
// 导出为excel
xlsxHandler.exportExcel(excelData, 'lang.xlsx');
} finally {
this.exportLoading = false;
}
},
// 导入语言包excel直接导出转换后的压缩包
async handleImport(fileInfo) {
try {
this.importLoading = true;
// 中英文映射
const langs = this.languageList.reduce((obj, item) => {
obj[item.country] = item.language;
return obj;
}, {});
// 读取excel文件解析为json数据
const data = await xlsxHandler.parseJson(fileInfo.file);
// 将json文件转换为以模块维度的数据用于进一步处理成压缩包文件数据
const jsonData = jszip.parseJsonZipData(data, langs);
// 生成zip的文件列表
const files = jszip.generateJsonZipFiles(jsonData);
// 下载为压缩包
jszip.downloadFiles2Zip({
zipName: 'lang',
files: files,
});
} finally {
this.importLoading = false;
}
},
// 导出菜单名称翻译列表
async handleExportBackendMenu(value, isSource = false) {
this.currentTranslateModule = value;
const exportFn = () => {
const sourceData = this.dict.type.international_configuration_template;
const isThingsModel = this.currentTranslateModule === 'things_model';
exportTranslate(value, isSource, isThingsModel ? this.productId : null).then((response) => {
let name = sourceData.find((item) => item.value === value).name;
if (isThingsModel) {
name += '_' + this.prodcutModels.find((item) => item.id === this.productId).name;
}
if (response.type === 'application/json') {
this.$modal.msgError(`导出异常`);
return;
}
const fileName = isSource ? `${name}原表数据.xlsx` : `${name}翻译数据.xlsx`;
downFileByBlob(response, fileName);
isThingsModel && this.closeProductModelDialog();
});
};
if (value === 'things_model') {
this.productModelVisible = true;
this.callback = () => {
exportFn();
};
} else {
exportFn();
}
},
async handleImportBackendMenu(fileInfo) {
let formData = new FormData();
formData.append('file', fileInfo.file);
const isThingsModel = this.currentTranslateModule === 'things_model';
const productId = isThingsModel && this.productId ? this.productId : '';
importTranslate(formData, this.currentTranslateModule, productId).then((res) => {
if (res.code === 200) {
this.$modal.msgSuccess('导入成功');
} else {
this.$modal.msgError(res.msg);
}
isThingsModel && this.closeProductModelDialog();
});
},
handleImportTranslate(value) {
this.currentTranslateModule = value;
if (value === 'things_model') {
this.productModelVisible = true;
this.callback = () => {
this.$refs.upload.$el.querySelector('input').click();
};
} else {
this.$refs.upload.$el.querySelector('input').click();
}
},
async getProductModels() {
const params = {
pageSize: 999,
showSenior: true,
};
const res = await listShortProduct(params);
if (res.code === 200) {
this.prodcutModels = res.data || [];
}
},
closeProductModelDialog() {
this.productModelVisible = false;
this.productId = '';
},
submitProdcutModel() {
if (!this.productId) {
this.$message.warning('请选择产品后再确认');
return;
}
this.callback && this.callback(this.productId);
},
},
};
</script>
<style lang="scss" scoped>
.system-app-lang {
padding: 20px;
.search-card {
margin-bottom: 15px;
padding: 3px 0;
}
.search-form {
margin-bottom: -22.5px;
}
}
</style>

View File

@@ -0,0 +1,95 @@
import JsZip from 'jszip';
import { saveAs } from 'file-saver';
/**
* 根据提供的JSON 数据生成 JSON 格式的 zip 文件列表。
* @param {Object} jsonData - 包含模块和其对应多语言数据的对象。
* @returns {Array} files - 包含每个文件信息的数组,每个文件信息包含文件夹名、文件名、文件类型和文件数据。
*/
export const generateJsonZipFiles = (jsonData) => {
const files = [];
// 遍历 jsonData 中的每个模块,为每个模块和语言组合生成一个文件对象
for (const [module, objects] of jsonData.entries()) {
for (const [lang, jsonObject] of Object.entries(objects)) {
// 将生成的文件信息添加到 files 数组中
files.push({
folderName: lang, //文件夹名
fileName: module, // 文件名
fileType: 'json', // 文件类型
fileData: convertToJsonBlob(jsonObject), // 文件数据
});
}
}
return files;
};
/**
* 解析JSON Zip数据
* @param jsonMap 包含模块名对应数据的JSON映射对象
* @param langFileNameEnum 语言文件名枚举对象,将实际语言文件名映射到枚举值
* @returns 返回一个Map对象其中键为模块名值为一个对象该对象的键为语言文件名根据langFileNameEnum转换值为对应语言文件的内容键为文件中的键值为文件中的值
*/
export const parseJsonZipData = (jsonMap, langFileNameEnum) => {
const dataMap = new Map();
for (const [module, rows] of jsonMap.entries()) {
const fileObjects = {};
const cellIndexMap = {};
rows.forEach((row, rowNumber) => {
if (rowNumber === 0) {
row.forEach((cell, colNumber) => {
if (colNumber !== 0) {
fileObjects[langFileNameEnum[cell]] = {};
cellIndexMap[colNumber] = langFileNameEnum[cell];
}
});
} else {
Object.keys(cellIndexMap)
.map((item) => Number(item))
.map((colNumber) => {
const key = row[0];
const value = row[colNumber];
const lang = cellIndexMap[colNumber];
fileObjects[lang][key] = value;
});
}
});
dataMap.set(module, fileObjects);
}
return dataMap;
};
export const convertToJsonBlob = (jsonObject) => {
const jsonString = JSON.stringify(jsonObject, null, 2);
return new Blob([jsonString], { type: 'application/json' });
};
/**
* 将多个文件下载并打包成一个 zip 文件。
* @param {Object} params 参数对象,包含待下载文件信息和 zip 文件名。
* @param {Array} params.files 待下载的文件数组。
* @param {string} params.zipName 打包后的 zip 文件名。
*/
export function downloadFiles2Zip(params) {
const zip = new JsZip();
// 待每个文件都写入完之后再生成 zip 文件
params.files.map((file) => handleEachFile(file, zip));
zip.generateAsync({ type: 'blob' }).then((blob) => {
saveAs(blob, `${params.zipName}.zip`);
});
}
/**
* 处理每个文件将其添加到zip文件中。
* @param {Object} param0 包含文件相关信息的对象
* @param {string} param0.folderName 文件所属的文件夹名称(可选)
* @param {string} param0.fileName 文件名
* @param {string} param0.fileType 文件类型
* @param {Blob|Uint8Array} param0.fileData 文件数据
* @param {JSZip} zip JSZip对象用于构建zip文件
*/
export const handleEachFile = ({ folderName, fileName, fileType, fileData }, zip) => {
if (folderName) {
zip.folder(folderName)?.file(`${fileName}.${fileType}`, fileData);
} else {
zip.file(`${filename}.${fileType}`, blob);
}
};

View File

@@ -0,0 +1,41 @@
// 获取语言包数据, new Map({fileName,{key:{ [lang]:value}}})
export const getLangJson = () => {
const all = require.context('@/lang', true, /^((?!index).)*\.json$/);
const modules = new Map();
all.keys().forEach((key) => {
const lang = key.match(/(?<=\/)[^\/]+(?=\/|$)/)[0];
const fileName = key.match(/(?<=\.\/[^\/]+\/)[^\.]+(?=\.\w+$)/)[0];
const obj = modules.get(fileName) || {};
Object.entries(all(key)).forEach(([key, value]) => {
const item = obj[key] || {};
item[lang] = value;
obj[key] = item;
});
modules.set(fileName, obj);
});
return modules;
};
// 转换为excel数据: [sheeNames:表,sheeData:单表数据]
export const transoformToExcel = (array2, langs) => {
const sheetNames = [];
const sheetData = [];
for (const [sheetName, arr] of array2.entries()) {
sheetNames.push(sheetName);
sheetData.push(transformToExcelSheet(arr, langs));
}
return [sheetNames, sheetData];
};
// 转换为excel单表数据[{'键值':'key','中文':'中文','英文':'English'}]
export const transformToExcelSheet = (jsonData, langs) => {
let data = [];
for (const [key, obj] of Object.entries(jsonData)) {
let item = { 键值: key };
for (const [lang, label] of Object.entries(langs)) {
item[label] = obj[lang] || '';
}
data.push(item);
}
return data;
};

View File

@@ -0,0 +1,98 @@
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
/**
* 导出excel
*/
export const exportExcel = ([sheetNames, sheetsData], filename = '多语言包.xlsx') => {
// 创建workbook对象
let wb = XLSX.utils.book_new();
sheetNames.forEach((sheetName, index) => {
const sheeData = sheetsData[index];
// 把json转为worksheet对象
let ws = XLSX.utils.json_to_sheet(sheeData);
// 计算每列最大字符数作为列宽
const colWidths = getColumnWidths(sheeData);
// 设置列宽
setColumnWidths(ws, colWidths);
// 添加worksheet 到 workbook
XLSX.utils.book_append_sheet(wb, ws, sheetName);
});
// 写出 arraybuffer 数据
let wb_out = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
// 构建Blob对象
let _blob = new Blob([wb_out], { type: 'application/octet-stream' });
//下载
saveAs(_blob, filename);
};
/**
* 获取表格列的宽度数组。
*/
export const getColumnWidths = (data) => {
const headers = Object.keys(data[0]);
// 计算列宽
const colWidths = [];
for (let i = 0; i < headers.length; i++) {
let maxWidth = getCellWidth(headers[i]);
let key = headers[i];
let cellWidth = 0;
for (let j = 0; j < data.length; j++) {
cellWidth = getCellWidth(data[j][key]);
if (data[j][key] && cellWidth > maxWidth) {
maxWidth = cellWidth;
}
}
colWidths.push(maxWidth);
}
return colWidths;
};
/**
* 计算单元格宽度
*/
const getCellWidth = (value) => {
if (/.*[\u4e00-\u9fa5]+.*$/.test(value)) {
return parseFloat(value.toString().length * 2.1);
} else {
return parseFloat(value.toString().length * 1.1);
}
};
/**
* 设置工作表的列宽
*/
export const setColumnWidths = (ws, columnWidths) => {
ws['!cols'] = columnWidths.map((_, i) => ({
wch: columnWidths[i] || 30,
}));
};
/**
* 解析文件为 JSON
* @param file 文件
* @returns 解析后的 JSON 数据
*/
export const parseJson = async (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const jsonMap = new Map();
for (let index = 0; index < workbook.SheetNames.length; index++) {
const sheetName = workbook.SheetNames[index];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
jsonMap.set(sheetName, jsonData);
}
resolve(jsonMap);
} catch (err) {
reject(err);
}
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
reader.readAsArrayBuffer(file);
});
};