mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
feat: 为应用管理(原生应用、运行环境和容器模板)添加搜索功能 (#1487)
* Initial plan * feat: 为应用管理(原生应用、运行环境和容器模板)添加搜索功能 后端通过 GET query 参数传入关键词,按名称和描述进行大小写不敏感过滤。 容器模板额外支持按 website 字段搜索。 前端在分类标签旁添加搜索输入框,输入变化时自动触发重新查询。 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * refactor: 优化搜索过滤代码格式和 TemplateView 一致性 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
This commit is contained in:
@@ -36,6 +36,7 @@ func (s *AppService) Categories(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
|
||||
category := r.URL.Query().Get("category")
|
||||
query := strings.ToLower(r.URL.Query().Get("query"))
|
||||
|
||||
all := s.appRepo.All()
|
||||
installedApps, err := s.appRepo.Installed()
|
||||
@@ -62,6 +63,11 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
|
||||
if category != "" && !strings.Contains(strings.Join(item.Categories, ","), category) {
|
||||
continue
|
||||
}
|
||||
if query != "" &&
|
||||
!strings.Contains(strings.ToLower(item.Name), query) &&
|
||||
!strings.Contains(strings.ToLower(item.Description), query) {
|
||||
continue
|
||||
}
|
||||
|
||||
app := types.AppDetail{
|
||||
Name: item.Name,
|
||||
|
||||
@@ -31,12 +31,18 @@ func (s *EnvironmentService) Types(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *EnvironmentService) List(w http.ResponseWriter, r *http.Request) {
|
||||
typ := r.URL.Query().Get("type")
|
||||
query := strings.ToLower(r.URL.Query().Get("query"))
|
||||
all := s.environmentRepo.All()
|
||||
var environments []types.EnvironmentDetail
|
||||
for _, item := range all {
|
||||
if typ != "" && !strings.EqualFold(item.Type, typ) {
|
||||
continue
|
||||
}
|
||||
if query != "" &&
|
||||
!strings.Contains(strings.ToLower(item.Name), query) &&
|
||||
!strings.Contains(strings.ToLower(item.Description), query) {
|
||||
continue
|
||||
}
|
||||
environments = append(environments, types.EnvironmentDetail{
|
||||
Type: item.Type,
|
||||
Name: item.Name,
|
||||
|
||||
@@ -27,12 +27,17 @@ func NewTemplateService(t *gotext.Locale, template biz.TemplateRepo, setting biz
|
||||
// List 获取所有模版
|
||||
func (s *TemplateService) List(w http.ResponseWriter, r *http.Request) {
|
||||
category := r.URL.Query().Get("category")
|
||||
query := r.URL.Query().Get("query")
|
||||
templates := s.templateRepo.List()
|
||||
|
||||
// 按分类过滤
|
||||
if category != "" {
|
||||
templates = templates.FilterByCategory(category)
|
||||
}
|
||||
// 按关键词过滤
|
||||
if query != "" {
|
||||
templates = templates.FilterByQuery(query)
|
||||
}
|
||||
|
||||
paged, total := Paginate(r, templates)
|
||||
Success(w, chix.M{
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -40,6 +41,20 @@ func (t Templates) FilterByCategory(category string) Templates {
|
||||
return filtered
|
||||
}
|
||||
|
||||
// FilterByQuery 按关键词过滤模版
|
||||
func (t Templates) FilterByQuery(query string) Templates {
|
||||
query = strings.ToLower(query)
|
||||
filtered := make(Templates, 0)
|
||||
for _, tpl := range t {
|
||||
if strings.Contains(strings.ToLower(tpl.Name), query) ||
|
||||
strings.Contains(strings.ToLower(tpl.Description), query) ||
|
||||
strings.Contains(strings.ToLower(tpl.Website), query) {
|
||||
filtered = append(filtered, tpl)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// Templates 返回所有模版
|
||||
func (r *API) Templates() (*Templates, error) {
|
||||
resp, err := r.client.R().SetResult(&Response{}).Get("/templates")
|
||||
|
||||
@@ -4,8 +4,8 @@ export default {
|
||||
// 获取分类列表
|
||||
categories: (): any => http.Get('/app/categories'),
|
||||
// 获取应用列表
|
||||
list: (page: number, limit: number, category?: string): any =>
|
||||
http.Get('/app/list', { params: { page, limit, category } }),
|
||||
list: (page: number, limit: number, category?: string, query?: string): any =>
|
||||
http.Get('/app/list', { params: { page, limit, category, query } }),
|
||||
// 安装应用
|
||||
install: (slug: string, channel: string | null): any =>
|
||||
http.Post('/app/install', { slug, channel }),
|
||||
|
||||
@@ -4,8 +4,8 @@ export default {
|
||||
// 获取环境类型列表
|
||||
types: (): any => http.Get('/environment/types'),
|
||||
// 获取环境列表
|
||||
list: (page: number, limit: number, type?: string): any =>
|
||||
http.Get('/environment/list', { params: { page, limit, type } }),
|
||||
list: (page: number, limit: number, type?: string, query?: string): any =>
|
||||
http.Get('/environment/list', { params: { page, limit, type, query } }),
|
||||
// 安装环境
|
||||
install: (type: string, slug: string): any => http.Post('/environment/install', { type, slug }),
|
||||
// 卸载环境
|
||||
|
||||
@@ -2,8 +2,8 @@ import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取模版列表
|
||||
list: (page: number, pageSize: number, category?: string): any =>
|
||||
http.Get(`/template`, { params: { page, limit: pageSize, category } }),
|
||||
list: (page: number, pageSize: number, category?: string, query?: string): any =>
|
||||
http.Get(`/template`, { params: { page, limit: pageSize, category, query } }),
|
||||
// 获取模版详情
|
||||
get: (slug: string): any => http.Get(`/template/${slug}`),
|
||||
// 使用模版创建编排
|
||||
|
||||
@@ -3,7 +3,7 @@ defineOptions({
|
||||
name: 'app-index'
|
||||
})
|
||||
|
||||
import { NButton, NDataTable, NFlex, NPopconfirm, NSwitch, NTag } from 'naive-ui'
|
||||
import { NButton, NDataTable, NFlex, NInput, NPopconfirm, NSwitch, NTag } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import app from '@/api/panel/app'
|
||||
@@ -19,6 +19,7 @@ const versionModalInfo = ref<any>({})
|
||||
|
||||
// 当前选中的分类
|
||||
const selectedCategory = ref<string>('')
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
@@ -169,13 +170,14 @@ const { data: categories } = useRequest(app.categories, {
|
||||
})
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
(page, pageSize) => app.list(page, pageSize, selectedCategory.value || undefined),
|
||||
(page, pageSize) =>
|
||||
app.list(page, pageSize, selectedCategory.value || undefined, searchQuery.value || undefined),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
initialPageSize: 20,
|
||||
total: (res: any) => res.total,
|
||||
data: (res: any) => res.items,
|
||||
watchingStates: [selectedCategory]
|
||||
watchingStates: [selectedCategory, searchQuery]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -219,25 +221,33 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-tag
|
||||
:type="selectedCategory === '' ? 'primary' : 'default'"
|
||||
:bordered="selectedCategory !== ''"
|
||||
style="cursor: pointer"
|
||||
@click="handleCategoryChange('')"
|
||||
>
|
||||
{{ $gettext('All') }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-for="cat in categories"
|
||||
:key="cat.value"
|
||||
:type="selectedCategory === cat.value ? 'primary' : 'default'"
|
||||
:bordered="selectedCategory !== cat.value"
|
||||
style="cursor: pointer"
|
||||
@click="handleCategoryChange(cat.value)"
|
||||
>
|
||||
{{ cat.label }}
|
||||
</n-tag>
|
||||
<n-flex justify="space-between">
|
||||
<n-flex>
|
||||
<n-tag
|
||||
:type="selectedCategory === '' ? 'primary' : 'default'"
|
||||
:bordered="selectedCategory !== ''"
|
||||
style="cursor: pointer"
|
||||
@click="handleCategoryChange('')"
|
||||
>
|
||||
{{ $gettext('All') }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-for="cat in categories"
|
||||
:key="cat.value"
|
||||
:type="selectedCategory === cat.value ? 'primary' : 'default'"
|
||||
:bordered="selectedCategory !== cat.value"
|
||||
style="cursor: pointer"
|
||||
@click="handleCategoryChange(cat.value)"
|
||||
>
|
||||
{{ cat.label }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
:placeholder="$gettext('Search')"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import environment from '@/api/panel/environment'
|
||||
import { router } from '@/router'
|
||||
import { renderLocalIcon } from '@/utils'
|
||||
import { NButton, NDataTable, NFlex, NPopconfirm, NTag } from 'naive-ui'
|
||||
import { NButton, NDataTable, NFlex, NInput, NPopconfirm, NTag } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const selectedType = ref<string>('')
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
const { data: types } = useRequest(environment.types, {
|
||||
initialData: []
|
||||
@@ -156,13 +157,14 @@ const columns: any = [
|
||||
]
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
(page, pageSize) => environment.list(page, pageSize, selectedType.value || undefined),
|
||||
(page, pageSize) =>
|
||||
environment.list(page, pageSize, selectedType.value || undefined, searchQuery.value || undefined),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
initialPageSize: 20,
|
||||
total: (res: any) => res.total,
|
||||
data: (res: any) => res.items,
|
||||
watchingStates: [selectedType]
|
||||
watchingStates: [selectedType, searchQuery]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -207,25 +209,33 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-tag
|
||||
:type="selectedType === '' ? 'primary' : 'default'"
|
||||
:bordered="selectedType !== ''"
|
||||
style="cursor: pointer"
|
||||
@click="handleTypeChange('')"
|
||||
>
|
||||
{{ $gettext('All') }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-for="type in types"
|
||||
:key="type.value"
|
||||
:type="selectedType === type.value ? 'primary' : 'default'"
|
||||
:bordered="selectedType !== type.value"
|
||||
style="cursor: pointer"
|
||||
@click="handleTypeChange(type.value)"
|
||||
>
|
||||
{{ type.label }}
|
||||
</n-tag>
|
||||
<n-flex justify="space-between">
|
||||
<n-flex>
|
||||
<n-tag
|
||||
:type="selectedType === '' ? 'primary' : 'default'"
|
||||
:bordered="selectedType !== ''"
|
||||
style="cursor: pointer"
|
||||
@click="handleTypeChange('')"
|
||||
>
|
||||
{{ $gettext('All') }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-for="type in types"
|
||||
:key="type.value"
|
||||
:type="selectedType === type.value ? 'primary' : 'default'"
|
||||
:bordered="selectedType !== type.value"
|
||||
style="cursor: pointer"
|
||||
@click="handleTypeChange(type.value)"
|
||||
>
|
||||
{{ type.label }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
:placeholder="$gettext('Search')"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
NFlex,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NInput,
|
||||
NPagination,
|
||||
NSpin,
|
||||
NTag
|
||||
@@ -21,11 +22,18 @@ import type { Template } from './types'
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const selectedCategory = ref<string>('')
|
||||
const searchQuery = ref<string>('')
|
||||
const deployModalShow = ref(false)
|
||||
const selectedTemplate = ref<Template | null>(null)
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
(page, pageSize) => template.list(page, pageSize, selectedCategory.value || undefined),
|
||||
(page, pageSize) =>
|
||||
template.list(
|
||||
page,
|
||||
pageSize,
|
||||
selectedCategory.value || undefined,
|
||||
searchQuery.value || undefined
|
||||
),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
initialPageSize: 12,
|
||||
@@ -50,6 +58,11 @@ const handleCategoryChange = (category: string) => {
|
||||
refresh()
|
||||
}
|
||||
|
||||
watch(searchQuery, () => {
|
||||
page.value = 1
|
||||
refresh()
|
||||
})
|
||||
|
||||
const handleDeploy = (tpl: Template) => {
|
||||
selectedTemplate.value = tpl
|
||||
deployModalShow.value = true
|
||||
@@ -62,25 +75,33 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<n-flex vertical :size="20">
|
||||
<n-flex>
|
||||
<n-tag
|
||||
:type="selectedCategory === '' ? 'primary' : 'default'"
|
||||
:bordered="selectedCategory !== ''"
|
||||
style="cursor: pointer"
|
||||
@click="handleCategoryChange('')"
|
||||
>
|
||||
{{ $gettext('All') }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-for="cat in categories"
|
||||
:key="cat.value"
|
||||
:type="selectedCategory === cat.value ? 'primary' : 'default'"
|
||||
:bordered="selectedCategory !== cat.value"
|
||||
style="cursor: pointer"
|
||||
@click="handleCategoryChange(cat.value)"
|
||||
>
|
||||
{{ cat.label }}
|
||||
</n-tag>
|
||||
<n-flex justify="space-between">
|
||||
<n-flex>
|
||||
<n-tag
|
||||
:type="selectedCategory === '' ? 'primary' : 'default'"
|
||||
:bordered="selectedCategory !== ''"
|
||||
style="cursor: pointer"
|
||||
@click="handleCategoryChange('')"
|
||||
>
|
||||
{{ $gettext('All') }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-for="cat in categories"
|
||||
:key="cat.value"
|
||||
:type="selectedCategory === cat.value ? 'primary' : 'default'"
|
||||
:bordered="selectedCategory !== cat.value"
|
||||
style="cursor: pointer"
|
||||
@click="handleCategoryChange(cat.value)"
|
||||
>
|
||||
{{ cat.label }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
:placeholder="$gettext('Search')"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</n-flex>
|
||||
|
||||
<n-spin :show="loading">
|
||||
|
||||
Reference in New Issue
Block a user