Compare commits

..

No commits in common. "master" and "Auto-Releases-0.1-2e2898603f" have entirely different histories.

71 changed files with 596 additions and 2912 deletions

5
.gitignore vendored
View File

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

View File

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

View File

@ -1,55 +0,0 @@
{
"$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,7 +9,6 @@
"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,45 +17,27 @@
"format": "prettier --write \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,astro,java}\""
},
"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",
"swagger-ui-dist": "^5.25.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vue": "^3.5.17",
"vue-i18n": "11",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.15.32",
"@types/swagger-ui-dist": "^3.30.6",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.1",
"@vue/tsconfig": "^0.7.0",
"alova": "^3.3.3",
"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",
"prettier-plugin-java": "^2.6.8",
"sass-embedded": "^1.89.2",
"typescript": "~5.8.0",
"vite": "~6.0.0",
"vite": "npm:rolldown-vite@latest",
"vite-plugin-vue-devtools": "^7.7.7",
"vue-tsc": "^2.2.10"
}

View File

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

View File

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

View File

@ -1,6 +1,5 @@
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;
@ -13,8 +12,8 @@ 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).*$}/**" })
@GetMapping(value = { "/", "/{path:^(?!static|apis).*$}/**" })
public ResponseEntity<StreamingResponseBody> index() {
StreamingResponseBody streamingResponseBody = s -> {
try (InputStream stream = this.getClass().getResourceAsStream("/html/index.html")) {

View File

@ -1,28 +0,0 @@
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

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

View File

@ -1,6 +1,5 @@
package com.mingliqiye.disk.model;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
@ -33,17 +32,7 @@ public class User {
private byte[] icon;
private boolean admin;
@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 creationTime;
private DateTime updateTime;
public User setPasswordNull() {

View File

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

View File

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

View File

@ -11,18 +11,8 @@ create table `users`
roles json,
icon mediumblob,
admin bool default false,
create_user_id binary(16) not 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)
creation_time timestamp(6) default current_timestamp(6) not null,
update_time timestamp(6) null
);
INSERT INTO users (id, username, password, nickname, prermissions, roles, icon, admin, create_user_id)
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);
INSERT INTO users (id, username, password, nickname, prermissions, roles, icon, admin)
VALUES (0x689EFC204D1F11F08134DB0063E177A7, 'admin', 'admin', '管理员', '[]', '[]', null, 1);

View File

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

View File

@ -2,32 +2,11 @@ import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueDevTools from 'vite-plugin-vue-devtools';
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({
root: path.resolve(__dirname, 'web-src'),
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',
}),
],
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'web-src'),
@ -44,11 +23,4 @@ export default defineConfig({
},
},
},
server: {
host: '0.0.0.0',
port: 5174,
proxy: {
'/apis': 'http://localhost:9963',
},
},
});

View File

@ -1,25 +1,85 @@
<script lang="ts" setup>
import { RouterLink, RouterView } from 'vue-router';
import HelloWorld from './components/HelloWorld.vue';
</script>
<template>
<n-config-provider :hljs="hljs" :theme="useSettingStore.theme === 'dark' ? darkTheme : lightTheme" abstract>
<n-loading-bar-provider>
<n-message-provider>
<Index />
</n-message-provider>
</n-loading-bar-provider>
</n-config-provider>
<header>
<img alt="Vue logo" class="logo" height="125" src="@/assets/icon.svg" width="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
<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';
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
hljs.registerLanguage('json', json);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('javascript', javascript);
.logo {
display: block;
margin: 0 auto 2rem;
}
const useSettingStore = UseSettingStore();
</script>
nav {
width: 100%;
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>

View File

@ -1,33 +0,0 @@
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;
}

View File

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

View File

@ -1,10 +0,0 @@
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

86
web-src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* 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;
}

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,17 +0,0 @@
: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

@ -1,143 +0,0 @@
@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;
}

35
web-src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
@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;
}
}

View File

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

View File

@ -0,0 +1,43 @@
<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

@ -1,41 +0,0 @@
<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

@ -1,21 +0,0 @@
<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

@ -0,0 +1,97 @@
<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

@ -0,0 +1,87 @@
<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

@ -0,0 +1,6 @@
<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

@ -0,0 +1,6 @@
<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

@ -0,0 +1,6 @@
<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

@ -0,0 +1,6 @@
<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

@ -0,0 +1,17 @@
<!-- 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="">
<head>
<meta charset="UTF-8">
<link href="./assets/index.svg" rel="icon">
<meta content="width=device-width, initial-scale=0.8" name="viewport">
<title>pan-disk</title>
<link href="./assets/icon.svg" rel="icon">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Vite App</title>
</head>
<body>
<main id="app"></main>
</body>
<div id="app"></div>
<script src="/main.ts" type="module"></script>
</body>
</html>

View File

@ -1,30 +0,0 @@
{
"$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

@ -1,35 +0,0 @@
{
"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

@ -1,35 +0,0 @@
{
"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,35 +0,0 @@
{
"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,35 +0,0 @@
{
"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,259 +0,0 @@
<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>

View File

@ -1,12 +0,0 @@
<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>

View File

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

View File

@ -1,47 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,58 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,52 +0,0 @@
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

@ -1,49 +0,0 @@
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

@ -1,8 +0,0 @@
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

@ -1,34 +0,0 @@
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;
},
},
});

23
web-src/router/index.ts Normal file
View File

@ -0,0 +1,23 @@
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;

12
web-src/stores/counter.ts Normal file
View File

@ -0,0 +1,12 @@
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 };
});

View File

@ -1,57 +0,0 @@
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

@ -1,25 +0,0 @@
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 };

View File

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

View File

@ -0,0 +1,15 @@
<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

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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
<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

@ -1,19 +0,0 @@
<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

@ -1,27 +0,0 @@
<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>