接入搜索引擎; 修改控制台配置
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
> 基于原作者v1.2.2代码修改
|
> 基于原作者v1.2.2代码修改
|
||||||
|
|
||||||
|
#### v1.2.2-3
|
||||||
|
1. 标题、描述接入搜索引擎;新增详情页
|
||||||
|
2. 控制台新增必填项;修改字数上限、时间格式
|
||||||
|
|
||||||
#### v1.2.2-2
|
#### v1.2.2-2
|
||||||
1. 新增类别接口`/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/types`(基于原作者v1.2.5代码)
|
1. 新增类别接口`/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/types`(基于原作者v1.2.5代码)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
version=1.2.2-2
|
version=1.2.2-3
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ public class DoubanPlugin extends BasePlugin {
|
|||||||
.setName("spec.name")
|
.setName("spec.name")
|
||||||
.setIndexFunc(
|
.setIndexFunc(
|
||||||
simpleAttribute(DoubanMovie.class, doubanMovie -> doubanMovie.getSpec().getName())));
|
simpleAttribute(DoubanMovie.class, doubanMovie -> doubanMovie.getSpec().getName())));
|
||||||
|
indexSpecs.add(new IndexSpec()
|
||||||
|
.setName("spec.cardSubtitle")
|
||||||
|
.setIndexFunc(simpleAttribute(DoubanMovie.class,
|
||||||
|
doubanMovie -> doubanMovie.getSpec().getCardSubtitle())));
|
||||||
indexSpecs.add(new IndexSpec()
|
indexSpecs.add(new IndexSpec()
|
||||||
.setName("spec.id")
|
.setName("spec.id")
|
||||||
.setIndexFunc(
|
.setIndexFunc(
|
||||||
|
|||||||
@@ -29,9 +29,18 @@ public class DoubanRouter {
|
|||||||
RouterFunction<ServerResponse> friendTemplateRoute() {
|
RouterFunction<ServerResponse> friendTemplateRoute() {
|
||||||
|
|
||||||
return RouterFunctions.route().GET("/books",this::handlerFunction)
|
return RouterFunctions.route().GET("/books",this::handlerFunction)
|
||||||
|
.GET("/books/{doubanMovieName:\\S+}", this::doubanMovieDetail)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> doubanMovieDetail(ServerRequest request) {
|
||||||
|
String doubanMovieName = request.pathVariable("doubanMovieName");
|
||||||
|
return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "douban-detail")
|
||||||
|
.flatMap(templateName -> ServerResponse.ok().render(templateName,
|
||||||
|
java.util.Map.of("douban", doubanFinder.get(doubanMovieName),
|
||||||
|
"title", getDoubanTitle())));
|
||||||
|
}
|
||||||
|
|
||||||
private Mono<ServerResponse> handlerFunction(ServerRequest request) {
|
private Mono<ServerResponse> handlerFunction(ServerRequest request) {
|
||||||
String type = request.queryParam("type")
|
String type = request.queryParam("type")
|
||||||
.filter(StringUtils::isNotBlank)
|
.filter(StringUtils::isNotBlank)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package la.moony.douban.search;
|
||||||
|
|
||||||
|
import static la.moony.douban.search.DoubanMovieHaloDocumentsProvider.DOUBAN_MOVIE_DOCUMENT_TYPE;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import la.moony.douban.extension.DoubanMovie;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.ExternalUrlSupplier;
|
||||||
|
import run.halo.app.search.HaloDocument;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DoubanMovieDocumentConverter implements Converter<DoubanMovie, Mono<HaloDocument>> {
|
||||||
|
|
||||||
|
private final ExternalUrlSupplier externalUrlSupplier;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Mono<HaloDocument> convert(DoubanMovie movie) {
|
||||||
|
var spec = movie.getSpec();
|
||||||
|
String title = StringUtils.defaultIfBlank(spec == null ? null : spec.getName(), "豆瓣记录");
|
||||||
|
String description = StringUtils.defaultIfBlank(
|
||||||
|
spec == null ? null : spec.getCardSubtitle(), "无描述");
|
||||||
|
var document = new HaloDocument();
|
||||||
|
document.setMetadataName(movie.getMetadata().getName());
|
||||||
|
document.setType(DOUBAN_MOVIE_DOCUMENT_TYPE);
|
||||||
|
document.setId(haloDocId(movie));
|
||||||
|
document.setTitle(title);
|
||||||
|
document.setDescription(description);
|
||||||
|
document.setContent(String.join(" ", title, description));
|
||||||
|
document.setExposed(true);
|
||||||
|
document.setTags(List.of());
|
||||||
|
document.setOwnerName("豆瓣");
|
||||||
|
document.setPublished(true);
|
||||||
|
document.setPermalink(externalUrlSupplier.get()
|
||||||
|
.resolve("books/" + movie.getMetadata().getName()).toString());
|
||||||
|
document.setCreationTimestamp(creationTimestamp(movie));
|
||||||
|
document.setUpdateTimestamp(creationTimestamp(movie));
|
||||||
|
return Mono.just(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
String haloDocId(DoubanMovie movie) {
|
||||||
|
return DOUBAN_MOVIE_DOCUMENT_TYPE + '-' + movie.getMetadata().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant creationTimestamp(DoubanMovie movie) {
|
||||||
|
if (movie.getFaves() != null && movie.getFaves().getCreateTime() != null) {
|
||||||
|
return movie.getFaves().getCreateTime();
|
||||||
|
}
|
||||||
|
return movie.getMetadata().getCreationTimestamp() == null
|
||||||
|
? Instant.EPOCH : movie.getMetadata().getCreationTimestamp();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package la.moony.douban.search;
|
||||||
|
|
||||||
|
import la.moony.douban.extension.DoubanMovie;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
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.PageRequest;
|
||||||
|
import run.halo.app.extension.PageRequestImpl;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.extension.router.selector.FieldSelector;
|
||||||
|
import run.halo.app.search.HaloDocument;
|
||||||
|
import run.halo.app.search.HaloDocumentsProvider;
|
||||||
|
|
||||||
|
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DoubanMovieHaloDocumentsProvider implements HaloDocumentsProvider {
|
||||||
|
|
||||||
|
public static final String DOUBAN_MOVIE_DOCUMENT_TYPE = "doubanmovie.douban.moony.la";
|
||||||
|
private static final int PAGE_SIZE = 200;
|
||||||
|
|
||||||
|
private final ReactiveExtensionClient client;
|
||||||
|
private final DoubanMovieDocumentConverter converter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<HaloDocument> fetchAll() {
|
||||||
|
var options = new ListOptions();
|
||||||
|
options.setFieldSelector(FieldSelector.of(isNull("metadata.deletionTimestamp")));
|
||||||
|
var pageRequest = PageRequestImpl.of(1, PAGE_SIZE,
|
||||||
|
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||||
|
return client.listBy(DoubanMovie.class, options, pageRequest)
|
||||||
|
.expand(result -> result.hasNext()
|
||||||
|
? client.listBy(DoubanMovie.class, options,
|
||||||
|
PageRequestImpl.of(result.getPage() + 1, result.getSize(), pageRequest.getSort()))
|
||||||
|
: Mono.empty())
|
||||||
|
.flatMap(result -> Flux.fromStream(result.get()))
|
||||||
|
.flatMap(converter::convert);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType() {
|
||||||
|
return DOUBAN_MOVIE_DOCUMENT_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package la.moony.douban.search;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import la.moony.douban.extension.DoubanMovie;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.ExtensionUtil;
|
||||||
|
import run.halo.app.extension.controller.Controller;
|
||||||
|
import run.halo.app.extension.controller.ControllerBuilder;
|
||||||
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
|
import run.halo.app.search.event.HaloDocumentAddRequestEvent;
|
||||||
|
import run.halo.app.search.event.HaloDocumentDeleteRequestEvent;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DoubanMovieSearchReconciler implements Reconciler<Reconciler.Request> {
|
||||||
|
|
||||||
|
private static final String FINALIZER = "douban-movie-search-protection";
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
private final ExtensionClient client;
|
||||||
|
private final DoubanMovieDocumentConverter converter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result reconcile(Request request) {
|
||||||
|
client.fetch(DoubanMovie.class, request.name()).ifPresent(movie -> {
|
||||||
|
if (ExtensionUtil.isDeleted(movie)) {
|
||||||
|
if (ExtensionUtil.removeFinalizers(movie.getMetadata(), Set.of(FINALIZER))) {
|
||||||
|
eventPublisher.publishEvent(new HaloDocumentDeleteRequestEvent(this,
|
||||||
|
List.of(converter.haloDocId(movie))));
|
||||||
|
client.update(movie);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ExtensionUtil.addFinalizers(movie.getMetadata(), Set.of(FINALIZER));
|
||||||
|
var document = converter.convert(movie).blockOptional().orElseThrow();
|
||||||
|
eventPublisher.publishEvent(new HaloDocumentAddRequestEvent(this, List.of(document)));
|
||||||
|
client.update(movie);
|
||||||
|
});
|
||||||
|
return Result.doNotRetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Controller setupWith(ControllerBuilder builder) {
|
||||||
|
return builder.extension(new DoubanMovie()).workerCount(1).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -182,8 +182,8 @@ class HALO_DOUBAN {
|
|||||||
}${
|
}${
|
||||||
movie.spec.year > 0 ? " · " + movie.spec.year : ""
|
movie.spec.year > 0 ? " · " + movie.spec.year : ""
|
||||||
}</div><div class="db--title"><a href="${
|
}</div><div class="db--title"><a href="${
|
||||||
movie.spec.link
|
`/books/${movie.metadata.name}`
|
||||||
}" target="_blank">${movie.spec.name}</a></div>
|
}">${movie.spec.name}</a></div>
|
||||||
|
|
||||||
</div>`;
|
</div>`;
|
||||||
})
|
})
|
||||||
@@ -212,8 +212,8 @@ class HALO_DOUBAN {
|
|||||||
}${
|
}${
|
||||||
item.spec.year > 0 ? " · " + item.spec.year : ""
|
item.spec.year > 0 ? " · " + item.spec.year : ""
|
||||||
}</div><div class="db--title"><a href="${
|
}</div><div class="db--title"><a href="${
|
||||||
item.spec.link
|
`/books/${item.metadata.name}`
|
||||||
}" target="_blank">${item.spec.name}</a></div>
|
}">${item.spec.name}</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
})
|
})
|
||||||
@@ -294,4 +294,4 @@ class HALO_DOUBAN {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new HALO_DOUBAN();
|
new HALO_DOUBAN();
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" xmlns:th="https://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title th:text="${douban != null ? douban.spec.name : '豆瓣记录'}">豆瓣记录</title>
|
||||||
|
<link rel="stylesheet" th:href="@{/plugins/plugin-douban/assets/static/db.min.css}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="layoutSingleColumn layoutSingleColumn--wide u-paddingTop50">
|
||||||
|
<section class="doulist-item" th:if="${douban != null}">
|
||||||
|
<div class="doulist-subject">
|
||||||
|
<div class="doulist-post"><img class="db--image" th:src="${douban.spec.poster}" alt=""></div>
|
||||||
|
<div class="doulist-content">
|
||||||
|
<div class="doulist-title" th:text="${douban.spec.name}">标题</div>
|
||||||
|
<div class="abstract" th:text="${douban.spec.cardSubtitle}">描述</div>
|
||||||
|
<p th:text="${douban.faves.remark}">备注</p>
|
||||||
|
<a th:href="${douban.spec.link}" target="_blank" rel="external nofollow">查看豆瓣原页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<p th:if="${douban == null}">未找到该条豆瓣记录。</p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -83,9 +83,9 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
<footer class="site--footer">
|
<footer class="site--footer">
|
||||||
<script th:src="@{/plugins/plugin-douban/assets/static/db.min.js?v={version}(version=${version})}"></script>
|
<script th:src="@{/plugins/plugin-douban/assets/static/db.js?v={version}(version=${version})}"></script>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ref, computed, nextTick, watch} from "vue";
|
|||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import {doubanCoreApiClient} from "@/api";
|
import {doubanCoreApiClient} from "@/api";
|
||||||
import type { DoubanMovie } from "@/api/generated";
|
import type { DoubanMovie } from "@/api/generated";
|
||||||
import {toDatetimeLocal, toISOString} from "@/utils/date";
|
import {toDateInput, toISOString} from "@/utils/date";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -55,6 +55,8 @@ const saving = ref<boolean>(false);
|
|||||||
const formVisible = ref(false);
|
const formVisible = ref(false);
|
||||||
const createTime = ref<string | undefined>(undefined);
|
const createTime = ref<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const getToday = () => toDateInput(new Date());
|
||||||
|
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.metadata.creationTimestamp;
|
return !!formState.value.metadata.creationTimestamp;
|
||||||
@@ -73,6 +75,9 @@ const onVisibleChange = (visible: boolean) => {
|
|||||||
|
|
||||||
const handleResetForm = () => {
|
const handleResetForm = () => {
|
||||||
formState.value = cloneDeep(initialFormState);
|
formState.value = cloneDeep(initialFormState);
|
||||||
|
const today = getToday();
|
||||||
|
createTime.value = today;
|
||||||
|
formState.value.faves.createTime = toISOString(today);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -94,9 +99,9 @@ watch(
|
|||||||
(doubanMovie) => {
|
(doubanMovie) => {
|
||||||
if (doubanMovie) {
|
if (doubanMovie) {
|
||||||
formState.value = cloneDeep(doubanMovie);
|
formState.value = cloneDeep(doubanMovie);
|
||||||
createTime.value = toDatetimeLocal(formState.value.faves.createTime);
|
createTime.value = toDateInput(formState.value.faves.createTime) || getToday();
|
||||||
}else {
|
}else {
|
||||||
createTime.value = undefined;
|
createTime.value = getToday();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -108,6 +113,9 @@ watch(
|
|||||||
() => createTime.value,
|
() => createTime.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
formState.value.faves.createTime = value ? toISOString(value) : undefined;
|
formState.value.faves.createTime = value ? toISOString(value) : undefined;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const annotationsFormRef = ref();
|
const annotationsFormRef = ref();
|
||||||
@@ -223,6 +231,7 @@ const handleSaveFriend = async () => {
|
|||||||
name="type"
|
name="type"
|
||||||
label="类型"
|
label="类型"
|
||||||
placeholder="输入类型 value,例如 movie"
|
placeholder="输入类型 value,例如 movie"
|
||||||
|
validation="required"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-if="formState.spec.dataType=='halo'"
|
v-if="formState.spec.dataType=='halo'"
|
||||||
@@ -231,12 +240,10 @@ const handleSaveFriend = async () => {
|
|||||||
name="cardSubtitle"
|
name="cardSubtitle"
|
||||||
label="描述"
|
label="描述"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
validation="required|length:0,300"
|
validation="required|length:0,1000"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
type="datetime-local"
|
type="date"
|
||||||
min="0000-01-01T00:00"
|
|
||||||
max="9999-12-31T23:59"
|
|
||||||
v-model="createTime"
|
v-model="createTime"
|
||||||
name="createTime"
|
name="createTime"
|
||||||
validation="required"
|
validation="required"
|
||||||
@@ -252,6 +259,7 @@ const handleSaveFriend = async () => {
|
|||||||
v-model="formState.faves.status"
|
v-model="formState.faves.status"
|
||||||
name="status"
|
name="status"
|
||||||
type="select"
|
type="select"
|
||||||
|
validation="required"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
|||||||
@@ -47,10 +47,16 @@ export function toDatetimeLocal(date: string | Date | undefined | null): string
|
|||||||
return dayjs(date).format("YYYY-MM-DDTHH:mm");
|
return dayjs(date).format("YYYY-MM-DDTHH:mm");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toDateInput(date: string | Date | undefined | null): string {
|
||||||
|
if (!date) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return dayjs(date).format("YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
export function toISOString(date: string | Date | undefined | null): string {
|
export function toISOString(date: string | Date | undefined | null): string {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return dayjs(date).utc(false).toISOString();
|
return dayjs(date).utc(false).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user