Compare commits

..

8 Commits

Author SHA1 Message Date
067889f096
Merge pull request 'dev' (#7) from dev into master
All checks were successful
Gitea Actions Build / Build (push) Successful in 1m40s
Reviewed-on: #7
2025-07-01 23:27:46 +08:00
953a370233
完善翻译 2025-07-01 23:27:03 +08:00
7f8defeee2
test gpg 2025-07-01 22:24:18 +08:00
67d99ccd42 no message 2025-07-01 22:10:15 +08:00
2a6672613a
no message
All checks were successful
Gitea Actions Build / Build (push) Successful in 1m23s
2025-07-01 08:03:23 +08:00
3071859a3d
no message 2025-07-01 10:25:43 +08:00
570c41bc11
Merge pull request '添加 i18n 重写API界面' (#6) from dev into master
All checks were successful
Gitea Actions Build / Build (push) Successful in 1m25s
Reviewed-on: #6
2025-07-01 00:19:07 +08:00
dac6928844
添加 i18n 重写API界面 2025-07-01 00:18:18 +08:00
25 changed files with 501 additions and 63 deletions

View File

@ -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",
"pattern": "^[a-z]{2}-[a-z]{2}$"
"description": "语言ID",
"pattern": "^[a-z]{2}-[A-Z]{2}$"
},
"file": {
"type": "string",
"description": "Language file name",
"pattern": "^[A-Z]{2}_[A-Z]{2}\\.json$"
"description": "语言文件在language文件夹的位置",
"pattern": "^[a-z]{2}-[A-Z]{2}\\.json$"
},
"icon": {
"type": "string",
"description": "语言的图标",
"pattern": "^.*:.*$"
}
},
"required": [
"title",
"id",
"file"
"file",
"icon"
],
"additionalProperties": false
}

View File

@ -9,6 +9,7 @@
"build-jar-auto": "run-p \"build-only\" && gradle -Dorg.gradle.java.home=/opt/jdk/21.0.7/ build-jar",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-preview": "run-p build-only && vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
@ -18,6 +19,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",
@ -26,7 +28,7 @@
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vue": "^3.5.17",
"vue-i18n": "12.0.0-alpha.2",
"vue-i18n": "11",
"vue-router": "^4.5.1"
},
"devDependencies": {
@ -42,10 +44,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",

View File

@ -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() {

View File

@ -1,5 +1,6 @@
package com.mingliqiye.disk.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.InputStream;
import org.springframework.http.ResponseEntity;
@ -12,7 +13,7 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
@RequestMapping
@Tag(name = "前端路由", description = "统一匹配路径指向VueRouter")
public class IndexController {
@Operation(summary = "VueRouter 主路由")
@GetMapping(value = { "/", "/{path:^(?!static|apis|blob).*$}/**" })
public ResponseEntity<StreamingResponseBody> index() {
StreamingResponseBody streamingResponseBody = s -> {

View 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;
}
}

View File

@ -46,6 +46,7 @@ export default defineConfig({
},
server: {
host: '0.0.0.0',
port: 5174,
proxy: {
'/apis': 'http://localhost:9963',
},

View File

@ -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>

17
web-src/assets/index.sass Normal file
View File

@ -0,0 +1,17 @@
:where(a)
color: #4990e2
text-decoration: none
.n-collapse-item__header
padding-top: 0 !important
.n-collapse-item__header-main
padding-left: 10px !important
.n-collapse-item__content-wrapper
border-top: 1px solid var(--color)
html
scroll-padding-top: 70px
scroll-behavior: smooth

View File

@ -9,11 +9,13 @@
html {
color: #3c3c3c;
--text-color: 60, 60, 60;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
html.dark {
color: #d5d5d5;
--text-color: 213, 213, 213;
}
.swagger-ui .info {

View File

@ -14,3 +14,5 @@
*[f] {
display: flex;
}

View File

@ -0,0 +1,21 @@
<template>
<n-code :code="recode" :language="language" show-line-numbers style="width: fit-content !important; padding: 10px" />
</template>
<script lang="ts" setup>
const recode = computed(() => {
if (typeof prop.code === 'object') {
return JSON.stringify(prop.code, null, 2);
} else {
return prop.code;
}
});
const prop = defineProps({
code: {
type: [String, Object],
required: true,
},
language: String,
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,15 +1,30 @@
{
"$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"
"title": "zh-CN 简体中文",
"id": "zh-CN",
"file": "zh-CN.json",
"icon": "emojione-v1:flag-for-china"
},
{
"title": "US-English",
"id": "en-us",
"file": "EN_US.json"
"title": "zh-TW 简体中文",
"id": "zh-TW",
"file": "zh-TW.json",
"icon": "emojione-v1:flag-for-china"
},
{
"title": "en-US English",
"id": "en-US",
"file": "en-US.json",
"icon": "emojione-v1:flag-for-united-states"
},
{
"title": "ja-JP 日本語",
"id": "ja-JP",
"file": "ja-JP.json",
"icon": "emojione-v1:flag-for-japan"
}
]
}

View File

@ -0,0 +1,35 @@
{
"message": {
"login": {
"plaselogin": "Plase login."
}
},
"nav": {
"title": {
"api": "API",
"file": "file",
"home": "Home",
"setting": "setting",
"user": "user"
}
},
"setting": {
"language": {
"title": "language"
},
"theme": {
"auto": "auto",
"dark": "dark",
"light": "light",
"title": "theme"
},
"title": "setting"
},
"view": {
"login": {
"password": "password",
"title": "login",
"username": "username"
}
}
}

View File

@ -0,0 +1,35 @@
{
"message": {
"login": {
"plaselogin": "ログインしてください"
}
},
"nav": {
"title": {
"api": "API",
"file": "ファイル",
"home": "ホーム",
"setting": "設定",
"user": "ユーザー"
}
},
"setting": {
"language": {
"title": "言語設定"
},
"theme": {
"auto": "自動",
"dark": "ダークモード",
"light": "ライトモード",
"title": "テーマ設定"
},
"title": "設定"
},
"view": {
"login": {
"password": "パスワード",
"title": "ログイン",
"username": "ユーザー名"
}
}
}

View File

@ -0,0 +1,35 @@
{
"message": {
"login": {
"plaselogin": "请登陆"
}
},
"nav": {
"title": {
"api": "API",
"file": "文件",
"home": "主页",
"setting": "设置",
"user": "用户"
}
},
"setting": {
"language": {
"title": "语言配置"
},
"theme": {
"auto": "自动",
"dark": "暗色",
"light": "亮色",
"title": "主题配置"
},
"title": "设置"
},
"view": {
"login": {
"password": "密码",
"title": "登陆",
"username": "用户名"
}
}
}

View File

@ -0,0 +1,35 @@
{
"message": {
"login": {
"plaselogin": "請登入"
}
},
"nav": {
"title": {
"api": "API",
"file": "檔案",
"home": "首頁",
"setting": "設定",
"user": "使用者"
}
},
"setting": {
"language": {
"title": "語言設定"
},
"theme": {
"auto": "自動",
"dark": "深色",
"light": "淺色",
"title": "主題設定"
},
"title": "設定"
},
"view": {
"login": {
"password": "密碼",
"title": "登入",
"username": "使用者名稱"
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<nav class="navmain">
<router-link :to="{ name: 'home' }" c-c f style="color: inherit; height: 100%; text-decoration: none">
<img height="50" src="@/assets/index.svg" width="50" />
<img alt="@/assets/index.svg" height="50" src="@/assets/index.svg" width="50" />
<div>
<label style="font-size: 20px; margin-left: 10px; cursor: pointer">{{ useSettingStore.appName }}</label>
<label style="margin-left: 5px; font-size: 12px; cursor: pointer">V{{ useSettingStore.appVersion }}</label>
@ -16,7 +16,7 @@
}"
@click="router.push({ name: 'home' })">
<Icon icon="material-symbols:home" />
主页
{{ t('nav.title.home') }}
<div class="after"></div>
</div>
<div
@ -26,7 +26,7 @@
}"
@click="openFile()">
<Icon icon="material-symbols:folder-rounded" />
文件
{{ t('nav.title.file') }}
<div class="after"></div>
</div>
<div
@ -36,7 +36,7 @@
}"
@click="router.push({ name: 'user' })">
<Icon icon="material-symbols:person-rounded" />
用户
{{ t('nav.title.user') }}
<div class="after"></div>
</div>
<div
@ -46,7 +46,7 @@
}"
@click="router.push({ name: 'api' })">
<icon icon="mdi:api" />
API
{{ t('nav.title.api') }}
<div class="after"></div>
</div>
<div
@ -55,22 +55,30 @@
}"
@click="activeSetting.on()">
<Icon icon="material-symbols:settings" />
设置
{{ t('nav.title.setting') }}
<div class="after"></div>
</div>
</div>
</nav>
<nav class="nav">
<n-drawer v-model:show="activeSetting.value" placement="right">
<n-drawer-content title="设置">
<n-drawer v-model:show="activeSetting.value" placement="right" width="auto">
<n-drawer-content :title="t('setting.title')">
<n-divider>{{ t('setting.theme.title') }}</n-divider>
<div c-c f>
<n-radio-group v-model:value="useSettingStore.themeMode" name="team">
<n-radio-button value="light">亮色</n-radio-button>
<n-radio-button value="dark">暗色</n-radio-button>
<n-radio-button value="auto">自动</n-radio-button>
<n-radio-button value="light">{{ t('setting.theme.light') }}</n-radio-button>
<n-radio-button value="dark">{{ t('setting.theme.dark') }}</n-radio-button>
<n-radio-button value="auto">{{ t('setting.theme.auto') }}</n-radio-button>
</n-radio-group>
</div>
<n-divider>主题配置</n-divider>
<n-divider>{{ t('setting.language.title') }}</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,18 +148,22 @@ getSysInfo().then((r) => {
});
useAuthStore.woIsMe();
useSettingStore.setLanguage(null);
function openFile() {
if (useAuthStore.isLogin) {
router.push({
path: `/file/${useAuthStore.username}`,
});
} else {
message.error('请先登录');
message.error(() => t('message.login.plaselogin'));
router.push({
path: `/login`,
});
}
}
console.log(t);
</script>
<style lang="scss" scoped>
html.dark {
@ -182,7 +242,7 @@ html.dark {
backdrop-filter: blur(5px);
height: 65px;
width: calc(100% - 40px);
z-index: 1;
z-index: 5;
padding: 0 20px;
display: flex;

View File

@ -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>

View File

@ -1,13 +1,15 @@
import './assets/index.scss';
import './assets/index.sass';
import { createApp } from 'vue';
import App from './App.vue';
import { pinia, router } from './plugin';
import { i18n, installI18n, pinia, router } from './plugin';
const app = createApp(App);
app.use(pinia);
app.use(i18n);
installI18n().then(() => {
app.use(router);
app.mount('#app');
});

View File

@ -1,12 +1,58 @@
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 const i18n = createI18n({
legacy: false,
locale: 'zh-cn',
fallbackLocale: 'zh-cn',
messages,
});
export default i18n;
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(`lang/${i.file}`);
}
}
export function t(d: string): string {
return i18n.global.t(d);
}

View 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[];
}

View File

@ -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;

View File

@ -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,
},
});

View File

@ -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,23 @@ 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;
document.documentElement.lang = this.language;
return;
}
this.language = data;
i18n.global.locale.value = data;
document.documentElement.lang = data;
},
},
});

View File

@ -6,20 +6,22 @@
content: true,
footer: 'soft',
}"
style="width: 430px"
title="登录">
:title="t('view.login.title')"
style="width: 430px">
<n-form border label-align="right" label-placement="left" label-width="auto">
<n-form-item label="用户名">
<n-form-item :label="t('view.login.username')">
<n-input />
</n-form-item>
<n-form-item label="密码">
<n-form-item :label="t('view.login.password')">
<n-input />
</n-form-item>
</n-form>
<n-button block secondary strong type="primary">登录</n-button>
<n-button block secondary strong type="primary">{{ t('view.login.title') }}</n-button>
</n-card>
</n-spin>
</div>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
import { t } from '@/plugin';
</script>
<style scoped></style>