Compare commits

...

12 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
c51326b698
Merge pull request '添加语言 languageSchema' (#5) from dev into master
All checks were successful
Gitea Actions Build / Build (push) Successful in 1m34s
Reviewed-on: #5
2025-06-30 10:11:00 +08:00
ddaa02da3d
添加语言 languageSchema 2025-06-30 10:10:25 +08:00
704da3a692
Merge pull request '编写一些前端' (#4) from dev into master
All checks were successful
Gitea Actions Build / Build (push) Successful in 1m29s
Reviewed-on: #4
2025-06-29 17:54:30 +08:00
10580a0d63
编写一些前端 2025-06-29 17:52:31 +08:00
71 changed files with 2911 additions and 595 deletions

5
.gitignore vendored
View File

@ -16,9 +16,9 @@ build/
bin/ bin/
!**/src/main/**/bin/ !**/src/main/**/bin/
!**/src/test/**/bin/ !**/src/test/**/bin/
pnpm-workspace.yaml
### IntelliJ IDEA ### ### IntelliJ IDEA ###
.idea
*.iws *.iws
*.iml *.iml
*.ipr *.ipr
@ -41,3 +41,6 @@ pnpm-lock.yaml
src/main/resources/html src/main/resources/html
log log
web-src/types
.idea

View File

@ -1,7 +1,7 @@
config: config:
port: 9963 port: 9963
app-name: maven-repository app-name: pan-disk
app-version: 1.0 app-version: 1.1
data-source: data-source:
mysql: mysql:
username: pan username: pan

55
languageSchema.json Normal file
View File

@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/language-config.schema.json",
"title": "Language Configuration",
"description": "Schema for validating language configuration files",
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"iconsUrl": {
"type": "string"
},
"languages": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "显示的语言名",
"pattern": "^[\\p{L}\\p{N}\\s-]+$"
},
"id": {
"type": "string",
"description": "语言ID",
"pattern": "^[a-z]{2}-[A-Z]{2}$"
},
"file": {
"type": "string",
"description": "语言文件在language文件夹的位置",
"pattern": "^[a-z]{2}-[A-Z]{2}\\.json$"
},
"icon": {
"type": "string",
"description": "语言的图标",
"pattern": "^.*:.*$"
}
},
"required": [
"title",
"id",
"file",
"icon"
],
"additionalProperties": false
}
}
},
"required": [
"languages"
],
"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-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 {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"build-preview": "run-p build-only && vite preview",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore", "lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
@ -17,27 +18,45 @@
"format": "prettier --write \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,astro,java}\"" "format": "prettier --write \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,astro,java}\""
}, },
"dependencies": { "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",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"swagger-ui-dist": "^5.25.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-i18n": "11",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "^22.0.2", "@tsconfig/node22": "^22.0.2",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.15.32", "@types/node": "^22.15.32",
"@types/swagger-ui-dist": "^3.30.6",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.1", "@vue/eslint-config-typescript": "^14.5.1",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"alova": "^3.3.3",
"eslint": "^9.29.0", "eslint": "^9.29.0",
"eslint-plugin-oxlint": "~1.1.0", "eslint-plugin-oxlint": "~1.1.0",
"eslint-plugin-vue": "~10.2.0", "eslint-plugin-vue": "~10.2.0",
"highlight.js": "^11.11.1",
"jiti": "^2.4.2", "jiti": "^2.4.2",
"js-base64": "^3.7.7",
"js-cookie": "^3.0.5",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"openapi-types": "^12.1.3",
"oxlint": "~1.1.0", "oxlint": "~1.1.0",
"pinia-plugin-persistedstate": "^4.4.0",
"prettier": "3.5.3", "prettier": "3.5.3",
"prettier-plugin-java": "^2.6.8", "prettier-plugin-java": "^2.6.8",
"sass-embedded": "^1.89.2",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"vite": "npm:rolldown-vite@latest", "vite": "~6.0.0",
"vite-plugin-vue-devtools": "^7.7.7", "vite-plugin-vue-devtools": "^7.7.7",
"vue-tsc": "^2.2.10" "vue-tsc": "^2.2.10"
} }

View File

@ -0,0 +1,49 @@
package com.mingliqiye.disk.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.mingliqiye.disk.time.DateTime;
import com.mingliqiye.disk.util.PanStpUtil;
import com.mingliqiye.disk.uuid.UUID;
import java.util.Objects;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusMetaObjectHandler implements MetaObjectHandler {
private static final String CREATE_USER_ID = "createUserId";
private static final String CREATE_TIME = "createTime";
private static final String UPDATE_USER_ID = "updateUserId";
private static final String UPDATE_TIME = "updateTime";
private static final UUID SYSTEM_ID_UUID = UUID.ofString("48126760-5483-11F0-B90C-6D1CD960F6F3");
@Override
public void insertFill(MetaObject metaObject) {
if (metaObject.hasSetter(CREATE_USER_ID)) {
this.strictInsertFill(
metaObject,
CREATE_USER_ID,
UUID.class,
Objects.requireNonNullElse(PanStpUtil.getLoginIdDefaultNull(), SYSTEM_ID_UUID)
);
}
if (metaObject.hasSetter(CREATE_TIME)) {
this.strictInsertFill(metaObject, CREATE_TIME, DateTime.class, DateTime.now());
}
}
@Override
public void updateFill(MetaObject metaObject) {
if (metaObject.hasSetter(UPDATE_USER_ID)) {
this.strictUpdateFill(
metaObject,
UPDATE_USER_ID,
UUID.class,
Objects.requireNonNullElse(PanStpUtil.getLoginIdDefaultNull(), SYSTEM_ID_UUID)
);
}
if (metaObject.hasSetter(UPDATE_TIME)) {
this.strictUpdateFill(metaObject, UPDATE_TIME, DateTime.class, DateTime.now());
}
}
}

View File

@ -1,5 +1,6 @@
package com.mingliqiye.disk.config; package com.mingliqiye.disk.config;
import com.mingliqiye.disk.configuration.Config;
import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
@ -9,9 +10,14 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License; 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; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Slf4j
@OpenAPIDefinition( @OpenAPIDefinition(
externalDocs = @ExternalDocumentation( externalDocs = @ExternalDocumentation(
description = "@git.mingliqiye", description = "@git.mingliqiye",
@ -29,23 +35,29 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class SpringDocConfig { public class SpringDocConfig {
private final Config config;
public SpringDocConfig(Config config) {
this.config = config;
}
@Bean @Bean
public OpenAPI openAPI() { public OpenAPI openAPI() {
return new OpenAPI() List<Server> servers = new ArrayList<>();
// 配置接口文档基本信息 servers.add(new Server().description("当前网页").url("/"));
.info(this.getApiInfo()); return new OpenAPI().info(this.getApiInfo()).servers(servers);
} }
private Info getApiInfo() { private Info getApiInfo() {
return new Info() return new Info()
.title("pan-disk") .title(config.getAppName())
.description("SpringBoot3 pan-disk Swagger3 ApiDoc") .version(config.getAppVersion())
.description("SpringBoot3 %s-V%s Swagger3 ApiDoc".formatted(config.getAppName(), config.getAppVersion()))
.contact( .contact(
new Contact().name("mingliqiye").url("https://www.mingliqiye.com").email("minglipro@mingliqiye.com") new Contact().name("mingliqiye").url("https://www.mingliqiye.com").email("minglipro@mingliqiye.com")
) )
.license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0")) .license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0"))
.summary("ApiDoc") .summary("ApiDoc")
.termsOfService("https://pan.mingliqiye.com/") .termsOfService("https://pan.mingliqiye.com/");
.version("1.0");
} }
} }

View File

@ -0,0 +1,42 @@
package com.mingliqiye.disk.config;
import cn.dev33.satoken.stp.StpInterface;
import com.mingliqiye.disk.mappers.UserMapper;
import com.mingliqiye.disk.model.User;
import com.mingliqiye.disk.uuid.UUID;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class StpInterfaceImpl implements StpInterface {
private final UserMapper userMapper;
public StpInterfaceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
User user = userMapper.selectById(UUID.ofString((String) loginId));
List<String> list = new ArrayList<>(user.getPrermissions());
if (user.isAdmin()) list.add("*.*.*");
user.getRoles().forEach(i -> list.add(String.format("%s.*.*", i)));
log.info(String.valueOf(list));
return list;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return userMapper.selectById(UUID.ofString((String) loginId)).getRoles();
}
}

View File

@ -4,7 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mingliqiye.disk.dto.auth.Login; import com.mingliqiye.disk.dto.auth.Login;
import com.mingliqiye.disk.exception.ExceptionCode; import com.mingliqiye.disk.exception.InternalServerException;
import com.mingliqiye.disk.http.Respose; import com.mingliqiye.disk.http.Respose;
import com.mingliqiye.disk.mappers.UserMapper; import com.mingliqiye.disk.mappers.UserMapper;
import com.mingliqiye.disk.model.User; import com.mingliqiye.disk.model.User;
@ -36,7 +36,7 @@ public class AuthController {
if (user != null && BCrypt.checkpw(loginBody.getPassword(), user.getPassword())) { if (user != null && BCrypt.checkpw(loginBody.getPassword(), user.getPassword())) {
return Respose.builder(PanStpUtil.login(user.getId())); return Respose.builder(PanStpUtil.login(user.getId()));
} }
return Respose.error(String.class, ExceptionCode.ERROR_INTERNAL_SERVER, "用户名或密码错误"); throw new InternalServerException("用户名或密码错误");
} }
@Operation(summary = "获取当前登陆用户的信息") @Operation(summary = "获取当前登陆用户的信息")
@ -47,8 +47,8 @@ public class AuthController {
} }
@Operation(summary = "登出", security = @SecurityRequirement(name = "Authorization-Bearer-Token")) @Operation(summary = "登出", security = @SecurityRequirement(name = "Authorization-Bearer-Token"))
@DeleteMapping("/logout")
@SaCheckLogin @SaCheckLogin
@DeleteMapping("/logout")
public Respose<Object> logout() { public Respose<Object> logout() {
StpUtil.logout(); StpUtil.logout();
return Respose.builder(); return Respose.builder();

View File

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

View File

@ -0,0 +1,28 @@
package com.mingliqiye.disk.controller;
import com.mingliqiye.disk.configuration.Config;
import com.mingliqiye.disk.dto.system.Info;
import com.mingliqiye.disk.http.Respose;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/apis/system")
@Tag(name = "系统路由", description = "访问系统的一些功能")
public class SystemController {
private final Config config;
public SystemController(Config config) {
this.config = config;
}
@Operation(summary = "系统信息")
@GetMapping("/info")
public Respose<Info> info() {
return Respose.builder(new Info(config.getAppName(), config.getAppVersion()));
}
}

View File

@ -0,0 +1,12 @@
package com.mingliqiye.disk.dto.system;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Info {
private String appName;
private String appVersion;
}

View File

@ -6,7 +6,6 @@ import cn.dev33.satoken.exception.NotRoleException;
import com.mingliqiye.disk.http.Respose; import com.mingliqiye.disk.http.Respose;
import com.mingliqiye.disk.util.StringUtil; import com.mingliqiye.disk.util.StringUtil;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -23,25 +22,21 @@ import org.springframework.web.servlet.NoHandlerFoundException;
public class BaseExceptionHandler { public class BaseExceptionHandler {
@ExceptionHandler(BaseException.class) @ExceptionHandler(BaseException.class)
public ResponseEntity<Respose<?>> exceptionHandler(BaseException e, HttpServletRequest request) { public ResponseEntity<Respose<?>> exceptionHandler(BaseException e) {
return ResponseEntity.status(e.getCode()).body( return ResponseEntity.status(e.getCode()).body(
Respose.builder().setCode(e.getCode()).setMessage(StringUtil.format("{}", e.getMessage())) Respose.builder().setCode(e.getCode()).setMessage(StringUtil.format("{}", e.getMessage()))
); );
} }
@ExceptionHandler(HttpRequestMethodNotSupportedException.class) @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Respose<?> exceptionHandler(HttpRequestMethodNotSupportedException e, HttpServletRequest request) { public Respose<?> exceptionHandler(HttpRequestMethodNotSupportedException e) {
return Respose.builder() return Respose.builder()
.setCode(ExceptionCode.ERROR_METHOD_NOT_ALLOWED.getValue()) .setCode(ExceptionCode.ERROR_METHOD_NOT_ALLOWED.getValue())
.setMessage(StringUtil.format("{} by {}", e.getMessage(), e.getClass().getName())); .setMessage(StringUtil.format("{} by {}", e.getMessage(), e.getClass().getName()));
} }
@ExceptionHandler(NoHandlerFoundException.class) @ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<Respose<?>> exceptionHandler( public ResponseEntity<Respose<?>> exceptionHandler(NoHandlerFoundException e) {
NoHandlerFoundException e,
HttpServletRequest request,
HttpServletResponse response
) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body( return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
Respose.builder() Respose.builder()
.setCode(ExceptionCode.ERROR_NOT_FOUND.getValue()) .setCode(ExceptionCode.ERROR_NOT_FOUND.getValue())
@ -50,26 +45,30 @@ public class BaseExceptionHandler {
} }
@ExceptionHandler(NotLoginException.class) @ExceptionHandler(NotLoginException.class)
public Respose<?> exceptionHandler(NotLoginException e, HttpServletRequest request) { public ResponseEntity<Respose<?>> exceptionHandler(NotLoginException e, HttpServletRequest request) {
return Respose.builder() return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
.setCode(ExceptionCode.ERROR_UNAUTHORIZED.getValue()) Respose.builder()
.setMessage(e.getMessage()) .setCode(ExceptionCode.ERROR_UNAUTHORIZED.getValue())
.setData(e.getType()); .setMessage(e.getMessage())
.setData(e.getType())
);
} }
@ExceptionHandler(HttpMessageNotReadableException.class) @ExceptionHandler(HttpMessageNotReadableException.class)
public Respose<?> exceptionHandler(HttpMessageNotReadableException e, HttpServletRequest request) { public ResponseEntity<Respose<?>> exceptionHandler(HttpMessageNotReadableException e) {
return Respose.builder() return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
.setCode(ExceptionCode.ERROR_FORBIDDEN.getValue()) Respose.builder().setCode(ExceptionCode.ERROR_FORBIDDEN.getValue()).setMessage(e.getMessage())
.setMessage(StringUtil.format("{} by {}", e.getMessage(), e.getClass().getName())); );
} }
@ExceptionHandler(NotRoleException.class) @ExceptionHandler(NotRoleException.class)
public Respose<?> exceptionHandler(NotRoleException e) { public ResponseEntity<Respose<?>> exceptionHandler(NotRoleException e) {
return Respose.builder() return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
.setCode(ExceptionCode.ERROR_FORBIDDEN.getValue()) Respose.builder()
.setMessage(e.getMessage()) .setCode(ExceptionCode.ERROR_FORBIDDEN.getValue())
.setData(e.getCode()); .setMessage(e.getMessage())
.setData(e.getCode())
);
} }
@ExceptionHandler(NotPermissionException.class) @ExceptionHandler(NotPermissionException.class)

View File

@ -1,5 +1,6 @@
package com.mingliqiye.disk.model; package com.mingliqiye.disk.model;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
@ -32,7 +33,17 @@ public class User {
private byte[] icon; private byte[] icon;
private boolean admin; private boolean admin;
private DateTime creationTime;
@TableField(fill = FieldFill.INSERT)
private UUID createUserId;
@TableField(fill = FieldFill.INSERT)
private DateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private UUID updateUserId;
@TableField(fill = FieldFill.INSERT_UPDATE)
private DateTime updateTime; private DateTime updateTime;
public User setPasswordNull() { public User setPasswordNull() {

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

@ -2,6 +2,8 @@ package com.mingliqiye.disk.util;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.mingliqiye.disk.uuid.UUID; import com.mingliqiye.disk.uuid.UUID;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PanStpUtil { public class PanStpUtil {
@ -9,7 +11,12 @@ public class PanStpUtil {
return UUID.ofString((String) StpUtil.getLoginId()); return UUID.ofString((String) StpUtil.getLoginId());
} }
public static String login(UUID uuid) { @Nullable
public static UUID getLoginIdDefaultNull() {
return UUID.ofString((String) StpUtil.getLoginId());
}
public static String login(@NotNull UUID uuid) {
StpUtil.login(uuid.toUUIDString()); StpUtil.login(uuid.toUUIDString());
return StpUtil.getTokenValue(); return StpUtil.getTokenValue();
} }

View File

@ -1,7 +1,7 @@
config: config:
port: 9963 port: 9963
app-name: maven-repository app-name: pan-disk
app-version: 1.0 app-version: 1.1
data-source: data-source:
mysql: mysql:
username: pan username: pan
@ -85,6 +85,6 @@ mybatis-plus:
springdoc: springdoc:
swagger-ui: swagger-ui:
path: /apis/swagger enabled: false
api-docs: api-docs:
path: /apis/swagger/api.json path: /apis/swagger/api.json

View File

@ -4,15 +4,25 @@ create table `users`
id binary(16) default (uuid_to_bin( id binary(16) default (uuid_to_bin(
uuid(), uuid(),
1)) primary key, 1)) primary key,
username varchar(256) unique not null, username varchar(256) unique not null,
password varchar(128) not null, password varchar(128) not null,
nickname varchar(256) not null, nickname varchar(256) not null,
prermissions json, prermissions json,
roles json, roles json,
icon mediumblob, icon mediumblob,
admin bool default false, admin bool default false,
creation_time timestamp(6) default current_timestamp(6) not null, create_user_id binary(16) not null,
update_time timestamp(6) null create_time timestamp(6) default now(6) not null,
update_user_id binary(16) null,
update_time timestamp(6) default now(6),
constraint foreign key (create_user_id) references users (id),
constraint foreign key (update_user_id) references users (id)
); );
INSERT INTO users (id, username, password, nickname, prermissions, roles, icon, admin) INSERT INTO users (id, username, password, nickname, prermissions, roles, icon, admin, create_user_id)
VALUES (0x689EFC204D1F11F08134DB0063E177A7, 'admin', 'admin', '管理员', '[]', '[]', null, 1); VALUES (0x48126760548311F0B90C6D1CD960F6F3, 'system', 'system', '系统', '[]', '[]', null, 1,
0x48126760548311F0B90C6D1CD960F6F3);
INSERT INTO users (id, username, password, nickname, prermissions, roles, icon, admin, create_user_id)
VALUES (0x689EFC204D1F11F08134DB0063E177A7, 'admin', '$2a$10$ti.W.FpQobTaW7eTTmHYJOuBS68uPP0Ikkw33UTCJSgcNhg0AGy7a',
'管理员', '[]', '[]', null, 1,
0x48126760548311F0B90C6D1CD960F6F3);

View File

@ -1,12 +1,20 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "web-src/**/*", "web-src/**/*.vue"], "include": [
"exclude": ["web-src/**/__tests__/*"], "env.d.ts",
"web-src/**/*",
"web-src/**/*.vue",
"web-src/types/**/*.d.ts"
],
"exclude": [
"web-src/**/__tests__/*"
],
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": { "paths": {
"@/*": ["./web-src/*"] "@/*": [
"./web-src/*"
]
} }
} }
} }

View File

@ -2,11 +2,32 @@ import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import vueDevTools from 'vite-plugin-vue-devtools'; import vueDevTools from 'vite-plugin-vue-devtools';
import path from 'node:path'; import path from 'node:path';
import AutoImport from 'unplugin-auto-import/vite';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
root: path.resolve(__dirname, 'web-src'), root: path.resolve(__dirname, 'web-src'),
plugins: [vue(), vueDevTools()], plugins: [
vue(),
vueDevTools(),
AutoImport({
imports: [
'vue',
'@vueuse/core',
'pinia',
'vue-router',
{
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
},
],
dts: 'types/AutoImport.d.ts',
}),
Components({
resolvers: [NaiveUiResolver()],
dts: 'types/Components.d.ts',
}),
],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, 'web-src'), '@': path.resolve(__dirname, 'web-src'),
@ -23,4 +44,11 @@ export default defineConfig({
}, },
}, },
}, },
server: {
host: '0.0.0.0',
port: 5174,
proxy: {
'/apis': 'http://localhost:9963',
},
},
}); });

View File

@ -1,85 +1,25 @@
<script lang="ts" setup>
import { RouterLink, RouterView } from 'vue-router';
import HelloWorld from './components/HelloWorld.vue';
</script>
<template> <template>
<header> <n-config-provider :hljs="hljs" :theme="useSettingStore.theme === 'dark' ? darkTheme : lightTheme" abstract>
<img alt="Vue logo" class="logo" height="125" src="@/assets/icon.svg" width="125" /> <n-loading-bar-provider>
<n-message-provider>
<div class="wrapper"> <Index />
<HelloWorld msg="You did it!" /> </n-message-provider>
</n-loading-bar-provider>
<nav> </n-config-provider>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template> </template>
<style scoped> <script lang="ts" setup>
header { import Index from '@/layout/index.vue';
line-height: 1.5; import { darkTheme, lightTheme } from 'naive-ui';
max-height: 100vh; 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';
.logo { hljs.registerLanguage('json', json);
display: block; hljs.registerLanguage('typescript', typescript);
margin: 0 auto 2rem; hljs.registerLanguage('javascript', javascript);
}
nav { const useSettingStore = UseSettingStore();
width: 100%; </script>
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>

33
web-src/api/auth.ts Normal file
View File

@ -0,0 +1,33 @@
import { alovaInstance, type AlovaResponseType } from '@/plugin';
export function login(data: loginDtoBody) {
return alovaInstance.Post<AlovaResponseType<string>>('/apis/auth/login', data);
}
export function woIsMe() {
return alovaInstance.Get<AlovaResponseType<UserDto>>('apis/auth/who-is-me');
}
export function logout() {
return alovaInstance.Delete<AlovaResponseType>('apis/auth/logout');
}
export interface loginDtoBody {
username: string;
password: string;
}
export interface UserDto {
id: string;
username: string;
password: string;
nickname: string;
prermissions: string[];
roles: string[];
icon: string;
admin: boolean;
createUserId: string;
createTime: string;
updateUserId: string;
updateTime: string;
}

1
web-src/api/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './auth';

10
web-src/api/system.ts Normal file
View File

@ -0,0 +1,10 @@
import { alovaInstance, type AlovaResponseType } from '@/plugin';
export function getSysInfo() {
return alovaInstance.Get<AlovaResponseType<SysInfo>>('apis/system/info');
}
export interface SysInfo {
appName: string;
appVersion: string;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +0,0 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

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

143
web-src/assets/index.scss Normal file
View File

@ -0,0 +1,143 @@
@use "var";
:where(*) {
padding: 0;
margin: 0;
border: 0;
transition: background-color 0.3s ease;
}
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 {
margin: 0 !important;
}
::-webkit-scrollbar {
width: 14px;
height: 14px;
}
::-webkit-scrollbar-button {
background-color: #8f8f8f !important;
}
::-webkit-scrollbar-track {
background-color: #f5f5f5 !important;
}
::-webkit-scrollbar-track-piece {
background-color: #e0e0e0 !important;
}
::-webkit-scrollbar-thumb {
height: 50px;
background-color: #bdbdbd !important;
border: 2px solid #e0e0e0 !important;
}
::-webkit-scrollbar-button:vertical:start:decrement {
background: linear-gradient(130deg, #cccccc 40%, transparent 41%),
linear-gradient(230deg, #cccccc 40%, transparent 41%),
linear-gradient(0deg, #cccccc 40%, transparent 31%);
background-color: #cccccc;
}
::-webkit-scrollbar-button:vertical:end:increment {
background: linear-gradient(310deg, #cccccc 40%, transparent 41%),
linear-gradient(50deg, #cccccc 40%, transparent 41%),
linear-gradient(180deg, #cccccc 40%, transparent 31%);
background-color: #cccccc;
}
::-webkit-scrollbar-button:horizontal:end:increment {
background: linear-gradient(210deg, #9e9e9e 40%, transparent 41%),
linear-gradient(330deg, #9e9e9e 40%, transparent 41%),
linear-gradient(90deg, #9e9e9e 30%, transparent 31%);
background-color: #ffffff;
}
::-webkit-scrollbar-button:horizontal:start:decrement {
background: linear-gradient(30deg, #9e9e9e 40%, transparent 41%),
linear-gradient(150deg, #9e9e9e 40%, transparent 41%),
linear-gradient(270deg, #9e9e9e 30%, transparent 31%);
background-color: #ffffff;
}
html.dark {
:where(body) {
background: #202020;
}
::-webkit-scrollbar {
width: 14px;
height: 14px;
}
::-webkit-scrollbar-button {
background-color: #3e4346 !important;
}
::-webkit-scrollbar-track {
background-color: #646464 !important;
}
::-webkit-scrollbar-track-piece {
background-color: #3e4346 !important;
}
::-webkit-scrollbar-thumb {
height: 50px;
background-color: #242424 !important;
border: 2px solid #3e4346 !important;
}
::-webkit-scrollbar-button:vertical:start:decrement {
background: linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%),
linear-gradient(230deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
linear-gradient(0deg, #696969 40%, rgba(0, 0, 0, 0) 31%);
background-color: #b6b6b6;
}
::-webkit-scrollbar-button:vertical:end:increment {
background: linear-gradient(310deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
linear-gradient(50deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
linear-gradient(180deg, #696969 40%, rgba(0, 0, 0, 0) 31%);
background-color: #b6b6b6;
}
::-webkit-scrollbar-button:horizontal:end:increment {
background: linear-gradient(210deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
linear-gradient(330deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
linear-gradient(90deg, #696969 30%, rgba(0, 0, 0, 0) 31%);
background-color: #b6b6b6;
}
::-webkit-scrollbar-button:horizontal:start:decrement {
background: linear-gradient(30deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
linear-gradient(150deg, #696969 40%, rgba(0, 0, 0, 0) 41%),
linear-gradient(270deg, #696969 30%, rgba(0, 0, 0, 0) 31%);
background-color: #b6b6b6;
}
}
.n-drawer-mask {
backdrop-filter: blur(2px);
}
.n-message {
padding: 8px;
}

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,35 +0,0 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

18
web-src/assets/var.scss Normal file
View File

@ -0,0 +1,18 @@
*[n-c] {
align-items: center;
}
*[c-n] {
justify-content: center;
}
*[c-c] {
justify-content: center;
align-items: center;
}
*[f] {
display: flex;
}

View File

@ -1,43 +0,0 @@
<script setup lang="ts">
defineProps<{
msg: string;
}>();
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a>
+
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>
. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="iconstyle">
<Icon :icon="icon" />
</div>
</template>
<script lang="ts" setup>
import { Icon } from '@iconify/vue';
defineProps({
icon: {
type: String,
required: true,
},
color: {
type: String,
required: false,
default: 'inherit',
},
size: {
type: String,
required: false,
default: '24px',
},
bSize: {
type: String,
required: false,
default: '24px',
},
});
</script>
<style lang="scss" scoped>
.iconstyle {
width: v-bind(bSize);
height: v-bind(bSize);
display: flex;
align-items: center;
justify-content: center;
font-size: v-bind(size);
color: v-bind(color);
}
</style>

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,97 +0,0 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue';
import DocumentationIcon from './icons/IconDocumentation.vue';
import ToolingIcon from './icons/IconTooling.vue';
import EcosystemIcon from './icons/IconEcosystem.vue';
import CommunityIcon from './icons/IconCommunity.vue';
import SupportIcon from './icons/IconSupport.vue';
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md');
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>
. The recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>
. If you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>
.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a>
.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>
,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>
,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>
, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>
. If you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener">StackOverflow</a>
. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>
.
</WelcomeItem>
</template>

View File

@ -1,87 +0,0 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -1,6 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z" />
</svg>
</template>

View File

@ -1,6 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z" />
</svg>
</template>

View File

@ -1,6 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z" />
</svg>
</template>

View File

@ -1,6 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z" />
</svg>
</template>

View File

@ -1,17 +0,0 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24">
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"></path>
</svg>
</template>

View File

@ -2,12 +2,12 @@
<html lang=""> <html lang="">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link href="./assets/icon.svg" rel="icon"> <link href="./assets/index.svg" rel="icon">
<meta content="width=device-width, initial-scale=1.0" name="viewport"> <meta content="width=device-width, initial-scale=0.8" name="viewport">
<title>Vite App</title> <title>pan-disk</title>
</head> </head>
<body> <body>
<div id="app"></div> <main id="app"></main>
<script src="/main.ts" type="module"></script>
</body> </body>
<script src="/main.ts" type="module"></script>
</html> </html>

View File

@ -0,0 +1,30 @@
{
"$schema": "../../languageSchema.json",
"iconsUrl": "https://icones.js.org/collection/emojione-v1?category=Flags",
"languages": [
{
"title": "zh-CN 简体中文",
"id": "zh-CN",
"file": "zh-CN.json",
"icon": "emojione-v1:flag-for-china"
},
{
"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": "使用者名稱"
}
}
}

259
web-src/layout/Head.vue Normal file
View File

@ -0,0 +1,259 @@
<template>
<nav class="navmain">
<router-link :to="{ name: 'home' }" c-c f style="color: inherit; height: 100%; text-decoration: none">
<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>
</div>
</router-link>
<div style="flex-grow: 1"></div>
<div c-c f style="height: 100%">
<div
v-bind:class="{
'router-item-atc': router.currentRoute.value.name === 'home',
'router-item': true,
}"
@click="router.push({ name: 'home' })">
<Icon icon="material-symbols:home" />
{{ t('nav.title.home') }}
<div class="after"></div>
</div>
<div
v-bind:class="{
'router-item-atc': router.currentRoute.value.path.startsWith('/file'),
'router-item': true,
}"
@click="openFile()">
<Icon icon="material-symbols:folder-rounded" />
{{ t('nav.title.file') }}
<div class="after"></div>
</div>
<div
v-bind:class="{
'router-item-atc': router.currentRoute.value.name === 'user',
'router-item': true,
}"
@click="router.push({ name: 'user' })">
<Icon icon="material-symbols:person-rounded" />
{{ t('nav.title.user') }}
<div class="after"></div>
</div>
<div
v-bind:class="{
'router-item-atc': router.currentRoute.value.name === 'api',
'router-item': true,
}"
@click="router.push({ name: 'api' })">
<icon icon="mdi:api" />
{{ t('nav.title.api') }}
<div class="after"></div>
</div>
<div
v-bind:class="{
'router-item': true,
}"
@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" 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">{{ 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>{{ 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>
</template>
<script lang="ts" setup>
import Icon from '@/components/Icon.vue';
import { UseBoolRef } from '@/util';
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();
const useAuthStore = UseAuthStore();
getSysInfo().then((r) => {
const data = r.json().data;
useSettingStore.appName = data.appName;
useSettingStore.appVersion = data.appVersion;
document.title = data.appName;
});
useAuthStore.woIsMe();
useSettingStore.setLanguage(null);
function openFile() {
if (useAuthStore.isLogin) {
router.push({
path: `/file/${useAuthStore.username}`,
});
} else {
message.error(() => t('message.login.plaselogin'));
router.push({
path: `/login`,
});
}
}
console.log(t);
</script>
<style lang="scss" scoped>
html.dark {
.router-item-atc {
--atc-color: #66ffad;
&:hover {
--atc-color: #66b3ff !important;
}
}
.router-item {
&:hover {
--atc-color: #965522;
background-color: rgba(255, 255, 255, 0.15);
}
}
}
.router-item-atc {
--atc-color: #64a8fc;
&:hover {
--atc-color: #42e3e8;
}
}
:where(.router-item) {
cursor: pointer;
min-width: 50px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
position: relative;
--atc-color: rgba(255, 255, 255, 0);
&:hover {
--atc-color: #ffd56f;
background-color: rgba(0, 0, 0, 0.15);
}
.after {
width: 100%;
height: 2px;
position: absolute;
bottom: 0;
background-color: var(--atc-color);
}
}
.nvr-button {
border-radius: 10px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.nvr-button:hover {
background-color: #d8d8d8;
}
html.dark {
.nvr-button:hover {
background-color: #404040;
}
}
.nav {
height: 66px;
width: 100%;
}
.navmain {
position: fixed;
backdrop-filter: blur(5px);
height: 65px;
width: calc(100% - 40px);
z-index: 5;
padding: 0 20px;
display: flex;
background-color: rgba(221, 221, 221, 0.6);
border-bottom: #d3d3d3 1px solid;
}
html.dark {
.navmain {
background-color: rgba(0, 0, 0, 0.6);
border-bottom: #424242 1px solid;
}
}
</style>

12
web-src/layout/Router.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<Head />
<n-back-top />
<router-view v-slot="{ Component }">
<div style="margin: 10px; height: calc(100% - 66px - 20px)">
<component :is="Component" />
</div>
</router-view>
</template>
<script lang="ts" setup>
import Head from '@/layout/Head.vue';
</script>

11
web-src/layout/index.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<RouterView />
</template>
<script lang="ts" setup>
import { useLoadingBar, useMessage } from 'naive-ui';
import { setLoadingBar, setMessage } from '@/plugin';
setMessage(useMessage());
setLoadingBar(useLoadingBar());
</script>
<style scoped></style>

View File

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

View File

@ -0,0 +1,47 @@
import { createAlova } from 'alova';
import adapterFetch from 'alova/fetch';
import { message, UseAuthStore } from '@/plugin';
export * from './type';
export const alovaInstance = createAlova({
requestAdapter: adapterFetch(),
cacheFor: { expire: 0 },
responded: async (response) => {
const text = await response.text();
const data = {
response: () => response,
text: () => text,
json: () => JSON.parse(text),
headers: () => {
const headers: { [key: string]: string } = {};
response.headers.forEach((k, v) => {
headers[k] = v;
});
return headers;
},
};
if (response.status !== 200) {
const messages = JSON.parse(text).message;
if (response.status === 500) message.error(messages);
throw new RequestError(data, messages);
}
return data;
},
beforeRequest(method) {
const useAuthStore = UseAuthStore();
if (useAuthStore.isLogin) {
method.config.headers.Authorization = `Bearer ${useAuthStore.token}`;
}
},
});
export class RequestError extends Error {
public data: any;
constructor(data: any, message: string) {
super(message);
this.data = data;
this.name = 'RequestError';
}
}

View File

@ -0,0 +1,13 @@
export interface AlovaResponseType<K = any> {
json: () => ResponseServer<K>;
text: () => string;
response: () => Response;
headers: () => { [key: string]: string };
}
export interface ResponseServer<K> {
code: number;
message: string;
data: K;
dateTime: string;
}

View File

@ -0,0 +1,58 @@
import { createI18n } from 'vue-i18n';
import type { languageIndexItemValueType, languageIndexType, languageType } from './type';
import { ref } from 'vue';
export * from './type';
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 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[];
}

18
web-src/plugin/index.ts Normal file
View File

@ -0,0 +1,18 @@
import type { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider';
import type { LoadingBarApiInjection } from 'naive-ui/es/loading-bar/src/LoadingBarProvider';
export * from './router';
export * from './stores';
export * from './alova';
export * from './i18n';
export let message: MessageApiInjection;
export let loadingBar: LoadingBarApiInjection;
export function setMessage(messages: MessageApiInjection) {
message = messages;
}
export function setLoadingBar(loadingBars: LoadingBarApiInjection) {
loadingBar = loadingBars;
}

View File

@ -0,0 +1,52 @@
import { createRouter, createWebHistory } from 'vue-router';
import { loadingBar } from '@/plugin';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/layout/Router.vue'),
children: [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/file/:pathMatch(.*)*',
name: 'file',
component: () => import('@/views/FileView.vue'),
},
{
path: '/user',
name: 'user',
component: () => import('@/views/UserView.vue'),
},
{
path: '/login',
name: 'login',
component: () => import('@/views/loginView.vue'),
},
{
path: '/register',
name: 'register',
component: () => import('@/views/RegisterView.vue'),
},
{
path: '/swagger',
name: 'api',
component: () => import('@/views/SwaggerView.vue'),
},
],
},
],
});
router.beforeEach((to, from, next) => {
loadingBar.start();
next();
setTimeout(() => loadingBar.finish(), 100);
});
export { router };

View File

@ -0,0 +1,49 @@
import { defineStore } from 'pinia';
import { login, logout, woIsMe } from '@/api';
import CookiesApi from '@/util/Cookies.ts';
export const UseAuthStore = defineStore('auth', {
state: () => ({
username: '',
password: '',
isAdmin: false,
isLogin: false,
userId: '',
prermissions: Array<string>(),
roles: Array<string>(),
token: '',
isRemberMe: false,
}),
actions: {
login(username: string, password: string) {
login({ username, password }).then((r) => {
if (this.isRemberMe) {
this.username = username;
this.password = password;
}
this.token = r.json().data;
this.isLogin = true;
this.woIsMe();
});
},
woIsMe() {
if (this.isLogin)
woIsMe()
.then((r) => {
const data = r.json().data;
this.username = data.username;
this.isAdmin = data.admin;
this.prermissions = data.prermissions;
this.roles = data.roles;
this.userId = data.id;
})
.catch(() => this.$reset());
},
logout() {
logout().then(() => this.$reset());
},
},
persist: {
storage: CookiesApi.StorageApi,
},
});

View File

@ -0,0 +1,8 @@
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
export * from './setting';
export * from './auth.ts';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
export { pinia };

View File

@ -0,0 +1,34 @@
import { defineStore } from 'pinia';
import { useColorMode } from '@vueuse/core';
import { i18n } from '@/plugin';
import { LocalStorageApi } from '@/util/Cookies.ts';
export const UseSettingStore = defineStore('setting', {
state: () => {
const { store, state } = useColorMode();
return {
themeMode: store,
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

@ -1,23 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
],
});
export default router;

View File

@ -1,12 +0,0 @@
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});

57
web-src/util/Cookies.ts Normal file
View File

@ -0,0 +1,57 @@
import Cookies from 'js-cookie';
import type { StorageLike } from 'pinia-plugin-persistedstate';
import { Base64 } from 'js-base64';
export const LocalStorageApi: {
Get: (key: string) => string | null;
Set: (key: string, value: string) => void;
StorageApi: StorageLike;
} = {
Get(key: string): string | null {
const data = localStorage.getItem(key);
if (data) {
return Base64.decode(data);
}
return null;
},
Set(key: string, value: string) {
localStorage.setItem(key, Base64.encode(value));
},
StorageApi: {
getItem(key: string) {
return LocalStorageApi.Get(key);
},
setItem(key: string, value: string) {
LocalStorageApi.Set(key, value);
},
},
};
const CookiesApi: {
Get: (key: string) => string | null;
Set: (key: string, value: string) => void;
StorageApi: StorageLike;
} = {
Get(key: string): string | null {
const data = Cookies.get(key);
if (data) {
return Base64.decode(data);
}
return null;
},
Set(key: string, value: string) {
Cookies.set(key, Base64.encode(value), {
expires: 265,
});
},
StorageApi: {
getItem(key: string) {
return CookiesApi.Get(key);
},
setItem(key: string, value: string) {
CookiesApi.Set(key, value);
},
},
};
export default CookiesApi;

View File

@ -0,0 +1,25 @@
import { ref } from 'vue';
function UseBoolRef(bool?: boolean) {
const data = ref(false);
if (bool) {
data.value = bool;
}
return {
get value() {
return data.value;
},
set value(v) {
data.value = v;
},
refdata: data,
on() {
data.value = true;
},
off() {
data.value = false;
},
};
}
export { UseBoolRef };

2
web-src/util/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './UseBoolRef';
export * from './Cookies';

View File

@ -1,15 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import { router } from '@/plugin';
console.log(router.currentRoute);
</script>
<style scoped></style>

View File

@ -1,9 +1,11 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue';
</script>
<template> <template>
<main> <div>
<TheWelcome /> <NButton @click="useAuthStore.login('admin', 'admin')">登录</NButton>
</main> </div>
</template> </template>
<script lang="ts" setup>
import { UseAuthStore } from '@/plugin';
const useAuthStore = UseAuthStore();
</script>
<style scoped></style>

View File

@ -0,0 +1,3 @@
<template></template>
<script lang="ts" setup></script>
<style scoped></style>

View File

@ -0,0 +1,18 @@
<template>
<div id="swaggerContainer"></div>
</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],
});
});
</script>
<style scoped></style>

View File

@ -0,0 +1,19 @@
<template>
<div>
<n-button @click="useAuthStore.logout()">注销</n-button>
</div>
</template>
<script lang="ts" setup>
import { router, UseAuthStore } from '@/plugin';
import { onMounted } from 'vue';
const useAuthStore = UseAuthStore();
onMounted(() => {
if (!useAuthStore.isLogin)
router.push({
name: 'login',
});
});
</script>
<style scoped></style>

View File

@ -0,0 +1,27 @@
<template>
<div c-c f style="height: calc(100vh - 66px - 20px)">
<n-spin :delay="1000" :show="false">
<n-card
:segmented="{
content: true,
footer: 'soft',
}"
:title="t('view.login.title')"
style="width: 430px">
<n-form border label-align="right" label-placement="left" label-width="auto">
<n-form-item :label="t('view.login.username')">
<n-input />
</n-form-item>
<n-form-item :label="t('view.login.password')">
<n-input />
</n-form-item>
</n-form>
<n-button block secondary strong type="primary">{{ t('view.login.title') }}</n-button>
</n-card>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { t } from '@/plugin';
</script>
<style scoped></style>