Scope knowledge and image libraries by brand
Frontend CI / Frontend (push) Successful in 2m59s
Backend CI / Backend (push) Successful in 14m58s

This commit is contained in:
2026-06-30 12:30:10 +08:00
parent 95ddb3057f
commit ced0c4ec0f
35 changed files with 1136 additions and 326 deletions
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { imagesApi } from '@/lib/api'
import { useCompanyStore } from '@/stores/company'
import type { ScheduleCoverMode, ScheduleCoverRandomScope } from '@geo/shared-types'
import { useQuery } from '@tanstack/vue-query'
import { computed, watch } from 'vue'
@@ -24,10 +25,12 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const companyStore = useCompanyStore()
const currentBrandId = computed(() => companyStore.currentBrandId)
const foldersQuery = useQuery({
queryKey: ['images', 'folders', 'cover-source'],
enabled: computed(() => !props.disabled),
queryKey: computed(() => ['images', 'folders', 'cover-source', currentBrandId.value]),
enabled: computed(() => !props.disabled && Boolean(currentBrandId.value)),
queryFn: () => imagesApi.listFolders(),
})
@@ -38,6 +41,13 @@ const folderOptions = computed(() =>
})),
)
watch(
currentBrandId,
() => {
emit('update:randomFolderId', null)
},
)
watch(
() => props.mode,
(mode) => {
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { imagesApi } from '@/lib/api'
import { formatBytes, formatPercentage } from '@/lib/display'
import { useCompanyStore } from '@/stores/company'
import type { ImageAssetItem } from '@geo/shared-types'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
@@ -20,6 +21,8 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const companyStore = useCompanyStore()
const currentBrandId = computed(() => companyStore.currentBrandId)
// State
const selectedFolderId = ref<number | null>(null)
@@ -31,7 +34,8 @@ const selectedId = ref<number | null>(null)
// Queries
const foldersQuery = useQuery({
queryKey: ['images', 'folders'],
queryKey: computed(() => ['images', 'folders', currentBrandId.value]),
enabled: computed(() => Boolean(currentBrandId.value)),
queryFn: () => imagesApi.listFolders(),
})
@@ -40,6 +44,7 @@ const imagesQuery = useQuery({
'images',
'list',
'picker',
currentBrandId.value,
{
folder_id: selectedFolderId.value,
q: debouncedSearch.value,
@@ -47,6 +52,7 @@ const imagesQuery = useQuery({
page_size: pageSize.value,
},
]),
enabled: computed(() => Boolean(currentBrandId.value)),
queryFn: () =>
imagesApi.list({
folder_id: selectedFolderId.value ?? undefined,
@@ -77,6 +83,12 @@ watch(searchKeyword, () => {
}, 300)
})
watch(currentBrandId, () => {
selectedFolderId.value = null
selectedId.value = null
currentPage.value = 1
})
function handleSelect(image: ImageAssetItem) {
selectedId.value = image.id
emit('select', image)
@@ -5,6 +5,7 @@ import { computed, watch } from 'vue'
import MarkdownPreview from '@/components/MarkdownPreview.vue'
import { knowledgeApi } from '@/lib/api'
import { formatDateTime } from '@/lib/display'
import { useCompanyStore } from '@/stores/company'
const props = defineProps<{
open: boolean
@@ -16,10 +17,12 @@ const emit = defineEmits<{
}>()
const queryClient = useQueryClient()
const enabled = computed(() => props.open && Boolean(props.itemId))
const companyStore = useCompanyStore()
const currentBrandId = computed(() => companyStore.currentBrandId)
const enabled = computed(() => props.open && Boolean(props.itemId) && Boolean(currentBrandId.value))
const detailQuery = useQuery({
queryKey: computed(() => ['knowledge', 'detail', props.itemId]),
queryKey: computed(() => ['knowledge', 'detail', currentBrandId.value, props.itemId]),
enabled,
queryFn: () => knowledgeApi.detail(props.itemId as number),
})
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue'
import { knowledgeApi } from '@/lib/api'
import { useCompanyStore } from '@/stores/company'
interface TreeNode {
title: string
@@ -29,8 +30,12 @@ const emit = defineEmits<{
'update:modelValue': [value: number[]]
}>()
const companyStore = useCompanyStore()
const currentBrandId = computed(() => companyStore.currentBrandId)
const groupsQuery = useQuery({
queryKey: ['knowledge', 'groups', 'selector'],
queryKey: computed(() => ['knowledge', 'groups', 'selector', currentBrandId.value]),
enabled: computed(() => Boolean(currentBrandId.value)),
queryFn: () => knowledgeApi.listGroups(),
})
+5
View File
@@ -355,6 +355,8 @@ const currentBrandScopedPathPatterns = [
/^\/api\/tenant\/enterprise-sites\/[^/]+\/publish(?:\/|$)/,
/^\/api\/tenant\/media-supply\/orders(?:\/|$)/,
/^\/api\/tenant\/prompt-rules(?:\/|$)/,
/^\/api\/tenant\/knowledge(?:\/|$)/,
/^\/api\/tenant\/images(?:\/|$)/,
]
function shouldAttachCurrentBrandHeader(url: unknown): boolean {
@@ -362,6 +364,9 @@ function shouldAttachCurrentBrandHeader(url: unknown): boolean {
return false
}
const pathname = normalizeRequestPath(url)
if (pathname === '/api/tenant/images/storage-usage') {
return false
}
return currentBrandScopedPathPatterns.some((pattern) => pattern.test(pathname))
}
+20 -2
View File
@@ -18,11 +18,14 @@ import { imagesApi } from '@/lib/api'
import { formatBytes, formatDateTime, formatPercentage } from '@/lib/display'
import { formatError } from '@/lib/errors'
import { IMAGE_UPLOAD_ACCEPT, useImageUploadProgress } from '@/lib/image-webp'
import { useCompanyStore } from '@/stores/company'
import type { ImageAssetItem, ImageFolder } from '@geo/shared-types'
const { t } = useI18n()
const queryClient = useQueryClient()
const imageUploadProgress = useImageUploadProgress()
const companyStore = useCompanyStore()
const currentBrandId = computed(() => companyStore.currentBrandId)
// State
const selectedFolderId = ref<number | null>(null) // null means "All" or is handled by folders query
@@ -51,7 +54,8 @@ const fileInput = ref<HTMLInputElement | null>(null)
// Queries
const foldersQuery = useQuery({
queryKey: ['images', 'folders'],
queryKey: computed(() => ['images', 'folders', currentBrandId.value]),
enabled: computed(() => Boolean(currentBrandId.value)),
queryFn: () => imagesApi.listFolders(),
})
@@ -59,6 +63,7 @@ const imagesQuery = useQuery({
queryKey: computed(() => [
'images',
'list',
currentBrandId.value,
{
folder_id: debouncedSearch.value ? undefined : selectedFolderId.value,
q: debouncedSearch.value,
@@ -66,6 +71,7 @@ const imagesQuery = useQuery({
page_size: pageSize.value,
},
]),
enabled: computed(() => Boolean(currentBrandId.value)),
queryFn: () =>
imagesApi.list({
folder_id: debouncedSearch.value ? undefined : (selectedFolderId.value ?? undefined),
@@ -76,7 +82,8 @@ const imagesQuery = useQuery({
})
const allImagesCountQuery = useQuery({
queryKey: ['images', 'list', 'all-count'],
queryKey: computed(() => ['images', 'list', 'all-count', currentBrandId.value]),
enabled: computed(() => Boolean(currentBrandId.value)),
queryFn: () =>
imagesApi.list({
page: 1,
@@ -181,6 +188,17 @@ watch(searchKeyword, () => {
}, 300)
})
watch(currentBrandId, () => {
selectedFolderId.value = null
movingImage.value = null
previewingImage.value = null
renamingImage.value = null
moveModalOpen.value = false
previewModalOpen.value = false
renameImageModalOpen.value = false
currentPage.value = 1
})
function handleFolderSelect(id: number | null) {
if (debouncedSearch.value) return
selectedFolderId.value = id
+19 -2
View File
@@ -16,6 +16,7 @@ import KnowledgeDetailDrawer from '@/components/KnowledgeDetailDrawer.vue'
import { knowledgeApi } from '@/lib/api'
import { formatDateTime } from '@/lib/display'
import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company'
type KnowledgeSourceType = 'document' | 'website' | 'text'
type KnowledgeGroupLevel = 'root' | 'child'
@@ -35,6 +36,8 @@ interface TreeNode {
}
const queryClient = useQueryClient()
const companyStore = useCompanyStore()
const currentBrandId = computed(() => companyStore.currentBrandId)
const selectedGroupId = ref<number | null>(null)
const groupModalOpen = ref(false)
@@ -59,12 +62,14 @@ const urlsText = ref('')
const textItems = ref([{ name: '', content: '' }])
const groupsQuery = useQuery({
queryKey: ['knowledge', 'groups'],
queryKey: computed(() => ['knowledge', 'groups', currentBrandId.value]),
enabled: computed(() => Boolean(currentBrandId.value)),
queryFn: () => knowledgeApi.listGroups(),
})
const itemsQuery = useQuery({
queryKey: computed(() => ['knowledge', 'items', selectedGroupId.value]),
queryKey: computed(() => ['knowledge', 'items', currentBrandId.value, selectedGroupId.value]),
enabled: computed(() => Boolean(currentBrandId.value)),
queryFn: () => knowledgeApi.listItems(selectedGroupId.value ?? undefined),
refetchInterval: (query) => {
const items = query.state.data as KnowledgeItem[] | undefined
@@ -267,6 +272,18 @@ const retryItemMutation = useMutation({
onError: (error) => message.error(formatError(error)),
})
watch(
currentBrandId,
() => {
selectedGroupId.value = null
groupModalOpen.value = false
editingGroup.value = null
itemModalOpen.value = false
previewItemId.value = null
previewDrawerOpen.value = false
},
)
watch(
() => groupsQuery.data.value,
(groups) => {