diff --git a/src/api/i18n.ts b/src/api/i18n.ts new file mode 100644 index 0000000..06f970a --- /dev/null +++ b/src/api/i18n.ts @@ -0,0 +1,201 @@ +import { http } from "@/utils/http"; +import type { LanguageInfo, TranslationInfo } from "types/i18n"; + +/** + * 添加语言类型参数 + */ +type AddLocaleParams = { + /**编码 */ + code: string; + /**名称 */ + name: string; +}; + +/** + * 添加语言类型 + * @param data + * @returns + */ +export const postAddLocaleAPI = (data: AddLocaleParams) => { + return http.request("post", "/api/i18n/addLocale", { + data + }); +}; + +/** + * 删除语言类型 + * @param id + * @returns + */ +export const deleteLocaleAPI = (id: string) => { + return http.request("post", `/api/i18n/deleteLocale/${id}`); +}; + +/** + * 修改语言类型 + */ + +export const putUpdateLocaleAPI = (data: AddLocaleParams, id: string) => { + return http.request("post", `/api/i18n/updateLocale/${id}`, { + data + }); +}; + +/** + * 获取语言类型信息 + */ +export const getLocaleInfoAPI = (id: string) => { + return http.request("get", `/api/i18n/locale/info/${id}`); +}; + +type GetLoacleListParams = { + /**页码 */ + page: number; + /**每页条数 */ + pageSize: number; + /**语言名称 */ + name?: string; + /**语言编码 */ + code?: string; +}; + +type GetLocaleListResult = { + /**语言列表 */ + result: LanguageInfo[]; + /**总条数 */ + total: number; + /**页码 */ + page: number; +}; +export const getLocaleListAPI = (params: GetLoacleListParams) => { + return http.request("get", "/api/i18n/locale/list", { + params + }); +}; + +/** + * 添加翻译 + */ +type AddI18nParams = { + /**键值 */ + key: string; + /**翻译内容 */ + translation: string; + /**语言ID */ + locale_id: string; +}; + +/** + * 添加翻译 + * @param data + * @returns + */ +export const postAddI18nAPI = (data: AddI18nParams) => { + return http.request("post", "/api/i18n/addI18n", { + data + }); +}; + +/** + * 获取翻译列表参数 + */ +type GetI18nListParams = { + /**页码 */ + page: number; + /**每页条数 */ + pageSize: number; + /**语言ID */ + locale_id?: string; + /**键值 */ + key?: string; + /**翻译内容 */ + translation?: string; +}; +/** + * 获取翻译列表 + */ +type GetI18nListResult = { + /**翻译列表 */ + result: TranslationInfo[]; + /**总条数 */ + total: number; + /**页码 */ + page: number; +}; + +/** + * 获取翻译列表 + * @param params + * @returns + */ +export const getI18nListAPI = (params: GetI18nListParams) => { + return http.request("get", "/api/i18n/list", { + params + }); +}; + +/** + * 获取翻译详情 + */ +export const getI18nInfoAPI = (id: string) => { + return http.request("get", `/api/i18n/info/${id}`); +}; + +/** + * 删除翻译 + * @param id + * @returns + */ +export const deleteI18nAPI = (id: string) => { + return http.request("post", `/api/i18n/deleteI18n/${id}`); +}; +/** + * 修改翻译 + * @param data + * @param id + * @returns + */ +export const putUpdateI18nAPI = (data: AddI18nParams, id: string) => { + return http.request("post", `/api/i18n/updateI18n/${id}`, { + data + }); +}; + +/** + * 获取国际化处理列表结果 + */ +type GetI18nHandleListResult = { + /** + * 翻译列表 + */ + data: object; + /** + * 名称 + */ + name: string; + /** + * 编码 + */ + locale: string; +}; + +/** + * 获取国际化处理列表 + * @param id + * @returns + */ +export const getI18nHandleListAPI = (id: string) => { + return http.request( + "get", + `/api/i18n/infoList/${id}` + ); +}; + +/** + * 获取国际化数据 + * @param locale 语言代码 + * @returns 国际化数据 + */ +export const getLocaleI18nAPI = (locale: string) => { + return http.request>("get", `/api/i18n/data/${locale}`); +}; diff --git a/src/router/utils.ts b/src/router/utils.ts index ab60a48..63d7c73 100644 --- a/src/router/utils.ts +++ b/src/router/utils.ts @@ -299,35 +299,83 @@ function handleAliveRoute({ name }: ToRouteType, mode?: string) { } } -/** 过滤后端传来的动态路由 重新生成规范路由 */ +/** + * 过滤后端传来的动态路由,重新生成规范路由 + */ function addAsyncRoutes(arrRoutes: Array) { if (!arrRoutes || !arrRoutes.length) return; + const modulesRoutesKeys = Object.keys(modulesRoutes); + arrRoutes.forEach((v: RouteRecordRaw) => { - // 将backstage属性加入meta,标识此路由为后端返回路由 + // 将 backstage 属性加入 meta,标识此路由为后端返回路由 v.meta.backstage = true; - // 父级的redirect属性取值:如果子级存在且父级的redirect属性不存在,默认取第一个子级的path;如果子级存在且父级的redirect属性存在,取存在的redirect属性,会覆盖默认值 - if (v?.children && v.children.length && !v.redirect) + // v.meta.title = transformI18n(v.meta.title); + + // 处理父级路由的 redirect 属性 + if (v?.children && v.children.length && !v.redirect) { v.redirect = v.children[0].path; - // 父级的name属性取值:如果子级存在且父级的name属性不存在,默认取第一个子级的name;如果子级存在且父级的name属性存在,取存在的name属性,会覆盖默认值(注意:测试中发现父级的name不能和子级name重复,如果重复会造成重定向无效(跳转404),所以这里给父级的name起名的时候后面会自动加上`Parent`,避免重复) - if (v?.children && v.children.length && !v.name) + } + + // 处理父级路由的 name 属性 + if (v?.children && v.children.length && !v.name) { v.name = (v.children[0].name as string) + "Parent"; + } + + // 处理 iframe 路由 if (v.meta?.frameSrc) { v.component = IFrame; + } else if (v.component) { + // 如果路由有 component 参数,直接加载对应的组件 + const index = modulesRoutesKeys.findIndex(ev => + ev.includes(v.component as any) + ); + if (index !== -1) { + v.component = modulesRoutes[modulesRoutesKeys[index]]; + } else { + console.warn(`未找到 ${v.component} 对应的组件文件`); + return; // 跳过无效路由 + } + } else if (v?.children && v.children.length) { + // 如果是一级菜单(没有 component),跳过组件加载逻辑 + console.log(`一级菜单 ${v.path} 不需要组件`); } else { - // 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会跟path保持一致) - const index = v?.component - ? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any)) - : modulesRoutesKeys.findIndex(ev => ev.includes(v.path)); - v.component = modulesRoutes[modulesRoutesKeys[index]]; + // 如果路由没有 component 参数,尝试加载文件夹下的第一个 .vue 文件 + const componentPath = findFirstVueFile(v.path); + if (componentPath) { + v.component = modulesRoutes[componentPath]; + } else { + console.warn(`未找到 ${v.path} 对应的组件文件`); + return; // 跳过无效路由 + } } + + // 递归处理子路由 if (v?.children && v.children.length) { addAsyncRoutes(v.children); } }); + return arrRoutes; } +/** + * 从文件夹中查找第一个 .vue 文件 + */ +function findFirstVueFile(folderPath: string): string | null { + const files = import.meta.glob("/src/views/**/*.vue"); // 匹配文件夹下的 .vue 文件 + const filePaths = Object.keys(files); + + // 查找与 folderPath 匹配的第一个 .vue 文件 + for (const filePath of filePaths) { + if (filePath.includes(`/src/views/${folderPath}/`)) { + return filePath; + } + } + + return null; // 未找到 .vue 文件 +} + /** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */ function getHistoryMode(routerHistory): RouterHistory { // len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1 diff --git a/src/views/system/i18n/components/form.vue b/src/views/system/i18n/components/form.vue new file mode 100644 index 0000000..232a6ae --- /dev/null +++ b/src/views/system/i18n/components/form.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/views/system/i18n/hook.tsx b/src/views/system/i18n/hook.tsx new file mode 100644 index 0000000..35ff347 --- /dev/null +++ b/src/views/system/i18n/hook.tsx @@ -0,0 +1,278 @@ +import dayjs from "dayjs"; +import editForm from "./components/form.vue"; +import { message } from "@/utils/message"; +import { type Ref, ref, reactive, onMounted, h } from "vue"; +import type { LanguageInfo, TranslationInfo } from "types/i18n"; +import type { PaginationProps } from "@pureadmin/table"; +import { addDialog } from "@/components/ReDialog"; +import { + deleteI18nAPI, + getI18nListAPI, + getLocaleListAPI, + postAddI18nAPI, + putUpdateI18nAPI +} from "@/api/i18n"; + +export const useI18n = (tableRef: Ref) => { + /** + * 查询表单 + */ + const form = reactive({ + key: "", + locale_id: "", + translation: "" + }); + /** + * 表单Ref + */ + const formRef = ref(null); + /** + * 数据列表 + */ + const dataList = ref([]); + /** + * 加载状态 + */ + const loading = ref(true); + /** + * 已选数量 + */ + const selectedNum = ref(0); + /** + * 分页参数 + */ + const pagination = reactive({ + total: 0, + pageSize: 10, + currentPage: 1, + background: true + }); + /** + * 表格列设置 + */ + const columns: TableColumnList = [ + { + label: "勾选列", // 如果需要表格多选,此处label必须设置 + type: "selection", + fixed: "left", + reserveSelection: true // 数据刷新后保留选项 + }, + { + label: "国际化key", + prop: "key" + }, + { + label: "国际化值", + prop: "translation" + }, + { + label: "语言编码", + prop: "locale_code" + }, + { + label: "语言名称", + prop: "locale_name" + }, + { + label: "创建时间", + prop: "create_time", + formatter: ({ create_time }) => + dayjs(create_time).format("YYYY-MM-DD HH:mm:ss") + }, + { + label: "操作", + fixed: "right", + width: 220, + slot: "operation" + } + ]; + + /** + * 初次查询 + */ + const onSearch = async () => { + loading.value = true; + const res = await getI18nListAPI({ + page: pagination.currentPage, + pageSize: pagination.pageSize, + key: form.key, + locale_id: form.locale_id, + translation: form.translation + }); + if (res.success) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + loading.value = false; + }; + /** + * 重置表单 + * @param formEl 表单ref + * @returns + */ + const resetForm = (formEl: any) => { + if (!formEl) return; + formEl.resetFields(); + onSearch(); + }; + /** + * 处理删除 + * @param row + */ + const handleDelete = async (row: TranslationInfo) => { + const res = await deleteI18nAPI(row.id); + if (res.success) { + onSearch(); + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + }; + /** + * 处理每页数量变化 + */ + const handleSizeChange = async (val: number) => { + const res = await getI18nListAPI({ + page: pagination.currentPage, + pageSize: val, + key: form.key, + locale_id: form.locale_id, + translation: form.translation + }); + if (res.success) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + }; + + /** + * 处理页码变化 + * @param val + */ + const handleCurrentChange = async (val: number) => { + const res = await getI18nListAPI({ + page: val, + pageSize: pagination.pageSize, + key: form.key, + locale_id: form.locale_id, + translation: form.translation + }); + if (res.code === 200) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + }; + /** 当CheckBox选择项发生变化时会触发该事件 */ + const handleSelectionChange = async (val: any) => { + selectedNum.value = val.length; + // 重置表格高度 + tableRef.value.setAdaptive(); + }; + + /** 取消选择 */ + const onSelectionCancel = async () => { + selectedNum.value = 0; + // 用于多选表格,清空用户的选择 + tableRef.value.getTableRef().clearSelection(); + }; + + const openDialog = async (title = "新增", row?: TranslationInfo) => { + addDialog({ + title: `${title}国际化项`, + props: { + formInline: { + title: title, + key: row?.key ?? "", + locale_id: row?.locale_id ?? "", + translation: row?.translation ?? "" + } + }, + width: "45%", + draggable: true, + fullscreenIcon: true, + closeOnClickModal: false, + contentRenderer: () => h(editForm, { ref: formRef }), + beforeSure: async (done, {}) => { + const FormData = formRef.value.newFormInline; + let form = { + key: FormData.key ?? "", + locale_id: FormData.locale_id ?? "", + translation: FormData.translation ?? "" + }; + if (title === "新增") { + const res = await postAddI18nAPI(form); + if (res.success) { + done(); + await onSearch(); + } + message(res.msg, { type: res.success ? "success" : "error" }); + } else { + const res = await putUpdateI18nAPI(form, row.id); + if (res.success) { + done(); + await onSearch(); + } + message(res.msg, { type: res.success ? "success" : "error" }); + } + } + }); + }; + + /**语言类型 */ + const localeList = ref([]); + + /** + * 获取语言类型 + */ + const getLocaleList = async () => { + const res = await getLocaleListAPI({ + page: 1, + pageSize: 9999 + }); + if (res.success) { + localeList.value = res.data.result; + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + }; + /** + * 页面加载执行 + */ + onMounted(async () => { + await onSearch(); + await getLocaleList(); + }); + + return { + form, + formRef, + dataList, + loading, + pagination, + columns, + selectedNum, + localeList, + onSearch, + openDialog, + resetForm, + handleDelete, + handleSizeChange, + handleCurrentChange, + handleSelectionChange, + onSelectionCancel, + getLocaleList + }; +}; diff --git a/src/views/system/i18n/index.vue b/src/views/system/i18n/index.vue new file mode 100644 index 0000000..0e41a38 --- /dev/null +++ b/src/views/system/i18n/index.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/src/views/system/language/components/form.vue b/src/views/system/language/components/form.vue new file mode 100644 index 0000000..f4674ea --- /dev/null +++ b/src/views/system/language/components/form.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/views/system/language/hook.tsx b/src/views/system/language/hook.tsx new file mode 100644 index 0000000..9eb9612 --- /dev/null +++ b/src/views/system/language/hook.tsx @@ -0,0 +1,273 @@ +import dayjs from "dayjs"; +import editForm from "./components/form.vue"; +import { message } from "@/utils/message"; +import { type Ref, ref, reactive, onMounted, h } from "vue"; +import type { LanguageInfo, TranslationInfo } from "types/i18n"; +import type { PaginationProps } from "@pureadmin/table"; +import { addDialog } from "@/components/ReDialog"; +import { + getLocaleListAPI, + deleteLocaleAPI, + postAddLocaleAPI, + putUpdateLocaleAPI, + getI18nHandleListAPI +} from "@/api/i18n"; + +import jsyaml from "js-yaml"; + +export const useLocale = (tableRef: Ref) => { + /** + * 查询表单 + */ + const form = reactive({ + name: "", + code: "" + }); + /** + * 表单Ref + */ + const formRef = ref(null); + /** + * 数据列表 + */ + const dataList = ref([]); + /** + * 加载状态 + */ + const loading = ref(true); + /** + * 已选数量 + */ + const selectedNum = ref(0); + /** + * 分页参数 + */ + const pagination = reactive({ + total: 0, + pageSize: 10, + currentPage: 1, + background: true + }); + /** + * 表格列设置 + */ + const columns: TableColumnList = [ + { + label: "勾选列", // 如果需要表格多选,此处label必须设置 + type: "selection", + fixed: "left", + reserveSelection: true // 数据刷新后保留选项 + }, + { + label: "语言编码", + prop: "code" + }, + { + label: "语言名称", + prop: "name" + }, + { + label: "创建时间", + prop: "create_time", + formatter: ({ create_time }) => + dayjs(create_time).format("YYYY-MM-DD HH:mm:ss") + }, + { + label: "操作", + fixed: "right", + width: 200, + slot: "operation" + } + ]; + + /** + * 初次查询 + */ + const onSearch = async () => { + loading.value = true; + const res = await getLocaleListAPI({ + page: pagination.currentPage, + pageSize: pagination.pageSize, + name: form.name, + code: form.code + }); + if (res.success) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + loading.value = false; + }; + /** + * 重置表单 + * @param formEl 表单ref + * @returns + */ + const resetForm = (formEl: any) => { + if (!formEl) return; + formEl.resetFields(); + onSearch(); + }; + /** + * 处理删除 + * @param row + */ + const handleDelete = async (row: TranslationInfo) => { + const res = await deleteLocaleAPI(row.id); + if (res.success) { + onSearch(); + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + }; + /** + * 处理每页数量变化 + */ + const handleSizeChange = async (val: number) => { + const res = await getLocaleListAPI({ + page: pagination.currentPage, + pageSize: val, + name: form.name, + code: form.code + }); + if (res.success) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + }; + + /** + * 处理页码变化 + * @param val + */ + const handleCurrentChange = async (val: number) => { + const res = await getLocaleListAPI({ + page: val, + pageSize: pagination.pageSize, + name: form.name, + code: form.code + }); + if (res.code === 200) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + message(res.msg, { + type: res.success ? "success" : "error" + }); + }; + /** 当CheckBox选择项发生变化时会触发该事件 */ + const handleSelectionChange = async (val: any) => { + selectedNum.value = val.length; + // 重置表格高度 + tableRef.value.setAdaptive(); + }; + + /** 取消选择 */ + const onSelectionCancel = async () => { + selectedNum.value = 0; + // 用于多选表格,清空用户的选择 + tableRef.value.getTableRef().clearSelection(); + }; + + const openDialog = async (title = "新增", row?: LanguageInfo) => { + addDialog({ + title: `${title}国际化项`, + props: { + formInline: { + title: title, + name: row?.name ?? "", + code: row?.code ?? "" + } + }, + width: "45%", + draggable: true, + fullscreenIcon: true, + closeOnClickModal: false, + contentRenderer: () => h(editForm, { ref: formRef }), + beforeSure: async (done, {}) => { + const FormData = formRef.value.newFormInline; + let form = { + name: FormData.name ?? "", + code: FormData.code ?? "" + }; + if (title === "新增") { + const res = await postAddLocaleAPI(form); + if (res.success) { + done(); + await onSearch(); + } + message(res.msg, { type: res.success ? "success" : "error" }); + } else { + const res = await putUpdateLocaleAPI(form, row.id); + if (res.success) { + done(); + await onSearch(); + } + message(res.msg, { type: res.success ? "success" : "error" }); + } + } + }); + }; + + /** + * 导出 YAML 文件 + */ + const export_to_yaml = async (row: LanguageInfo) => { + const res = await getI18nHandleListAPI(row.id); // 调用 API 获取数据 + if (res.success) { + // 将 JSON 转换为 YAML + const yamlString = jsyaml.dump(res.data.data); + + // 创建 Blob 对象 + const blob = new Blob([yamlString], { type: "text/yaml" }); + + // 生成下载链接 + const url = URL.createObjectURL(blob); + + // 创建 元素并触发下载 + const link = document.createElement("a"); + link.href = url; + link.download = `${row.code}.yaml`; // 设置下载文件名 + document.body.appendChild(link); // 将 元素添加到 DOM 中 + link.click(); // 模拟点击下载 + + // 清理 URL 对象 + URL.revokeObjectURL(url); + document.body.removeChild(link); // 移除 元素 + } + }; + /** + * 页面加载执行 + */ + onMounted(async () => { + await onSearch(); + }); + + return { + form, + formRef, + dataList, + loading, + pagination, + columns, + selectedNum, + onSearch, + openDialog, + resetForm, + export_to_yaml, + handleDelete, + handleSizeChange, + handleCurrentChange, + handleSelectionChange, + onSelectionCancel + }; +}; diff --git a/src/views/system/language/index.vue b/src/views/system/language/index.vue new file mode 100644 index 0000000..e2983c1 --- /dev/null +++ b/src/views/system/language/index.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/types/i18n.d.ts b/types/i18n.d.ts new file mode 100644 index 0000000..e5e6e35 --- /dev/null +++ b/types/i18n.d.ts @@ -0,0 +1,40 @@ +/** 语言信息类型 */ +export type LanguageInfo = { + /** 语言ID */ + id: string; + /** 语言代码 */ + code: string; + /** 语言名称 */ + name: string; + /** 创建时间 */ + create_time: string; + /** 更新时间 */ + update_time: string; + /** 创建人 */ + create_by: string; + /** 更新人 */ + update_by: string; +}; +/** 翻译信息类型 */ +export type TranslationInfo = { + /** 翻译记录ID */ + id: string; + /** 键值 */ + key: string; + /** 翻译内容 */ + translation: string; + /** 语言ID */ + locale_id: string; + /** 语言代码 */ + locale_code: string; + /** 语言名称 */ + locale_name: string; + /** 创建时间 */ + create_time: string; + /** 修改时间 */ + update_time: string; + /** 创建人 */ + create_by: string; + /** 修改人 */ + update_by: string; +};