feat: 添加屏保管理页面和相关功能
- 在前端添加 `ScreenSaverManagement` 页面,支持屏保的创建、编辑、删除和状态切换 - 在 `AiModelController` 中添加 Swagger 注解以描述 API 操作 - 在 `pom.xml` 中添加 `springdoc-openapi-starter-webmvc-ui` 依赖 - 更新 `role-permission` 和 `tenants` 页面的分页逻辑 - 在 `sys-params` 页面中使用 `ListTable` 组件并优化分页显示dev_na
parent
b1fd9de87d
commit
6107e611f4
|
|
@ -161,6 +161,11 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme.In;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@SecurityScheme(
|
||||
name = OpenApiConfig.BEARER_AUTH_SCHEME,
|
||||
type = SecuritySchemeType.HTTP,
|
||||
scheme = "bearer",
|
||||
bearerFormat = "JWT",
|
||||
in = SecuritySchemeIn.HEADER
|
||||
)
|
||||
public class OpenApiConfig {
|
||||
|
||||
public static final String BEARER_AUTH_SCHEME = "bearerAuth";
|
||||
|
||||
@Bean
|
||||
public OpenAPI imeetingOpenApi() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("iMeeting 接口文档")
|
||||
.description("iMeeting 后端 REST 接口与兼容接口文档")
|
||||
.version("v0.1.0"))
|
||||
.addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH_SCHEME))
|
||||
.schemaRequirement(BEARER_AUTH_SCHEME, new io.swagger.v3.oas.models.security.SecurityScheme()
|
||||
.type(Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(In.HEADER));
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,8 @@ import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
|||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.realtime.AndroidRealtimeSessionTicketService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
|
@ -35,6 +37,7 @@ import java.time.LocalDateTime;
|
|||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "Android实时会议")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/meeting")
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -52,6 +55,7 @@ public class AndroidMeetingRealtimeController {
|
|||
private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver;
|
||||
private final GrpcServerProperties grpcServerProperties;
|
||||
|
||||
@Operation(summary = "创建Android实时会议")
|
||||
@PostMapping("/realtime/create")
|
||||
public ApiResponse<AndroidCreateRealtimeMeetingVO> createRealtimeMeeting(HttpServletRequest request,
|
||||
@RequestBody(required = false) AndroidCreateRealtimeMeetingCommand command) {
|
||||
|
|
@ -99,6 +103,7 @@ public class AndroidMeetingRealtimeController {
|
|||
return ApiResponse.ok(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询Android实时会议状态")
|
||||
@GetMapping("/{id}/realtime/session-status")
|
||||
public ApiResponse<RealtimeMeetingSessionStatusVO> getRealtimeSessionStatus(@PathVariable Long id, HttpServletRequest request) {
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
|
|
@ -107,6 +112,7 @@ public class AndroidMeetingRealtimeController {
|
|||
return ApiResponse.ok(realtimeMeetingSessionStateService.getStatus(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询Android会议转写")
|
||||
@GetMapping("/{id}/transcripts")
|
||||
public ApiResponse<List<MeetingTranscriptVO>> getTranscripts(@PathVariable Long id, HttpServletRequest request) {
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
|
|
@ -115,6 +121,7 @@ public class AndroidMeetingRealtimeController {
|
|||
return ApiResponse.ok(meetingQueryService.getTranscripts(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "暂停Android实时会议")
|
||||
@PostMapping("/{id}/realtime/pause")
|
||||
public ApiResponse<RealtimeMeetingSessionStatusVO> pauseRealtimeMeeting(@PathVariable Long id, HttpServletRequest request) {
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
|
|
@ -123,6 +130,7 @@ public class AndroidMeetingRealtimeController {
|
|||
return ApiResponse.ok(realtimeMeetingSessionStateService.pause(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成Android实时会议")
|
||||
@PostMapping("/{id}/realtime/complete")
|
||||
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id,
|
||||
HttpServletRequest request,
|
||||
|
|
@ -138,6 +146,7 @@ public class AndroidMeetingRealtimeController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "打开Android实时会议gRPC会话")
|
||||
@PostMapping("/{id}/realtime/grpc-session")
|
||||
public ApiResponse<AndroidRealtimeGrpcSessionVO> openRealtimeGrpcSession(@PathVariable Long id,
|
||||
HttpServletRequest request,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.AndroidScreenSaverCatalogVO;
|
||||
import com.imeeting.dto.android.AndroidScreenSaverItemVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.ScreenSaverService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "Android屏保")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/screensavers")
|
||||
@RequiredArgsConstructor
|
||||
public class AndroidScreenSaverController {
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final ScreenSaverService screenSaverService;
|
||||
|
||||
@Operation(summary = "获取当前生效屏保")
|
||||
@GetMapping("/active")
|
||||
public ApiResponse<AndroidScreenSaverCatalogVO> active(HttpServletRequest request) {
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
ScreenSaverSelectionResult selection = screenSaverService.getActiveSelection(authContext == null ? null : authContext.getUserId());
|
||||
AndroidScreenSaverCatalogVO vo = new AndroidScreenSaverCatalogVO();
|
||||
vo.setRefreshIntervalSec(300);
|
||||
vo.setPlayMode("SEQUENTIAL");
|
||||
vo.setSourceScope(selection.getSourceScope());
|
||||
vo.setItems(selection.getItems().stream().map(item -> {
|
||||
AndroidScreenSaverItemVO child = new AndroidScreenSaverItemVO();
|
||||
child.setId(item.getId());
|
||||
child.setName(item.getName());
|
||||
child.setImageUrl(item.getImageUrl());
|
||||
child.setDescription(item.getDescription());
|
||||
child.setDisplayDurationSec(item.getDisplayDurationSec());
|
||||
child.setSortOrder(item.getSortOrder());
|
||||
child.setUpdatedAt(item.getUpdatedAt());
|
||||
return child;
|
||||
}).toList());
|
||||
return ApiResponse.ok(vo);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ import com.unisbase.dto.SysRoleDTO;
|
|||
import com.unisbase.dto.SysUserDTO;
|
||||
import com.unisbase.dto.TokenResponse;
|
||||
import com.unisbase.service.AuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
|
@ -21,6 +23,7 @@ import org.springframework.util.StringUtils;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "兼容认证接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -28,6 +31,7 @@ public class LegacyAuthController {
|
|||
|
||||
private final AuthService authService;
|
||||
|
||||
@Operation(summary = "兼容登录")
|
||||
@PostMapping("/login")
|
||||
public LegacyApiResponse<LegacyLoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
TokenResponse tokenResponse = null;
|
||||
|
|
@ -43,6 +47,7 @@ public class LegacyAuthController {
|
|||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容刷新令牌")
|
||||
@PostMapping("/refresh")
|
||||
public LegacyApiResponse<LegacyRefreshTokenResponse> refresh(@RequestBody(required = false) RefreshRequest request,
|
||||
@RequestHeader(value = "Authorization", required = false) String authorization,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ package com.imeeting.controller.android.legacy;
|
|||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyClientDownloadResponse;
|
||||
import com.imeeting.service.android.legacy.LegacyCatalogAdapterService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "兼容客户端下载接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/clients")
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -16,6 +19,7 @@ public class LegacyClientController {
|
|||
|
||||
private final LegacyCatalogAdapterService legacyCatalogAdapterService;
|
||||
|
||||
@Operation(summary = "查询平台最新客户端")
|
||||
@GetMapping("/latest/by-platform")
|
||||
public LegacyApiResponse<LegacyClientDownloadResponse> latestByPlatform(@RequestParam(value = "platform_code", required = false) String platformCode,
|
||||
@RequestParam(value = "platform_type", required = false) String platformType,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.imeeting.controller.android.legacy;
|
|||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyExternalAppItemResponse;
|
||||
import com.imeeting.service.android.legacy.LegacyCatalogAdapterService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
|
@ -11,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "兼容外部应用接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/external-apps")
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -18,6 +21,7 @@ public class LegacyExternalAppController {
|
|||
|
||||
private final LegacyCatalogAdapterService legacyCatalogAdapterService;
|
||||
|
||||
@Operation(summary = "查询启用的外部应用")
|
||||
@GetMapping("/active")
|
||||
public LegacyApiResponse<List<LegacyExternalAppItemResponse>> active(@RequestParam(value = "is_active", required = false) Integer ignoredIsActive) {
|
||||
return LegacyApiResponse.ok(legacyCatalogAdapterService.listActiveExternalApps());
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import com.imeeting.dto.biz.AiModelVO;
|
|||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
|
@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Tag(name = "兼容模型接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/llm-models")
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -23,6 +26,7 @@ public class LegacyLlmModelController {
|
|||
|
||||
private final AiModelService aiModelService;
|
||||
|
||||
@Operation(summary = "查询启用的大模型列表")
|
||||
@GetMapping("/active")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<List<LegacyLlmModelItemResponse>> activeModels() {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ import com.unisbase.dto.PageResult;
|
|||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
|
@ -56,6 +58,7 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Tag(name = "兼容会议接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/meetings")
|
||||
|
||||
|
|
@ -125,6 +128,7 @@ public class LegacyMeetingController {
|
|||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容创建会议")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyMeetingCreateResponse> create(@RequestBody LegacyMeetingCreateRequest request) {
|
||||
|
|
@ -132,6 +136,7 @@ public class LegacyMeetingController {
|
|||
return LegacyApiResponse.ok(new LegacyMeetingCreateResponse(meeting.getId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容上传会议音频")
|
||||
@PostMapping("/upload-audio")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<Void> uploadAudio(@RequestParam("meeting_id") Long meetingId,
|
||||
|
|
@ -150,6 +155,7 @@ public class LegacyMeetingController {
|
|||
return LegacyApiResponse.ok("上传成功", null);
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容分页查询会议")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyMeetingListResponse> list(@RequestParam(value = "user_id", required = false) Long ignoredUserId,
|
||||
|
|
@ -181,12 +187,14 @@ public class LegacyMeetingController {
|
|||
return LegacyApiResponse.ok(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容查询会议预览数据")
|
||||
@GetMapping("/{meetingId}/preview-data")
|
||||
public LegacyApiResponse<?> previewData(@PathVariable Long meetingId) {
|
||||
LegacyMeetingPreviewResult result = buildPreviewResult(meetingId);
|
||||
return new LegacyApiResponse<>(result.getCode(), result.getMessage(), result.getData());
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容更新会议访问密码")
|
||||
@PutMapping("/{meetingId}/access-password")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyMeetingAccessPasswordResponse> updateAccessPassword(@PathVariable Long meetingId,
|
||||
|
|
@ -202,6 +210,7 @@ public class LegacyMeetingController {
|
|||
return LegacyApiResponse.ok(new LegacyMeetingAccessPasswordResponse(password));
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容删除会议")
|
||||
@DeleteMapping("/{meetingId}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<Void> delete(@PathVariable Long meetingId) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.imeeting.dto.biz.PromptTemplateVO;
|
|||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
|
@ -18,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Tag(name = "兼容提示词接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/prompts")
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -27,6 +30,7 @@ public class LegacyPromptController {
|
|||
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
|
||||
@Operation(summary = "查询场景提示词")
|
||||
@GetMapping("/active/{scene}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyPromptListResponse> activePrompts(@PathVariable String scene) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
|
||||
import com.imeeting.service.android.legacy.LegacyScreenSaverAdapterService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "兼容屏保接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/screensavers")
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyScreenSaverController {
|
||||
|
||||
private final LegacyScreenSaverAdapterService legacyScreenSaverAdapterService;
|
||||
|
||||
@Operation(summary = "查询启用的屏保列表")
|
||||
@GetMapping("/active")
|
||||
public LegacyApiResponse<List<LegacyScreenSaverItemResponse>> active() {
|
||||
return LegacyApiResponse.ok(legacyScreenSaverAdapterService.listActiveScreenSavers());
|
||||
}
|
||||
}
|
||||
|
|
@ -4,17 +4,19 @@ package com.imeeting.controller.biz;
|
|||
import com.imeeting.dto.biz.AiModelDTO;
|
||||
import com.imeeting.dto.biz.AiLocalProfileVO;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "AI模型管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/aimodel")
|
||||
public class AiModelController {
|
||||
|
|
@ -25,12 +27,14 @@ public class AiModelController {
|
|||
this.aiModelService = aiModelService;
|
||||
}
|
||||
|
||||
@Operation(summary = "新增AI模型")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<AiModelVO> save(@RequestBody AiModelDTO dto) {
|
||||
return ApiResponse.ok(aiModelService.saveModel(dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新AI模型")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<AiModelVO> update(@RequestBody AiModelDTO dto) {
|
||||
|
|
@ -54,6 +58,7 @@ public class AiModelController {
|
|||
return ApiResponse.ok(aiModelService.updateModel(dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除AI模型")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id, @RequestParam String type) {
|
||||
|
|
@ -74,6 +79,7 @@ public class AiModelController {
|
|||
return ApiResponse.ok(aiModelService.removeModelById(id, type));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询AI模型")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<AiModelVO>>> page(
|
||||
|
|
@ -85,6 +91,7 @@ public class AiModelController {
|
|||
return ApiResponse.ok(aiModelService.pageModels(current, size, name, type, loginUser.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "拉取远程模型列表")
|
||||
@GetMapping("/remote-list")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<String>> remoteList(
|
||||
|
|
@ -94,6 +101,7 @@ public class AiModelController {
|
|||
return ApiResponse.ok(aiModelService.fetchRemoteModels(provider, baseUrl, apiKey));
|
||||
}
|
||||
|
||||
@Operation(summary = "测试本地模型连通性")
|
||||
@PostMapping("/local-connectivity-test")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<AiLocalProfileVO> testLocalConnectivity(@RequestBody AiModelDTO dto) {
|
||||
|
|
@ -106,6 +114,7 @@ public class AiModelController {
|
|||
return ApiResponse.ok(aiModelService.testLocalConnectivity(dto.getBaseUrl(), dto.getApiKey()));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取默认AI模型")
|
||||
@GetMapping("/default")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<AiModelVO> getDefault(@RequestParam String type) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import com.imeeting.entity.biz.ClientDownload;
|
|||
import com.imeeting.service.biz.ClientDownloadService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
|
@ -24,6 +26,7 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "客户端下载管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/clients")
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -31,6 +34,7 @@ public class ClientDownloadController {
|
|||
|
||||
private final ClientDownloadService clientDownloadService;
|
||||
|
||||
@Operation(summary = "查询客户端下载包列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> list(@RequestParam(value = "platformCode", required = false) String platformCode,
|
||||
|
|
@ -46,18 +50,21 @@ public class ClientDownloadController {
|
|||
return ApiResponse.ok(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "新增客户端下载包")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ClientDownload> create(@RequestBody ClientDownloadDTO dto) {
|
||||
return ApiResponse.ok(clientDownloadService.create(dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改客户端下载包")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ClientDownload> update(@PathVariable Long id, @RequestBody ClientDownloadDTO dto) {
|
||||
return ApiResponse.ok(clientDownloadService.update(id, dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除客户端下载包")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
|
|
@ -65,6 +72,7 @@ public class ClientDownloadController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "上传客户端安装包")
|
||||
@PostMapping("/upload")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> upload(@RequestParam("platformCode") String platformCode,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import com.imeeting.dto.biz.MeetingVO;
|
|||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
|
@ -12,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "工作台")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/dashboard")
|
||||
public class DashboardController {
|
||||
|
|
@ -22,6 +25,7 @@ public class DashboardController {
|
|||
this.meetingQueryService = meetingQueryService;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取工作台统计数据")
|
||||
@GetMapping("/stats")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> getStats() {
|
||||
|
|
@ -30,6 +34,7 @@ public class DashboardController {
|
|||
return ApiResponse.ok(meetingQueryService.getDashboardStats(user.getTenantId(), user.getUserId(), isAdmin));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取最近会议列表")
|
||||
@GetMapping("/recent")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<MeetingVO>> getRecent() {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import com.imeeting.entity.biz.ExternalApp;
|
|||
import com.imeeting.service.biz.ExternalAppService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
|
@ -23,6 +25,7 @@ import java.io.IOException;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "外部应用管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/external-apps")
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -30,6 +33,7 @@ public class ExternalAppController {
|
|||
|
||||
private final ExternalAppService externalAppService;
|
||||
|
||||
@Operation(summary = "查询外部应用列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<Map<String, Object>>> list(@RequestParam(value = "appType", required = false) String appType,
|
||||
|
|
@ -37,18 +41,21 @@ public class ExternalAppController {
|
|||
return ApiResponse.ok(externalAppService.listForAdmin(currentLoginUser(), appType, status));
|
||||
}
|
||||
|
||||
@Operation(summary = "新增外部应用")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ExternalApp> create(@RequestBody ExternalAppDTO dto) {
|
||||
return ApiResponse.ok(externalAppService.create(dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改外部应用")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ExternalApp> update(@PathVariable Long id, @RequestBody ExternalAppDTO dto) {
|
||||
return ApiResponse.ok(externalAppService.update(id, dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除外部应用")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
|
|
@ -56,12 +63,14 @@ public class ExternalAppController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "上传外部应用APK")
|
||||
@PostMapping("/upload-apk")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> uploadApk(@RequestParam("apkFile") MultipartFile apkFile) throws IOException {
|
||||
return ApiResponse.ok(externalAppService.uploadApk(apkFile));
|
||||
}
|
||||
|
||||
@Operation(summary = "上传外部应用图标")
|
||||
@PostMapping("/upload-icon")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> uploadIcon(@RequestParam("iconFile") MultipartFile iconFile) throws IOException {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import com.imeeting.service.biz.HotWordService;
|
|||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
|
@ -18,6 +20,7 @@ import org.springframework.web.bind.annotation.*;
|
|||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Tag(name = "热词管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/hotword")
|
||||
public class HotWordController {
|
||||
|
|
@ -35,6 +38,7 @@ public class HotWordController {
|
|||
return Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin());
|
||||
}
|
||||
|
||||
@Operation(summary = "新增热词")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<HotWordVO> save(@RequestBody HotWordDTO hotWordDTO) {
|
||||
|
|
@ -48,6 +52,7 @@ public class HotWordController {
|
|||
return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改热词")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<HotWordVO> update(@RequestBody HotWordDTO hotWordDTO) {
|
||||
|
|
@ -76,6 +81,7 @@ public class HotWordController {
|
|||
return ApiResponse.ok(hotWordService.updateHotWord(hotWordDTO));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除热词")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
|
|
@ -97,6 +103,7 @@ public class HotWordController {
|
|||
return ApiResponse.ok(hotWordService.removeById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询热词")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<HotWordVO>>> page(
|
||||
|
|
@ -136,6 +143,7 @@ public class HotWordController {
|
|||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "生成热词拼音")
|
||||
@GetMapping("/pinyin")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<String>> getPinyin(@RequestParam String word) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
|||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
|
@ -57,6 +59,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Tag(name = "会议管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/meeting")
|
||||
public class MeetingController {
|
||||
|
|
@ -94,6 +97,7 @@ public class MeetingController {
|
|||
this.resourcePrefix = resourcePrefix;
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议处理进度")
|
||||
@GetMapping("/{id}/progress")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> getProgress(@PathVariable Long id) {
|
||||
|
|
@ -125,6 +129,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(fallback);
|
||||
}
|
||||
|
||||
@Operation(summary = "上传会议音频")
|
||||
@PostMapping("/upload")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
|
|
@ -141,6 +146,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(baseResourcePrefix + "audio/" + fileName);
|
||||
}
|
||||
|
||||
@Operation(summary = "创建离线会议")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingVO> create(@Valid @RequestBody CreateMeetingCommand command) {
|
||||
|
|
@ -154,6 +160,7 @@ public class MeetingController {
|
|||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建实时会议")
|
||||
@PostMapping("/realtime/start")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingVO> createRealtime(@Valid @RequestBody CreateRealtimeMeetingCommand command) {
|
||||
|
|
@ -167,6 +174,7 @@ public class MeetingController {
|
|||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询会议")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<MeetingVO>>> page(
|
||||
|
|
@ -190,6 +198,7 @@ public class MeetingController {
|
|||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingVO> getDetail(@PathVariable Long id) {
|
||||
|
|
@ -199,6 +208,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(meetingQueryService.getDetail(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "导出会议摘要")
|
||||
@GetMapping("/{id}/summary/export")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<byte[]> exportSummary(@PathVariable Long id, @RequestParam(defaultValue = "pdf") String format) {
|
||||
|
|
@ -219,6 +229,7 @@ public class MeetingController {
|
|||
.body(exportResult.getContent());
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议转写记录")
|
||||
@GetMapping("/{id}/transcripts")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<MeetingTranscriptVO>> getTranscripts(@PathVariable Long id) {
|
||||
|
|
@ -228,6 +239,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(meetingQueryService.getTranscripts(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询实时会议状态")
|
||||
@GetMapping("/{id}/realtime/session-status")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<RealtimeMeetingSessionStatusVO> getRealtimeSessionStatus(@PathVariable Long id) {
|
||||
|
|
@ -237,6 +249,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(realtimeMeetingSessionStateService.getStatus(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量查询实时会议状态")
|
||||
@PostMapping("/realtime/session-status/batch")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<Long, RealtimeMeetingSessionStatusVO>> getRealtimeSessionStatuses(@RequestBody List<Long> ids) {
|
||||
|
|
@ -265,6 +278,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "追加实时转写片段")
|
||||
@PostMapping("/{id}/realtime/transcripts")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> appendRealtimeTranscripts(@PathVariable Long id, @RequestBody List<RealtimeTranscriptItemDTO> items) {
|
||||
|
|
@ -275,6 +289,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "暂停实时会议")
|
||||
@PostMapping("/{id}/realtime/pause")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<RealtimeMeetingSessionStatusVO> pauseRealtimeMeeting(@PathVariable Long id) {
|
||||
|
|
@ -284,6 +299,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(realtimeMeetingSessionStateService.pause(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "打开实时会议Socket会话")
|
||||
@PostMapping("/{id}/realtime/socket-session")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<RealtimeSocketSessionVO> openRealtimeSocketSession(@PathVariable Long id,
|
||||
|
|
@ -304,6 +320,7 @@ public class MeetingController {
|
|||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成实时会议")
|
||||
@PostMapping("/{id}/realtime/complete")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id, @RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
|
||||
|
|
@ -318,6 +335,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会议讲话人")
|
||||
@PutMapping("/speaker")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> updateSpeaker(@RequestBody MeetingSpeakerUpdateDTO dto) {
|
||||
|
|
@ -328,6 +346,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会议转写")
|
||||
@PutMapping("/{id}/transcripts/{transcriptId}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> updateTranscript(@PathVariable Long id,
|
||||
|
|
@ -342,6 +361,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新参会人员")
|
||||
@PutMapping("/{id}/participants")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> updateParticipants(@PathVariable Long id,
|
||||
|
|
@ -354,6 +374,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "重新生成会议摘要")
|
||||
@PostMapping("/{id}/summary/regenerate")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> reSummary(@PathVariable Long id, @Valid @RequestBody MeetingResummaryDTO dto) {
|
||||
|
|
@ -366,6 +387,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "重试音频转写")
|
||||
@PostMapping("/{id}/transcripts/regenerate")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> retryTranscription(@PathVariable Long id) {
|
||||
|
|
@ -376,6 +398,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会议基础信息")
|
||||
@PutMapping("/{id}/basic")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> updateBasic(@PathVariable Long id, @RequestBody UpdateMeetingBasicCommand command) {
|
||||
|
|
@ -387,6 +410,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会议摘要")
|
||||
@PutMapping("/{id}/summary")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> updateSummary(@PathVariable Long id, @RequestBody UpdateMeetingSummaryCommand command) {
|
||||
|
|
@ -398,6 +422,7 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除会议")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ import com.imeeting.entity.biz.Meeting;
|
|||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "会议公开预览")
|
||||
@RestController
|
||||
@RequestMapping("/api/public/meetings")
|
||||
public class MeetingPublicPreviewController {
|
||||
|
|
@ -25,6 +28,7 @@ public class MeetingPublicPreviewController {
|
|||
this.meetingAccessService = meetingAccessService;
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议预览访问要求")
|
||||
@GetMapping("/{id}/preview/access")
|
||||
public ApiResponse<MeetingPreviewAccessVO> getPreviewAccess(@PathVariable Long id) {
|
||||
try {
|
||||
|
|
@ -35,6 +39,7 @@ public class MeetingPublicPreviewController {
|
|||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会议公开预览内容")
|
||||
@GetMapping("/{id}/preview")
|
||||
public ApiResponse<PublicMeetingPreviewVO> getPreview(@PathVariable Long id,
|
||||
@RequestParam(required = false) String accessPassword) {
|
||||
|
|
|
|||
|
|
@ -4,17 +4,19 @@ package com.imeeting.controller.biz;
|
|||
import com.imeeting.dto.biz.PromptTemplateDTO;
|
||||
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||
import com.imeeting.entity.biz.PromptTemplate;
|
||||
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "提示词模板管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/prompt")
|
||||
public class PromptTemplateController {
|
||||
|
|
@ -25,6 +27,7 @@ public class PromptTemplateController {
|
|||
this.promptTemplateService = promptTemplateService;
|
||||
}
|
||||
|
||||
@Operation(summary = "新增提示词模板")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PromptTemplateVO> save(@RequestBody PromptTemplateDTO dto) {
|
||||
|
|
@ -46,6 +49,7 @@ public class PromptTemplateController {
|
|||
return ApiResponse.ok(promptTemplateService.saveTemplate(dto, loginUser.getUserId(), loginUser.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改提示词模板")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PromptTemplateVO> update(@RequestBody PromptTemplateDTO dto) {
|
||||
|
|
@ -73,6 +77,7 @@ public class PromptTemplateController {
|
|||
return ApiResponse.ok(promptTemplateService.updateTemplate(dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新提示词模板状态")
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
|
|
@ -110,6 +115,7 @@ public class PromptTemplateController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除提示词模板")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
|
|
@ -137,6 +143,7 @@ public class PromptTemplateController {
|
|||
return ApiResponse.ok(promptTemplateService.removeById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询提示词模板")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<PromptTemplateVO>>> page(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.ScreenSaverAdminVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverDTO;
|
||||
import com.imeeting.dto.biz.ScreenSaverImageUploadVO;
|
||||
import com.imeeting.entity.biz.ScreenSaver;
|
||||
import com.imeeting.service.biz.ScreenSaverService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "屏保管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/screen-savers")
|
||||
@RequiredArgsConstructor
|
||||
public class ScreenSaverController {
|
||||
|
||||
private final ScreenSaverService screenSaverService;
|
||||
|
||||
@Operation(summary = "查询屏保列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<ScreenSaverAdminVO>> list(@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "scopeType", required = false) String scopeType,
|
||||
@RequestParam(value = "ownerUserId", required = false) Long ownerUserId) {
|
||||
return ApiResponse.ok(screenSaverService.listForAdmin(currentLoginUser(), keyword, status, scopeType, ownerUserId));
|
||||
}
|
||||
|
||||
@Operation(summary = "新增屏保")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ScreenSaver> create(@RequestBody ScreenSaverDTO dto) {
|
||||
return ApiResponse.ok(screenSaverService.create(dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改屏保")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ScreenSaver> update(@PathVariable Long id, @RequestBody ScreenSaverDTO dto) {
|
||||
return ApiResponse.ok(screenSaverService.update(id, dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除屏保")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
screenSaverService.removeScreenSaver(id, currentLoginUser());
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "上传屏保图片")
|
||||
@PostMapping("/upload-image")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ScreenSaverImageUploadVO> uploadImage(@RequestParam("imageFile") MultipartFile imageFile) throws IOException {
|
||||
return ApiResponse.ok(screenSaverService.uploadImage(imageFile));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,15 @@ import com.imeeting.service.biz.SpeakerService;
|
|||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "讲话人管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/speaker")
|
||||
public class SpeakerController {
|
||||
|
|
@ -22,6 +25,7 @@ public class SpeakerController {
|
|||
this.speakerService = speakerService;
|
||||
}
|
||||
|
||||
@Operation(summary = "注册讲话人样本")
|
||||
@PostMapping("/register")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<SpeakerVO> register(@ModelAttribute SpeakerRegisterDTO registerDTO) {
|
||||
|
|
@ -33,6 +37,7 @@ public class SpeakerController {
|
|||
return ApiResponse.ok(speakerService.register(registerDTO, loginUser));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询讲话人")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<SpeakerVO>>> page(
|
||||
|
|
@ -46,6 +51,7 @@ public class SpeakerController {
|
|||
return ApiResponse.ok(speakerService.pageVisible(current, size, name, loginUser));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询可见讲话人")
|
||||
@GetMapping("/list")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<SpeakerVO>> list() {
|
||||
|
|
@ -56,6 +62,7 @@ public class SpeakerController {
|
|||
return ApiResponse.ok(speakerService.listVisible(loginUser));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除讲话人")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.imeeting.dto.android;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AndroidScreenSaverCatalogVO {
|
||||
private Integer refreshIntervalSec;
|
||||
private String playMode;
|
||||
private String sourceScope;
|
||||
private List<AndroidScreenSaverItemVO> items;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.imeeting.dto.android;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AndroidScreenSaverItemVO {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String imageUrl;
|
||||
private String description;
|
||||
private Integer displayDurationSec;
|
||||
private Integer sortOrder;
|
||||
private String updatedAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.imeeting.dto.biz.ScreenSaverAdminVO;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LegacyScreenSaverItemResponse {
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
@JsonProperty("image_url")
|
||||
private String imageUrl;
|
||||
|
||||
private String description;
|
||||
|
||||
@JsonProperty("display_duration_sec")
|
||||
private Integer displayDurationSec;
|
||||
|
||||
@JsonProperty("sort_order")
|
||||
private Integer sortOrder;
|
||||
|
||||
@JsonProperty("is_active")
|
||||
private Integer isActive;
|
||||
|
||||
@JsonProperty("created_at")
|
||||
private String createdAt;
|
||||
|
||||
@JsonProperty("updated_at")
|
||||
private String updatedAt;
|
||||
|
||||
@JsonProperty("created_by")
|
||||
private Long createdBy;
|
||||
|
||||
@JsonProperty("creator_username")
|
||||
private String creatorUsername;
|
||||
|
||||
public static LegacyScreenSaverItemResponse from(ScreenSaverAdminVO source) {
|
||||
LegacyScreenSaverItemResponse response = new LegacyScreenSaverItemResponse();
|
||||
response.setId(source.getId());
|
||||
response.setName(source.getName());
|
||||
response.setImageUrl(source.getImageUrl());
|
||||
response.setDescription(source.getDescription());
|
||||
response.setDisplayDurationSec(source.getDisplayDurationSec());
|
||||
response.setSortOrder(source.getSortOrder());
|
||||
response.setIsActive(Integer.valueOf(1).equals(source.getStatus()) ? 1 : 0);
|
||||
response.setCreatedAt(source.getCreatedAt());
|
||||
response.setUpdatedAt(source.getUpdatedAt());
|
||||
response.setCreatedBy(source.getCreatedBy());
|
||||
response.setCreatorUsername(source.getCreatorUsername());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import com.imeeting.entity.biz.ScreenSaver;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ScreenSaverAdminVO {
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private String scopeType;
|
||||
private Long ownerUserId;
|
||||
private String name;
|
||||
private String imageUrl;
|
||||
private String description;
|
||||
private Integer displayDurationSec;
|
||||
private Integer imageWidth;
|
||||
private Integer imageHeight;
|
||||
private String imageFormat;
|
||||
private Integer sortOrder;
|
||||
private Integer status;
|
||||
private String remark;
|
||||
private Long createdBy;
|
||||
private String creatorUsername;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
public static ScreenSaverAdminVO from(ScreenSaver entity, String creatorUsername) {
|
||||
ScreenSaverAdminVO vo = new ScreenSaverAdminVO();
|
||||
vo.setId(entity.getId());
|
||||
vo.setTenantId(entity.getTenantId());
|
||||
vo.setScopeType(entity.getScopeType());
|
||||
vo.setOwnerUserId(entity.getOwnerUserId());
|
||||
vo.setName(entity.getName());
|
||||
vo.setImageUrl(entity.getImageUrl());
|
||||
vo.setDescription(entity.getDescription());
|
||||
vo.setDisplayDurationSec(entity.getDisplayDurationSec());
|
||||
vo.setImageWidth(entity.getImageWidth());
|
||||
vo.setImageHeight(entity.getImageHeight());
|
||||
vo.setImageFormat(entity.getImageFormat());
|
||||
vo.setSortOrder(entity.getSortOrder());
|
||||
vo.setStatus(entity.getStatus());
|
||||
vo.setRemark(entity.getRemark());
|
||||
vo.setCreatedBy(entity.getCreatedBy());
|
||||
vo.setCreatorUsername(creatorUsername);
|
||||
vo.setCreatedAt(entity.getCreatedAt() == null ? null : entity.getCreatedAt().toString());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt() == null ? null : entity.getUpdatedAt().toString());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ScreenSaverDTO {
|
||||
private String scopeType;
|
||||
private Long ownerUserId;
|
||||
private String name;
|
||||
private String imageUrl;
|
||||
private String description;
|
||||
private Integer displayDurationSec;
|
||||
private Integer imageWidth;
|
||||
private Integer imageHeight;
|
||||
private String imageFormat;
|
||||
private Integer sortOrder;
|
||||
private Integer status;
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ScreenSaverImageUploadVO {
|
||||
private String imageUrl;
|
||||
private Long fileSize;
|
||||
private Integer imageWidth;
|
||||
private Integer imageHeight;
|
||||
private String imageFormat;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class ScreenSaverSelectionResult {
|
||||
private String sourceScope;
|
||||
private List<ScreenSaverAdminVO> items;
|
||||
}
|
||||
|
|
@ -5,37 +5,50 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Schema(description = "AI任务实体")
|
||||
@TableName(value = "biz_ai_tasks", autoResultMap = true)
|
||||
public class AiTask {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "任务ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "会议ID")
|
||||
private Long meetingId;
|
||||
|
||||
@Schema(description = "任务类型")
|
||||
private String taskType;
|
||||
|
||||
@Schema(description = "任务状态")
|
||||
private Integer status;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@Schema(description = "任务请求参数")
|
||||
private Map<String, Object> requestData;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@Schema(description = "任务响应结果")
|
||||
private Map<String, Object> responseData;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@Schema(description = "任务运行配置")
|
||||
private Map<String, Object> taskConfig;
|
||||
|
||||
@Schema(description = "结果文件路径")
|
||||
private String resultFilePath;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMsg;
|
||||
|
||||
@Schema(description = "开始时间")
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
@Schema(description = "完成时间")
|
||||
private LocalDateTime completedAt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
|||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
|
@ -13,27 +14,38 @@ import java.util.Map;
|
|||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "语音识别模型实体")
|
||||
@TableName(value = "biz_asr_models", autoResultMap = true)
|
||||
public class AsrModel extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "模型ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description = "服务提供商")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "服务基础地址")
|
||||
private String baseUrl;
|
||||
|
||||
@Schema(description = "接口密钥")
|
||||
private String apiKey;
|
||||
|
||||
@Schema(description = "模型编码")
|
||||
private String modelCode;
|
||||
|
||||
@Schema(description = "WebSocket地址")
|
||||
private String wsUrl;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@Schema(description = "音频媒体参数配置")
|
||||
private Map<String, Object> mediaConfig;
|
||||
|
||||
@Schema(description = "是否默认模型")
|
||||
private Integer isDefault;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,35 +4,49 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "客户端下载包实体")
|
||||
@TableName("biz_client_downloads")
|
||||
public class ClientDownload extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "下载包ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "平台类型")
|
||||
private String platformType;
|
||||
|
||||
@Schema(description = "平台名称")
|
||||
private String platformName;
|
||||
|
||||
@Schema(description = "平台编码")
|
||||
private String platformCode;
|
||||
|
||||
@Schema(description = "版本号")
|
||||
private String version;
|
||||
|
||||
@Schema(description = "版本编码")
|
||||
private Long versionCode;
|
||||
|
||||
@Schema(description = "下载地址")
|
||||
private String downloadUrl;
|
||||
|
||||
@Schema(description = "文件大小")
|
||||
private Long fileSize;
|
||||
|
||||
@Schema(description = "版本说明")
|
||||
private String releaseNotes;
|
||||
|
||||
@Schema(description = "是否最新版本")
|
||||
private Integer isLatest;
|
||||
|
||||
@Schema(description = "最低系统版本")
|
||||
private String minSystemVersion;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Long createdBy;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
|||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
|
@ -13,23 +14,32 @@ import java.util.Map;
|
|||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "外部应用实体")
|
||||
@TableName(value = "biz_external_apps", autoResultMap = true)
|
||||
public class ExternalApp extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "应用ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "应用名称")
|
||||
private String appName;
|
||||
|
||||
@Schema(description = "应用类型")
|
||||
private String appType;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@Schema(description = "应用扩展信息")
|
||||
private Map<String, Object> appInfo;
|
||||
|
||||
@Schema(description = "图标地址")
|
||||
private String iconUrl;
|
||||
|
||||
@Schema(description = "应用描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "排序值")
|
||||
private Integer sortOrder;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Long createdBy;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
|||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
|
@ -13,27 +14,38 @@ import java.util.List;
|
|||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "热词实体")
|
||||
@TableName(value = "biz_hot_words", autoResultMap = true)
|
||||
public class HotWord extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "热词ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "热词内容")
|
||||
private String word;
|
||||
|
||||
@Schema(description = "是否公共热词")
|
||||
private Integer isPublic;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Long creatorId;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@Schema(description = "拼音列表")
|
||||
private List<String> pinyinList;
|
||||
|
||||
@Schema(description = "匹配策略")
|
||||
private Integer matchStrategy;
|
||||
|
||||
@Schema(description = "热词分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "权重")
|
||||
private Integer weight;
|
||||
|
||||
@Schema(description = "是否已同步")
|
||||
private Integer isSynced;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
|
@ -11,28 +12,40 @@ import java.math.BigDecimal;
|
|||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "大语言模型实体")
|
||||
@TableName("biz_llm_models")
|
||||
public class LlmModel extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "模型ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description = "服务提供商")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "服务基础地址")
|
||||
private String baseUrl;
|
||||
|
||||
@Schema(description = "接口路径")
|
||||
private String apiPath;
|
||||
|
||||
@Schema(description = "接口密钥")
|
||||
private String apiKey;
|
||||
|
||||
@Schema(description = "模型编码")
|
||||
private String modelCode;
|
||||
|
||||
@Schema(description = "温度参数")
|
||||
private BigDecimal temperature;
|
||||
|
||||
@Schema(description = "Top P参数")
|
||||
private BigDecimal topP;
|
||||
|
||||
@Schema(description = "是否默认模型")
|
||||
private Integer isDefault;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
|
@ -12,37 +13,53 @@ import java.time.LocalDateTime;
|
|||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "会议实体")
|
||||
@TableName(value = "biz_meetings", autoResultMap = true)
|
||||
public class Meeting extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "会议ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "会议标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "会议时间")
|
||||
private LocalDateTime meetingTime;
|
||||
|
||||
@Schema(description = "参会人员")
|
||||
private String participants;
|
||||
|
||||
@Schema(description = "会议标签")
|
||||
private String tags;
|
||||
|
||||
@Schema(description = "音频地址")
|
||||
private String audioUrl;
|
||||
|
||||
@Schema(description = "音频保存状态")
|
||||
private String audioSaveStatus;
|
||||
|
||||
@Schema(description = "音频保存说明")
|
||||
private String audioSaveMessage;
|
||||
|
||||
@Schema(description = "访问密码")
|
||||
private String accessPassword;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Long creatorId;
|
||||
|
||||
@Schema(description = "创建人名称")
|
||||
private String creatorName;
|
||||
|
||||
@Schema(description = "主持人用户ID")
|
||||
private Long hostUserId;
|
||||
|
||||
@Schema(description = "主持人名称")
|
||||
private String hostName;
|
||||
|
||||
@Schema(description = "最新摘要任务ID")
|
||||
private Long latestSummaryTaskId;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "会议摘要内容")
|
||||
private String summaryContent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,43 @@ package com.imeeting.entity.biz;
|
|||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(description = "会议转写片段实体")
|
||||
@TableName("biz_meeting_transcripts")
|
||||
public class MeetingTranscript {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "转写片段ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "会议ID")
|
||||
private Long meetingId;
|
||||
|
||||
@Schema(description = "讲话人ID")
|
||||
private String speakerId;
|
||||
|
||||
@Schema(description = "讲话人名称")
|
||||
private String speakerName;
|
||||
|
||||
@Schema(description = "讲话人标签")
|
||||
private String speakerLabel;
|
||||
|
||||
@Schema(description = "转写内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "开始时间,单位毫秒")
|
||||
private Integer startTime;
|
||||
|
||||
@Schema(description = "结束时间,单位毫秒")
|
||||
private Integer endTime;
|
||||
|
||||
@Schema(description = "排序值")
|
||||
private Integer sortOrder;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,32 +4,44 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "提示词模板实体")
|
||||
@TableName(value = "biz_prompt_templates", autoResultMap = true)
|
||||
public class PromptTemplate extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "模板ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "模板名称")
|
||||
private String templateName;
|
||||
|
||||
@Schema(description = "模板描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "模板分类")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "是否系统内置")
|
||||
private Integer isSystem;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Long creatorId;
|
||||
|
||||
@com.baomidou.mybatisplus.annotation.TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
|
||||
@Schema(description = "业务标签列表")
|
||||
private java.util.List<String> tags;
|
||||
|
||||
@Schema(description = "使用次数")
|
||||
private Integer usageCount;
|
||||
|
||||
@Schema(description = "提示词内容")
|
||||
private String promptContent;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,23 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "用户提示词配置实体")
|
||||
@TableName("biz_prompt_template_user_config")
|
||||
public class PromptTemplateUserConfig extends BaseEntity {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "配置ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "模板ID")
|
||||
private Long templateId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.imeeting.entity.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "屏保素材实体")
|
||||
@TableName("biz_screen_savers")
|
||||
public class ScreenSaver extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "屏保ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "作用域类型")
|
||||
private String scopeType;
|
||||
|
||||
@Schema(description = "所属用户ID")
|
||||
private Long ownerUserId;
|
||||
|
||||
@Schema(description = "屏保名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "图片地址")
|
||||
private String imageUrl;
|
||||
|
||||
@Schema(description = "屏保描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "展示时长,单位秒")
|
||||
private Integer displayDurationSec;
|
||||
|
||||
@Schema(description = "图片宽度")
|
||||
private Integer imageWidth;
|
||||
|
||||
@Schema(description = "图片高度")
|
||||
private Integer imageHeight;
|
||||
|
||||
@Schema(description = "图片格式")
|
||||
private String imageFormat;
|
||||
|
||||
@Schema(description = "排序值")
|
||||
private Integer sortOrder;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Long createdBy;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -4,32 +4,44 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.unisbase.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "讲话人实体")
|
||||
@TableName("biz_speakers")
|
||||
public class Speaker extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "讲话人ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Long creatorId;
|
||||
|
||||
@Schema(description = "关联用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "外部讲话人ID")
|
||||
private String externalSpeakerId;
|
||||
|
||||
@Schema(description = "讲话人名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "音频样本路径")
|
||||
private String voicePath;
|
||||
|
||||
@Schema(description = "音频扩展名")
|
||||
private String voiceExt;
|
||||
|
||||
@Schema(description = "音频大小")
|
||||
private Long voiceSize;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
// Note: status, createdAt, updatedAt, isDeleted are in BaseEntity
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package com.imeeting.mapper.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.imeeting.entity.biz.ScreenSaver;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ScreenSaverMapper extends BaseMapper<ScreenSaver> {
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.imeeting.service.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface LegacyScreenSaverAdapterService {
|
||||
List<LegacyScreenSaverItemResponse> listActiveScreenSavers();
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.imeeting.service.android.legacy.impl;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
|
||||
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
|
||||
import com.imeeting.service.android.legacy.LegacyScreenSaverAdapterService;
|
||||
import com.imeeting.service.biz.ScreenSaverService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyScreenSaverAdapterServiceImpl implements LegacyScreenSaverAdapterService {
|
||||
|
||||
private final ScreenSaverService screenSaverService;
|
||||
|
||||
@Override
|
||||
public List<LegacyScreenSaverItemResponse> listActiveScreenSavers() {
|
||||
ScreenSaverSelectionResult selection = screenSaverService.getActiveSelection(null);
|
||||
if (selection == null || selection.getItems() == null || selection.getItems().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return selection.getItems().stream()
|
||||
.map(LegacyScreenSaverItemResponse::from)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.imeeting.service.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.imeeting.dto.biz.ScreenSaverAdminVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverDTO;
|
||||
import com.imeeting.dto.biz.ScreenSaverImageUploadVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
|
||||
import com.imeeting.entity.biz.ScreenSaver;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public interface ScreenSaverService extends IService<ScreenSaver> {
|
||||
List<ScreenSaverAdminVO> listForAdmin(LoginUser loginUser, String keyword, Integer status, String scopeType, Long ownerUserId);
|
||||
|
||||
ScreenSaver create(ScreenSaverDTO dto, LoginUser loginUser);
|
||||
|
||||
ScreenSaver update(Long id, ScreenSaverDTO dto, LoginUser loginUser);
|
||||
|
||||
void removeScreenSaver(Long id, LoginUser loginUser);
|
||||
|
||||
ScreenSaverImageUploadVO uploadImage(MultipartFile file) throws IOException;
|
||||
|
||||
ScreenSaverSelectionResult getActiveSelection(Long userId);
|
||||
}
|
||||
|
|
@ -447,12 +447,15 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
taskRecord.setRequestData(req);
|
||||
this.updateById(taskRecord);
|
||||
|
||||
String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
|
||||
String url = appendPath(llmModel.getBaseUrl(),
|
||||
(llmModel.getApiPath() == null || llmModel.getApiPath().isBlank())
|
||||
? "v1/chat/completions"
|
||||
: llmModel.getApiPath());
|
||||
String requestBody = objectMapper.writeValueAsString(req);
|
||||
log.info("Sending LLM summary request to url={}, body={}", url, requestBody);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.uri(buildUri(url))
|
||||
.header("Content-Type", "application/json; charset=UTF-8")
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "Bearer " + llmModel.getApiKey())
|
||||
|
|
@ -535,7 +538,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
|
||||
private String postJson(String url, Object body, String apiKey) throws Exception {
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.uri(buildUri(url))
|
||||
.header("Content-Type", "application/json");
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
builder.header("Authorization", "Bearer " + apiKey);
|
||||
|
|
@ -547,7 +550,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
}
|
||||
|
||||
private String get(String url, String apiKey) throws Exception {
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(URI.create(url));
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(buildUri(url));
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
builder.header("Authorization", "Bearer " + apiKey);
|
||||
}
|
||||
|
|
@ -555,10 +558,40 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
}
|
||||
|
||||
private String appendPath(String baseUrl, String path) {
|
||||
if (baseUrl.endsWith("/")) {
|
||||
return baseUrl + path;
|
||||
String normalizedBaseUrl = normalizeUrlComponent(baseUrl, "baseUrl");
|
||||
String normalizedPath = normalizePath(path);
|
||||
if (normalizedPath.isEmpty()) {
|
||||
return normalizedBaseUrl;
|
||||
}
|
||||
return baseUrl + "/" + path;
|
||||
if (normalizedPath.startsWith("http://") || normalizedPath.startsWith("https://")) {
|
||||
return normalizedPath;
|
||||
}
|
||||
if (normalizedBaseUrl.endsWith("/")) {
|
||||
return normalizedBaseUrl + normalizedPath;
|
||||
}
|
||||
return normalizedBaseUrl + "/" + normalizedPath;
|
||||
}
|
||||
|
||||
private URI buildUri(String rawUrl) {
|
||||
return URI.create(normalizeUrlComponent(rawUrl, "url"));
|
||||
}
|
||||
|
||||
private String normalizeUrlComponent(String value, String fieldName) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(fieldName + " cannot be blank");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String normalizePath(String path) {
|
||||
if (path == null || path.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String normalized = path.trim();
|
||||
while (normalized.startsWith("/")) {
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String sanitizeSummaryContent(String content) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,390 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.imeeting.dto.biz.ScreenSaverAdminVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverDTO;
|
||||
import com.imeeting.dto.biz.ScreenSaverImageUploadVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
|
||||
import com.imeeting.entity.biz.ScreenSaver;
|
||||
import com.imeeting.mapper.biz.ScreenSaverMapper;
|
||||
import com.imeeting.service.biz.ScreenSaverService;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ScreenSaverServiceImpl extends ServiceImpl<ScreenSaverMapper, ScreenSaver> implements ScreenSaverService {
|
||||
|
||||
private static final long GLOBAL_TENANT_ID = 0L;
|
||||
private static final String SCOPE_PLATFORM = "PLATFORM";
|
||||
private static final String SCOPE_USER = "USER";
|
||||
private static final int REQUIRED_WIDTH = 1280;
|
||||
private static final int REQUIRED_HEIGHT = 800;
|
||||
private static final Set<String> ALLOWED_FORMATS = Set.of("jpg", "jpeg", "png");
|
||||
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
@Value("${unisbase.app.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
@Value("${unisbase.app.resource-prefix:/api/static/}")
|
||||
private String resourcePrefix;
|
||||
|
||||
@Override
|
||||
public List<ScreenSaverAdminVO> listForAdmin(LoginUser loginUser, String keyword, Integer status, String scopeType, Long ownerUserId) {
|
||||
LambdaQueryWrapper<ScreenSaver> wrapper = new LambdaQueryWrapper<ScreenSaver>()
|
||||
.orderByAsc(ScreenSaver::getSortOrder)
|
||||
.orderByDesc(ScreenSaver::getId);
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
String trimmed = keyword.trim();
|
||||
wrapper.and(w -> w.like(ScreenSaver::getName, trimmed)
|
||||
.or()
|
||||
.like(ScreenSaver::getDescription, trimmed));
|
||||
}
|
||||
if (status != null) {
|
||||
wrapper.eq(ScreenSaver::getStatus, status);
|
||||
}
|
||||
if (StringUtils.hasText(scopeType)) {
|
||||
wrapper.eq(ScreenSaver::getScopeType, normalizeScopeType(scopeType));
|
||||
}
|
||||
if (ownerUserId != null) {
|
||||
wrapper.eq(ScreenSaver::getOwnerUserId, ownerUserId);
|
||||
}
|
||||
return toAdminVOs(this.list(wrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ScreenSaver create(ScreenSaverDTO dto, LoginUser loginUser) {
|
||||
validate(dto, false, null);
|
||||
ScreenSaver entity = new ScreenSaver();
|
||||
applyDto(entity, dto, false);
|
||||
entity.setTenantId(GLOBAL_TENANT_ID);
|
||||
entity.setCreatedBy(loginUser.getUserId());
|
||||
if (entity.getStatus() == null) {
|
||||
entity.setStatus(1);
|
||||
}
|
||||
this.save(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ScreenSaver update(Long id, ScreenSaverDTO dto, LoginUser loginUser) {
|
||||
ScreenSaver entity = requireExisting(id);
|
||||
String previousImageUrl = entity.getImageUrl();
|
||||
validate(dto, true, entity);
|
||||
applyDto(entity, dto, true);
|
||||
entity.setTenantId(GLOBAL_TENANT_ID);
|
||||
this.updateById(entity);
|
||||
if (dto.getImageUrl() != null && !Objects.equals(previousImageUrl, entity.getImageUrl())) {
|
||||
deleteManagedFileIfUnused(previousImageUrl, entity.getId());
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeScreenSaver(Long id, LoginUser loginUser) {
|
||||
ScreenSaver entity = requireExisting(id);
|
||||
String imageUrl = entity.getImageUrl();
|
||||
this.removeById(entity.getId());
|
||||
deleteManagedFileIfUnused(imageUrl, entity.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScreenSaverImageUploadVO uploadImage(MultipartFile file) throws IOException {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new RuntimeException("image file is required");
|
||||
}
|
||||
String originalName = sanitizeFileName(file.getOriginalFilename());
|
||||
String format = resolveAndValidateFormat(originalName, file.getContentType());
|
||||
ImageMetadata metadata = readImageMetadata(file);
|
||||
if (metadata.width() != REQUIRED_WIDTH || metadata.height() != REQUIRED_HEIGHT) {
|
||||
throw new RuntimeException("image must be 1280x800");
|
||||
}
|
||||
|
||||
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||
Path targetDir = Paths.get(basePath, "screen-savers", "images");
|
||||
Files.createDirectories(targetDir);
|
||||
String targetName = UUID.randomUUID() + "_" + originalName;
|
||||
Path target = targetDir.resolve(targetName);
|
||||
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
ScreenSaverImageUploadVO vo = new ScreenSaverImageUploadVO();
|
||||
vo.setImageUrl(buildResourceUrl("screen-savers/images/" + target.getFileName()));
|
||||
vo.setFileSize(file.getSize());
|
||||
vo.setImageWidth(metadata.width());
|
||||
vo.setImageHeight(metadata.height());
|
||||
vo.setImageFormat(format);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScreenSaverSelectionResult getActiveSelection(Long userId) {
|
||||
List<ScreenSaver> selected = selectActiveEntities(userId);
|
||||
String sourceScope = selected.isEmpty() ? SCOPE_PLATFORM : normalizeScopeType(selected.get(0).getScopeType());
|
||||
return new ScreenSaverSelectionResult(sourceScope, toAdminVOs(selected));
|
||||
}
|
||||
|
||||
private List<ScreenSaver> selectActiveEntities(Long userId) {
|
||||
if (userId != null) {
|
||||
List<ScreenSaver> userScoped = listActiveByScope(SCOPE_USER, userId);
|
||||
if (!userScoped.isEmpty()) {
|
||||
return userScoped;
|
||||
}
|
||||
}
|
||||
return listActiveByScope(SCOPE_PLATFORM, null);
|
||||
}
|
||||
|
||||
private List<ScreenSaver> listActiveByScope(String scopeType, Long ownerUserId) {
|
||||
LambdaQueryWrapper<ScreenSaver> wrapper = new LambdaQueryWrapper<ScreenSaver>()
|
||||
.eq(ScreenSaver::getStatus, 1)
|
||||
.eq(ScreenSaver::getScopeType, scopeType)
|
||||
.orderByAsc(ScreenSaver::getSortOrder)
|
||||
.orderByDesc(ScreenSaver::getId);
|
||||
if (ownerUserId == null) {
|
||||
wrapper.isNull(ScreenSaver::getOwnerUserId);
|
||||
} else {
|
||||
wrapper.eq(ScreenSaver::getOwnerUserId, ownerUserId);
|
||||
}
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
private List<ScreenSaverAdminVO> toAdminVOs(List<ScreenSaver> entities) {
|
||||
if (entities == null || entities.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, String> creatorNames = resolveCreatorNames(entities);
|
||||
return entities.stream()
|
||||
.map(item -> ScreenSaverAdminVO.from(item, creatorNames.get(item.getCreatedBy())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Map<Long, String> resolveCreatorNames(List<ScreenSaver> entities) {
|
||||
List<Long> creatorIds = entities.stream()
|
||||
.map(ScreenSaver::getCreatedBy)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (creatorIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return sysUserMapper.selectBatchIds(creatorIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
SysUser::getUserId,
|
||||
user -> user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(),
|
||||
(left, right) -> left,
|
||||
HashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
private void validate(ScreenSaverDTO dto, boolean partial, ScreenSaver existing) {
|
||||
if (dto == null) {
|
||||
throw new RuntimeException("payload is required");
|
||||
}
|
||||
if (!partial) {
|
||||
if (!StringUtils.hasText(dto.getName())) {
|
||||
throw new RuntimeException("name is required");
|
||||
}
|
||||
if (!StringUtils.hasText(dto.getImageUrl())) {
|
||||
throw new RuntimeException("imageUrl is required");
|
||||
}
|
||||
requireImageMetadata(dto);
|
||||
}
|
||||
if (dto.getImageUrl() != null || dto.getImageWidth() != null || dto.getImageHeight() != null || dto.getImageFormat() != null) {
|
||||
requireImageMetadata(dto);
|
||||
}
|
||||
|
||||
String resolvedScopeType = resolveScopeTypeForValidation(dto, existing);
|
||||
Long resolvedOwnerUserId = dto.getOwnerUserId() != null ? dto.getOwnerUserId() : existing == null ? null : existing.getOwnerUserId();
|
||||
if (SCOPE_USER.equals(resolvedScopeType) && resolvedOwnerUserId == null) {
|
||||
throw new RuntimeException("ownerUserId is required when scopeType is USER");
|
||||
}
|
||||
if (!SCOPE_PLATFORM.equals(resolvedScopeType) && !SCOPE_USER.equals(resolvedScopeType)) {
|
||||
throw new RuntimeException("scopeType only supports PLATFORM or USER");
|
||||
}
|
||||
if (dto.getDisplayDurationSec() != null && (dto.getDisplayDurationSec() < 3 || dto.getDisplayDurationSec() > 3600)) {
|
||||
throw new RuntimeException("displayDurationSec must be between 3 and 3600");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireImageMetadata(ScreenSaverDTO dto) {
|
||||
if (!StringUtils.hasText(dto.getImageUrl())) {
|
||||
throw new RuntimeException("imageUrl is required");
|
||||
}
|
||||
if (dto.getImageWidth() == null || dto.getImageHeight() == null || !StringUtils.hasText(dto.getImageFormat())) {
|
||||
throw new RuntimeException("image metadata is required");
|
||||
}
|
||||
if (dto.getImageWidth() != REQUIRED_WIDTH || dto.getImageHeight() != REQUIRED_HEIGHT) {
|
||||
throw new RuntimeException("image must be 1280x800");
|
||||
}
|
||||
if (!ALLOWED_FORMATS.contains(dto.getImageFormat().trim().toLowerCase())) {
|
||||
throw new RuntimeException("imageFormat only supports jpg/jpeg/png");
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveScopeTypeForValidation(ScreenSaverDTO dto, ScreenSaver existing) {
|
||||
if (dto.getScopeType() != null) {
|
||||
return normalizeScopeType(dto.getScopeType());
|
||||
}
|
||||
if (existing != null && StringUtils.hasText(existing.getScopeType())) {
|
||||
return normalizeScopeType(existing.getScopeType());
|
||||
}
|
||||
return SCOPE_PLATFORM;
|
||||
}
|
||||
|
||||
private void applyDto(ScreenSaver entity, ScreenSaverDTO dto, boolean partial) {
|
||||
if (!partial || dto.getScopeType() != null) {
|
||||
entity.setScopeType(resolveScopeTypeForValidation(dto, entity));
|
||||
} else if (!StringUtils.hasText(entity.getScopeType())) {
|
||||
entity.setScopeType(SCOPE_PLATFORM);
|
||||
}
|
||||
if (!partial || dto.getOwnerUserId() != null || (dto.getScopeType() != null && SCOPE_PLATFORM.equals(normalizeScopeType(dto.getScopeType())))) {
|
||||
entity.setOwnerUserId(SCOPE_PLATFORM.equals(entity.getScopeType()) ? null : dto.getOwnerUserId());
|
||||
}
|
||||
if (!partial || dto.getName() != null) {
|
||||
entity.setName(trimToNull(dto.getName()));
|
||||
}
|
||||
if (!partial || dto.getImageUrl() != null) {
|
||||
entity.setImageUrl(trimToNull(dto.getImageUrl()));
|
||||
}
|
||||
if (!partial || dto.getDescription() != null) {
|
||||
entity.setDescription(trimToNull(dto.getDescription()));
|
||||
}
|
||||
if (!partial || dto.getDisplayDurationSec() != null) {
|
||||
entity.setDisplayDurationSec(dto.getDisplayDurationSec());
|
||||
}
|
||||
if (!partial || dto.getImageWidth() != null) {
|
||||
entity.setImageWidth(dto.getImageWidth());
|
||||
}
|
||||
if (!partial || dto.getImageHeight() != null) {
|
||||
entity.setImageHeight(dto.getImageHeight());
|
||||
}
|
||||
if (!partial || dto.getImageFormat() != null) {
|
||||
entity.setImageFormat(trimToNull(dto.getImageFormat()));
|
||||
}
|
||||
if (!partial || dto.getSortOrder() != null) {
|
||||
entity.setSortOrder(dto.getSortOrder());
|
||||
}
|
||||
if (!partial || dto.getStatus() != null) {
|
||||
entity.setStatus(dto.getStatus());
|
||||
}
|
||||
if (!partial || dto.getRemark() != null) {
|
||||
entity.setRemark(trimToNull(dto.getRemark()));
|
||||
}
|
||||
}
|
||||
|
||||
private ScreenSaver requireExisting(Long id) {
|
||||
ScreenSaver entity = this.getById(id);
|
||||
if (entity == null) {
|
||||
throw new RuntimeException("screen saver not found");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private String resolveAndValidateFormat(String fileName, String contentType) {
|
||||
String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
if (!ALLOWED_FORMATS.contains(extension)) {
|
||||
throw new RuntimeException("only jpg/jpeg/png are supported");
|
||||
}
|
||||
if (StringUtils.hasText(contentType)) {
|
||||
String normalized = contentType.trim().toLowerCase();
|
||||
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/jpg")) {
|
||||
throw new RuntimeException("invalid image content type");
|
||||
}
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
private ImageMetadata readImageMetadata(MultipartFile file) throws IOException {
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
BufferedImage image = ImageIO.read(inputStream);
|
||||
if (image == null) {
|
||||
throw new RuntimeException("invalid image file");
|
||||
}
|
||||
return new ImageMetadata(image.getWidth(), image.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeFileName(String fileName) {
|
||||
String value = fileName == null || fileName.isBlank() ? "image.png" : fileName;
|
||||
value = value.replace('\\', '/');
|
||||
int slashIndex = value.lastIndexOf('/');
|
||||
if (slashIndex >= 0) {
|
||||
value = value.substring(slashIndex + 1);
|
||||
}
|
||||
value = value.replaceAll("[^A-Za-z0-9._-]", "_");
|
||||
return value.isBlank() ? "image.png" : value;
|
||||
}
|
||||
|
||||
private String buildResourceUrl(String relativePath) {
|
||||
String prefix = resourcePrefix.endsWith("/") ? resourcePrefix : resourcePrefix + "/";
|
||||
return prefix + relativePath.replace('\\', '/');
|
||||
}
|
||||
|
||||
private void deleteManagedFileIfUnused(String imageUrl, Long excludeId) {
|
||||
if (!StringUtils.hasText(imageUrl)) {
|
||||
return;
|
||||
}
|
||||
long references = this.count(new LambdaQueryWrapper<ScreenSaver>()
|
||||
.eq(ScreenSaver::getImageUrl, imageUrl)
|
||||
.ne(excludeId != null, ScreenSaver::getId, excludeId));
|
||||
if (references > 0) {
|
||||
return;
|
||||
}
|
||||
String prefix = resourcePrefix.endsWith("/") ? resourcePrefix : resourcePrefix + "/";
|
||||
if (!imageUrl.startsWith(prefix)) {
|
||||
return;
|
||||
}
|
||||
String relativePath = imageUrl.substring(prefix.length());
|
||||
if (!relativePath.startsWith("screen-savers/images/")) {
|
||||
return;
|
||||
}
|
||||
Path target = Paths.get(uploadPath, relativePath.replace('/', java.io.File.separatorChar));
|
||||
try {
|
||||
Files.deleteIfExists(target);
|
||||
} catch (IOException ignored) {
|
||||
// Ignore cleanup failure to avoid breaking main flow.
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeScopeType(String scopeType) {
|
||||
return scopeType == null ? SCOPE_PLATFORM : scopeType.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private record ImageMetadata(int width, int height) {
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,14 @@ spring:
|
|||
write-dates-as-timestamps: false
|
||||
time-zone: GMT+8
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
|
@ -53,6 +61,9 @@ unisbase:
|
|||
- /api/auth/**
|
||||
- /api/static/**
|
||||
- /api/public/meetings/**
|
||||
- /v3/api-docs/**
|
||||
- /swagger-ui.html
|
||||
- /swagger-ui/**
|
||||
- /ws/**
|
||||
internal-auth:
|
||||
enabled: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.imeeting.service.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyScreenSaverItemResponse;
|
||||
import com.imeeting.dto.biz.ScreenSaverAdminVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
|
||||
import com.imeeting.service.android.legacy.impl.LegacyScreenSaverAdapterServiceImpl;
|
||||
import com.imeeting.service.biz.ScreenSaverService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class LegacyScreenSaverAdapterServiceImplTest {
|
||||
|
||||
@Test
|
||||
void listActiveScreenSaversShouldMapLegacyFields() {
|
||||
ScreenSaverService screenSaverService = mock(ScreenSaverService.class);
|
||||
|
||||
ScreenSaverAdminVO item = new ScreenSaverAdminVO();
|
||||
item.setId(9L);
|
||||
item.setName("欢迎屏");
|
||||
item.setImageUrl("/api/static/screen-savers/images/a.jpg");
|
||||
item.setDescription("主大厅欢迎屏");
|
||||
item.setDisplayDurationSec(12);
|
||||
item.setSortOrder(3);
|
||||
item.setStatus(1);
|
||||
item.setCreatedAt("2026-04-17T16:00:00");
|
||||
item.setUpdatedAt("2026-04-17T16:10:00");
|
||||
item.setCreatedBy(7L);
|
||||
item.setCreatorUsername("admin");
|
||||
|
||||
when(screenSaverService.getActiveSelection(null))
|
||||
.thenReturn(new ScreenSaverSelectionResult("PLATFORM", List.of(item)));
|
||||
|
||||
LegacyScreenSaverAdapterServiceImpl service = new LegacyScreenSaverAdapterServiceImpl(screenSaverService);
|
||||
|
||||
List<LegacyScreenSaverItemResponse> result = service.listActiveScreenSavers();
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(9L, result.get(0).getId());
|
||||
assertEquals("欢迎屏", result.get(0).getName());
|
||||
assertEquals("/api/static/screen-savers/images/a.jpg", result.get(0).getImageUrl());
|
||||
assertEquals(12, result.get(0).getDisplayDurationSec());
|
||||
assertEquals(1, result.get(0).getIsActive());
|
||||
assertEquals("admin", result.get(0).getCreatorUsername());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +1,77 @@
|
|||
//package com.imeeting.service.biz.impl;
|
||||
//
|
||||
//import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
//import com.imeeting.entity.biz.AiTask;
|
||||
//import com.imeeting.entity.biz.Meeting;
|
||||
//import com.imeeting.mapper.biz.AiTaskMapper;
|
||||
//import com.imeeting.mapper.biz.MeetingMapper;
|
||||
//import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
//import com.imeeting.support.TaskSecurityContextRunner;
|
||||
//import com.imeeting.service.biz.AiModelService;
|
||||
//import com.imeeting.service.biz.HotWordService;
|
||||
//import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
//import com.unisbase.mapper.SysUserMapper;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
//import org.springframework.test.util.ReflectionTestUtils;
|
||||
//
|
||||
//import java.util.HashMap;
|
||||
//import java.util.Map;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
//import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
//import static org.mockito.ArgumentMatchers.any;
|
||||
//import static org.mockito.Mockito.mock;
|
||||
//import static org.mockito.Mockito.when;
|
||||
//
|
||||
//class AiTaskServiceImplTest {
|
||||
//
|
||||
// @Test
|
||||
// void processSummaryTaskShouldFailWhenSummaryModelMissing() {
|
||||
// MeetingMapper meetingMapper = mock(MeetingMapper.class);
|
||||
// MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
|
||||
// AiModelService aiModelService = mock(AiModelService.class);
|
||||
// AiTaskMapper aiTaskMapper = mock(AiTaskMapper.class);
|
||||
//
|
||||
// when(aiTaskMapper.updateById(any(AiTask.class))).thenReturn(1);
|
||||
// when(aiModelService.getModelById(99L, "LLM")).thenReturn(null);
|
||||
//
|
||||
// AiTaskServiceImpl service = new AiTaskServiceImpl(
|
||||
// meetingMapper,
|
||||
// transcriptMapper,
|
||||
// aiModelService,
|
||||
// new com.fasterxml.jackson.databind.ObjectMapper(),
|
||||
// mock(SysUserMapper.class),
|
||||
// mock(HotWordService.class),
|
||||
// mock(StringRedisTemplate.class),
|
||||
// mock(MeetingSummaryFileService.class),
|
||||
// mock(TaskSecurityContextRunner.class)
|
||||
// );
|
||||
// ReflectionTestUtils.setField(service, BaseMapper.class, "baseMapper", aiTaskMapper);
|
||||
//
|
||||
// Meeting meeting = new Meeting();
|
||||
// meeting.setId(1L);
|
||||
//
|
||||
// AiTask summaryTask = new AiTask();
|
||||
// summaryTask.setId(10L);
|
||||
// summaryTask.setMeetingId(1L);
|
||||
// summaryTask.setTaskType("SUMMARY");
|
||||
// summaryTask.setStatus(0);
|
||||
// Map<String, Object> taskConfig = new HashMap<>();
|
||||
// taskConfig.put("summaryModelId", 99L);
|
||||
// summaryTask.setTaskConfig(taskConfig);
|
||||
//
|
||||
// RuntimeException exception = assertThrows(RuntimeException.class, () ->
|
||||
// ReflectionTestUtils.invokeMethod(service, "processSummaryTask", meeting, "speaker: test", summaryTask)
|
||||
// );
|
||||
//
|
||||
// assertEquals("LLM模型配置不存在", exception.getMessage());
|
||||
// assertEquals(3, summaryTask.getStatus());
|
||||
// assertEquals("LLM model not found: 99", summaryTask.getErrorMsg());
|
||||
// }
|
||||
//}
|
||||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
import com.imeeting.support.TaskSecurityContextRunner;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class AiTaskServiceImplTest {
|
||||
|
||||
@Test
|
||||
void appendPathShouldTrimWhitespaceAndNormalizeSlashBoundaries() {
|
||||
AiTaskServiceImpl service = createService();
|
||||
|
||||
String url = ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"appendPath",
|
||||
" http://10.100.52.43:1234/ ",
|
||||
" /v1/chat/completions "
|
||||
);
|
||||
|
||||
assertEquals("http://10.100.52.43:1234/v1/chat/completions", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendPathShouldReturnAbsolutePathWhenApiPathIsFullUrl() {
|
||||
AiTaskServiceImpl service = createService();
|
||||
|
||||
String url = ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"appendPath",
|
||||
" http://10.100.52.43:1234/ ",
|
||||
" https://example.com/custom-endpoint "
|
||||
);
|
||||
|
||||
assertEquals("https://example.com/custom-endpoint", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUriShouldTrimWhitespaceBeforeUriCreate() {
|
||||
AiTaskServiceImpl service = createService();
|
||||
|
||||
URI uri = ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"buildUri",
|
||||
" http://10.100.52.43:1234/v1/chat/completions "
|
||||
);
|
||||
|
||||
assertEquals("http://10.100.52.43:1234/v1/chat/completions", uri.toString());
|
||||
}
|
||||
|
||||
private AiTaskServiceImpl createService() {
|
||||
return new AiTaskServiceImpl(
|
||||
mock(MeetingMapper.class),
|
||||
mock(MeetingTranscriptMapper.class),
|
||||
mock(AiModelService.class),
|
||||
new ObjectMapper(),
|
||||
mock(SysUserMapper.class),
|
||||
mock(HotWordService.class),
|
||||
mock(StringRedisTemplate.class),
|
||||
mock(MeetingSummaryFileService.class),
|
||||
mock(MeetingSummaryPromptAssembler.class),
|
||||
mock(TaskSecurityContextRunner.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Logs
|
||||
logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import http from "../http";
|
||||
|
||||
export type ScreenSaverScopeType = "PLATFORM" | "USER";
|
||||
|
||||
export interface ScreenSaverVO {
|
||||
id: number;
|
||||
tenantId?: number;
|
||||
scopeType: ScreenSaverScopeType;
|
||||
ownerUserId?: number | null;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
description?: string;
|
||||
displayDurationSec: number;
|
||||
imageWidth?: number;
|
||||
imageHeight?: number;
|
||||
imageFormat?: string;
|
||||
sortOrder?: number;
|
||||
status: number;
|
||||
remark?: string;
|
||||
createdBy?: number;
|
||||
creatorUsername?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface ScreenSaverDTO {
|
||||
scopeType: ScreenSaverScopeType;
|
||||
ownerUserId?: number | null;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
description?: string;
|
||||
displayDurationSec: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
imageFormat: string;
|
||||
sortOrder?: number;
|
||||
status: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ScreenSaverUploadResult {
|
||||
imageUrl: string;
|
||||
fileSize: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
imageFormat: string;
|
||||
}
|
||||
|
||||
export async function listScreenSavers(params?: {
|
||||
keyword?: string;
|
||||
status?: number;
|
||||
scopeType?: ScreenSaverScopeType;
|
||||
ownerUserId?: number;
|
||||
}) {
|
||||
const resp = await http.get("/api/screen-savers", { params });
|
||||
return resp.data.data as ScreenSaverVO[];
|
||||
}
|
||||
|
||||
export async function createScreenSaver(payload: ScreenSaverDTO) {
|
||||
const resp = await http.post("/api/screen-savers", payload);
|
||||
return resp.data.data as ScreenSaverVO;
|
||||
}
|
||||
|
||||
export async function updateScreenSaver(id: number, payload: Partial<ScreenSaverDTO>) {
|
||||
const resp = await http.put(`/api/screen-savers/${id}`, payload);
|
||||
return resp.data.data as ScreenSaverVO;
|
||||
}
|
||||
|
||||
export async function deleteScreenSaver(id: number) {
|
||||
const resp = await http.delete(`/api/screen-savers/${id}`);
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
|
||||
export async function uploadScreenSaverImage(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append("imageFile", file);
|
||||
const resp = await http.post("/api/screen-savers/upload-image", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
timeout: 600000
|
||||
});
|
||||
return resp.data.data as ScreenSaverUploadResult;
|
||||
}
|
||||
|
|
@ -203,7 +203,10 @@ export async function fetchLogs(params: any) {
|
|||
const resp = await http.get("/sys/api/logs", { params });
|
||||
return resp.data.data;
|
||||
}
|
||||
|
||||
export async function cleanLogs(logType: string) {
|
||||
const resp = await http.delete("/sys/api/logs/clean", { params: { logType } });
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
export async function fetchLogModules() {
|
||||
const resp = await http.get("/sys/api/logs/modules");
|
||||
return resp.data.data as string[];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
/* 列表表格容器 */
|
||||
.list-table-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 行选中样式 */
|
||||
|
|
|
|||
|
|
@ -16,12 +16,17 @@
|
|||
|
||||
.main-content {
|
||||
background: var(--bg-color-secondary);
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0;
|
||||
min-height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -834,18 +834,39 @@ body::after {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-content,
|
||||
.ant-table-wrapper .ant-table-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-body {
|
||||
overflow-y: auto !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination,
|
||||
.app-global-pagination.ant-pagination {
|
||||
margin: auto 0 0 0 !important;
|
||||
flex-shrink: 0;
|
||||
flex: 1 1 100%;
|
||||
flex: none;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 24px;
|
||||
background: var(--app-bg-card);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Avatar, Button, Card, Col, Drawer, Empty, Form, Input, List, Radio, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Tooltip, Tree, Typography, Row, App } from 'antd';
|
||||
import { Avatar, Button, Card, Col, Drawer, Empty, Form, Input, List, Pagination, Radio, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Tooltip, Tree, Typography, Row } from "antd";
|
||||
import type { DataNode } from "antd/es/tree";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
|
|
@ -35,7 +35,6 @@ import {
|
|||
} from "@/api";
|
||||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
|
||||
|
|
@ -167,7 +166,6 @@ function getDataScopeDescription(scopeType: string) {
|
|||
const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`;
|
||||
|
||||
export default function Roles() {
|
||||
const { message } = App.useApp();
|
||||
const { can } = usePermission();
|
||||
const { items: statusDict } = useDict("sys_common_status");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -434,7 +432,7 @@ export default function Roles() {
|
|||
<div className="roles-layout">
|
||||
<Row gutter={24} className="roles-layout__row">
|
||||
<Col span={7} className="roles-layout__side">
|
||||
<Card title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} variant="borderless" className="app-page__panel-card roles-side-card">
|
||||
<Card title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} bordered={false} className="app-page__panel-card roles-side-card">
|
||||
<div className="role-search-panel">
|
||||
{isPlatformMode && (
|
||||
<Select
|
||||
|
|
@ -494,11 +492,8 @@ export default function Roles() {
|
|||
/>
|
||||
</div>
|
||||
<div className="role-list-pagination">
|
||||
<AppPagination
|
||||
current={rolePage.current}
|
||||
pageSize={rolePage.size}
|
||||
total={rolePage.total}
|
||||
onChange={handleRolePageChange}
|
||||
<Pagination
|
||||
{...getStandardPagination(rolePage.total, rolePage.current, rolePage.size, handleRolePageChange, { size: "small", showSizeChanger: true, pageSizeOptions: ["10", "20", "50"] })}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -508,19 +503,12 @@ export default function Roles() {
|
|||
{selectedRole ? (
|
||||
<Card
|
||||
className="app-page__panel-card roles-detail-card"
|
||||
variant="borderless"
|
||||
bordered={false}
|
||||
title={<div className="role-detail-header"><div className="role-detail-icon"><SafetyCertificateOutlined /></div><div className="role-detail-heading"><div className="role-detail-title">{selectedRole.roleName}</div><Text type="secondary" className="role-detail-code">{selectedRole.roleCode}</Text></div></div>}
|
||||
extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handlePrimarySave} disabled={saveDisabled} style={{ borderRadius: "6px" }}>{saveLabel}</Button>}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as RoleTabKey)}
|
||||
className="role-detail-tabs"
|
||||
items={[
|
||||
{
|
||||
key: "permissions",
|
||||
label: <Space><KeyOutlined />{"功能权限"}</Space>,
|
||||
children: (
|
||||
<Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as RoleTabKey)} className="role-detail-tabs">
|
||||
<Tabs.TabPane tab={<Space><KeyOutlined />{"功能权限"}</Space>} key="permissions">
|
||||
<div className="role-detail-pane">
|
||||
<div className="permission-tree-wrapper">
|
||||
<Tree
|
||||
|
|
@ -539,12 +527,8 @@ export default function Roles() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "dataScope",
|
||||
label: <Space><ApartmentOutlined />{"数据权限"}</Space>,
|
||||
children: (
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope">
|
||||
<div className="role-detail-pane">
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Radio.Group value={dataScopeType} onChange={(event) => setDataScopeType(event.target.value)} optionType="button" buttonStyle="solid">
|
||||
|
|
@ -572,12 +556,8 @@ export default function Roles() {
|
|||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前范围不需要选择部门" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "users",
|
||||
label: <Space><TeamOutlined />{`成员管理 (${roleUsers.length})`}</Space>,
|
||||
children: (
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={<Space><TeamOutlined />{`成员管理 (${roleUsers.length})`}</Space>} key="users">
|
||||
<div className="role-detail-pane">
|
||||
<div className="role-members-toolbar">
|
||||
<Title level={5} style={{ margin: 0 }}>{"已绑定用户"}</Title>
|
||||
|
|
@ -588,7 +568,7 @@ export default function Roles() {
|
|||
size="small"
|
||||
loading={loadingUsers}
|
||||
dataSource={roleUsers}
|
||||
pagination={getStandardPagination(roleUsers.length, 1, 10)}
|
||||
pagination={{ ...getStandardPagination(roleUsers.length, 1, 10, undefined, { size: "small", showSizeChanger: false }), current: undefined }}
|
||||
columns={[
|
||||
{
|
||||
title: "用户信息",
|
||||
|
|
@ -617,10 +597,8 @@ export default function Roles() {
|
|||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="app-page__empty-state"><Empty description="请选择左侧角色查看详情" /></div>
|
||||
|
|
@ -629,14 +607,14 @@ export default function Roles() {
|
|||
</Row>
|
||||
</div>
|
||||
|
||||
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnHidden>
|
||||
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Input placeholder="搜索用户名或显示名称" prefix={<SearchOutlined />} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear />
|
||||
</div>
|
||||
<Table rowKey="userId" size="small" dataSource={filteredModalUsers} pagination={getStandardPagination(filteredModalUsers.length, 1, 6)} rowSelection={{ selectedRowKeys: selectedUserKeys, onChange: (keys) => setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} />
|
||||
<Table rowKey="userId" size="small" dataSource={filteredModalUsers} pagination={{ ...getStandardPagination(filteredModalUsers.length, 1, 6, undefined, { size: "small", showSizeChanger: false }), current: undefined }} rowSelection={{ selectedRowKeys: selectedUserKeys, onChange: (keys) => setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} />
|
||||
</Modal>
|
||||
|
||||
<Drawer title={editing ? "编辑角色" : "新增角色"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{"取消"}</Button><Button type="primary" loading={saving} onClick={() => void submitBasic()}>{"保存"}</Button></div>}>
|
||||
<Drawer title={editing ? "编辑角色" : "新增角色"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{"取消"}</Button><Button type="primary" loading={saving} onClick={() => void submitBasic()}>{"保存"}</Button></div>}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="租户" name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
|
||||
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} disabled={!!editing} />
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { Avatar, Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Switch, Table, Tag, TreeSelect, Typography, Upload, message,App } from "antd";
|
||||
import { Avatar, Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Switch, Tag, TreeSelect, Typography, Upload, message } from "antd";
|
||||
import type { DefaultOptionType } from "antd/es/select";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ApartmentOutlined, DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SearchOutlined, ShopOutlined, UploadOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { createUser, deleteUser, getUserDetail, listOrgs, listRoles, listTenants, listUserRoles, listUsers, saveUserRoles, updateUser, uploadPlatformAsset } from "@/api";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import { createUser, deleteUser, getUserDetail, listOrgs, listRoles, listTenants, listUserRoles, listUsers, updateUser, uploadPlatformAsset } from "@/api";
|
||||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types";
|
||||
import "./index.less";
|
||||
|
||||
|
|
@ -64,7 +64,6 @@ function MembershipOrgSelect({ fieldProps, name, tenantId }: { fieldProps: any;
|
|||
}
|
||||
|
||||
export default function Users() {
|
||||
const { message } = App.useApp();
|
||||
const { t } = useTranslation();
|
||||
const { can } = usePermission();
|
||||
const { items: statusDict } = useDict("sys_common_status");
|
||||
|
|
@ -398,9 +397,17 @@ export default function Users() {
|
|||
|
||||
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||
<Table rowKey="userId" columns={columns} dataSource={filteredData} loading={loading} size="middle" scroll={{ x: "max-content" }} pagination={false} />
|
||||
<ListTable rowKey="userId" columns={columns} dataSource={filteredData} loading={loading} scroll={{ y: "calc(100vh - 420px)" }} pagination={false} />
|
||||
</div>
|
||||
<AppPagination current={current} pageSize={pageSize} total={filteredData.length} onChange={(page, size) => { setCurrent(page); setPageSize(size); }} />
|
||||
<AppPagination
|
||||
current={current}
|
||||
pageSize={pageSize}
|
||||
total={filteredData.length}
|
||||
onChange={(page, size) => {
|
||||
setCurrent(page);
|
||||
setPageSize(size);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer title={<div className="user-drawer-title"><UserOutlined className="mr-2" aria-hidden="true" />{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { ClusterOutlined, KeyOutlined, SafetyCertificateOutlined, SaveOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "@/api";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import type { SysPermission, SysRole } from "@/types";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -219,7 +220,7 @@ export default function RolePermissionBinding() {
|
|||
onClick: () => setSelectedRoleId(record.roleId),
|
||||
className: "cursor-pointer"
|
||||
})}
|
||||
pagination={{ pageSize: 10, showTotal: (total) => t("common.total", { total }) }}
|
||||
pagination={{ ...getStandardPagination(filteredRoles.length, 1, 10, undefined, { showSizeChanger: false }), current: undefined }}
|
||||
columns={[
|
||||
{
|
||||
title: t("roles.roleName"),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { listRoles, listUserRoles, listUsers, saveUserRoles } from "@/api";
|
|||
import { SaveOutlined, SearchOutlined, TeamOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import type { SysRole, SysUser } from "@/types";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
|
@ -139,7 +140,7 @@ export default function UserRoleBinding() {
|
|||
onClick: () => setSelectedUserId(record.userId),
|
||||
className: "cursor-pointer"
|
||||
})}
|
||||
pagination={{ pageSize: 10, showTotal: (total) => t("common.total", { total }) }}
|
||||
pagination={{ ...getStandardPagination(filteredUsers.length, 1, 10, undefined, { showSizeChanger: false }), current: undefined }}
|
||||
columns={[
|
||||
{
|
||||
title: t("users.userInfo"),
|
||||
|
|
|
|||
|
|
@ -298,16 +298,29 @@ function formatPlayerTime(seconds: number) {
|
|||
return `${minutes.toString().padStart(2, '0')}:${remainSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => void }> = ({ meetingId, onComplete }) => {
|
||||
const MeetingProgressDisplay: React.FC<{
|
||||
meetingId: number;
|
||||
onComplete: () => void;
|
||||
onProgressUpdate?: (meeting: MeetingVO) => void;
|
||||
compact?: boolean;
|
||||
}> = ({ meetingId, onComplete, onProgressUpdate, compact }) => {
|
||||
const [progress, setProgress] = useState<MeetingProgress | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProgress = async () => {
|
||||
try {
|
||||
const res = await getMeetingProgress(meetingId);
|
||||
if (res.data?.data) {
|
||||
setProgress(res.data.data);
|
||||
if (res.data.data.percent === 100) {
|
||||
const [progressRes, detailRes] = await Promise.all([
|
||||
getMeetingProgress(meetingId),
|
||||
getMeetingDetail(meetingId),
|
||||
]);
|
||||
|
||||
if (detailRes.data?.data) {
|
||||
onProgressUpdate?.(detailRes.data.data);
|
||||
}
|
||||
|
||||
if (progressRes.data?.data) {
|
||||
setProgress(progressRes.data.data);
|
||||
if (progressRes.data.data.percent === 100) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
|
@ -319,7 +332,7 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
|
|||
fetchProgress();
|
||||
const timer = setInterval(fetchProgress, 3000);
|
||||
return () => clearInterval(timer);
|
||||
}, [meetingId, onComplete]);
|
||||
}, [meetingId, onComplete, onProgressUpdate]);
|
||||
|
||||
const percent = progress?.percent || 0;
|
||||
const isError = percent < 0;
|
||||
|
|
@ -332,6 +345,41 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
|
|||
return remainSeconds > 0 ? `${minutes} 分 ${remainSeconds} 秒` : `${minutes} 分钟`;
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '40px 20px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Title level={4} style={{ marginBottom: 24 }}>
|
||||
AI 智能总结中
|
||||
</Title>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={isError ? 100 : percent}
|
||||
status={isError ? 'exception' : percent === 100 ? 'success' : 'active'}
|
||||
strokeColor={isError ? '#ff4d4f' : { '0%': '#6c73ff', '100%': '#8d63ff' }}
|
||||
size={140}
|
||||
strokeWidth={10}
|
||||
/>
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Text strong style={{ fontSize: 16, color: isError ? '#ff4d4f' : '#5d67ff', display: 'block', marginBottom: 4 }}>
|
||||
{progress?.message || '正在分析内容...'}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
预计剩余:{isError ? '--' : formatEta(progress?.eta)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -368,19 +416,25 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
|
|||
<Col span={8}>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text type="secondary">当前进度</Text>
|
||||
<Title level={4} style={{ margin: 0 }}>{isError ? 'ERROR' : `${percent}%`}</Title>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
{isError ? 'ERROR' : `${percent}%`}
|
||||
</Title>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text type="secondary">预计剩余</Text>
|
||||
<Title level={4} style={{ margin: 0 }}>{isError ? '--' : formatEta(progress?.eta)}</Title>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
{isError ? '--' : formatEta(progress?.eta)}
|
||||
</Title>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text type="secondary">任务状态</Text>
|
||||
<Title level={4} style={{ margin: 0, color: isError ? '#ff4d4f' : '#52c41a' }}>{isError ? '已中断' : '正常'}</Title>
|
||||
<Title level={4} style={{ margin: 0, color: isError ? '#ff4d4f' : '#52c41a' }}>
|
||||
{isError ? '已中断' : '正常'}
|
||||
</Title>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
@ -611,6 +665,18 @@ const MeetingDetail: React.FC = () => {
|
|||
const [showFloatingTranscriptPlayer, setShowFloatingTranscriptPlayer] = useState(false);
|
||||
const [floatingTranscriptPlayerLayout, setFloatingTranscriptPlayerLayout] = useState<{ left: number; width: number } | null>(null);
|
||||
|
||||
const fetchData = useCallback(async (meetingId: number) => {
|
||||
try {
|
||||
const [detailRes, transcriptRes] = await Promise.all([getMeetingDetail(meetingId), getTranscripts(meetingId)]);
|
||||
setMeeting(detailRes.data.data);
|
||||
setTranscripts(transcriptRes.data.data || []);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const analysis = useMemo(
|
||||
() => buildMeetingAnalysis(meeting?.analysis, meeting?.summaryContent, meeting?.tags || ''),
|
||||
[meeting?.analysis, meeting?.summaryContent, meeting?.tags],
|
||||
|
|
@ -703,14 +769,14 @@ const MeetingDetail: React.FC = () => {
|
|||
root?.removeEventListener('scroll', updateFloatingPlayerState);
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, [meeting?.audioUrl]);
|
||||
}, [meeting?.audioUrl, meeting?.status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
fetchData(Number(id));
|
||||
loadAiConfigs();
|
||||
loadUsers();
|
||||
}, [id]);
|
||||
}, [id, fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedKeywords((current) => current.filter((item) => analysis.keywords.includes(item)));
|
||||
|
|
@ -731,7 +797,6 @@ const MeetingDetail: React.FC = () => {
|
|||
setSharePasswordDraft(normalizedPassword);
|
||||
}, [sharePopoverOpen, meeting?.accessPassword]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return undefined;
|
||||
|
|
@ -763,19 +828,7 @@ const MeetingDetail: React.FC = () => {
|
|||
audio.removeEventListener('pause', handlePause);
|
||||
audio.removeEventListener('ended', handleEnded);
|
||||
};
|
||||
}, [meeting?.audioUrl, audioPlaybackRate]);
|
||||
|
||||
const fetchData = useCallback(async (meetingId: number) => {
|
||||
try {
|
||||
const [detailRes, transcriptRes] = await Promise.all([getMeetingDetail(meetingId), getTranscripts(meetingId)]);
|
||||
setMeeting(detailRes.data.data);
|
||||
setTranscripts(transcriptRes.data.data || []);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [meeting?.audioUrl, audioPlaybackRate, meeting?.status]);
|
||||
|
||||
const loadAiConfigs = async () => {
|
||||
try {
|
||||
|
|
@ -1354,8 +1407,16 @@ const MeetingDetail: React.FC = () => {
|
|||
</Card>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
{meeting.status === 1 || meeting.status === 2 ? (
|
||||
<MeetingProgressDisplay meetingId={meeting.id} onComplete={() => fetchData(meeting.id)} />
|
||||
{meeting.status === 1 ? (
|
||||
<MeetingProgressDisplay
|
||||
meetingId={meeting.id}
|
||||
onComplete={() => fetchData(meeting.id)}
|
||||
onProgressUpdate={(updated) => {
|
||||
if (updated.status !== meeting.status) {
|
||||
void fetchData(updated.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Row gutter={24} style={{ height: '100%' }}>
|
||||
<Col xs={24} lg={14} style={{ height: '100%' }}>
|
||||
|
|
@ -1618,7 +1679,15 @@ const MeetingDetail: React.FC = () => {
|
|||
styles={{ body: { padding: 24, height: '100%', overflowY: 'auto', overflowX: 'hidden', minWidth: 0 } }}
|
||||
>
|
||||
<div ref={summaryPdfRef} className="markdown-shell">
|
||||
{meeting.summaryContent ? (
|
||||
{meeting.status === 2 ? (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<MeetingProgressDisplay
|
||||
meetingId={meeting.id}
|
||||
onComplete={() => fetchData(meeting.id)}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
) : meeting.summaryContent ? (
|
||||
isEditingSummary ? (
|
||||
<Input.TextArea
|
||||
value={summaryDraft}
|
||||
|
|
@ -1632,14 +1701,7 @@ const MeetingDetail: React.FC = () => {
|
|||
)
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', marginTop: 100 }}>
|
||||
{meeting.status === 2 ? (
|
||||
<Space direction="vertical">
|
||||
<LoadingOutlined style={{ fontSize: 24 }} spin />
|
||||
<Text type="secondary">正在重新总结...</Text>
|
||||
</Space>
|
||||
) : (
|
||||
<Empty description="暂无总结" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
.screen-saver-page {
|
||||
--screen-saver-border: rgba(15, 23, 42, 0.08);
|
||||
--screen-saver-shadow: 0 18px 45px rgba(15, 23, 42, 0.08);
|
||||
--screen-saver-accent: #1677ff;
|
||||
--screen-saver-dark: #10233f;
|
||||
--screen-saver-muted: #5b6b84;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-card {
|
||||
border: 1px solid var(--screen-saver-border);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: var(--screen-saver-shadow);
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-card .ant-card-head {
|
||||
padding: 0 24px;
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-card .ant-card-head-wrapper {
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-card .ant-card-head-title {
|
||||
color: var(--screen-saver-dark);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-card .ant-card-extra {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
padding: 24px 24px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-wrap .ant-table-wrapper,
|
||||
.screen-saver-page .screen-saver-table-wrap .ant-spin-nested-loading,
|
||||
.screen-saver-page .screen-saver-table-wrap .ant-spin-container,
|
||||
.screen-saver-page .screen-saver-table-wrap .ant-table,
|
||||
.screen-saver-page .screen-saver-table-wrap .ant-table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-wrap .ant-table-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-preview-pill,
|
||||
.screen-saver-drawer .screen-saver-preview-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--screen-saver-accent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-visual {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-thumb {
|
||||
width: 120px;
|
||||
aspect-ratio: 8 / 5;
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
background: linear-gradient(135deg, rgba(222, 231, 245, 0.9), rgba(241, 245, 251, 0.92));
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.screen-saver-drawer .screen-saver-drawer__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.screen-saver-drawer .screen-saver-preview-card .ant-card-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.screen-saver-drawer .screen-saver-preview-card .ant-space {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screen-saver-drawer .screen-saver-preview-card .ant-space-item:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.screen-saver-drawer .screen-saver-preview-card {
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
background: linear-gradient(180deg, rgba(248, 250, 255, 0.98), rgba(255, 255, 255, 1));
|
||||
}
|
||||
|
||||
.screen-saver-drawer .screen-saver-preview-stage {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
aspect-ratio: 8 / 5;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(140deg, rgba(11, 24, 48, 0.92), rgba(34, 59, 102, 0.88));
|
||||
}
|
||||
|
||||
.screen-saver-drawer .screen-saver-preview-stage::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.18));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.screen-saver-drawer .screen-saver-preview-stage img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal .ant-modal-content {
|
||||
overflow: hidden;
|
||||
border-radius: 26px;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal .ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) 320px;
|
||||
min-height: 620px;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding: 28px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(22, 119, 255, 0.18), transparent 28%),
|
||||
linear-gradient(160deg, #081326, #12284b 55%, #17315b 100%);
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__stage-head {
|
||||
color: rgba(233, 242, 252, 0.92);
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__stage-head h3 {
|
||||
margin: 0 0 8px;
|
||||
color: #f8fbff;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__stage-head p {
|
||||
margin: 0;
|
||||
color: rgba(223, 233, 247, 0.72);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__viewport {
|
||||
position: relative;
|
||||
width: min(100%, 640px);
|
||||
aspect-ratio: 8 / 5;
|
||||
align-self: center;
|
||||
overflow: hidden;
|
||||
border-radius: 30px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(6, 13, 28, 0.78);
|
||||
box-shadow: 0 24px 50px rgba(5, 12, 25, 0.4);
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__viewport.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__viewport::before,
|
||||
.screen-saver-crop-modal__viewport::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__viewport::before {
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__viewport::after {
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.14) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.14) 1px, transparent 1px);
|
||||
background-size: 33.333% 50%;
|
||||
opacity: 0.36;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__image {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: rgba(232, 241, 252, 0.72);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding: 28px 24px;
|
||||
background: linear-gradient(180deg, #ffffff, #f7f9fd);
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__sidebar-card {
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__sidebar-card h4 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--screen-saver-dark);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__sidebar-card p {
|
||||
margin: 0;
|
||||
color: var(--screen-saver-muted);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.screen-saver-crop-modal__layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__stage {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.screen-saver-crop-modal__sidebar {
|
||||
padding: 20px 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.screen-saver-page .screen-saver-table-card .ant-card-head {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-card .ant-card-head-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-card .ant-card-extra {
|
||||
margin-inline-start: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-card .ant-card-extra .ant-space {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-wrap {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-preview-stage {
|
||||
width: min(100%, 360px);
|
||||
max-height: 225px;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-visual {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.screen-saver-page .screen-saver-table-thumb {
|
||||
width: 88px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,866 @@
|
|||
import "./ScreenSaverManagement.css";
|
||||
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Select,
|
||||
Slider,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Upload,
|
||||
} from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PictureOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SaveOutlined,
|
||||
ScissorOutlined,
|
||||
SearchOutlined,
|
||||
TeamOutlined,
|
||||
UploadOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { UploadProps } from "antd";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import {
|
||||
createScreenSaver,
|
||||
deleteScreenSaver,
|
||||
listScreenSavers,
|
||||
type ScreenSaverDTO,
|
||||
type ScreenSaverScopeType,
|
||||
type ScreenSaverUploadResult,
|
||||
type ScreenSaverVO,
|
||||
updateScreenSaver,
|
||||
uploadScreenSaverImage,
|
||||
} from "@/api/business/screenSaver";
|
||||
import { listUsers } from "@/api";
|
||||
import type { SysUser } from "@/types";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const CROP_WIDTH = 1280;
|
||||
const CROP_HEIGHT = 800;
|
||||
const VIEWPORT_WIDTH = 640;
|
||||
const VIEWPORT_HEIGHT = 400;
|
||||
const ALLOWED_TYPES = new Map<string, "image/jpeg" | "image/png">([
|
||||
["image/jpeg", "image/jpeg"],
|
||||
["image/jpg", "image/jpeg"],
|
||||
["image/png", "image/png"],
|
||||
]);
|
||||
|
||||
type ScreenSaverFormValues = {
|
||||
scopeType: ScreenSaverScopeType;
|
||||
ownerUserId?: number;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
description?: string;
|
||||
displayDurationSec: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
imageFormat: string;
|
||||
sortOrder?: number;
|
||||
statusEnabled: boolean;
|
||||
remark?: string;
|
||||
};
|
||||
|
||||
type CropModalState = {
|
||||
open: boolean;
|
||||
src: string;
|
||||
fileName: string;
|
||||
mimeType: "image/jpeg" | "image/png";
|
||||
};
|
||||
|
||||
type DragState = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
originX: number;
|
||||
originY: number;
|
||||
};
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function normalizeOwnerLabel(user?: SysUser) {
|
||||
if (!user) {
|
||||
return "未指定";
|
||||
}
|
||||
return user.displayName || user.username || `用户 ${user.userId}`;
|
||||
}
|
||||
|
||||
function getImageFormatLabel(format?: string) {
|
||||
if (!format) {
|
||||
return "-";
|
||||
}
|
||||
return format.toUpperCase();
|
||||
}
|
||||
|
||||
function validateImageFile(file: File) {
|
||||
const normalizedType = ALLOWED_TYPES.get(file.type.toLowerCase());
|
||||
if (!normalizedType) {
|
||||
throw new Error("仅支持 jpg、jpeg、png 图片");
|
||||
}
|
||||
return normalizedType;
|
||||
}
|
||||
|
||||
async function readFileAsDataUrl(file: File) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result || ""));
|
||||
reader.onerror = () => reject(new Error("读取图片失败"));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function createImage(src: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error("图片加载失败"));
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
type CropDialogProps = {
|
||||
state: CropModalState;
|
||||
onCancel: () => void;
|
||||
onConfirm: (file: File) => Promise<void>;
|
||||
};
|
||||
|
||||
function ScreenSaverCropDialog({ state, onCancel, onConfirm }: CropDialogProps) {
|
||||
const { message } = App.useApp();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [naturalSize, setNaturalSize] = useState({ width: 0, height: 0 });
|
||||
const [minZoom, setMinZoom] = useState(1);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
const dragRef = useRef<DragState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.open) {
|
||||
return;
|
||||
}
|
||||
let active = true;
|
||||
void createImage(state.src)
|
||||
.then((image) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
const nextMinZoom = Math.max(VIEWPORT_WIDTH / image.width, VIEWPORT_HEIGHT / image.height);
|
||||
setNaturalSize({ width: image.width, height: image.height });
|
||||
setMinZoom(nextMinZoom);
|
||||
setZoom(nextMinZoom);
|
||||
setOffset({ x: 0, y: 0 });
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
message.error(error.message || "图片加载失败");
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [message, state.open, state.src]);
|
||||
|
||||
const displaySize = useMemo(() => ({
|
||||
width: naturalSize.width * zoom,
|
||||
height: naturalSize.height * zoom,
|
||||
}), [naturalSize, zoom]);
|
||||
|
||||
const clampOffset = (x: number, y: number, currentZoom = zoom) => {
|
||||
const width = naturalSize.width * currentZoom;
|
||||
const height = naturalSize.height * currentZoom;
|
||||
const maxX = Math.max(0, (width - VIEWPORT_WIDTH) / 2);
|
||||
const maxY = Math.max(0, (height - VIEWPORT_HEIGHT) / 2);
|
||||
return {
|
||||
x: clamp(x, -maxX, maxX),
|
||||
y: clamp(y, -maxY, maxY),
|
||||
};
|
||||
};
|
||||
|
||||
const handleZoomChange = (nextZoom: number) => {
|
||||
const nextOffset = clampOffset(offset.x, offset.y, nextZoom);
|
||||
setZoom(nextZoom);
|
||||
setOffset(nextOffset);
|
||||
};
|
||||
|
||||
const beginDrag = (clientX: number, clientY: number) => {
|
||||
dragRef.current = {
|
||||
startX: clientX,
|
||||
startY: clientY,
|
||||
originX: offset.x,
|
||||
originY: offset.y,
|
||||
};
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
const updateDrag = (clientX: number, clientY: number) => {
|
||||
if (!dragRef.current) {
|
||||
return;
|
||||
}
|
||||
const deltaX = clientX - dragRef.current.startX;
|
||||
const deltaY = clientY - dragRef.current.startY;
|
||||
setOffset(clampOffset(dragRef.current.originX + deltaX, dragRef.current.originY + deltaY));
|
||||
};
|
||||
|
||||
const endDrag = () => {
|
||||
dragRef.current = null;
|
||||
setDragging(false);
|
||||
};
|
||||
|
||||
const exportCroppedFile = async () => {
|
||||
const image = await createImage(state.src);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = CROP_WIDTH;
|
||||
canvas.height = CROP_HEIGHT;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("浏览器不支持图片裁剪");
|
||||
}
|
||||
const previewScale = CROP_WIDTH / VIEWPORT_WIDTH;
|
||||
const exportedWidth = image.width * zoom * previewScale;
|
||||
const exportedHeight = image.height * zoom * previewScale;
|
||||
const drawX = CROP_WIDTH / 2 - exportedWidth / 2 + offset.x * previewScale;
|
||||
const drawY = CROP_HEIGHT / 2 - exportedHeight / 2 + offset.y * previewScale;
|
||||
context.drawImage(image, drawX, drawY, exportedWidth, exportedHeight);
|
||||
const extension = state.mimeType === "image/png" ? "png" : "jpg";
|
||||
const fileName = state.fileName.replace(/\.[^.]+$/, "") + `_1280x800.${extension}`;
|
||||
const blob = await new Promise<Blob | null>((resolve) => {
|
||||
if (state.mimeType === "image/png") {
|
||||
canvas.toBlob(resolve, "image/png");
|
||||
} else {
|
||||
canvas.toBlob(resolve, "image/jpeg", 0.92);
|
||||
}
|
||||
});
|
||||
if (!blob) {
|
||||
throw new Error("导出裁剪图片失败");
|
||||
}
|
||||
return new File([blob], fileName, { type: state.mimeType });
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const file = await exportCroppedFile();
|
||||
await onConfirm(file);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "裁剪上传失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={state.open}
|
||||
onCancel={loading ? undefined : onCancel}
|
||||
footer={null}
|
||||
width={1100}
|
||||
centered
|
||||
destroyOnHidden
|
||||
className="screen-saver-crop-modal"
|
||||
maskClosable={!loading}
|
||||
closable={!loading}
|
||||
>
|
||||
<div className="screen-saver-crop-modal__layout">
|
||||
<div className="screen-saver-crop-modal__stage">
|
||||
<div className="screen-saver-crop-modal__stage-head">
|
||||
<h3>裁剪成屏保成品图</h3>
|
||||
<p>请在固定 8:5 取景框内调整画面位置。导出尺寸固定为 1280 × 800,安卓端将直接使用该成品图展示。</p>
|
||||
</div>
|
||||
<div
|
||||
className={`screen-saver-crop-modal__viewport${dragging ? " is-dragging" : ""}`}
|
||||
onMouseDown={(event) => beginDrag(event.clientX, event.clientY)}
|
||||
onMouseMove={(event) => updateDrag(event.clientX, event.clientY)}
|
||||
onMouseUp={endDrag}
|
||||
onMouseLeave={endDrag}
|
||||
onTouchStart={(event) => {
|
||||
const touch = event.touches[0];
|
||||
if (touch) {
|
||||
beginDrag(touch.clientX, touch.clientY);
|
||||
}
|
||||
}}
|
||||
onTouchMove={(event) => {
|
||||
const touch = event.touches[0];
|
||||
if (touch) {
|
||||
updateDrag(touch.clientX, touch.clientY);
|
||||
}
|
||||
}}
|
||||
onTouchEnd={endDrag}
|
||||
>
|
||||
{state.src ? (
|
||||
<img
|
||||
src={state.src}
|
||||
alt="待裁剪屏保"
|
||||
className="screen-saver-crop-modal__image"
|
||||
draggable={false}
|
||||
style={{
|
||||
width: displaySize.width,
|
||||
height: displaySize.height,
|
||||
transform: `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="screen-saver-crop-modal__meta">
|
||||
<span>原图:{naturalSize.width || "-"} × {naturalSize.height || "-"}</span>
|
||||
<span>输出:{CROP_WIDTH} × {CROP_HEIGHT}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="screen-saver-crop-modal__sidebar">
|
||||
<div className="screen-saver-crop-modal__sidebar-card">
|
||||
<h4>缩放与构图</h4>
|
||||
<p>拖动画面调整主体位置。保留足够安全边距,避免标题、人物或徽标在不同设备上被视觉切边。</p>
|
||||
<div style={{ marginTop: 18 }}>
|
||||
<Text type="secondary">缩放</Text>
|
||||
<Slider
|
||||
min={minZoom}
|
||||
max={Math.max(minZoom * 3, minZoom + 0.2)}
|
||||
step={0.01}
|
||||
value={zoom}
|
||||
tooltip={{ formatter: (value) => `${Math.round((value || 1) / minZoom * 100)}%` }}
|
||||
onChange={handleZoomChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="screen-saver-crop-modal__sidebar-card">
|
||||
<h4>交付标准</h4>
|
||||
<p>仅支持 JPG / JPEG / PNG。导出的屏保图片会以 1280 × 800 固定尺寸上传,后端仅做格式与尺寸校验,不再重新裁剪。</p>
|
||||
</div>
|
||||
<div className="screen-saver-crop-modal__footer">
|
||||
<Button onClick={onCancel} disabled={loading}>取消</Button>
|
||||
<Button type="primary" icon={<ScissorOutlined />} loading={loading} onClick={() => void handleConfirm()}>
|
||||
生成并上传
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ScreenSaverManagement() {
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm<ScreenSaverFormValues>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<ScreenSaverVO | null>(null);
|
||||
const [records, setRecords] = useState<ScreenSaverVO[]>([]);
|
||||
const [users, setUsers] = useState<SysUser[]>([]);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | "enabled" | "disabled">("all");
|
||||
const [scopeFilter, setScopeFilter] = useState<"all" | ScreenSaverScopeType>("all");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [cropState, setCropState] = useState<CropModalState>({
|
||||
open: false,
|
||||
src: "",
|
||||
fileName: "",
|
||||
mimeType: "image/jpeg",
|
||||
});
|
||||
|
||||
const userMap = useMemo(() => {
|
||||
return new Map<number, SysUser>(users.map((user) => [user.userId, user]));
|
||||
}, [users]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [screenSavers, userList] = await Promise.all([
|
||||
listScreenSavers(),
|
||||
listUsers().catch(() => [] as SysUser[]),
|
||||
]);
|
||||
setRecords(screenSavers || []);
|
||||
setUsers(userList || []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
}, []);
|
||||
|
||||
const filteredRecords = useMemo(() => {
|
||||
const keyword = searchValue.trim().toLowerCase();
|
||||
return records.filter((item) => {
|
||||
if (scopeFilter !== "all" && item.scopeType !== scopeFilter) {
|
||||
return false;
|
||||
}
|
||||
if (statusFilter === "enabled" && item.status !== 1) {
|
||||
return false;
|
||||
}
|
||||
if (statusFilter === "disabled" && item.status === 1) {
|
||||
return false;
|
||||
}
|
||||
if (!keyword) {
|
||||
return true;
|
||||
}
|
||||
const ownerName = normalizeOwnerLabel(item.ownerUserId ? userMap.get(item.ownerUserId) : undefined);
|
||||
return [item.name, item.description, item.creatorUsername, ownerName, item.imageFormat]
|
||||
.some((field) => String(field || "").toLowerCase().includes(keyword));
|
||||
});
|
||||
}, [records, scopeFilter, statusFilter, searchValue, userMap]);
|
||||
|
||||
const pagedRecords = useMemo(() => {
|
||||
const start = (page - 1) * pageSize;
|
||||
return filteredRecords.slice(start, start + pageSize);
|
||||
}, [filteredRecords, page, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [searchValue, statusFilter, scopeFilter]);
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
scopeType: "PLATFORM",
|
||||
displayDurationSec: 15,
|
||||
sortOrder: 0,
|
||||
statusEnabled: true,
|
||||
imageWidth: CROP_WIDTH,
|
||||
imageHeight: CROP_HEIGHT,
|
||||
imageFormat: "jpg",
|
||||
});
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (record: ScreenSaverVO) => {
|
||||
setEditing(record);
|
||||
form.setFieldsValue({
|
||||
scopeType: record.scopeType,
|
||||
ownerUserId: record.ownerUserId || undefined,
|
||||
name: record.name,
|
||||
imageUrl: record.imageUrl,
|
||||
description: record.description,
|
||||
displayDurationSec: record.displayDurationSec,
|
||||
imageWidth: record.imageWidth || CROP_WIDTH,
|
||||
imageHeight: record.imageHeight || CROP_HEIGHT,
|
||||
imageFormat: record.imageFormat || "jpg",
|
||||
sortOrder: record.sortOrder,
|
||||
statusEnabled: record.status === 1,
|
||||
remark: record.remark,
|
||||
});
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (record: ScreenSaverVO) => {
|
||||
await deleteScreenSaver(record.id);
|
||||
message.success("屏保已删除");
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const values = await form.validateFields();
|
||||
const payload: ScreenSaverDTO = {
|
||||
scopeType: values.scopeType,
|
||||
ownerUserId: values.scopeType === "USER" ? values.ownerUserId ?? null : null,
|
||||
name: values.name.trim(),
|
||||
imageUrl: values.imageUrl.trim(),
|
||||
description: values.description?.trim(),
|
||||
displayDurationSec: values.displayDurationSec,
|
||||
imageWidth: values.imageWidth,
|
||||
imageHeight: values.imageHeight,
|
||||
imageFormat: values.imageFormat.trim().toLowerCase(),
|
||||
sortOrder: values.sortOrder,
|
||||
status: values.statusEnabled ? 1 : 0,
|
||||
remark: values.remark?.trim(),
|
||||
};
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (editing) {
|
||||
await updateScreenSaver(editing.id, payload);
|
||||
message.success("屏保已更新");
|
||||
} else {
|
||||
await createScreenSaver(payload);
|
||||
message.success("屏保已创建");
|
||||
}
|
||||
setDrawerOpen(false);
|
||||
await loadData();
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openCropper = async (file: File) => {
|
||||
const mimeType = validateImageFile(file);
|
||||
const src = await readFileAsDataUrl(file);
|
||||
setCropState({
|
||||
open: true,
|
||||
src,
|
||||
fileName: file.name,
|
||||
mimeType,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadCroppedImage = async (file: File) => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const result: ScreenSaverUploadResult = await uploadScreenSaverImage(file);
|
||||
form.setFieldsValue({
|
||||
imageUrl: result.imageUrl,
|
||||
imageWidth: result.imageWidth,
|
||||
imageHeight: result.imageHeight,
|
||||
imageFormat: result.imageFormat,
|
||||
});
|
||||
setCropState({ open: false, src: "", fileName: "", mimeType: "image/jpeg" });
|
||||
message.success("屏保图片已上传");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadProps: UploadProps = {
|
||||
showUploadList: false,
|
||||
beforeUpload: (file) => {
|
||||
void openCropper(file as File).catch((error: Error) => {
|
||||
message.error(error.message || "图片处理失败");
|
||||
});
|
||||
return Upload.LIST_IGNORE;
|
||||
},
|
||||
};
|
||||
|
||||
const handleToggleStatus = async (record: ScreenSaverVO, checked: boolean) => {
|
||||
await updateScreenSaver(record.id, { status: checked ? 1 : 0 });
|
||||
message.success(checked ? "屏保已启用" : "屏保已停用");
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const columns: ColumnsType<ScreenSaverVO> = [
|
||||
{
|
||||
title: "屏保画面",
|
||||
key: "visual",
|
||||
width: 330,
|
||||
render: (_, record) => (
|
||||
<div className="screen-saver-table-visual">
|
||||
<div className="screen-saver-table-thumb">
|
||||
{record.imageUrl ? <img src={record.imageUrl} alt={record.name} /> : null}
|
||||
</div>
|
||||
<Space direction="vertical" size={3}>
|
||||
<Text strong>{record.name}</Text>
|
||||
<Text type="secondary">{record.description || "暂无描述"}</Text>
|
||||
<Space wrap size={[8, 6]}>
|
||||
<span className="screen-saver-preview-pill">{getImageFormatLabel(record.imageFormat)}</span>
|
||||
<span className="screen-saver-preview-pill">{record.imageWidth || CROP_WIDTH} × {record.imageHeight || CROP_HEIGHT}</span>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "作用域",
|
||||
key: "scope",
|
||||
width: 220,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={4}>
|
||||
{record.scopeType === "USER" ? (
|
||||
<Tag color="gold" icon={<UserOutlined />}>用户级</Tag>
|
||||
) : (
|
||||
<Tag color="blue" icon={<TeamOutlined />}>平台级</Tag>
|
||||
)}
|
||||
<Text type="secondary">
|
||||
{record.scopeType === "USER"
|
||||
? `归属:${normalizeOwnerLabel(record.ownerUserId ? userMap.get(record.ownerUserId) : undefined)}`
|
||||
: "全平台共用"}
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "播放与状态",
|
||||
key: "status",
|
||||
width: 210,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={6}>
|
||||
<Text>{record.displayDurationSec} 秒 / 张</Text>
|
||||
<Text type="secondary">排序值:{record.sortOrder ?? 0}</Text>
|
||||
<Switch size="small" checked={record.status === 1} onChange={(checked) => void handleToggleStatus(record, checked)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建信息",
|
||||
key: "creator",
|
||||
width: 180,
|
||||
render: (_, record) => {
|
||||
const timeValue = record.updatedAt || record.createdAt;
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text>{record.creatorUsername || "-"}</Text>
|
||||
<Text type="secondary">
|
||||
{timeValue ? dayjs(timeValue).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
</Text>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 140,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space size={4}>
|
||||
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
|
||||
<Popconfirm title="确认删除该屏保吗?" onConfirm={() => void handleDelete(record)}>
|
||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const currentImageUrl = Form.useWatch("imageUrl", form);
|
||||
const currentScopeType = Form.useWatch("scopeType", form) || "PLATFORM";
|
||||
const currentOwnerUserId = Form.useWatch("ownerUserId", form);
|
||||
|
||||
return (
|
||||
<div className="app-page screen-saver-page">
|
||||
<Card
|
||||
className="app-page__content-card shadow-sm screen-saver-table-card"
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
||||
title="屏保管理"
|
||||
extra={(
|
||||
<Space wrap>
|
||||
<Input
|
||||
placeholder="搜索名称、描述、创建人或归属用户"
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
style={{ width: 280 }}
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
/>
|
||||
<Select
|
||||
value={scopeFilter}
|
||||
style={{ width: 140 }}
|
||||
onChange={(value) => setScopeFilter(value)}
|
||||
options={[
|
||||
{ label: "全部作用域", value: "all" },
|
||||
{ label: "平台级", value: "PLATFORM" },
|
||||
{ label: "用户级", value: "USER" },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
style={{ width: 140 }}
|
||||
onChange={(value) => setStatusFilter(value)}
|
||||
options={[
|
||||
{ label: "全部状态", value: "all" },
|
||||
{ label: "已启用", value: "enabled" },
|
||||
{ label: "已停用", value: "disabled" },
|
||||
]}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新增屏保
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
>
|
||||
<div className="app-page__table-wrap screen-saver-table-wrap">
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={pagedRecords}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
locale={{
|
||||
emptyText: <Empty description="暂无屏保素材" />,
|
||||
}}
|
||||
scroll={{ x: 1100, y: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<AppPagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={filteredRecords.length}
|
||||
onChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage);
|
||||
setPageSize(nextSize);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
title={editing ? "编辑屏保" : "新增屏保"}
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
width={760}
|
||||
destroyOnHidden
|
||||
forceRender
|
||||
className="screen-saver-drawer"
|
||||
styles={{ body: { padding: 24 } }}
|
||||
footer={(
|
||||
<div className="screen-saver-drawer__footer">
|
||||
<Button onClick={() => setDrawerOpen(false)}>取消</Button>
|
||||
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => void handleSubmit()}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Form form={form} layout="vertical" className="screen-saver-drawer__form">
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={14}>
|
||||
<Form.Item name="name" label="屏保名称" rules={[{ required: true, message: "请输入屏保名称" }]}>
|
||||
<Input placeholder="例如:大厅欢迎屏、品牌发布屏" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={10}>
|
||||
<Form.Item name="displayDurationSec" label="展示时长(秒)" rules={[{ required: true, message: "请输入展示时长" }]}>
|
||||
<InputNumber min={3} max={3600} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="scopeType" label="作用域" rules={[{ required: true, message: "请选择作用域" }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "平台级(全平台共用)", value: "PLATFORM" },
|
||||
{ label: "用户级(指定用户优先)", value: "USER" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="ownerUserId"
|
||||
label="归属用户"
|
||||
rules={currentScopeType === "USER" ? [{ required: true, message: "请选择归属用户" }] : []}
|
||||
>
|
||||
<Select
|
||||
disabled={currentScopeType !== "USER"}
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={currentScopeType === "USER" ? "请选择用户" : "平台级无需选择"}
|
||||
optionFilterProp="label"
|
||||
options={users.map((user) => ({
|
||||
value: user.userId,
|
||||
label: `${normalizeOwnerLabel(user)} / ${user.username}`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
className="screen-saver-preview-card"
|
||||
style={{ marginBottom: 18 }}
|
||||
styles={{ body: { padding: 18 } }}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<Space wrap style={{ width: "100%", justifyContent: "space-between" }}>
|
||||
<div>
|
||||
<Title level={5} style={{ margin: 0 }}>屏保成片预览</Title>
|
||||
<Text type="secondary">固定 8:5 构图,导出 1280 × 800。上传后后端只做校验与存储。</Text>
|
||||
</div>
|
||||
<Upload {...uploadProps}>
|
||||
<Button type="primary" icon={<UploadOutlined />} loading={uploading}>
|
||||
选择图片并裁剪
|
||||
</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
<div className="screen-saver-preview-stage">
|
||||
{currentImageUrl ? (
|
||||
<img src={currentImageUrl} alt="屏保预览" />
|
||||
) : (
|
||||
<div style={{ height: "100%", display: "grid", placeItems: "center", color: "rgba(235,244,255,.72)" }}>
|
||||
<Space direction="vertical" align="center" size={10}>
|
||||
<ScissorOutlined style={{ fontSize: 26 }} />
|
||||
<span>请选择图片并完成裁剪后上传</span>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Space wrap>
|
||||
<span className="screen-saver-preview-pill">输出规格 1280 × 800</span>
|
||||
<span className="screen-saver-preview-pill">当前作用域 {currentScopeType === "USER" ? "用户级" : "平台级"}</span>
|
||||
{currentScopeType === "USER" && currentOwnerUserId ? (
|
||||
<span className="screen-saver-preview-pill">
|
||||
归属 {normalizeOwnerLabel(userMap.get(currentOwnerUserId))}
|
||||
</span>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="imageFormat" label="图片格式" rules={[{ required: true, message: "请先上传图片" }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} md={8}>
|
||||
<Form.Item name="imageWidth" label="宽度">
|
||||
<InputNumber disabled style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} md={8}>
|
||||
<Form.Item name="imageHeight" label="高度">
|
||||
<InputNumber disabled style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="imageUrl" label="图片地址" rules={[{ required: true, message: "请先上传裁剪后的屏保图片" }]}>
|
||||
<Input placeholder="上传完成后自动回填" readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="描述">
|
||||
<TextArea rows={3} placeholder="描述这张屏保用于什么场景、展示何种品牌信息或氛围。" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="sortOrder" label="排序值">
|
||||
<InputNumber min={0} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input placeholder="例如:大厅屏、品牌发布期、用户专属欢迎页" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="statusEnabled" label="启用状态" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
|
||||
<ScreenSaverCropDialog
|
||||
state={cropState}
|
||||
onCancel={() => setCropState({ open: false, src: "", fileName: "", mimeType: "image/jpeg" })}
|
||||
onConfirm={handleUploadCroppedImage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { createTenant, deleteTenant, listTenants, updateTenant } from "@/api";
|
|||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
|
||||
import type { SysTenant } from "@/types";
|
||||
|
||||
|
|
@ -162,26 +163,28 @@ export default function Tenants() {
|
|||
</Space>
|
||||
</Card>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-2">
|
||||
<Card
|
||||
className="app-page__content-card shadow-sm"
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
||||
>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflowX: "hidden", overflowY: "auto", padding: "24px" }}>
|
||||
<List
|
||||
grid={{ gutter: 24, xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }}
|
||||
loading={loading}
|
||||
dataSource={data}
|
||||
renderItem={renderTenantCard}
|
||||
pagination={{
|
||||
total,
|
||||
current: params.current,
|
||||
pageSize: params.size,
|
||||
onChange: (page, size) => setParams({ ...params, current: page, size: size || params.size }),
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (count) => t("common.total", { total: count }),
|
||||
pageSizeOptions: ["10", "20", "50", "100"],
|
||||
style: { marginTop: "24px", marginBottom: "24px" }
|
||||
}}
|
||||
pagination={false}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("tenantsExt.emptyText")} /> }}
|
||||
/>
|
||||
</div>
|
||||
<AppPagination
|
||||
current={params.current}
|
||||
pageSize={params.size}
|
||||
total={total}
|
||||
onChange={(page, size) => setParams({ ...params, current: page, size: size || params.size })}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer title={<Space><ShopOutlined aria-hidden="true" /><span>{editing ? t("tenants.drawerTitleEdit") : t("tenants.drawerTitleCreate")}</span></Space>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={480} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
||||
<Form form={form} layout="vertical">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Card, Col, Drawer, Empty, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Table, Tag, Typography, App } from 'antd';
|
||||
import { Button, Card, Col, Drawer, Empty, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Table, Tag, Typography, message } from "antd";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BookOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ProfileOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
|
|
@ -13,7 +13,6 @@ import "./index.less";
|
|||
const { Text } = Typography;
|
||||
|
||||
export default function Dictionaries() {
|
||||
const { message } = App.useApp();
|
||||
const { t } = useTranslation();
|
||||
const { can } = usePermission();
|
||||
const { items: statusDict } = useDict("sys_common_status");
|
||||
|
|
@ -174,7 +173,12 @@ export default function Dictionaries() {
|
|||
rowKey="dictTypeId"
|
||||
loading={loadingTypes}
|
||||
dataSource={types}
|
||||
pagination={getStandardPagination(typeTotal, typeParams.current, typeParams.size, (page, size) => setTypeParams({ ...typeParams, current: page, size }))}
|
||||
pagination={{
|
||||
...getStandardPagination(typeTotal, typeParams.current, typeParams.size, (page, size) => setTypeParams({ ...typeParams, current: page, size })),
|
||||
simple: true,
|
||||
size: "small",
|
||||
position: ["bottomCenter"]
|
||||
}}
|
||||
size="small"
|
||||
showHeader={false}
|
||||
scroll={{ y: "calc(100vh - 480px)" }}
|
||||
|
|
@ -245,7 +249,7 @@ export default function Dictionaries() {
|
|||
</Col>
|
||||
</Row>
|
||||
|
||||
<Drawer title={<Space><BookOutlined aria-hidden="true" /><span>{editingType ? t("dicts.drawerTitleTypeEdit") : t("dicts.drawerTitleTypeCreate")}</span></Space>} open={typeDrawerVisible} onClose={() => setTypeDrawerVisible(false)} width={400} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setTypeDrawerVisible(false)}>{t("common.cancel")}</Button><Button type="primary" onClick={handleTypeSubmit}>{t("common.save")}</Button></div>}>
|
||||
<Drawer title={<Space><BookOutlined aria-hidden="true" /><span>{editingType ? t("dicts.drawerTitleTypeEdit") : t("dicts.drawerTitleTypeCreate")}</span></Space>} open={typeDrawerVisible} onClose={() => setTypeDrawerVisible(false)} width={400} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setTypeDrawerVisible(false)}>{t("common.cancel")}</Button><Button type="primary" onClick={handleTypeSubmit}>{t("common.save")}</Button></div>}>
|
||||
<Form form={typeForm} layout="vertical">
|
||||
<Form.Item label={t("dicts.typeCode")} name="typeCode" rules={[{ required: true, message: t("dicts.typeCode") }]}>
|
||||
<Input disabled={!!editingType} placeholder={t("dictsExt.typeCodePlaceholder")} />
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Button, Card, DatePicker, Descriptions, Input, Modal, Select, Space, Tabs, Tag, Typography } from "antd";
|
||||
import { Button, Card, DatePicker, Descriptions, Input, Modal, Popconfirm, Select, Space, Tabs, Tag, Typography, message } from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EyeOutlined, InfoCircleOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { fetchLogModules, fetchLogs } from "@/api";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import { DeleteOutlined, EyeOutlined, InfoCircleOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { cleanLogs, fetchLogModules, fetchLogs } from "@/api";
|
||||
import { useDict } from "@/hooks/useDict";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import type { SysLog, UserProfile } from "@/types";
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
|
@ -21,6 +21,7 @@ export default function Logs() {
|
|||
const [moduleOptions, setModuleOptions] = useState<string[]>([]);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [selectedLog, setSelectedLog] = useState<SysLog | null>(null);
|
||||
const [cleaning, setCleaning] = useState(false);
|
||||
const [params, setParams] = useState({
|
||||
current: 1,
|
||||
size: 20,
|
||||
|
|
@ -47,6 +48,11 @@ export default function Logs() {
|
|||
}, []);
|
||||
|
||||
const isPlatformAdmin = Boolean(userProfile?.isPlatformAdmin);
|
||||
const activeLogTypeLabel = useMemo(() => {
|
||||
const dictLabel = logTypeDict.find((item) => item.itemValue === activeTab)?.itemLabel;
|
||||
if (dictLabel) return dictLabel;
|
||||
return activeTab === "OPERATION" ? t("logs.opLog") : t("logs.loginLog");
|
||||
}, [activeTab, logTypeDict, t]);
|
||||
|
||||
const loadData = async (currentParams = params) => {
|
||||
setLoading(true);
|
||||
|
|
@ -71,10 +77,11 @@ export default function Logs() {
|
|||
fetchLogModules().then((items) => setModuleOptions(items || [])).catch(() => setModuleOptions([]));
|
||||
}, [activeTab]);
|
||||
|
||||
const handleTableChange = (_pagination: any, _filters: any, sorter: any) => {
|
||||
const handleTableChange = (pagination: any, _filters: any, sorter: any) => {
|
||||
setParams({
|
||||
...params,
|
||||
current: 1,
|
||||
current: pagination.current,
|
||||
size: pagination.pageSize,
|
||||
sortField: sorter.field || "createdAt",
|
||||
sortOrder: sorter.order || "descend"
|
||||
});
|
||||
|
|
@ -103,6 +110,19 @@ export default function Logs() {
|
|||
loadData(resetParams);
|
||||
};
|
||||
|
||||
const handleClean = async () => {
|
||||
setCleaning(true);
|
||||
try {
|
||||
await cleanLogs(activeTab);
|
||||
message.success(t("logsExt.cleanSuccess", { type: activeLogTypeLabel }));
|
||||
const nextParams = { ...params, current: 1 };
|
||||
setParams(nextParams);
|
||||
await loadData(nextParams);
|
||||
} finally {
|
||||
setCleaning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDuration = (ms?: number) => {
|
||||
if (!ms && ms !== 0) return "-";
|
||||
let color = "";
|
||||
|
|
@ -261,18 +281,25 @@ export default function Logs() {
|
|||
onChange={(key) => { setActiveTab(key); setParams((prev) => ({ ...prev, current: 1, moduleName: "" })); }}
|
||||
size="large"
|
||||
className="flex-shrink-0"
|
||||
items={
|
||||
logTypeDict.length > 0
|
||||
? logTypeDict.map((item) => ({
|
||||
key: item.itemValue,
|
||||
label: <span>{item.itemValue === "OPERATION" ? <InfoCircleOutlined aria-hidden="true" /> : <UserOutlined aria-hidden="true" />}{item.itemLabel}</span>
|
||||
}))
|
||||
: [
|
||||
{ key: "OPERATION", label: <span><InfoCircleOutlined aria-hidden="true" />{t("logs.opLog")}</span> },
|
||||
{ key: "LOGIN", label: <span><UserOutlined aria-hidden="true" />{t("logs.loginLog")}</span> }
|
||||
]
|
||||
}
|
||||
/>
|
||||
tabBarExtraContent={(
|
||||
<Popconfirm
|
||||
title={t("logsExt.cleanConfirmTitle", { type: activeLogTypeLabel })}
|
||||
description={t("logsExt.cleanConfirmDescription")}
|
||||
okText={t("common.confirm")}
|
||||
cancelText={t("common.cancel")}
|
||||
okButtonProps={{ danger: true, loading: cleaning }}
|
||||
onConfirm={() => void handleClean()}
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined aria-hidden="true" />} loading={cleaning}>
|
||||
{t("logsExt.cleanCurrent", { type: activeLogTypeLabel })}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
>
|
||||
{logTypeDict.length > 0
|
||||
? logTypeDict.map((item) => <Tabs.TabPane tab={<span>{item.itemValue === "OPERATION" ? <InfoCircleOutlined aria-hidden="true" /> : <UserOutlined aria-hidden="true" />}{item.itemLabel}</span>} key={item.itemValue} />)
|
||||
: <><Tabs.TabPane tab={<span><InfoCircleOutlined aria-hidden="true" />{t("logs.opLog")}</span>} key="OPERATION" /><Tabs.TabPane tab={<span><UserOutlined aria-hidden="true" />{t("logs.loginLog")}</span>} key="LOGIN" /></>}
|
||||
</Tabs>
|
||||
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||
<ListTable
|
||||
|
|
@ -282,11 +309,18 @@ export default function Logs() {
|
|||
loading={loading}
|
||||
onChange={handleTableChange}
|
||||
totalCount={total}
|
||||
scroll={{ x: "max-content" }}
|
||||
scroll={{ y: "calc(100vh - 460px)" }}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
<AppPagination current={params.current} pageSize={params.size} total={total} onChange={(page, size) => setParams((prev) => ({ ...prev, current: page, size }))} />
|
||||
<AppPagination
|
||||
current={params.current}
|
||||
pageSize={params.size}
|
||||
total={total}
|
||||
onChange={(page, pageSize) => {
|
||||
setParams({ ...params, current: page, size: pageSize });
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal title={t("logs.detailTitle")} open={detailModalVisible} onCancel={() => setDetailModalVisible(false)} footer={[<Button key="close" onClick={() => setDetailModalVisible(false)}>{t("logsExt.close")}</Button>]} width={700}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Card, Col, Form, Input, Row, Upload, App } from 'antd';
|
||||
import { Button, Card, Col, Form, Input, Row, Upload, message } from "antd";
|
||||
import { FileTextOutlined, GlobalOutlined, PictureOutlined, SaveOutlined, UploadOutlined } from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -25,7 +25,6 @@ function ImagePreview({ url, label, hint }: { url?: string; label: string; hint:
|
|||
}
|
||||
|
||||
export default function PlatformSettings() {
|
||||
const { message } = App.useApp();
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -54,7 +53,7 @@ export default function PlatformSettings() {
|
|||
form.setFieldValue(fieldName, url);
|
||||
message.success(t("common.success"));
|
||||
} catch (error) {
|
||||
// message.error(error instanceof Error ? error.message : t("common.error"));
|
||||
message.error(error instanceof Error ? error.message : t("common.error"));
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -87,7 +86,13 @@ export default function PlatformSettings() {
|
|||
<Row gutter={24}>
|
||||
<Col span={24}>
|
||||
<Card title={<><GlobalOutlined className="mr-2" />{t("platformSettings.basicInfo")}</>} className="app-page__content-card mb-6" loading={loading}>
|
||||
<Form.Item label={t("platformSettings.projectName")} name="projectName" rules={[{ required: true, message: t("platformSettings.projectNameRequired") }]}>
|
||||
<Form.Item label={t("platformSettings.projectName")} name="projectName" rules={[
|
||||
{ required: true, message: t("platformSettings.projectNameRequired") },
|
||||
{
|
||||
max:50,
|
||||
message:t('common.maxLength',{length:50})
|
||||
}
|
||||
]}>
|
||||
<Input placeholder={t("platformSettings.projectNamePlaceholder")} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("platformSettings.desc")} name="systemDescription">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Switch, Table, Tag, Tooltip, Typography, message } from "antd";
|
||||
import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Switch, Tag, Tooltip, Typography, message } from "antd";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DeleteOutlined, EditOutlined, InfoCircleOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from "@ant-design/icons";
|
||||
import { createParam, deleteParam, pageParams, updateParam } from "@/api";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import type { SysParamQuery, SysParamVO } from "@/types";
|
||||
import "./index.less";
|
||||
|
||||
|
|
@ -188,18 +189,25 @@ export default function SysParams() {
|
|||
</Card>
|
||||
|
||||
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
||||
<Table
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||
<ListTable
|
||||
rowKey="paramId"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
size="middle"
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: "max-content" }}
|
||||
scroll={{ y: "calc(100vh - 350px)" }}
|
||||
totalCount={total}
|
||||
pagination={false}
|
||||
/>
|
||||
<AppPagination current={queryParams.pageNum || 1} pageSize={queryParams.pageSize || 10} total={total} onChange={handlePageChange} />
|
||||
|
||||
</div>
|
||||
<AppPagination
|
||||
current={queryParams.pageNum || 1}
|
||||
pageSize={queryParams.pageSize || 10}
|
||||
total={total}
|
||||
onChange={(page, pageSize) => {
|
||||
handlePageChange(page, pageSize);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const UserRoleBinding = lazy(() => import("@/pages/bindings/user-role"));
|
|||
const RolePermissionBinding = lazy(() => import("@/pages/bindings/role-permission"));
|
||||
const ClientManagement = lazy(() => import("@/pages/business/ClientManagement"));
|
||||
const ExternalAppManagement = lazy(() => import("@/pages/business/ExternalAppManagement"));
|
||||
const ScreenSaverManagement = lazy(() => import("@/pages/business/ScreenSaverManagement"));
|
||||
|
||||
import SpeakerReg from "../pages/business/SpeakerReg";
|
||||
const RealtimeAsrSession = lazy(async () => {
|
||||
|
|
@ -64,6 +65,7 @@ export const menuRoutes: MenuRoute[] = [
|
|||
{ path: "/aimodels", label: "模型配置", element: <AiModels />, perm: "menu:aimodel" },
|
||||
{ path: "/clients", label: "客户端管理", element: <ClientManagement />, perm: "menu:clients" },
|
||||
{ path: "/external-apps", label: "外部应用管理", element: <ExternalAppManagement />, perm: "menu:external-apps" },
|
||||
{ path: "/screen-savers", label: "屏保管理", element: <ScreenSaverManagement />, perm: "menu:screen-savers" },
|
||||
{ path: "/meetings", label: "会议中心", element: <Meetings />, perm: "menu:meeting" },
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue