Compare commits

..

2 Commits

Author SHA1 Message Date
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
56 changed files with 2275 additions and 589 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,3 +41,6 @@ pnpm-lock.yaml
src/main/resources/html
log
web-src/types
.idea

View File

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

View File

@ -17,27 +17,41 @@
"format": "prettier --write \"**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,vue,astro,java}\""
},
"dependencies": {
"@iconify/vue": "^5.0.0",
"@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-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",
"jiti": "^2.4.2",
"js-base64": "^3.7.7",
"js-cookie": "^3.0.5",
"npm-run-all2": "^8.0.4",
"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": "npm:rolldown-vite@latest",
"vite": "~6.0.0",
"vite-plugin-vue-devtools": "^7.7.7",
"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;
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;
@ -9,9 +10,13 @@ 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.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",
@ -29,23 +34,36 @@ 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() {
Server server = new Server();
server.setUrl("/");
server.setDescription("当前服务");
Server server2 = new Server();
server2.setUrl("http://localhost:" + config.getPort() + "/");
server2.setDescription("本机服务");
return new OpenAPI()
// 配置接口文档基本信息
.info(this.getApiInfo());
.info(this.getApiInfo())
.servers(List.of(server, server2));
}
private Info getApiInfo() {
return new Info()
.title("pan-disk")
.description("SpringBoot3 pan-disk Swagger3 ApiDoc")
.title(config.getAppName())
.version(config.getAppVersion())
.description("SpringBoot3 %s-V%s Swagger3 ApiDoc".formatted(config.getAppName(), config.getAppVersion()))
.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/")
.version("1.0");
.termsOfService("https://pan.mingliqiye.com/");
}
}

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 com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.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()));
}
return Respose.error(String.class, ExceptionCode.ERROR_INTERNAL_SERVER, "用户名或密码错误");
throw new InternalServerException("用户名或密码错误");
}
@Operation(summary = "获取当前登陆用户的信息")
@ -47,8 +47,8 @@ public class AuthController {
}
@Operation(summary = "登出", security = @SecurityRequirement(name = "Authorization-Bearer-Token"))
@DeleteMapping("/logout")
@SaCheckLogin
@DeleteMapping("/logout")
public Respose<Object> logout() {
StpUtil.logout();
return Respose.builder();

View File

@ -13,7 +13,7 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
@Tag(name = "前端路由", description = "统一匹配路径指向VueRouter")
public class IndexController {
@GetMapping(value = { "/", "/{path:^(?!static|apis).*$}/**" })
@GetMapping(value = { "/", "/{path:^(?!static|apis|blob).*$}/**" })
public ResponseEntity<StreamingResponseBody> index() {
StreamingResponseBody streamingResponseBody = s -> {
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.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;
@ -23,25 +22,21 @@ import org.springframework.web.servlet.NoHandlerFoundException;
public class BaseExceptionHandler {
@ExceptionHandler(BaseException.class)
public ResponseEntity<Respose<?>> exceptionHandler(BaseException e, HttpServletRequest request) {
public ResponseEntity<Respose<?>> exceptionHandler(BaseException e) {
return ResponseEntity.status(e.getCode()).body(
Respose.builder().setCode(e.getCode()).setMessage(StringUtil.format("{}", e.getMessage()))
);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Respose<?> exceptionHandler(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
public Respose<?> exceptionHandler(HttpRequestMethodNotSupportedException e) {
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,
HttpServletRequest request,
HttpServletResponse response
) {
public ResponseEntity<Respose<?>> exceptionHandler(NoHandlerFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
Respose.builder()
.setCode(ExceptionCode.ERROR_NOT_FOUND.getValue())
@ -50,26 +45,30 @@ public class BaseExceptionHandler {
}
@ExceptionHandler(NotLoginException.class)
public Respose<?> exceptionHandler(NotLoginException e, HttpServletRequest request) {
return Respose.builder()
.setCode(ExceptionCode.ERROR_UNAUTHORIZED.getValue())
.setMessage(e.getMessage())
.setData(e.getType());
public ResponseEntity<Respose<?>> exceptionHandler(NotLoginException e, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Respose.builder()
.setCode(ExceptionCode.ERROR_UNAUTHORIZED.getValue())
.setMessage(e.getMessage())
.setData(e.getType())
);
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public Respose<?> exceptionHandler(HttpMessageNotReadableException e, HttpServletRequest request) {
return Respose.builder()
.setCode(ExceptionCode.ERROR_FORBIDDEN.getValue())
.setMessage(StringUtil.format("{} by {}", e.getMessage(), e.getClass().getName()));
public ResponseEntity<Respose<?>> exceptionHandler(HttpMessageNotReadableException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Respose.builder().setCode(ExceptionCode.ERROR_FORBIDDEN.getValue()).setMessage(e.getMessage())
);
}
@ExceptionHandler(NotRoleException.class)
public Respose<?> exceptionHandler(NotRoleException e) {
return Respose.builder()
.setCode(ExceptionCode.ERROR_FORBIDDEN.getValue())
.setMessage(e.getMessage())
.setData(e.getCode());
public ResponseEntity<Respose<?>> exceptionHandler(NotRoleException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
Respose.builder()
.setCode(ExceptionCode.ERROR_FORBIDDEN.getValue())
.setMessage(e.getMessage())
.setData(e.getCode())
);
}
@ExceptionHandler(NotPermissionException.class)

View File

@ -1,5 +1,6 @@
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;
@ -32,7 +33,17 @@ public class User {
private byte[] icon;
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;
public User setPasswordNull() {

View File

@ -2,6 +2,8 @@ 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 {
@ -9,7 +11,12 @@ public class PanStpUtil {
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());
return StpUtil.getTokenValue();
}

View File

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

View File

@ -4,15 +4,25 @@ create table `users`
id binary(16) default (uuid_to_bin(
uuid(),
1)) primary key,
username varchar(256) unique not null,
password varchar(128) not null,
nickname varchar(256) not null,
username varchar(256) unique not null,
password varchar(128) not null,
nickname varchar(256) not null,
prermissions json,
roles json,
icon mediumblob,
admin bool default false,
creation_time timestamp(6) default current_timestamp(6) not null,
update_time timestamp(6) null
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)
);
INSERT INTO users (id, username, password, nickname, prermissions, roles, icon, admin)
VALUES (0x689EFC204D1F11F08134DB0063E177A7, 'admin', 'admin', '管理员', '[]', '[]', null, 1);
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);

View File

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

View File

@ -2,11 +2,32 @@ 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()],
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: {
alias: {
'@': path.resolve(__dirname, 'web-src'),
@ -23,4 +44,10 @@ export default defineConfig({
},
},
},
server: {
host: '0.0.0.0',
proxy: {
'/apis': 'http://localhost:9963',
},
},
});

View File

@ -1,85 +1,7 @@
<script lang="ts" setup>
import { RouterLink, RouterView } from 'vue-router';
import HelloWorld from './components/HelloWorld.vue';
</script>
<template>
<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 />
<Index />
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
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>
<script lang="ts" setup>
import Index from '@/layout/index.vue';
</script>

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

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

@ -0,0 +1,141 @@
@use "var";
:where(*) {
padding: 0;
margin: 0;
border: 0;
transition: background-color 0.3s ease;
}
html {
color: #3c3c3c;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
html.dark {
color: #d5d5d5;
}
.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;
}
}

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

@ -0,0 +1,16 @@
*[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

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

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

@ -0,0 +1,175 @@
<template>
<nav class="navmain">
<router-link :to="{ name: 'home' }" c-c f style="color: inherit; height: 100%; text-decoration: none">
<img height="50" src="@/assets/index.svg" width="50" />
<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" />
主页
<div class="after"></div>
</div>
<div class="router-item">
<Icon icon="material-symbols:folder-rounded" />
文件
<div class="after"></div>
</div>
<div class="router-item">
<Icon icon="material-symbols:person-rounded" />
用户
<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" />
API
<div class="after"></div>
</div>
<div
v-bind:class="{
'router-item': true,
}"
@click="activeSetting.on()">
<Icon icon="material-symbols:settings" />
设置
<div class="after"></div>
</div>
</div>
</nav>
<nav class="nav">
<n-drawer v-model:show="activeSetting.value" placement="right">
<n-drawer-content title="设置">
<div c-c f>
<n-radio-group v-model:value="useSettingStore.themeMode" name="team">
<n-radio-button value="light">亮色</n-radio-button>
<n-radio-button value="dark">暗色</n-radio-button>
<n-radio-button value="auto">自动</n-radio-button>
</n-radio-group>
</div>
<n-divider>主题配置</n-divider>
</n-drawer-content>
</n-drawer>
</nav>
</template>
<script lang="ts" setup>
import Icon from '@/components/Icon.vue';
import { UseBoolRef } from '@/util';
import { router, UseAuthStore, UseSettingStore } from '@/plugin';
import { getSysInfo } from '@/api/system.ts';
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;
});
useAuthStore.woIsMe();
</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: 1;
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>

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

@ -0,0 +1,15 @@
<template>
<Head />
<div style="margin: 10px">
<RouterView />
</div>
</template>
<script lang="ts" setup>
import { RouterView } from 'vue-router';
import { useMessage } from 'naive-ui';
import Head from '@/layout/Head.vue';
import { setMessage } from '@/plugin';
setMessage(useMessage());
</script>
<style scoped></style>

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

@ -0,0 +1,15 @@
<template>
<n-config-provider :theme="useSettingStore.theme === 'dark' ? darkTheme : lightTheme" abstract>
<n-message-provider>
<RouterView />
</n-message-provider>
</n-config-provider>
</template>
<script lang="ts" setup>
import { UseSettingStore } from '@/plugin';
import { RouterView } from 'vue-router';
import { darkTheme, lightTheme } from 'naive-ui';
const useSettingStore = UseSettingStore();
</script>
<style scoped></style>

View File

@ -1,14 +1,13 @@
import './assets/main.css';
import './assets/index.scss';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import { pinia, router } from './plugin';
const app = createApp(App);
app.use(createPinia());
app.use(pinia);
app.use(router);
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;
}

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

@ -0,0 +1,11 @@
import type { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider';
export * from './router';
export * from './stores';
export * from './alova';
export let message: MessageApiInjection;
export function setMessage(messages: MessageApiInjection) {
message = messages;
}

View File

@ -0,0 +1,25 @@
import { createRouter, createWebHistory } from 'vue-router';
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: '/swagger',
name: 'api',
component: () => import('@/views/SwaggerView.vue'),
},
],
},
],
});
export { router };

View File

@ -0,0 +1,46 @@
import { defineStore } from 'pinia';
import { login, woIsMe } from '@/api';
import { LocalStorageApi } from '@/util';
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());
},
},
persist: {
storage: LocalStorageApi.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,19 @@
import { defineStore } from 'pinia';
import { useColorMode } from '@vueuse/core';
import { LocalStorageApi } from '@/util/Cookies.ts';
export const UseSettingStore = defineStore('setting', {
state: () => {
const { store, state } = useColorMode();
return {
themeMode: store,
theme: state,
appName: '',
appVersion: '',
};
},
persist: {
storage: LocalStorageApi.StorageApi,
},
});

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

@ -1,9 +1,10 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue';
</script>
<template>
<main>
<TheWelcome />
</main>
<NButton @click="useAuthStore.login('admin', 'admin')">登录</NButton>
</template>
<script lang="ts" setup>
import { UseAuthStore } from '@/plugin';
const useAuthStore = UseAuthStore();
console.log(useAuthStore);
</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>