1.0.2原版

This commit is contained in:
2026-07-26 14:13:39 +08:00
commit 975b937d9c
50 changed files with 10241 additions and 0 deletions
@@ -0,0 +1,51 @@
package com.kunkunyu.equipment;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Objects;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
/**
* @author ryanwang
*/
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "equipment.kunkunyu.com", version = "v1alpha1", kind = "Equipment", plural = "equipments",
singular = "equipment")
public class Equipment extends AbstractExtension {
private EquipmentSpec spec;
@Data
public static class EquipmentSpec {
@Schema(requiredMode = REQUIRED)
private String displayName;
private String specification;
private String description;
@Schema(requiredMode = REQUIRED)
private String cover;
private String url;
private Integer priority;
@Schema(requiredMode = REQUIRED, pattern = "^\\S+$")
private String groupName;
}
@JsonIgnore
public boolean isDeleted() {
return Objects.equals(true,
getMetadata().getDeletionTimestamp() != null
);
}
}
@@ -0,0 +1,56 @@
package com.kunkunyu.equipment;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListResult;
import com.kunkunyu.equipment.service.EquipmentService;
/**
* A custom endpoint for {@link Equipment}.
*
*/
@Component
@RequiredArgsConstructor
public class EquipmentEndpoint implements CustomEndpoint {
private final EquipmentService equipmentService;
@Override
public RouterFunction<ServerResponse> endpoint() {
final var tag = "console.api.equipment.kunkunyu.com/v1alpha1/Equipment";
return route()
.GET("equipments", this::listEquipment,
builder -> {
builder.operationId("ListEquipments")
.description("List equipment.")
.tag(tag)
.response(responseBuilder().implementation(
ListResult.generateGenericClass(Equipment.class)));
EquipmentQuery.buildParameters(builder);
}
)
.build();
}
@Override
public GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("console.api.equipment.kunkunyu.com/v1alpha1");
}
private Mono<ServerResponse> listEquipment(ServerRequest serverRequest) {
EquipmentQuery query = new EquipmentQuery(serverRequest.exchange());
return equipmentService.listEquipment(query)
.flatMap(equipments -> ServerResponse.ok().bodyValue(equipments));
}
}
@@ -0,0 +1,46 @@
package com.kunkunyu.equipment;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "equipment.kunkunyu.com", version = "v1alpha1", kind = "EquipmentGroup",
plural = "equipmentgroups", singular = "equipmentgroup")
public class EquipmentGroup extends AbstractExtension {
@Schema(required = true)
private EquipmentGroupSpec spec;
@Schema
private EquipmentGroupStatus status;
@Data
public static class EquipmentGroupSpec {
@Schema(required = true)
private String displayName;
private String description;
private Integer priority;
}
@JsonIgnore
public EquipmentGroupStatus getStatusOrDefault() {
if (this.status == null) {
this.status = new EquipmentGroupStatus();
}
return this.status;
}
@Data
public static class EquipmentGroupStatus {
public Integer equipmentCount;
}
}
@@ -0,0 +1,78 @@
package com.kunkunyu.equipment;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListResult;
import com.kunkunyu.equipment.service.EquipmentGroupService;
/**
* A custom endpoint for {@link Equipment}.
*/
@Component
@RequiredArgsConstructor
public class EquipmentGroupEndpoint implements CustomEndpoint {
private final EquipmentGroupService equipmentGroupService;
@Override
public RouterFunction<ServerResponse> endpoint() {
return route()
.GET("equipmentgroups", this::listEquipmentGroup,
builder -> {
builder.operationId("ListEquipments")
.description("List equipment.")
.response(responseBuilder().implementation(
ListResult.generateGenericClass(EquipmentGroup.class))
);
EquipmentQuery.buildParameters(builder);
}
)
.DELETE("equipmentgroups/{name}", this::deleteEquipmentGroup,
builder -> builder.operationId("DeleteEquipmentGroup")
.description("Delete equipment group.")
.parameter(parameterBuilder()
.name("name")
.in(ParameterIn.PATH)
.description("Equipment group name")
.implementation(String.class)
.required(true)
)
.response(responseBuilder().implementation(EquipmentGroup.class))
)
.build();
}
@Override
public GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("console.api.equipment.kunkunyu.com/v1alpha1");
}
private Mono<ServerResponse> deleteEquipmentGroup(ServerRequest serverRequest) {
String name = serverRequest.pathVariable("name");
if (StringUtils.isBlank(name)) {
throw new ServerWebInputException("Equipment group name must not be blank.");
}
return equipmentGroupService.deleteEquipmentGroup(name)
.flatMap(equipmentGroup -> ServerResponse.ok().bodyValue(equipmentGroup));
}
private Mono<ServerResponse> listEquipmentGroup(ServerRequest serverRequest) {
var request = new EquipmentQuery(serverRequest.exchange());
return equipmentGroupService.listEquipmentGroup(request)
.flatMap(equipmentGroups -> ServerResponse.ok().bodyValue(equipmentGroups));
}
}
@@ -0,0 +1,60 @@
package com.kunkunyu.equipment;
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;
import org.springframework.stereotype.Component;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.index.IndexSpec;
import run.halo.app.plugin.BasePlugin;
import run.halo.app.plugin.PluginContext;
@Component
public class EquipmentPlugin extends BasePlugin {
private final SchemeManager schemeManager;
public EquipmentPlugin(PluginContext pluginContext, SchemeManager schemeManager) {
super(pluginContext);
this.schemeManager = schemeManager;
}
@Override
public void start() {
schemeManager.register(Equipment.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.groupName")
.setIndexFunc(simpleAttribute(Equipment.class, equipment ->
equipment.getSpec() == null ? "" : equipment.getSpec().getGroupName()
))
);
indexSpecs.add(new IndexSpec()
.setName("spec.displayName")
.setIndexFunc(simpleAttribute(Equipment.class, equipment ->
equipment.getSpec() == null ? "" : equipment.getSpec().getDisplayName()
))
);
indexSpecs.add(new IndexSpec()
.setName("spec.priority")
.setIndexFunc(simpleAttribute(Equipment.class, equipment ->
equipment.getSpec() == null || equipment.getSpec().getPriority() == null
? String.valueOf(0) : equipment.getSpec().getPriority().toString()
))
);
});
schemeManager.register(EquipmentGroup.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.priority")
.setIndexFunc(simpleAttribute(EquipmentGroup.class, group ->
group.getSpec() == null || group.getSpec().getPriority() == null
? String.valueOf(0) : group.getSpec().getPriority().toString()
))
);
});
}
@Override
public void stop() {
schemeManager.unregister(Scheme.buildFromType(Equipment.class));
schemeManager.unregister(Scheme.buildFromType(EquipmentGroup.class));
}
}
@@ -0,0 +1,52 @@
package com.kunkunyu.equipment;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebExchange;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.SortableRequest;
/**
* A query object for {@link Equipment} list.
*
*/
public class EquipmentQuery extends SortableRequest {
public EquipmentQuery(ServerWebExchange exchange) {
super(exchange);
}
@Schema(description = "Equipments filtered by group.")
public String getGroup() {
return queryParams.getFirst("group");
}
@Nullable
@Schema(description = "Equipments filtered by keyword.")
public String getKeyword() {
return queryParams.getFirst("keyword");
}
public static void buildParameters(Builder builder) {
IListRequest.buildParameters(builder);
builder.parameter(sortParameter())
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("keyword")
.description("Equipments filtered by keyword.")
.implementation(String.class)
.required(false))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("group")
.description("equipment group name")
.implementation(String.class)
.required(false))
;
}
}
@@ -0,0 +1,116 @@
package com.kunkunyu.equipment;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.ReactiveSettingFetcher;
import run.halo.app.theme.TemplateNameResolver;
import run.halo.app.theme.router.PageUrlUtils;
import run.halo.app.theme.router.UrlContextListResult;
import com.kunkunyu.equipment.finders.EquipmentFinder;
import com.kunkunyu.equipment.vo.EquipmentGroupVo;
import com.kunkunyu.equipment.vo.EquipmentVo;
/**
* Provides a <code>/equipments</code> route for the topic end to handle routing.
*
*/
@Component
@AllArgsConstructor
public class EquipmentRouter {
private static final String GROUP_PARAM = "group";
private EquipmentFinder equipmentFinder;
private final ReactiveSettingFetcher settingFetcher;
private final TemplateNameResolver templateNameResolver;
/**
* Provides a <code>/equipments</code> route for the topic end to handle routing.
*
* @return a {@link RouterFunction} instance
*/
@Bean
RouterFunction<ServerResponse> equipmentRouter() {
return route(GET("/equipments").or(GET("/equipments/page/{page:\\d+}")),
handlerFunction()
);
}
private HandlerFunction<ServerResponse> handlerFunction() {
return request -> templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "equipments")
.flatMap(templateName -> ServerResponse.ok().render(templateName,
Map.of("groups", equipmentGroups(),
"equipments", equipmentList(request),
ModelConst.TEMPLATE_ID, templateName,
"title", getEquipmentTitle()
))
);
}
private Mono<UrlContextListResult<EquipmentVo>> equipmentList(ServerRequest request) {
String path = request.path();
int pageNum = pageNumInPathVariable(request);
String group = groupPathQueryParam(request);
return this.settingFetcher.get("base")
.map(item -> item.get("pageSize").asInt(10))
.defaultIfEmpty(10)
.flatMap(pageSize -> equipmentFinder.list(pageNum, pageSize, group)
.map(list -> new UrlContextListResult.Builder<EquipmentVo>()
.listResult(list)
.nextUrl(appendGroupParam(
PageUrlUtils.nextPageUrl(path, totalPage(list)), group)
)
.prevUrl(appendGroupParam(PageUrlUtils.prevPageUrl(path), group))
.build()
)
);
}
private static String appendGroupParam(String path, String group) {
return UriComponentsBuilder.fromPath(path)
.queryParamIfPresent(GROUP_PARAM, Optional.ofNullable(group))
.build()
.toString();
}
private int pageNumInPathVariable(ServerRequest request) {
String page = request.pathVariables().get("page");
return NumberUtils.toInt(page, 1);
}
private String groupPathQueryParam(ServerRequest request) {
return request.queryParam(GROUP_PARAM)
.filter(StringUtils::isNotBlank)
.orElse(null);
}
Mono<String> getEquipmentTitle() {
return this.settingFetcher.get("base").map(
setting -> setting.get("title").asText("装备")).defaultIfEmpty(
"装备");
}
private Mono<List<EquipmentGroupVo>> equipmentGroups() {
return equipmentFinder.groupBy().collectList();
}
}
@@ -0,0 +1,11 @@
package com.kunkunyu.equipment;
/**
* Static variable keys for view model.
*
*/
public enum ModelConst {
;
public static final String TEMPLATE_ID = "_templateId";
public static final Integer DEFAULT_PAGE_SIZE = 10;
}
@@ -0,0 +1,57 @@
package com.kunkunyu.equipment.finders;
import com.kunkunyu.equipment.Equipment;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult;
import com.kunkunyu.equipment.vo.EquipmentGroupVo;
import com.kunkunyu.equipment.vo.EquipmentVo;
/**
* A finder for {@link Equipment}.
*
*/
public interface EquipmentFinder {
/**
* List all equipment.
*
* @return a flux of equipment vo
*/
Flux<EquipmentVo> listAll();
/**
* List equipment by page.
*
* @param page page number
* @param size page size
* @return a mono of list result
*/
Mono<ListResult<EquipmentVo>> list(Integer page, Integer size);
/**
* List equipment by page and group.
*
* @param page page number
* @param size page size
* @param group group name
* @return a mono of list result
*/
Mono<ListResult<EquipmentVo>> list(Integer page, Integer size, String group);
/**
* List equipment by group.
*
* @param group group name
* @return a flux of equipment vo
*/
Flux<EquipmentVo> listBy(String group);
/**
* List all groups.
*
* @return a flux of equipment group vo
*/
Flux<EquipmentGroupVo> groupBy();
}
@@ -0,0 +1,95 @@
package com.kunkunyu.equipment.finders.impl;
import static org.springframework.data.domain.Sort.Order.asc;
import static org.springframework.data.domain.Sort.Order.desc;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.theme.finders.Finder;
import com.kunkunyu.equipment.Equipment;
import com.kunkunyu.equipment.EquipmentGroup;
import com.kunkunyu.equipment.finders.EquipmentFinder;
import com.kunkunyu.equipment.vo.EquipmentGroupVo;
import com.kunkunyu.equipment.vo.EquipmentVo;
@Finder("equipmentFinder")
public class EquipmentFInderImpl implements EquipmentFinder {
private final ReactiveExtensionClient client;
public EquipmentFInderImpl(ReactiveExtensionClient client) {
this.client = client;
}
@Override
public Flux<EquipmentVo> listAll() {
return this.client.listAll(Equipment.class, ListOptions.builder().build(), defaultSort())
.map(EquipmentVo::from);
}
@Override
public Mono<ListResult<EquipmentVo>> list(Integer page, Integer size) {
return list(page, size, null);
}
@Override
public Mono<ListResult<EquipmentVo>> list(Integer page, Integer size, String group) {
return pageEquipment(page, size, group);
}
private Mono<ListResult<EquipmentVo>> pageEquipment(Integer page, Integer size, String group) {
var builder = ListOptions.builder();
if (StringUtils.isNotEmpty(group)) {
builder.andQuery(QueryFactory.equal("spec.groupName", group));
}
return client.listBy(Equipment.class, builder.build(),
PageRequestImpl.of(page, size, defaultSort()))
.flatMap(listResult -> Flux.fromStream(listResult.get())
.map(EquipmentVo::from)
.collectList()
.map(list -> new ListResult<>(
listResult.getPage(), listResult.getSize(), listResult.getTotal(), list
))
);
}
@Override
public Flux<EquipmentVo> listBy(String groupName) {
var options = ListOptions.builder()
.andQuery(QueryFactory.equal("spec.groupName", groupName))
.build();
return client.listAll(Equipment.class, options, defaultSort()).map(EquipmentVo::from);
}
@Override
public Flux<EquipmentGroupVo> groupBy() {
return this.client.listAll(EquipmentGroup.class, ListOptions.builder().build(), defaultSort())
.concatMap(group -> {
var builder = EquipmentGroupVo.from(group);
return this.listBy(group.getMetadata().getName())
.collectList()
.doOnNext(equipments -> {
EquipmentGroup.EquipmentGroupStatus status = group.getStatus();
status.setEquipmentCount(equipments.size());
builder.status(status);
builder.equipments(equipments);
})
.then(Mono.fromSupplier(builder::build));
});
}
static Sort defaultSort() {
return Sort.by(
asc("spec.priority"),
desc("metadata.creationTimestamp"),
asc("metadata.name")
);
}
}
@@ -0,0 +1,30 @@
package com.kunkunyu.equipment.service;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.router.IListRequest.QueryListRequest;
import com.kunkunyu.equipment.EquipmentGroup;
/**
* A service for {@link EquipmentGroup}.
*
*/
public interface EquipmentGroupService {
/**
* List equipment groups.
*
* @param request request
* @return a mono of list result
*/
Mono<ListResult<EquipmentGroup>> listEquipmentGroup(QueryListRequest request);
/**
* Create a equipment group.
*
* @param name name
* @return a mono of equipment group
*/
Mono<EquipmentGroup> deleteEquipmentGroup(String name);
}
@@ -0,0 +1,21 @@
package com.kunkunyu.equipment.service;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult;
import com.kunkunyu.equipment.Equipment;
import com.kunkunyu.equipment.EquipmentQuery;
/**
* A service for {@link Equipment}.
*
*/
public interface EquipmentService {
/**
* List equipment.
*
* @param query query
* @return a mono of list result
*/
Mono<ListResult<Equipment>> listEquipment(EquipmentQuery query);
}
@@ -0,0 +1,92 @@
package com.kunkunyu.equipment.service.impl;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest.QueryListRequest;
import com.kunkunyu.equipment.Equipment;
import com.kunkunyu.equipment.EquipmentGroup;
import com.kunkunyu.equipment.service.EquipmentGroupService;
/**
* Service implementation for {@link Equipment}.
*
*/
@Component
class EquipmentGroupServiceImpl implements EquipmentGroupService {
private final ReactiveExtensionClient client;
public EquipmentGroupServiceImpl(ReactiveExtensionClient client) {
this.client = client;
}
@Override
public Mono<ListResult<EquipmentGroup>> listEquipmentGroup(QueryListRequest query) {
return this.client.listBy(
EquipmentGroup.class,
toListOptions(query),
PageRequestImpl.of(query.getPage(), query.getSize())
)
.flatMap(listResult -> Flux.fromStream(listResult.get())
.flatMap(this::populateEquipments)
.collectList()
.map(groups -> new ListResult<>(
listResult.getPage(),
listResult.getSize(),
listResult.getTotal(),
groups
))
);
}
@Override
public Mono<EquipmentGroup> deleteEquipmentGroup(String name) {
return this.client.fetch(EquipmentGroup.class, name)
.flatMap(this.client::delete)
.flatMap(deleted -> {
var listOptions = ListOptions.builder()
.andQuery(QueryFactory.equal("spec.groupName", name))
.build();
return this.client.listAll(Equipment.class, listOptions, Sort.unsorted())
.flatMap(this.client::delete)
.then()
.thenReturn(deleted);
}
);
}
private Mono<EquipmentGroup> populateEquipments(EquipmentGroup equipmentGroup) {
return fetchEquipmentCount(equipmentGroup)
.doOnNext(count -> equipmentGroup.getStatusOrDefault().setEquipmentCount(count))
.thenReturn(equipmentGroup);
}
Mono<Integer> fetchEquipmentCount(EquipmentGroup equipmentGroup) {
Assert.notNull(equipmentGroup, "The equipmentGroup must not be null.");
String name = equipmentGroup.getMetadata().getName();
return client.list(
Equipment.class,
equipment -> !equipment.isDeleted() && equipment.getSpec().getGroupName().equals(name),
null
)
.count()
.defaultIfEmpty(0L)
.map(Long::intValue);
}
ListOptions toListOptions(QueryListRequest query) {
return labelAndFieldSelectorToListOptions(
query.getLabelSelector(), query.getFieldSelector()
);
}
}
@@ -0,0 +1,52 @@
package com.kunkunyu.equipment.service.impl;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import com.kunkunyu.equipment.Equipment;
import com.kunkunyu.equipment.EquipmentQuery;
import com.kunkunyu.equipment.service.EquipmentService;
/**
* Service implementation for {@link Equipment}.
*
*/
@Component
class EquipmentServiceImpl implements EquipmentService {
private final ReactiveExtensionClient client;
public EquipmentServiceImpl(ReactiveExtensionClient client) {
this.client = client;
}
@Override
public Mono<ListResult<Equipment>> listEquipment(EquipmentQuery query) {
return this.client.listBy(
Equipment.class,
toListOptions(query),
PageRequestImpl.of(query.getPage(), query.getSize(), query.getSort())
);
}
ListOptions toListOptions(EquipmentQuery query) {
var builder = ListOptions.builder(labelAndFieldSelectorToListOptions(
query.getLabelSelector(), query.getFieldSelector())
);
if (StringUtils.isNotBlank(query.getKeyword())) {
builder.andQuery(QueryFactory.contains("spec.displayName", query.getKeyword()));
}
if (StringUtils.isNotBlank(query.getGroup())) {
builder.andQuery(QueryFactory.equal("spec.groupName", query.getGroup()));
}
return builder.build();
}
}
@@ -0,0 +1,29 @@
package com.kunkunyu.equipment.vo;
import java.util.List;
import lombok.Builder;
import lombok.Value;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.theme.finders.vo.ExtensionVoOperator;
import com.kunkunyu.equipment.EquipmentGroup;
@Value
@Builder
public class EquipmentGroupVo implements ExtensionVoOperator {
MetadataOperator metadata;
EquipmentGroup.EquipmentGroupSpec spec;
EquipmentGroup.EquipmentGroupStatus status;
List<EquipmentVo> equipments;
public static EquipmentGroupVoBuilder from(EquipmentGroup EquipmentGroup) {
return EquipmentGroupVo.builder()
.metadata(EquipmentGroup.getMetadata())
.spec(EquipmentGroup.getSpec())
.status(EquipmentGroup.getStatusOrDefault())
.equipments(List.of());
}
}
@@ -0,0 +1,24 @@
package com.kunkunyu.equipment.vo;
import lombok.Builder;
import lombok.Value;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.theme.finders.vo.ExtensionVoOperator;
import com.kunkunyu.equipment.Equipment;
@Value
@Builder
public class EquipmentVo implements ExtensionVoOperator {
MetadataOperator metadata;
Equipment.EquipmentSpec spec;
public static EquipmentVo from(Equipment equipment) {
return EquipmentVo.builder()
.metadata(equipment.getMetadata())
.spec(equipment.getSpec())
.build();
}
}
@@ -0,0 +1,30 @@
apiVersion: v1alpha1
kind: Role
metadata:
name: role-template-equipment-view
labels:
halo.run/role-template: "true"
annotations:
rbac.authorization.halo.run/module: "装备"
rbac.authorization.halo.run/ui-permissions: '["plugin:equipment:view"]'
rbac.authorization.halo.run/display-name: "装备查看"
rules:
- apiGroups: [ "equipment.kunkunyu.com", "console.api.equipment.kunkunyu.com"]
resources: [ "equipments", "equipmentgroups" ]
verbs: [ "get", "list" ]
---
apiVersion: v1alpha1
kind: Role
metadata:
name: role-template-equipment-manage
labels:
halo.run/role-template: "true"
annotations:
rbac.authorization.halo.run/dependencies: '["role-template-equipment-view"]'
rbac.authorization.halo.run/ui-permissions: '["plugin:equipment:manage"]'
rbac.authorization.halo.run/module: "装备"
rbac.authorization.halo.run/display-name: "装备管理"
rules:
- apiGroups: [ "equipment.kunkunyu.com", "console.api.equipment.kunkunyu.com"]
resources: [ "equipments", "equipmentgroups" ]
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
@@ -0,0 +1,19 @@
apiVersion: v1alpha1
kind: Setting
metadata:
name: equipment-settings
spec:
forms:
- group: base
label: 基本设置
formSchema:
- $formkit: text
label: 页面标题
name: title
validation: required
value: "装备"
- $formkit: text
label: 装备列表显示条数
name: pageSize
validation: required|Number
value: 20
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12L6 9H4a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h2zm6 0l3-3h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-2zm-3 3l-3 3v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2zm0-6L9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2z"/></svg>

After

Width:  |  Height:  |  Size: 378 B

+24
View File
@@ -0,0 +1,24 @@
apiVersion: plugin.halo.run/v1alpha1
kind: Plugin
metadata:
name: equipment
annotations:
"store.halo.run/app-id": "app-ytygyqml"
spec:
enabled: true
version: 1.0.2
requires: ">=2.21.0"
author:
name: 困困鱼
website: https://github.com/chengzhongxue
logo: logo.svg
homepage: https://www.halo.run/store/apps/app-ytygyqml
repo: https://github.com/chengzhongxue/plugin-equipment
issues: https://github.com/chengzhongxue/plugin-equipment/issues
configMapName: equipment-configmap
settingName: equipment-settings
displayName: "装备管理"
description: "装备管理模块"
license:
- name: "GPL-3.0"
url: "https://github.com/chengzhongxue/plugin-equipment/blob/main/LICENSE"
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title th:text="${site.title+'-'+title}"></title>
<link data-n-head="true" rel="icon" type="image/x-icon" th:href="${site.favicon}">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4A5568',
secondary: '#718096'
},
borderRadius: {
'none': '0px',
'sm': '2px',
DEFAULT: '4px',
'md': '8px',
'lg': '12px',
'xl': '16px',
'2xl': '20px',
'3xl': '24px',
'full': '9999px',
'button': '4px'
}
}
}
}
</script>
<style>
body {
font-family: 'Noto Sans SC', sans-serif;
min-height: 100vh;
background-color: #f7fafc;
}
@media (min-width: 1024px) {
body {
min-height: 1024px;
}
}
.product-card {
transition: all 0.3s ease;
}
.product-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.group-block {
margin-bottom: 3rem;
}
.group-block:last-child {
margin-bottom: 0;
}
</style>
</head>
<body class="text-gray-800">
<div class="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8 lg:py-12">
<th:block th:each="group : ${equipmentFinder.groupBy()}">
<div class="mb-6 group-block">
<h2 class="text-2xl sm:text-3xl font-bold mb-2 sm:mb-4" th:text="${group.spec.displayName}"></h2>
<p class="text-gray-600 text-base sm:text-lg" th:text="${group.spec.description}"></p>
</div>
<div class="grid grid-cols-1 mb-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4 lg:gap-5 ">
<div th:each="equipment : ${group.equipments}"
class="bg-white rounded-lg p-6 shadow-sm product-card flex flex-col h-full">
<div class="aspect-[4/3] mb-6 overflow-hidden -mx-6 -mt-6 rounded-t-lg">
<img th:src="${equipment.spec.cover}"
class="w-full h-full object-cover object-center" th:alt="${equipment.spec.displayName}">
</div>
<div class="flex-1">
<h3 class="text-lg sm:text-xl font-bold mb-2" th:text="${equipment.spec.displayName}"></h3>
<p class="text-gray-600 mb-2" th:text="${equipment.spec.specification}"></p>
<p class="text-gray-700" th:text="${equipment.spec.description}"></p>
</div>
<div th:if="${equipment.spec.url}" class="flex justify-start items-center mt-4">
<a th:href="${equipment.spec.url}" target="_blank" rel="noopener" class="px-6 py-1 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors whitespace-nowrap">
详情
</a>
</div>
</div>
</div>
</th:block>
</div>
</body>
</html>