Merge pull request '添加 i18n 重写API界面' (#6) from dev into master
All checks were successful
Gitea Actions Build / Build (push) Successful in 1m25s
All checks were successful
Gitea Actions Build / Build (push) Successful in 1m25s
Reviewed-on: #6
This commit is contained in:
commit
570c41bc11
@ -5,6 +5,12 @@
|
||||
"description": "Schema for validating language configuration files",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"iconsUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"languages": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
@ -13,24 +19,30 @@
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Display name of the language",
|
||||
"pattern": "^[\\w\\s-]+$"
|
||||
"description": "显示的语言名",
|
||||
"pattern": "^[\\p{L}\\p{N}\\s-]+$"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Language identifier in locale format",
|
||||
"description": "语言ID",
|
||||
"pattern": "^[a-z]{2}-[a-z]{2}$"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "Language file name",
|
||||
"description": "语言文件在language文件夹的位置",
|
||||
"pattern": "^[A-Z]{2}_[A-Z]{2}\\.json$"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "语言的图标",
|
||||
"pattern": "^.*:.*$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"id",
|
||||
"file"
|
||||
"file",
|
||||
"icon"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@intlify/vue-i18n-core": "^11.1.7",
|
||||
"@types/vue-router": "^2.0.0",
|
||||
"@vueuse/core": "^13.4.0",
|
||||
"naive-ui": "^2.42.0",
|
||||
@ -42,10 +43,12 @@
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-oxlint": "~1.1.0",
|
||||
"eslint-plugin-vue": "~10.2.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jiti": "^2.4.2",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-cookie": "^3.0.5",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"openapi-types": "^12.1.3",
|
||||
"oxlint": "~1.1.0",
|
||||
"pinia-plugin-persistedstate": "^4.4.0",
|
||||
"prettier": "3.5.3",
|
||||
|
@ -11,6 +11,7 @@ import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@ -42,16 +43,9 @@ public class SpringDocConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
Server server = new Server();
|
||||
server.setUrl("/");
|
||||
server.setDescription("当前服务");
|
||||
Server server2 = new Server();
|
||||
server2.setUrl("http://localhost:" + config.getPort() + "/");
|
||||
server2.setDescription("本机服务");
|
||||
return new OpenAPI()
|
||||
// 配置接口文档基本信息
|
||||
.info(this.getApiInfo())
|
||||
.servers(List.of(server, server2));
|
||||
List<Server> servers = new ArrayList<>();
|
||||
servers.add(new Server().description("当前网页").url("/"));
|
||||
return new OpenAPI().info(this.getApiInfo()).servers(servers);
|
||||
}
|
||||
|
||||
private Info getApiInfo() {
|
||||
|
@ -3,7 +3,6 @@ package com.mingliqiye.disk.controller;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.io.InputStream;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
@ -13,7 +12,7 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
|
||||
@Tag(name = "前端路由", description = "统一匹配路径指向VueRouter")
|
||||
public class IndexController {
|
||||
|
||||
@GetMapping(value = { "/", "/{path:^(?!static|apis|blob).*$}/**" })
|
||||
@RequestMapping(value = { "/", "/{path:^(?!static|apis|blob).*$}/**" })
|
||||
public ResponseEntity<StreamingResponseBody> index() {
|
||||
StreamingResponseBody streamingResponseBody = s -> {
|
||||
try (InputStream stream = this.getClass().getResourceAsStream("/html/index.html")) {
|
||||
|
78
src/main/java/com/mingliqiye/disk/util/LocalHostUtil.java
Normal file
78
src/main/java/com/mingliqiye/disk/util/LocalHostUtil.java
Normal file
@ -0,0 +1,78 @@
|
||||
package com.mingliqiye.disk.util;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 本地主机工具类
|
||||
*
|
||||
* @author MingliPro
|
||||
* @since 2019年11月13日09:04:36
|
||||
*/
|
||||
public class LocalHostUtil {
|
||||
|
||||
/**
|
||||
* 获取主机名称
|
||||
*
|
||||
* @return
|
||||
* @throws UnknownHostException
|
||||
*/
|
||||
public static String getHostName() throws UnknownHostException {
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统首选IP
|
||||
*
|
||||
* @return
|
||||
* @throws UnknownHostException
|
||||
*/
|
||||
public static String getLocalIP() throws UnknownHostException {
|
||||
return InetAddress.getLocalHost().getHostAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有网卡IP,排除回文地址、虚拟地址
|
||||
*
|
||||
* @return
|
||||
* @throws SocketException
|
||||
*/
|
||||
public static String[] getLocalIPs() throws SocketException {
|
||||
List<String> list = new ArrayList<>();
|
||||
Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces();
|
||||
while (enumeration.hasMoreElements()) {
|
||||
NetworkInterface intf = enumeration.nextElement();
|
||||
if (intf.isLoopback() || intf.isVirtual()) { //
|
||||
continue;
|
||||
}
|
||||
Enumeration<InetAddress> inets = intf.getInetAddresses();
|
||||
while (inets.hasMoreElements()) {
|
||||
InetAddress addr = inets.nextElement();
|
||||
if (addr.isLoopbackAddress() || !addr.isSiteLocalAddress() || addr.isAnyLocalAddress()) {
|
||||
continue;
|
||||
}
|
||||
list.add(addr.getHostAddress());
|
||||
}
|
||||
}
|
||||
return list.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断操作系统是否是Windows
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static boolean isWindowsOS() {
|
||||
boolean isWindowsOS = false;
|
||||
String osName = System.getProperty("os.name");
|
||||
if (osName.toLowerCase().contains("windows")) {
|
||||
isWindowsOS = true;
|
||||
}
|
||||
return isWindowsOS;
|
||||
}
|
||||
}
|
@ -46,6 +46,7 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5174,
|
||||
proxy: {
|
||||
'/apis': 'http://localhost:9963',
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-config-provider :theme="useSettingStore.theme === 'dark' ? darkTheme : lightTheme" abstract>
|
||||
<n-config-provider :hljs="hljs" :theme="useSettingStore.theme === 'dark' ? darkTheme : lightTheme" abstract>
|
||||
<n-loading-bar-provider>
|
||||
<n-message-provider>
|
||||
<Index />
|
||||
@ -11,8 +11,15 @@
|
||||
<script lang="ts" setup>
|
||||
import Index from '@/layout/index.vue';
|
||||
import { darkTheme, lightTheme } from 'naive-ui';
|
||||
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import { UseSettingStore } from '@/plugin';
|
||||
|
||||
hljs.registerLanguage('json', json);
|
||||
hljs.registerLanguage('typescript', typescript);
|
||||
hljs.registerLanguage('javascript', javascript);
|
||||
|
||||
const useSettingStore = UseSettingStore();
|
||||
</script>
|
||||
|
16
web-src/assets/index.sass
Normal file
16
web-src/assets/index.sass
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
:where(a)
|
||||
color: #4990e2
|
||||
text-decoration: none
|
||||
|
||||
|
||||
.n-collapse-item__header
|
||||
padding-top: 0 !important
|
||||
padding-left: 10px !important
|
||||
|
||||
.n-collapse-item__content-wrapper
|
||||
border-top: 1px solid var(--color)
|
||||
|
||||
html
|
||||
scroll-padding-top: 70px
|
||||
scroll-behavior: smooth
|
@ -14,3 +14,5 @@
|
||||
*[f] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
|
25
web-src/components/MCode.vue
Normal file
25
web-src/components/MCode.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-code :code="recode" :language="language" show-line-numbers />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const recode = ref('');
|
||||
|
||||
const prop = defineProps({
|
||||
code: {
|
||||
type: [String, Object],
|
||||
required: true,
|
||||
},
|
||||
language: String,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof prop.code === 'object') {
|
||||
recode.value = JSON.stringify(prop.code, null, 2);
|
||||
} else {
|
||||
recode.value = prop.code;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
181
web-src/components/OpenApiDoc.vue
Normal file
181
web-src/components/OpenApiDoc.vue
Normal file
@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-tabs v-model:value="tabValue" :bar-width="28" placement="left" style="height: 100%" type="line">
|
||||
<n-tab-pane name="api信息" tab="api信息">
|
||||
<NCard>
|
||||
<div class="title">
|
||||
<label class="lable">{{ data?.info.title }}</label>
|
||||
<n-tag :bordered="false" style="font-weight: 900" type="success">V{{ data?.info.version }}</n-tag>
|
||||
<n-tag :bordered="false" style="font-weight: 900" type="info">openapi-{{ data?.openapi }}</n-tag>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px">
|
||||
<a :href="apiUrl">{{ apiUrl }}</a>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px">{{ data?.info.summary }}</div>
|
||||
<div style="margin-bottom: 20px">{{ data?.info.description }}</div>
|
||||
<div><a :href="data?.info.termsOfService">Terms of service</a></div>
|
||||
<div>
|
||||
<a :href="data?.info.contact?.url">{{ data?.info.contact?.name }} - Website</a>
|
||||
</div>
|
||||
<div>
|
||||
<a :href="data?.info.contact?.url">Send email to {{ data?.info.contact?.name }}</a>
|
||||
</div>
|
||||
<div>
|
||||
<a :href="data?.info.license?.url">{{ data?.info.license?.name }}</a>
|
||||
</div>
|
||||
<div>
|
||||
<a :href="data?.externalDocs?.url">{{ data?.externalDocs?.description }}</a>
|
||||
</div>
|
||||
</NCard>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-for="(v, k) in tags" :name="k" :tab="k">
|
||||
<NCard
|
||||
:segmented="{
|
||||
content: true,
|
||||
footer: 'soft',
|
||||
}">
|
||||
<template #header>
|
||||
<div>
|
||||
<label style="font-weight: 700; font-size: 27px">{{ k }}</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-weight: 700; font-size: 20px">{{ v.description }}</label>
|
||||
</div>
|
||||
</template>
|
||||
<n-collapse accordion default-expanded-names="1">
|
||||
<template v-for="(v2, k2) in v.data">
|
||||
<template v-for="(v3, k3) in v2">
|
||||
<OpenApiDocMethod :id="`${k}+${k3}-${k2}`" :type="k3" :url="k2" />
|
||||
</template>
|
||||
</template>
|
||||
</n-collapse>
|
||||
</NCard>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="架构" tab="架构">
|
||||
<NCard
|
||||
:segmented="{
|
||||
content: true,
|
||||
footer: 'soft',
|
||||
}">
|
||||
<template #header>
|
||||
<div>
|
||||
<label style="font-weight: 700; font-size: 27px">架构</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-weight: 700; font-size: 20px">一些类型而已</label>
|
||||
</div>
|
||||
</template>
|
||||
<n-collapse accordion default-expanded-names="2">
|
||||
<OpenApiDocSchema v-for="(v, k) in data?.components?.schemas" :data="v" :name="k" />
|
||||
</n-collapse>
|
||||
</NCard>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="权限验证" tab="权限验证">
|
||||
<NCard
|
||||
:segmented="{
|
||||
content: true,
|
||||
footer: 'soft',
|
||||
}">
|
||||
<template #header>
|
||||
<div>
|
||||
<label style="font-weight: 700; font-size: 27px">权限验证</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-weight: 700; font-size: 20px">验证你的权限</label>
|
||||
</div>
|
||||
</template>
|
||||
<n-collapse accordion default-expanded-names="3">
|
||||
<OpenApiDocSecuritySchemes v-for="(v, k) in data?.components?.securitySchemes" :data="v" :name="k" />
|
||||
</n-collapse>
|
||||
</NCard>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, type PropType, ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import type { OpenAPIV3_1 } from 'openapi-types';
|
||||
import OpenApiDocMethod from '@/components/OpenApiDocMethod.vue';
|
||||
import { router } from '@/plugin';
|
||||
import type { RouteLocationNormalizedGeneric } from 'vue-router';
|
||||
import OpenApiDocSchema from '@/components/OpenApiDocSchema.vue';
|
||||
import OpenApiDocSecuritySchemes from '@/components/OpenApiDocSecuritySchemes.vue';
|
||||
|
||||
const data = ref<OpenAPIV3_1.Document>();
|
||||
const tags = ref<{
|
||||
[key: string]: { data: { [key: string]: OpenAPIV3_1.PathItemObject }; description: string };
|
||||
}>({});
|
||||
export type TFunctionType = (args: string) => string;
|
||||
export type getApiFunType = (args: string) => PromiseLike<string>;
|
||||
|
||||
onMounted(() => {
|
||||
prop
|
||||
.getApiFun(prop.apiUrl)
|
||||
.then((res) => {
|
||||
data.value = JSON.parse(res);
|
||||
return data.value;
|
||||
})
|
||||
.then((res) => {
|
||||
for (const pathsKey in res?.paths) {
|
||||
const models = res?.paths[pathsKey];
|
||||
for (const model in models) {
|
||||
for (const i of models[model]?.tags) {
|
||||
if (!tags.value[i]) {
|
||||
tags.value[i] = { data: {}, description: data.value?.tags?.find((is) => is.name === i)?.description };
|
||||
}
|
||||
tags.value[i].data[pathsKey] = models;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
hashback(router.currentRoute.value);
|
||||
});
|
||||
});
|
||||
|
||||
function hashback(to: RouteLocationNormalizedGeneric) {
|
||||
if (!to.hash) return;
|
||||
const tab = to.hash.split('+')?.[0].replace('#', '');
|
||||
setTimeout(() => {
|
||||
tabValue.value = tab;
|
||||
document.getElementById(to.hash.replace('#', ''))?.scrollIntoView();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
const removeAfterEach = router.afterEach(hashback);
|
||||
|
||||
onUnmounted(() => removeAfterEach());
|
||||
|
||||
const tabValue = ref<string>('api信息');
|
||||
|
||||
const prop = defineProps({
|
||||
apiUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
getApiFun: {
|
||||
type: Function as PropType<getApiFunType>,
|
||||
default: async (url: string) => (await fetch(url)).text(),
|
||||
},
|
||||
i18nFun: {
|
||||
type: Function as PropType<TFunctionType>,
|
||||
default: (s: string) => s,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lable {
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
</style>
|
83
web-src/components/OpenApiDocMethod.vue
Normal file
83
web-src/components/OpenApiDocMethod.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<n-collapse-item
|
||||
:name="`${url}-${type}`"
|
||||
:title="url"
|
||||
v-bind:class="{
|
||||
get: type === 'get',
|
||||
put: type === 'put',
|
||||
post: type === 'post',
|
||||
delete: type === 'delete',
|
||||
options: type === 'options',
|
||||
head: type === 'head',
|
||||
patch: type === 'patch',
|
||||
}">
|
||||
<template #header>
|
||||
<div f n-c style="padding: 10px; height: calc(100% - 10px); display: flex; gap: 20px">
|
||||
<div
|
||||
c-c
|
||||
f
|
||||
style="
|
||||
width: 80px;
|
||||
height: 30px;
|
||||
background: var(--color);
|
||||
border-radius: 2px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
">
|
||||
{{ type.toUpperCase() }}
|
||||
</div>
|
||||
|
||||
<label style="font-weight: 700">{{ url }}</label>
|
||||
</div>
|
||||
</template>
|
||||
<div></div>
|
||||
</n-collapse-item>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const prop = defineProps({
|
||||
type: {
|
||||
type: String as PropType<'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace' | 'connect'>,
|
||||
default: 'get',
|
||||
},
|
||||
url: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@mixin method-style($color) {
|
||||
background: rgba($color, 0.1);
|
||||
border-radius: 3px;
|
||||
border: 1px solid $color !important;
|
||||
--color: #{$color};
|
||||
}
|
||||
|
||||
.get {
|
||||
@include method-style(#61affe);
|
||||
}
|
||||
|
||||
.put {
|
||||
@include method-style(#fca130ff);
|
||||
}
|
||||
|
||||
.post {
|
||||
@include method-style(#49cc90);
|
||||
}
|
||||
|
||||
.delete {
|
||||
@include method-style(#f93e3e);
|
||||
}
|
||||
|
||||
.options {
|
||||
@include method-style(#0d5aa7);
|
||||
}
|
||||
|
||||
.head {
|
||||
@include method-style(#9012fe);
|
||||
}
|
||||
|
||||
.patch {
|
||||
@include method-style(#50e3c2);
|
||||
}
|
||||
</style>
|
30
web-src/components/OpenApiDocSchema.vue
Normal file
30
web-src/components/OpenApiDocSchema.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<n-collapse-item :name="name" :title="name" class="datas">
|
||||
<template #header>
|
||||
<div f n-c style="padding: 10px; height: calc(100% - 10px); display: flex; gap: 20px">
|
||||
<label style="font-weight: 700">{{ name }}</label>
|
||||
</div>
|
||||
</template>
|
||||
<div></div>
|
||||
</n-collapse-item>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@mixin method-style($color) {
|
||||
background: rgba($color, 0.1);
|
||||
border-radius: 3px;
|
||||
border: 1px solid $color !important;
|
||||
--color: #{$color};
|
||||
}
|
||||
|
||||
.datas {
|
||||
@include method-style(#4182b8);
|
||||
}
|
||||
</style>
|
30
web-src/components/OpenApiDocSecuritySchemes.vue
Normal file
30
web-src/components/OpenApiDocSecuritySchemes.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<n-collapse-item :name="name" :title="name" class="datas">
|
||||
<template #header>
|
||||
<div f n-c style="padding: 10px; height: calc(100% - 10px); display: flex; gap: 20px">
|
||||
<label style="font-weight: 700">{{ name }}</label>
|
||||
</div>
|
||||
</template>
|
||||
<div></div>
|
||||
</n-collapse-item>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@mixin method-style($color) {
|
||||
background: rgba($color, 0.1);
|
||||
border-radius: 3px;
|
||||
border: 1px solid $color !important;
|
||||
--color: #{$color};
|
||||
}
|
||||
|
||||
.datas {
|
||||
@include method-style(#ac44af);
|
||||
}
|
||||
</style>
|
7
web-src/language/EN_US.json
Normal file
7
web-src/language/EN_US.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"nav": {
|
||||
"title": {
|
||||
"home": "Home"
|
||||
}
|
||||
}
|
||||
}
|
7
web-src/language/ZH_CN.json
Normal file
7
web-src/language/ZH_CN.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"nav": {
|
||||
"title": {
|
||||
"home": "主页"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,18 @@
|
||||
{
|
||||
"$schema": "https://git.mingliqiye.com/mingliqiye/pan-disk/raw/branch/master/languageSchema.json",
|
||||
"$schema": "../../languageSchema.json",
|
||||
"iconsUrl": "https://icones.js.org/collection/emojione-v1?category=Flags",
|
||||
"languages": [
|
||||
{
|
||||
"title": "中国-汉语",
|
||||
"id": "zh-cn",
|
||||
"file": "ZH_CN.json"
|
||||
"file": "ZH_CN.json",
|
||||
"icon": "emojione-v1:flag-for-china"
|
||||
},
|
||||
{
|
||||
"title": "US-English",
|
||||
"id": "en-us",
|
||||
"file": "EN_US.json"
|
||||
"file": "EN_US.json",
|
||||
"icon": "emojione-v1:flag-for-united-states"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
}"
|
||||
@click="router.push({ name: 'home' })">
|
||||
<Icon icon="material-symbols:home" />
|
||||
主页
|
||||
{{ t('nav.title.home') }}
|
||||
<div class="after"></div>
|
||||
</div>
|
||||
<div
|
||||
@ -63,6 +63,7 @@
|
||||
<nav class="nav">
|
||||
<n-drawer v-model:show="activeSetting.value" placement="right">
|
||||
<n-drawer-content title="设置">
|
||||
<n-divider>主题配置</n-divider>
|
||||
<div c-c f>
|
||||
<n-radio-group v-model:value="useSettingStore.themeMode" name="team">
|
||||
<n-radio-button value="light">亮色</n-radio-button>
|
||||
@ -70,7 +71,14 @@
|
||||
<n-radio-button value="auto">自动</n-radio-button>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
<n-divider>主题配置</n-divider>
|
||||
<n-divider>语言配置</n-divider>
|
||||
<n-select
|
||||
:options="messageData"
|
||||
:render-label="renderLabel"
|
||||
:render-tag="renderMultipleSelectTag"
|
||||
:value="useSettingStore.language"
|
||||
filterable
|
||||
@update-value="useSettingStore.setLanguage"></n-select>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</nav>
|
||||
@ -78,8 +86,56 @@
|
||||
<script lang="ts" setup>
|
||||
import Icon from '@/components/Icon.vue';
|
||||
import { UseBoolRef } from '@/util';
|
||||
import { message, router, UseAuthStore, UseSettingStore } from '@/plugin';
|
||||
import {
|
||||
type languageIndexItemValueType,
|
||||
message,
|
||||
messageData,
|
||||
router,
|
||||
t,
|
||||
UseAuthStore,
|
||||
UseSettingStore,
|
||||
} from '@/plugin';
|
||||
import { getSysInfo } from '@/api/system.ts';
|
||||
import type { SelectOption } from 'naive-ui/es/select/src/interface';
|
||||
import type { VNodeChild } from 'vue';
|
||||
|
||||
export type RenderTag = (
|
||||
props: {
|
||||
option: SelectOption;
|
||||
handleClose: () => void;
|
||||
} & languageIndexItemValueType,
|
||||
) => VNodeChild;
|
||||
|
||||
export type RenderTagSelect = (props: {
|
||||
option: SelectOption & languageIndexItemValueType;
|
||||
handleClose: () => void;
|
||||
}) => VNodeChild;
|
||||
const renderLabel: RenderTag = (data) => {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
f: '',
|
||||
'n-c': '',
|
||||
style: {
|
||||
gap: '10px',
|
||||
},
|
||||
},
|
||||
[h(Icon, { icon: data.icon }), data.title],
|
||||
);
|
||||
};
|
||||
const renderMultipleSelectTag: RenderTagSelect = ({ option: data }) => {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
f: '',
|
||||
'n-c': '',
|
||||
style: {
|
||||
gap: '10px',
|
||||
},
|
||||
},
|
||||
[h(Icon, { icon: data.icon }), data.title],
|
||||
);
|
||||
};
|
||||
|
||||
const useSettingStore = UseSettingStore();
|
||||
const activeSetting = UseBoolRef();
|
||||
@ -92,6 +148,8 @@ getSysInfo().then((r) => {
|
||||
});
|
||||
useAuthStore.woIsMe();
|
||||
|
||||
useSettingStore.setLanguage(null);
|
||||
|
||||
function openFile() {
|
||||
if (useAuthStore.isLogin) {
|
||||
router.push({
|
||||
@ -182,7 +240,7 @@ html.dark {
|
||||
backdrop-filter: blur(5px);
|
||||
height: 65px;
|
||||
width: calc(100% - 40px);
|
||||
z-index: 1;
|
||||
z-index: 5;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Head />
|
||||
<n-back-top />
|
||||
<router-view v-slot="{ Component }">
|
||||
<div style="margin: 10px">
|
||||
<div style="margin: 10px; height: calc(100% - 66px - 20px)">
|
||||
<component :is="Component" />
|
||||
</div>
|
||||
</router-view>
|
||||
|
@ -1,13 +1,16 @@
|
||||
import './assets/index.scss';
|
||||
import './assets/index.sass';
|
||||
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import App from './App.vue';
|
||||
import { pinia, router } from './plugin';
|
||||
import { installI18n, pinia, router } from './plugin';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
|
||||
app.mount('#app');
|
||||
installI18n().then((i18n) => {
|
||||
const app = createApp(App);
|
||||
app.use(i18n);
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
console.log(i18n);
|
||||
});
|
||||
|
@ -1,12 +1,72 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import type { languageIndexItemValueType, languageIndexType, languageType } from './type';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export type languageType = { [key: string]: string | languageType };
|
||||
const messages: languageType | any = {};
|
||||
export * from './type';
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API
|
||||
locale: 'zh', // 默认语言
|
||||
const languagedatas = import.meta.glob('@/language/*.json');
|
||||
|
||||
const loadIndexFile = async (): Promise<languageIndexType> => {
|
||||
try {
|
||||
const module = (await languagedatas['/language/index.json']()) as {
|
||||
default: languageIndexType;
|
||||
};
|
||||
return module.default;
|
||||
} catch (error) {
|
||||
console.error('加载语言索引文件失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const loadLanguageFile = async (fileName: string): Promise<languageType> => {
|
||||
try {
|
||||
const module = (await languagedatas[`/language/${fileName}`]()) as { default: languageType };
|
||||
return module.default;
|
||||
} catch (error) {
|
||||
console.error(`加载语言文件失败:${fileName}`, error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const messages: languageType | any = {};
|
||||
|
||||
export const messageData = ref<languageIndexItemValueType[]>([]);
|
||||
|
||||
export let i18n: ReturnType<
|
||||
typeof createI18n<
|
||||
{
|
||||
legacy: boolean;
|
||||
locale: string;
|
||||
fallbackLocale: string;
|
||||
messages: any;
|
||||
},
|
||||
any,
|
||||
any
|
||||
>
|
||||
>;
|
||||
|
||||
export function t(data: string): string {
|
||||
return i18n.global.t(data);
|
||||
}
|
||||
|
||||
export async function installI18n() {
|
||||
const data: languageIndexType = await loadIndexFile();
|
||||
for (const i of data.languages) {
|
||||
messageData.value.push({
|
||||
title: i.title,
|
||||
value: i.id,
|
||||
icon: i.icon,
|
||||
});
|
||||
messages[i.id] = await loadLanguageFile(i.file);
|
||||
}
|
||||
console.log(messages);
|
||||
console.log(languagedatas);
|
||||
console.log(messageData);
|
||||
i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'zh-cn',
|
||||
fallbackLocale: 'zh-cn',
|
||||
messages,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
});
|
||||
return i18n;
|
||||
}
|
||||
|
18
web-src/plugin/i18n/type.ts
Normal file
18
web-src/plugin/i18n/type.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export type languageType = { [key: string]: string | languageType };
|
||||
|
||||
export interface languageIndexItemType {
|
||||
title: string;
|
||||
id: string;
|
||||
file: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface languageIndexItemValueType {
|
||||
title: string;
|
||||
value: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface languageIndexType {
|
||||
languages: languageIndexItemType[];
|
||||
}
|
@ -4,6 +4,7 @@ import type { LoadingBarApiInjection } from 'naive-ui/es/loading-bar/src/Loading
|
||||
export * from './router';
|
||||
export * from './stores';
|
||||
export * from './alova';
|
||||
export * from './i18n';
|
||||
|
||||
export let message: MessageApiInjection;
|
||||
export let loadingBar: LoadingBarApiInjection;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { login, logout, woIsMe } from '@/api';
|
||||
import { LocalStorageApi } from '@/util';
|
||||
import CookiesApi from '@/util/Cookies.ts';
|
||||
|
||||
export const UseAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
@ -44,6 +44,6 @@ export const UseAuthStore = defineStore('auth', {
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
storage: LocalStorageApi.StorageApi,
|
||||
storage: CookiesApi.StorageApi,
|
||||
},
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { useColorMode } from '@vueuse/core';
|
||||
import { i18n } from '@/plugin';
|
||||
import { LocalStorageApi } from '@/util/Cookies.ts';
|
||||
|
||||
export const UseSettingStore = defineStore('setting', {
|
||||
@ -11,9 +12,21 @@ export const UseSettingStore = defineStore('setting', {
|
||||
theme: state,
|
||||
appName: '',
|
||||
appVersion: '',
|
||||
language: 'zh-cn',
|
||||
};
|
||||
},
|
||||
persist: {
|
||||
storage: LocalStorageApi.StorageApi,
|
||||
pick: ['themeMode', 'appName', 'appVersion', 'language'],
|
||||
},
|
||||
actions: {
|
||||
setLanguage(data: string | null) {
|
||||
if (data == null) {
|
||||
i18n.global.locale.value = this.language;
|
||||
return;
|
||||
}
|
||||
this.language = data;
|
||||
i18n.global.locale.value = data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,18 +1,7 @@
|
||||
<template>
|
||||
<div id="swaggerContainer"></div>
|
||||
<OpenApiDoc api-url="/apis/swagger/api.json" style="margin: 20px; height: calc(100% - 30px)" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import 'swagger-ui-dist/swagger-ui.css';
|
||||
import '@/assets/SwaggerDark.scss';
|
||||
import { onMounted } from 'vue';
|
||||
import { SwaggerUIBundle } from 'swagger-ui-dist';
|
||||
|
||||
onMounted(() => {
|
||||
SwaggerUIBundle({
|
||||
dom_id: '#swaggerContainer',
|
||||
url: '/apis/swagger/api.json',
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
|
||||
});
|
||||
});
|
||||
import OpenApiDoc from '@/components/OpenApiDoc.vue';
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user