diff --git a/backend/.env b/backend/.env index 6bbb5b2..276aca2 100644 --- a/backend/.env +++ b/backend/.env @@ -15,3 +15,11 @@ ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:8080,http:// ALLOWED_METHODS=* ALLOWED_HEADERS=* ALLOWED_EXPOSE_HEADERS=* +# minio +MINIO_ENDPOINT=http://host.docker.internal:9000 +MINIO_EXPOSE_PORT=9000 +MINIO_WEB_PORT=9001 +MINIO_STORE=~/docker/store/mjga/minio/data +MINIO_ROOT_USER=minio +MINIO_ROOT_PASSWORD=minio123 +MINIO_DEFAULT_BUCKETS=zhilu diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 81114d6..0047b5f 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation("org.apache.commons:commons-lang3:3.17.0") implementation("org.apache.commons:commons-collections4:4.4") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") + implementation("io.minio:minio:8.5.17") implementation("org.jooq:jooq-meta:$jooqVersion") implementation("com.auth0:java-jwt:4.4.0") implementation("org.flywaydb:flyway-core:$flywayVersion") diff --git a/backend/src/main/java/com/zl/mjga/component/UserRolePermissionOperatorTool.java b/backend/src/main/java/com/zl/mjga/component/UserRolePermissionOperatorTool.java index 6e9bc1d..8f0aec4 100644 --- a/backend/src/main/java/com/zl/mjga/component/UserRolePermissionOperatorTool.java +++ b/backend/src/main/java/com/zl/mjga/component/UserRolePermissionOperatorTool.java @@ -36,16 +36,9 @@ public class UserRolePermissionOperatorTool { if (user != null) { throw new BusinessException("用户已存在"); } - identityAccessService.upsertUser(new UserUpsertDto(null, name, name, true)); + identityAccessService.upsertUser(new UserUpsertDto(null, name, name, true, null)); } - // @Tool(value = "查询用户") - // List queryUser(@P(value = "用户名",required = false) String username, @P(value = - // "开始日期",required = false) LocalDateTime startDate, @P(value = "结束日期",required = false) - // LocalDateTime endDate) { - // return userRepository.fetchBy(new UserQueryDto(username, startDate, endDate)); - // } - @Tool(value = "删除用户") void deleteUser(@P(value = "用户名") String username) { userRepository.deleteByUsername(username); @@ -56,7 +49,7 @@ public class UserRolePermissionOperatorTool { @P(value = "用户名") String name, @P(value = "密码", required = false) String password, @P(value = "是否开启", required = false) Boolean enable) { - identityAccessService.upsertUser(new UserUpsertDto(null, name, password, enable)); + identityAccessService.upsertUser(new UserUpsertDto(null, name, password, enable, null)); } @Tool(value = {"给用户绑定角色", "给用户分配角色"}) diff --git a/backend/src/main/java/com/zl/mjga/config/minio/MinIoConfig.java b/backend/src/main/java/com/zl/mjga/config/minio/MinIoConfig.java new file mode 100644 index 0000000..d2f3fd3 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/minio/MinIoConfig.java @@ -0,0 +1,26 @@ +package com.zl.mjga.config.minio; + +import io.minio.MinioClient; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Setter +@Getter +@ConfigurationProperties(prefix = "minio") +@Slf4j +public class MinIoConfig { + private String endpoint; + private String accessKey; + private String secretKey; + private String defaultBucket; + + @Bean + public MinioClient minioClient() { + return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java b/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java index e16d759..d717cf0 100644 --- a/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java +++ b/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java @@ -1,5 +1,6 @@ package com.zl.mjga.controller; +import com.zl.mjga.config.minio.MinIoConfig; import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageResponseDto; import com.zl.mjga.dto.department.DepartmentBindDto; @@ -12,17 +13,23 @@ import com.zl.mjga.repository.PermissionRepository; import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.UserRepository; import com.zl.mjga.service.IdentityAccessService; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.errors.*; import jakarta.validation.Valid; +import java.awt.image.BufferedImage; import java.security.Principal; import java.util.List; +import javax.imageio.ImageIO; import lombok.RequiredArgsConstructor; import org.jooq.generated.mjga.tables.pojos.User; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.DisabledException; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; -@SuppressWarnings("PMD.AvoidDuplicateLiterals") @RestController @RequestMapping("/iam") @RequiredArgsConstructor @@ -32,6 +39,34 @@ public class IdentityAccessController { private final UserRepository userRepository; private final RoleRepository roleRepository; private final PermissionRepository permissionRepository; + private final MinioClient minioClient; + private final MinIoConfig minIoConfig; + + @PostMapping( + value = "/avatar/upload", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + produces = MediaType.TEXT_PLAIN_VALUE) + public String uploadAvatar(Principal principal, @RequestPart("file") MultipartFile multipartFile) + throws Exception { + String objectName = String.format("avatar/%s/avatar.jpg", principal.getName()); + if (multipartFile.isEmpty()) { + throw new BusinessException("上传的文件不能为空"); + } + long size = multipartFile.getSize(); + if (size > 200 * 1024) { + throw new BusinessException("头像文件大小不能超过200KB"); + } + BufferedImage img = ImageIO.read(multipartFile.getInputStream()); + if (img == null) { + throw new BusinessException("非法的上传文件"); + } + minioClient.putObject( + PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream( + multipartFile.getInputStream(), size, -1) + .contentType(multipartFile.getContentType()) + .build()); + return objectName; + } @GetMapping("/me") UserRolePermissionDto currentUser(Principal principal) { diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java index 5b253cf..fc0a2f9 100644 --- a/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java +++ b/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java @@ -19,6 +19,8 @@ public class UserRolePermissionDto { @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; + private String avatar; + private Boolean enable; @Builder.Default private List roles = new LinkedList<>(); diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/UserUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/UserUpsertDto.java index 51e6f0f..7e24eca 100644 --- a/backend/src/main/java/com/zl/mjga/dto/urp/UserUpsertDto.java +++ b/backend/src/main/java/com/zl/mjga/dto/urp/UserUpsertDto.java @@ -14,4 +14,5 @@ public class UserUpsertDto { @NotEmpty private String username; private String password; @NotNull private Boolean enable; + private String avatar; } diff --git a/backend/src/main/java/com/zl/mjga/repository/UserRepository.java b/backend/src/main/java/com/zl/mjga/repository/UserRepository.java index 8769595..04312c0 100644 --- a/backend/src/main/java/com/zl/mjga/repository/UserRepository.java +++ b/backend/src/main/java/com/zl/mjga/repository/UserRepository.java @@ -36,6 +36,7 @@ public class UserRepository extends UserDao { value(user.getId()).as("id"), value(user.getUsername()).as("username"), value(user.getPassword()).as("password"), + value(user.getAvatar()).as("avatar"), value(user.getEnable()).as("enable")) .asTable("newUser")) .on(USER.ID.eq(DSL.field(DSL.name("newUser", "id"), Long.class))) @@ -46,11 +47,13 @@ public class UserRepository extends UserDao { StringUtils.isNotEmpty(user.getPassword()) ? DSL.field(DSL.name("newUser", "password"), String.class) : USER.PASSWORD) + .set(USER.AVATAR, DSL.field(DSL.name("newUser", "avatar"), String.class)) .set(USER.ENABLE, DSL.field(DSL.name("newUser", "enable"), Boolean.class)) - .whenNotMatchedThenInsert(USER.USERNAME, USER.PASSWORD, USER.ENABLE) + .whenNotMatchedThenInsert(USER.USERNAME, USER.PASSWORD, USER.AVATAR, USER.ENABLE) .values( DSL.field(DSL.name("newUser", "username"), String.class), DSL.field(DSL.name("newUser", "password"), String.class), + DSL.field(DSL.name("newUser", "avatar"), String.class), DSL.field(DSL.name("newUser", "enable"), Boolean.class)) .execute(); } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 1eb0005..29b542e 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -27,9 +27,18 @@ spring: enabled: true locations: classpath:db/migration default-schema: ${DATABASE_DEFAULT_SCHEMA} + servlet: + multipart: + max-file-size: 1MB + max-request-size: 10MB springdoc: swagger-ui: path: /swagger-ui.html jwt: secret: ${JWT_SECRET:secret} expiration-min: ${JWT_EXPIRATION_MIN:100} +minio: + endpoint: ${MINIO_ENDPOINT} + access-key: ${MINIO_ROOT_USER} + secret-key: ${MINIO_ROOT_PASSWORD} + default-bucket: ${MINIO_DEFAULT_BUCKETS} \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1_0_0__init_table.sql b/backend/src/main/resources/db/migration/V1_0_0__init_table.sql index da173d8..e76556f 100644 --- a/backend/src/main/resources/db/migration/V1_0_0__init_table.sql +++ b/backend/src/main/resources/db/migration/V1_0_0__init_table.sql @@ -3,6 +3,7 @@ CREATE SCHEMA IF NOT EXISTS mjga; CREATE TABLE mjga.user ( id BIGSERIAL PRIMARY KEY, username VARCHAR NOT NULL UNIQUE, + avatar VARCHAR, create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, password VARCHAR NOT NULL, enable BOOLEAN NOT NULL DEFAULT TRUE diff --git a/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java b/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java index 0eecf94..25a1671 100644 --- a/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java +++ b/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java @@ -5,6 +5,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.zl.mjga.config.minio.MinIoConfig; import com.zl.mjga.config.security.HttpFireWallConfig; import com.zl.mjga.controller.IdentityAccessController; import com.zl.mjga.dto.PageRequestDto; @@ -15,6 +16,7 @@ import com.zl.mjga.repository.PermissionRepository; import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.UserRepository; import com.zl.mjga.service.IdentityAccessService; +import io.minio.MinioClient; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -34,6 +36,8 @@ public class JacksonAnnotationMvcTest { @MockBean private UserRepository userRepository; @MockBean private RoleRepository roleRepository; @MockBean private PermissionRepository permissionRepository; + @MockBean private MinioClient minioClient; + @MockBean private MinIoConfig minIoConfig; @Test @WithMockUser diff --git a/backend/src/test/java/com/zl/mjga/integration/mvc/UserRolePermissionMvcTest.java b/backend/src/test/java/com/zl/mjga/integration/mvc/UserRolePermissionMvcTest.java index a0164c5..61ad8b5 100644 --- a/backend/src/test/java/com/zl/mjga/integration/mvc/UserRolePermissionMvcTest.java +++ b/backend/src/test/java/com/zl/mjga/integration/mvc/UserRolePermissionMvcTest.java @@ -7,6 +7,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import com.zl.mjga.config.minio.MinIoConfig; import com.zl.mjga.config.security.HttpFireWallConfig; import com.zl.mjga.controller.IdentityAccessController; import com.zl.mjga.dto.PageRequestDto; @@ -16,6 +17,7 @@ import com.zl.mjga.repository.PermissionRepository; import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.UserRepository; import com.zl.mjga.service.IdentityAccessService; +import io.minio.MinioClient; import java.util.List; import org.jooq.generated.mjga.tables.pojos.User; import org.junit.jupiter.api.Test; @@ -36,6 +38,8 @@ class UserRolePermissionMvcTest { @MockBean private UserRepository userRepository; @MockBean private RoleRepository roleRepository; @MockBean private PermissionRepository permissionRepository; + @MockBean private MinioClient minioClient; + @MockBean private MinIoConfig minIoConfig; @Test @WithMockUser diff --git a/backend/src/test/java/com/zl/mjga/integration/persistence/AbstractDataAccessLayerTest.java b/backend/src/test/java/com/zl/mjga/integration/persistence/AbstractDataAccessLayerTest.java index 766182a..cf6c28a 100644 --- a/backend/src/test/java/com/zl/mjga/integration/persistence/AbstractDataAccessLayerTest.java +++ b/backend/src/test/java/com/zl/mjga/integration/persistence/AbstractDataAccessLayerTest.java @@ -16,7 +16,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; public class AbstractDataAccessLayerTest { public static PostgreSQLContainer postgres = - new PostgreSQLContainer<>("postgres:17.3-alpine").withDatabaseName("mjga"); + new PostgreSQLContainer<>("pgvector/pgvector:pg17").withDatabaseName("mjga"); @DynamicPropertySource static void postgresProperties(DynamicPropertyRegistry registry) { diff --git a/backend/src/test/java/com/zl/mjga/integration/persistence/SortByDALTest.java b/backend/src/test/java/com/zl/mjga/integration/persistence/SortByDALTest.java index 2d17d41..6be03cd 100644 --- a/backend/src/test/java/com/zl/mjga/integration/persistence/SortByDALTest.java +++ b/backend/src/test/java/com/zl/mjga/integration/persistence/SortByDALTest.java @@ -34,9 +34,9 @@ public class SortByDALTest extends AbstractDataAccessLayerTest { void userPageFetchWithNoSort() { UserQueryDto rbacQueryDto = new UserQueryDto("test", null, null); Result records = userRepository.pageFetchBy(PageRequestDto.of(1, 10), rbacQueryDto); - assertThat(records.get(0).get(USER.ID)).isEqualTo(1); + assertThat(records.get(2).get(USER.ID)).isEqualTo(1); assertThat(records.get(1).get(USER.ID)).isEqualTo(2); - assertThat(records.get(2).get(USER.ID)).isEqualTo(3); + assertThat(records.get(0).get(USER.ID)).isEqualTo(3); } @Test diff --git a/backend/src/test/java/com/zl/mjga/integration/persistence/UserRolePermissionDALTest.java b/backend/src/test/java/com/zl/mjga/integration/persistence/UserRolePermissionDALTest.java index 9981a9f..0c35239 100644 --- a/backend/src/test/java/com/zl/mjga/integration/persistence/UserRolePermissionDALTest.java +++ b/backend/src/test/java/com/zl/mjga/integration/persistence/UserRolePermissionDALTest.java @@ -100,8 +100,8 @@ public class UserRolePermissionDALTest extends AbstractDataAccessLayerTest { Result records = userRepository.pageFetchBy(PageRequestDto.of(1, 10), rbacQueryDto); assertThat(records.size()).isEqualTo(2); - assertThat(records.get(0).get(USER.ID)).isEqualTo(1); - assertThat(records.get(1).get(USER.ID)).isEqualTo(2); + assertThat(records.get(1).get(USER.ID)).isEqualTo(1); + assertThat(records.get(0).get(USER.ID)).isEqualTo(2); } @Test @@ -142,8 +142,8 @@ public class UserRolePermissionDALTest extends AbstractDataAccessLayerTest { roleQueryDto.setBindState(BindState.ALL); Result records = roleRepository.pageFetchBy(PageRequestDto.of(1, 10), roleQueryDto); assertThat(records.get(0).getValue("total_role")).isEqualTo(2); - assertThat(records.get(0).getValue(ROLE.NAME)).isEqualTo("testRoleA"); - assertThat(records.get(1).getValue(ROLE.NAME)).isEqualTo("testRoleB"); + assertThat(records.get(1).getValue(ROLE.NAME)).isEqualTo("testRoleA"); + assertThat(records.get(0).getValue(ROLE.NAME)).isEqualTo("testRoleB"); roleQueryDto = new RoleQueryDto(); roleQueryDto.setRoleCode("testRoleA"); diff --git a/backend/src/test/java/com/zl/mjga/security/AuthenticationAndAuthorityTest.java b/backend/src/test/java/com/zl/mjga/security/AuthenticationAndAuthorityTest.java index 90f670b..8523b53 100644 --- a/backend/src/test/java/com/zl/mjga/security/AuthenticationAndAuthorityTest.java +++ b/backend/src/test/java/com/zl/mjga/security/AuthenticationAndAuthorityTest.java @@ -6,6 +6,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.zl.mjga.config.minio.MinIoConfig; import com.zl.mjga.config.security.HttpFireWallConfig; import com.zl.mjga.config.security.Jwt; import com.zl.mjga.config.security.UserDetailsServiceImpl; @@ -19,6 +20,7 @@ import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.UserRepository; import com.zl.mjga.service.IdentityAccessService; import com.zl.mjga.service.SignService; +import io.minio.MinioClient; import jakarta.servlet.http.HttpServletRequest; import java.util.Collections; import java.util.List; @@ -49,6 +51,8 @@ public class AuthenticationAndAuthorityTest { @MockBean private UserRepository userRepository; @MockBean private RoleRepository roleRepository; @MockBean private PermissionRepository permissionRepository; + @MockBean private MinioClient minioClient; + @MockBean private MinIoConfig minIoConfig; @Test public void givenRequestOnPublicService_shouldSucceedWith200() throws Exception { diff --git a/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql b/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql index a61d3be..a77b7de 100644 --- a/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql +++ b/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql @@ -3,7 +3,8 @@ CREATE SCHEMA IF NOT EXISTS mjga; CREATE TABLE mjga.user ( id BIGSERIAL PRIMARY KEY, username VARCHAR NOT NULL UNIQUE, - create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + avatar VARCHAR, + create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, password VARCHAR NOT NULL, enable BOOLEAN NOT NULL DEFAULT TRUE ); diff --git a/frontend/.env b/frontend/.env index bd687de..d076a73 100644 --- a/frontend/.env +++ b/frontend/.env @@ -3,9 +3,12 @@ VITE_SOURCE_MAP=true # mock #VITE_ENABLE_MOCK=true #VITE_BASE_URL=http://localhost:5173 +#VITE_STATIC_URL=http://localhost:5173/static # local VITE_ENABLE_MOCK=false VITE_BASE_URL=http://localhost:8080 +VITE_STATIC_URL=http://localhost:8080/static # dev #VITE_ENABLE_MOCK=false #VITE_BASE_URL=https://localhost/api +#VITE_STATIC_URL=https://localhost/ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2461849..1004992 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@vuepic/vue-datepicker": "^11.0.2", "@vueuse/core": "^13.0.0", "apexcharts": "^3.46.0", + "compressorjs": "^1.2.1", "dayjs": "^1.11.13", "dompurify": "^3.2.6", "flowbite": "^3.1.2", @@ -2901,6 +2902,12 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/blueimp-canvas-to-blob": { + "version": "3.29.0", + "resolved": "http://mirrors.tencent.com/npm/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", + "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3166,6 +3173,16 @@ "node": ">=14" } }, + "node_modules/compressorjs": { + "version": "1.2.1", + "resolved": "http://mirrors.tencent.com/npm/compressorjs/-/compressorjs-1.2.1.tgz", + "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==", + "license": "MIT", + "dependencies": { + "blueimp-canvas-to-blob": "^3.29.0", + "is-blob": "^2.1.0" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -3919,6 +3936,18 @@ "dev": true, "license": "ISC" }, + "node_modules/is-blob": { + "version": "2.1.0", + "resolved": "http://mirrors.tencent.com/npm/is-blob/-/is-blob-2.1.0.tgz", + "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index aa55f31..8764eae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@vuepic/vue-datepicker": "^11.0.2", "@vueuse/core": "^13.0.0", "apexcharts": "^3.46.0", + "compressorjs": "^1.2.1", "dayjs": "^1.11.13", "dompurify": "^3.2.6", "flowbite": "^3.1.2", diff --git a/frontend/src/api/schema/openapi.json b/frontend/src/api/schema/openapi.json index 3ed7c66..867fa18 100644 --- a/frontend/src/api/schema/openapi.json +++ b/frontend/src/api/schema/openapi.json @@ -610,6 +610,44 @@ } } }, + "/iam/avatar/upload": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "uploadAvatar", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "file" + ], + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/department": { "post": { "tags": [ @@ -1300,6 +1338,9 @@ }, "enable": { "type": "boolean" + }, + "avatar": { + "type": "string" } } }, @@ -1732,6 +1773,9 @@ "type": "string", "writeOnly": true }, + "avatar": { + "type": "string" + }, "enable": { "type": "boolean" }, diff --git a/frontend/src/api/types/schema.d.ts b/frontend/src/api/types/schema.d.ts index 8dc8d97..608aa30 100644 --- a/frontend/src/api/types/schema.d.ts +++ b/frontend/src/api/types/schema.d.ts @@ -292,6 +292,22 @@ export interface paths { patch?: never; trace?: never; }; + "/iam/avatar/upload": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["uploadAvatar"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/department": { parameters: { query?: never; @@ -631,6 +647,7 @@ export interface components { username: string; password?: string; enable: boolean; + avatar?: string; }; RoleUpsertDto: { /** Format: int64 */ @@ -780,6 +797,7 @@ export interface components { id?: number; username?: string; password?: string; + avatar?: string; enable?: boolean; roles?: components["schemas"]["RoleDto"][]; /** Format: date-time */ @@ -1402,6 +1420,33 @@ export interface operations { }; }; }; + uploadAvatar: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "multipart/form-data": { + /** Format: binary */ + file: string; + }; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; upsertDepartment: { parameters: { query?: never; diff --git a/frontend/src/components/Assistant.vue b/frontend/src/components/Assistant.vue index fa04232..c3ca469 100644 --- a/frontend/src/components/Assistant.vue +++ b/frontend/src/components/Assistant.vue @@ -111,8 +111,15 @@ import useUserStore from "../composables/store/useUserStore"; import { useUserUpsert } from "../composables/user/useUserUpsert"; import type { UserUpsertSubmitModel } from "../types/user"; -const { messages, chat, isLoading, cancel, searchAction, executeAction, clearConversation } = - useAiChat(); +const { + messages, + chat, + isLoading, + cancel, + searchAction, + executeAction, + clearConversation, +} = useAiChat(); const { user } = useUserStore(); const userUpsertModal = ref(); const departmentUpsertModal = ref(); diff --git a/frontend/src/components/UserUpsertModal.vue b/frontend/src/components/UserUpsertModal.vue index 485c6a6..8074f4a 100644 --- a/frontend/src/components/UserUpsertModal.vue +++ b/frontend/src/components/UserUpsertModal.vue @@ -41,6 +41,10 @@ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required placeholder="编辑时非必填" /> + +