1
0

feat: 增加首页过滤和搜索

This commit is contained in:
2024-11-08 12:15:41 +08:00
parent 6925600043
commit d9ebc287f7
6 changed files with 323 additions and 141 deletions

View File

@@ -41,6 +41,7 @@ dependencies {
implementation("cn.bigmodel.openapi:oapi-java-sdk:release-V4-2.3.0") implementation("cn.bigmodel.openapi:oapi-java-sdk:release-V4-2.3.0")
implementation("com.baidubce:qianfan:0.1.1") implementation("com.baidubce:qianfan:0.1.1")
implementation("org.jsoup:jsoup:1.18.1") implementation("org.jsoup:jsoup:1.18.1")
implementation("com.blinkfox:fenix-spring-boot-starter:3.0.0")
val hutoolVersion = "5.8.32" val hutoolVersion = "5.8.32"
implementation("cn.hutool:hutool-core:$hutoolVersion") implementation("cn.hutool:hutool-core:$hutoolVersion")

View File

@@ -1,5 +1,6 @@
package com.lanyuanxiaoyao.digtal.market package com.lanyuanxiaoyao.digtal.market
import com.blinkfox.fenix.EnableFenix
import com.lanyuanxiaoyao.digtal.market.runner.NewsRunner import com.lanyuanxiaoyao.digtal.market.runner.NewsRunner
import com.lanyuanxiaoyao.digtal.market.runner.PushRunner import com.lanyuanxiaoyao.digtal.market.runner.PushRunner
import com.lanyuanxiaoyao.squirrel.core.common.Management import com.lanyuanxiaoyao.squirrel.core.common.Management
@@ -20,7 +21,6 @@ import org.springframework.context.ApplicationListener
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.context.event.ContextClosedEvent import org.springframework.context.event.ContextClosedEvent
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@@ -41,7 +41,8 @@ data class MailProperties @ConstructorBinding constructor(
val targets: List<String>, val targets: List<String>,
) )
@EnableScheduling // @EnableScheduling
@EnableFenix
@ConfigurationPropertiesScan @ConfigurationPropertiesScan
@SpringBootApplication @SpringBootApplication
class Application : ApplicationRunner, ApplicationListener<ContextClosedEvent> { class Application : ApplicationRunner, ApplicationListener<ContextClosedEvent> {

View File

@@ -1,11 +1,11 @@
package com.lanyuanxiaoyao.digtal.market package com.lanyuanxiaoyao.digtal.market
import com.blinkfox.fenix.specification.FenixJpaSpecificationExecutor
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.Id import jakarta.persistence.Id
import java.util.Date import java.util.Date
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param import org.springframework.data.repository.query.Param
@@ -30,12 +30,18 @@ class Article(
) )
@Repository @Repository
interface ArticleRepository : JpaRepository<Article, String>, JpaSpecificationExecutor<Article> { interface ArticleRepository : JpaRepository<Article, String>, FenixJpaSpecificationExecutor<Article> {
fun findAllByDescriptionIsNullAndTextIsNotNull(): List<Article> fun findAllByDescriptionIsNullAndTextIsNotNull(): List<Article>
fun findAllByHtmlIsNotNull(): List<Article> fun findAllByHtmlIsNotNull(): List<Article>
fun existsByCode(code: String): Boolean fun existsByCode(code: String): Boolean
@Query("select distinct article.category from Article article where article.category is not null and article.category <> ''")
fun findAllCategory(): List<String>
@Query("select distinct article.author from Article article where article.author is not null and article.author <> ''")
fun findAllAuthor(): List<String>
@Modifying @Modifying
@Transactional @Transactional
@Query("update Article article set article.pushed = :pushed where article.id = :id") @Query("update Article article set article.pushed = :pushed where article.id = :id")

View File

@@ -1,9 +1,13 @@
package com.lanyuanxiaoyao.digtal.market.controller package com.lanyuanxiaoyao.digtal.market.controller
import cn.hutool.core.date.LocalDateTimeUtil
import com.lanyuanxiaoyao.digtal.market.ArticleRepository import com.lanyuanxiaoyao.digtal.market.ArticleRepository
import com.lanyuanxiaoyao.digtal.market.sites import com.lanyuanxiaoyao.digtal.market.sites
import com.lanyuanxiaoyao.squirrel.core.common.Site import com.lanyuanxiaoyao.squirrel.core.common.Site
import jakarta.annotation.Resource import jakarta.annotation.Resource
import jakarta.persistence.criteria.Predicate
import java.sql.Date
import java.time.Instant
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
@@ -23,11 +27,66 @@ class OverviewController {
@GetMapping("news") @GetMapping("news")
fun news( fun news(
@RequestParam("code", required = false) code: String?, @RequestParam("code", required = false) code: String?,
@RequestParam("filter_keyword", required = false) filterKeyword: String?,
@RequestParam("filter_date", required = false) filterDate: Long?,
@RequestParam("filter_date_start", required = false) filterDateStart: Long?,
@RequestParam("filter_date_end", required = false) filterDateEnd: Long?,
@RequestParam("filter_source", required = false) filterSource: String?,
@RequestParam("filter_author", required = false) filterAuthor: String?,
@RequestParam("filter_category", required = false) filterCategory: String?,
@RequestParam("page", defaultValue = "1") page: Int, @RequestParam("page", defaultValue = "1") page: Int,
@RequestParam("count", defaultValue = "10") count: Int, @RequestParam("count", defaultValue = "10") count: Int,
): Map<String, Any> { ): Map<String, Any> {
val request = PageRequest.of(page - 1, count, Sort.by(Sort.Direction.DESC, "createTime")) val pageRequest = PageRequest.of(page - 1, count, Sort.by(Sort.Direction.DESC, "createTime"))
val result = articleRepository.findAll(request) val result = articleRepository.findAll({ root, _, builder ->
val predictions = mutableListOf<Predicate>()
filterKeyword?.let {
predictions.add(
builder.or(
builder.like(root.get("title"), "%${it}%"),
builder.like(root.get("subtitle"), "%${it}%"),
builder.like(root.get("text"), "%${it}%"),
builder.like(root.get("description"), "%${it}%"),
)
)
}
if (filterDate != null) {
val datetime = LocalDateTimeUtil.of(filterDate * 1000)
predictions.add(
builder.between(
root.get<Date>("createTime"),
Date.from(Instant.ofEpochMilli(LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.beginOfDay(datetime)))),
Date.from(Instant.ofEpochMilli(LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.endOfDay(datetime))))
)
)
} else if (filterDateStart != null && filterDateEnd != null) {
predictions.add(
builder.between(root.get<Date>("createTime"), Date.from(Instant.ofEpochSecond(filterDateStart)), Date.from(Instant.ofEpochSecond(filterDateEnd)))
)
}
filterSource?.let {
val site = sites.firstOrNull { site -> site.name == it }
if (site != null) {
predictions.add(
builder.equal(root.get<String>("code"), site.code)
)
}
}
filterAuthor?.let {
predictions.add(
builder.equal(root.get<String>("author"), it)
)
}
filterCategory?.let {
predictions.add(
builder.equal(root.get<String>("category"), it)
)
}
builder.and(*predictions.toTypedArray())
}, pageRequest)
articleRepository.findAll({ builder ->
builder.build()
}, pageRequest)
return mapOf( return mapOf(
"items" to result.content.map { "items" to result.content.map {
val site = sites.find { site -> site.code == it.code }!! val site = sites.find { site -> site.code == it.code }!!
@@ -49,4 +108,13 @@ class OverviewController {
"total" to result.totalElements, "total" to result.totalElements,
) )
} }
@GetMapping("all_source")
fun allSource() = sites.map { it.name }
@GetMapping("all_category")
fun allCategory() = articleRepository.findAllCategory()
@GetMapping("all_author")
fun allAuthor() = articleRepository.findAllAuthor()
} }

View File

@@ -43,4 +43,7 @@ messenger:
binary-path: /Users/lanyuanxiaoyao/Downloads/chromium/128/macOS-1289987/Chromium.app/Contents/MacOS/Chromium binary-path: /Users/lanyuanxiaoyao/Downloads/chromium/128/macOS-1289987/Chromium.app/Contents/MacOS/Chromium
database: database:
h2-path: ./database/database.db h2-path: ./database/database.db
json-path: ./database/database.json json-path: ./database/database.json
fenix:
print-banner: false
print-sql: false

View File

@@ -30,161 +30,264 @@ function overviewTab() {
return { return {
title: '总览', title: '总览',
tab: { tab: {
id: 'news_list', type: 'crud',
columnClassName: 'px-2 pt-2', syncLocation: false,
type: 'service',
api: { api: {
method: 'get', method: 'get',
url: '${base}/overview/news', url: '${base}/overview/news',
data: { data: {
page: '${page|default:1}', page: '${page|default:1}',
count: '${count|default:10}', count: '${count|default:10}',
filter_keyword: '${filter_keyword|default:undefined}',
filter_date: '${filter_date|default:undefined}',
filter_date_start: '${filter_date_start|default:undefined}',
filter_date_end: '${filter_date_end|default:undefined}',
filter_source: '${filter_source|default:undefined}',
filter_author: '${filter_author|default:undefined}',
filter_category: '${filter_category|default:undefined}',
} }
}, },
body: [ filterTogglable: true,
{ filterDefaultVisible: false,
type: 'action', mode: 'list',
icon: 'fa fa-refresh', headerToolbar: [
label: '刷新', 'reload',
className: 'mb-2', 'filter-toggler',
actionType: 'reload', 'pagination',
target: 'news_list', ],
footerToolbar: [],
filter: {
title: '搜索',
mode: 'horizontal',
horizontal: {
leftFixed: 'sm',
}, },
{ body: [
...pagination(), {
className: 'float-right', type: 'group',
},
{
type: 'list',
listItem: {
body: [ body: [
{ {
type: 'grid', type: 'input-text',
columns: [ label: '关键字',
{ name: 'filter_keyword',
body: { placeholder: '关键字搜索',
type: 'wrapper', },
size: 'none', ]
className: 'py-2', },
body: [ {
{ type: 'group',
type: 'wrapper', body: [
className: 'text-md font-bold', {
size: 'none', type: 'input-date',
body: [ label: '日期',
'${title}' name: 'filter_date',
] columnRatio: 4,
}, },
{ {
type: 'wrapper', type: 'input-date-range',
className: 'text-sm font-medium text-secondary', label: '日期范围',
size: 'none', name: 'filter_date_start',
body: [ extraName: 'filter_date_end',
'${subtitle}' columnRatio: 8,
] },
}, ]
] },
} {
}, type: 'group',
{ body: [
sm: 2, {
md: 2, type: 'select',
lg: 2, label: '来源',
columnClassName: 'content-center', name: 'filter_source',
body: { multiple: true,
type: 'operation', searchable: true,
className: 'text-right', source: {
buttons: [ method: 'get',
{ url: '${base}/overview/all_source',
visibleOn: '${!iframe}', adaptor: (payload, response, api, context) => {
type: 'action', console.log(payload, response, api, context)
label: '原文', return payload.map(i => {
icon: 'fa fa-paperclip', return {label: i, value: i}
actionType: 'url', })
url: '${url}',
blank: true,
},
{
visibleOn: '${iframe}',
type: 'action',
label: '原文',
icon: 'fa fa-firefox',
actionType: 'dialog',
dialog: {
title: '原文',
...readOnlyDialogOptions(),
size: 'xl',
body: {
type: 'iframe',
src: '${url}',
height: 1000,
}
}
},
{
type: 'action',
label: '搜索',
icon: 'fa fa-search',
actionType: 'url',
url: 'https://www.bing.com/search?q=${title}',
blank: true,
},
]
}
} }
] }
}, },
{ {
type: 'tpl', type: 'select',
className: 'text-current', label: '作者',
tpl: '${description}', name: 'filter_author',
multiple: true,
searchable: true,
source: {
method: 'get',
url: '${base}/overview/all_author',
adaptor: (payload, response, api, context) => {
console.log(payload, response, api, context)
return payload.map(i => {
return {label: i, value: i}
})
}
}
}, },
{ {
type: 'tpl', type: 'select',
className: 'text-blue-900 text-sm mt-2', label: '关键词',
tpl: '${name}', name: 'filter_category',
multiple: true,
searchable: true,
source: {
method: 'get',
url: '${base}/overview/all_category',
adaptor: (payload, response, api, context) => {
console.log(payload, response, api, context)
return payload.map(i => {
return {label: i, value: i}
})
}
}
},
]
}
],
actions: [
{
type: 'reset',
label: '清空'
},
{
type: 'submit',
label: '搜索',
level: 'primary',
},
]
},
canAccessSuperData: false,
listItem: {
body: [
{
type: 'grid',
columns: [
{
body: {
type: 'wrapper',
size: 'none',
className: 'py-2',
body: [
{
type: 'wrapper',
className: 'text-md font-bold',
size: 'none',
body: [
'${title}'
]
},
{
type: 'wrapper',
className: 'text-sm font-medium text-secondary',
size: 'none',
body: [
'${subtitle}'
]
},
]
}
}, },
{ {
type: 'wrapper', sm: 2,
size: 'none', md: 2,
className: 'mt-2', lg: 2,
body: [ columnClassName: 'content-center',
{ body: {
type: 'tag', type: 'operation',
label: '${DATETOSTR(DATE(createTime), \'YYYY-MM-DD HH:mm\')}', className: 'text-right',
displayMode: 'rounded', buttons: [
color: '#4096ff', {
}, visibleOn: '${!iframe}',
{ type: 'action',
type: 'tag', label: '原文',
label: '${category}', icon: 'fa fa-paperclip',
displayMode: 'rounded', actionType: 'url',
color: '#2fa15d', url: '${url}',
}, blank: true,
{ },
type: 'tag', {
label: '${author}', visibleOn: '${iframe}',
displayMode: 'rounded', type: 'action',
color: '#bd6464', label: '原文',
}, icon: 'fa fa-firefox',
] actionType: 'dialog',
}, dialog: {
{ title: '原文',
type: 'each', ...readOnlyDialogOptions(),
className: 'mt-2', size: 'xl',
source: "${SPLIT(tags, ',')}", body: {
items: { type: 'iframe',
type: 'tag', src: '${url}',
label: '${item}', height: 1000,
displayMode: 'rounded', }
color: '#6b3481', }
},
{
type: 'action',
label: '搜索',
icon: 'fa fa-search',
actionType: 'url',
url: 'https://www.bing.com/search?q=${title}',
blank: true,
},
]
} }
} }
] ]
}, },
}, {
pagination(), type: 'tpl',
] className: 'text-current',
} tpl: '${description}',
},
{
type: 'tpl',
className: 'text-blue-900 text-sm mt-2',
tpl: '${name}',
},
{
type: 'wrapper',
size: 'none',
className: 'mt-2',
body: [
{
type: 'tag',
label: '${DATETOSTR(DATE(createTime), \'YYYY-MM-DD HH:mm\')}',
displayMode: 'rounded',
color: '#4096ff',
},
{
type: 'tag',
label: '${category}',
displayMode: 'rounded',
color: '#2fa15d',
},
{
type: 'tag',
label: '${author}',
displayMode: 'rounded',
color: '#bd6464',
},
]
},
{
type: 'each',
className: 'mt-2',
source: "${SPLIT(tags, ',')}",
items: {
type: 'tag',
label: '${item}',
displayMode: 'rounded',
color: '#6b3481',
}
}
]
},
},
} }
} }