From ffbb8e1b0211d8b1a224ccd71e270ec2a6cf066b Mon Sep 17 00:00:00 2001 From: wizardchen Date: Mon, 2 Feb 2026 22:45:13 +0800 Subject: [PATCH] feat(organization): enhance organization selection and sharing UI - Updated organization selection dropdowns to include new SVG icons for better visual representation. - Improved organization option layout with additional metadata display, including member count and share count. - Enhanced share item display with organization icons and improved action buttons for better user interaction. - Adjusted styles for organization-related components to improve usability and aesthetics. This update enhances the user experience by providing clearer organization visuals and more intuitive sharing options. --- .../components/ShareKnowledgeBaseDialog.vue | 207 ++++++++++++------ .../src/views/knowledge/KnowledgeBaseList.vue | 38 +++- .../knowledge/settings/KBShareSettings.vue | 178 +++++++++------ .../views/organization/OrganizationList.vue | 67 +++++- .../OrganizationSettingsModal.vue | 16 +- internal/application/service/organization.go | 18 +- internal/types/organization.go | 28 +-- 7 files changed, 383 insertions(+), 169 deletions(-) diff --git a/frontend/src/components/ShareKnowledgeBaseDialog.vue b/frontend/src/components/ShareKnowledgeBaseDialog.vue index bae1fbbb..0086b693 100644 --- a/frontend/src/components/ShareKnowledgeBaseDialog.vue +++ b/frontend/src/components/ShareKnowledgeBaseDialog.vue @@ -18,6 +18,8 @@ v-model="shareForm.organization_id" :placeholder="$t('organization.share.selectOrgPlaceholder')" :loading="loadingOrgs" + class="org-select-dropdown" + :popup-props="{ overlayClassName: 'org-select-dropdown-popup' }" >
-
- {{ org.name }} - - {{ $t('organization.owner') }} - - - {{ $t(`organization.role.${org.my_role}`) }} - +
+
-
{{ org.description }}
-
- - - {{ org.member_count || 0 }} {{ $t('organization.members') }} - - - - {{ org.share_count }} {{ $t('organization.share.sharedKBs') }} - +
+
+ {{ org.name }} + + {{ $t('organization.owner') }} + + + {{ $t(`organization.role.${org.my_role}`) }} + +
+
+ + + {{ org.member_count || 0 }} {{ $t('organization.members') }} + + + + {{ org.share_count }} {{ $t('organization.share.sharedKBs') }} + +
@@ -90,19 +96,27 @@
@@ -113,11 +127,13 @@ import { ref, computed, watch } from 'vue' import { MessagePlugin } from 'tdesign-vue-next' import { useI18n } from 'vue-i18n' +import { useRouter } from 'vue-router' import { useOrganizationStore } from '@/stores/organization' import { shareKnowledgeBase, listKBShares, removeShare } from '@/api/organization' import type { KnowledgeBaseShare } from '@/api/organization' const { t } = useI18n() +const router = useRouter() const orgStore = useOrganizationStore() interface Props { @@ -241,6 +257,16 @@ async function handleUnshare(share: KnowledgeBaseShare) { function handleClose() { emit('update:visible', false) } + +// Navigate to organization settings +function handleGoToOrgSettings(orgId: string) { + router.push({ + path: '/platform/organizations', + query: { orgId } + }) + // 关闭当前弹窗 + emit('update:visible', false) +} diff --git a/frontend/src/views/knowledge/KnowledgeBaseList.vue b/frontend/src/views/knowledge/KnowledgeBaseList.vue index 62260fe2..5dc6344f 100644 --- a/frontend/src/views/knowledge/KnowledgeBaseList.vue +++ b/frontend/src/views/knowledge/KnowledgeBaseList.vue @@ -248,11 +248,11 @@
-
- - {{ kb.org_name }} -
-
+
+ + {{ kb.org_name }} +
+
@@ -342,7 +342,7 @@ - +
@@ -416,7 +416,7 @@
- + {{ shared.org_name }}
@@ -551,8 +551,8 @@
-
- +
+ {{ currentSharedKb.org_name }}
@@ -1224,11 +1224,11 @@ const handleUploadFinishedEvent = (event: Event) => { } } -// 来源组织 +// 来源组织(空间图标) .org-source { display: flex; align-items: center; - gap: 4px; + gap: 6px; padding: 2px 8px; background: rgba(0, 0, 0, 0.04); border-radius: 4px; @@ -1242,6 +1242,12 @@ const handleUploadFinishedEvent = (event: Event) => { white-space: nowrap; } + .org-source-icon { + width: 14px; + height: 14px; + flex-shrink: 0; + } + .t-icon { color: #999; flex-shrink: 0; @@ -1517,6 +1523,16 @@ const handleUploadFinishedEvent = (event: Event) => { color: #00000099; } + .form-value-org { + gap: 8px; + } + + .form-value-org-icon { + width: 18px; + height: 18px; + flex-shrink: 0; + } + .permission-list { margin-top: 20px; display: flex; diff --git a/frontend/src/views/knowledge/settings/KBShareSettings.vue b/frontend/src/views/knowledge/settings/KBShareSettings.vue index 39dd1f9a..88b467ac 100644 --- a/frontend/src/views/knowledge/settings/KBShareSettings.vue +++ b/frontend/src/views/knowledge/settings/KBShareSettings.vue @@ -14,7 +14,8 @@ v-model="selectedOrgId" :placeholder="$t('organization.share.selectOrgPlaceholder')" :loading="loadingOrgs" - class="org-select" + class="org-select org-select-dropdown" + :popup-props="{ overlayClassName: 'org-select-dropdown-popup' }" >
-
- {{ org.name }} - - {{ $t('organization.owner') }} - - - {{ $t(`organization.role.${org.my_role}`) }} - +
+
-
{{ org.description }}
-
- - - {{ org.member_count || 0 }} {{ $t('organization.members') }} - - - - {{ org.share_count }} {{ $t('organization.share.sharedKBs') }} - +
+
+ {{ org.name }} + + {{ $t('organization.owner') }} + + + {{ $t(`organization.role.${org.my_role}`) }} + +
+
+ + + {{ org.member_count || 0 }} {{ $t('organization.members') }} + + + + {{ org.share_count }} {{ $t('organization.share.sharedKBs') }} + +
@@ -87,7 +92,7 @@
@@ -605,6 +613,13 @@ const inviteBodyWrapStyle = computed(() => { return { maxHeight: `${px}px`, minHeight: `${px}px` } }) +// 预览中空间 ID 的简短显示(前 8 位 + …) +const shortPreviewSpaceId = computed(() => { + const id = invitePreviewData.value?.id + if (!id) return '' + return id.length > 8 ? `${id.slice(0, 8)}…` : id +}) + // 根据当前 body 内容更新高度(用于过渡动画) function updateInviteBodyHeight() { const el = inviteBodyInnerRef.value @@ -1090,6 +1105,16 @@ function viewOrganizationFromPreview() { showSettingsModal.value = true } +// 复制预览中的空间 ID +function copyPreviewSpaceId() { + if (!invitePreviewData.value?.id) return + navigator.clipboard.writeText(invitePreviewData.value.id).then(() => { + MessagePlugin.success(t('common.copied')) + }).catch(() => { + MessagePlugin.error('复制失败') + }) +} + // 从搜索列表加入空间(通过空间 ID,无需邀请码)- 在预览确认后调用 async function joinBySearchOrg() { if (!invitePreviewData.value || invitePreviewData.value.is_already_member) return @@ -1134,6 +1159,18 @@ onMounted(async () => { if (code) { await handleInvitePreview(code) } + + // 检查 URL 中是否有 orgId,如果有则打开空间设置 + const orgId = route.query.orgId as string + if (orgId) { + settingsOrgId.value = orgId + settingsMode.value = 'edit' + showSettingsModal.value = true + // 清除 URL 中的 orgId 参数,避免刷新时重复打开 + const newQuery = { ...route.query } + delete newQuery.orgId + router.replace({ path: route.path, query: newQuery }) + } }) onUnmounted(() => { @@ -2407,12 +2444,33 @@ onUnmounted(() => { letter-spacing: -0.02em; } -.preview-detail-id { +.preview-detail-id-row { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 0; font-size: 12px; color: #94a3b8; - margin: 0; font-family: "PingFang SC", system-ui, sans-serif; - word-break: break-all; +} + +.preview-detail-id-label { + flex-shrink: 0; +} + +.preview-detail-id-value { + font-family: ui-monospace, "SF Mono", Menlo, monospace; + font-size: 11px; + letter-spacing: 0.02em; + color: #64748b; +} + +.preview-detail-id-copy { + padding: 2px; + color: #94a3b8; +} +.preview-detail-id-copy:hover { + color: #0f172a; } .preview-detail-content { @@ -2571,7 +2629,6 @@ onUnmounted(() => { .invite-preview-footer { padding: 20px 24px; border-top: 1px solid #e2e8f0; - background: #f8fafc; display: flex; justify-content: flex-end; gap: 12px; diff --git a/frontend/src/views/organization/OrganizationSettingsModal.vue b/frontend/src/views/organization/OrganizationSettingsModal.vue index 23cfb9f9..238e9061 100644 --- a/frontend/src/views/organization/OrganizationSettingsModal.vue +++ b/frontend/src/views/organization/OrganizationSettingsModal.vue @@ -492,9 +492,8 @@ class="shared-item" @click="handleShareClick(share)" > -
- - {{ share.knowledge_base_type === 'faq' ? (share.chunk_count || 0) : (share.knowledge_count || 0) }} +
+
{{ share.knowledge_base_name }} @@ -2258,6 +2257,17 @@ watch(currentSection, (section) => { color: #0052d9; } + &.shared-icon-org { + background: rgba(0, 82, 217, 0.06); + color: #0052d9; + } + + .org-icon-img { + width: 18px; + height: 18px; + flex-shrink: 0; + } + .badge-count { font-size: 12px; font-weight: 500; diff --git a/internal/application/service/organization.go b/internal/application/service/organization.go index b090c765..a1b2b5b5 100644 --- a/internal/application/service/organization.go +++ b/internal/application/service/organization.go @@ -240,15 +240,15 @@ func (s *organizationService) SearchSearchableOrganizations(ctx context.Context, items := make([]types.SearchableOrganizationItem, 0, len(orgs)) for _, org := range orgs { items = append(items, types.SearchableOrganizationItem{ - ID: org.ID, - Name: org.Name, - Description: org.Description, - Avatar: org.Avatar, - MemberCount: int(memberCounts[org.ID]), - MemberLimit: org.MemberLimit, - ShareCount: int(shareCounts[org.ID]), - IsAlreadyMember: memberOrgIDs[org.ID], - RequireApproval: org.RequireApproval, + ID: org.ID, + Name: org.Name, + Description: org.Description, + Avatar: org.Avatar, + MemberCount: int(memberCounts[org.ID]), + MemberLimit: org.MemberLimit, + ShareCount: int(shareCounts[org.ID]), + IsAlreadyMember: memberOrgIDs[org.ID], + RequireApproval: org.RequireApproval, }) } return &types.ListSearchableOrganizationsResponse{ diff --git a/internal/types/organization.go b/internal/types/organization.go index 1d02ce27..6bd435c4 100644 --- a/internal/types/organization.go +++ b/internal/types/organization.go @@ -229,9 +229,9 @@ type UpdateOrganizationRequest struct { Description *string `json:"description" binding:"omitempty,max=1000"` Avatar *string `json:"avatar" binding:"omitempty,max=512"` // optional avatar URL RequireApproval *bool `json:"require_approval"` - Searchable *bool `json:"searchable"` // open for search so others can discover and join + Searchable *bool `json:"searchable"` // open for search so others can discover and join InviteCodeValidityDays *int `json:"invite_code_validity_days"` // 0=never, 1, 7, 30 - MemberLimit *int `json:"member_limit"` // max members; 0=unlimited + MemberLimit *int `json:"member_limit"` // max members; 0=unlimited } // AddMemberRequest represents a request to add a member to an organization @@ -350,28 +350,28 @@ type ListOrganizationsResponse struct { // SearchableOrganizationItem is a searchable org item for discovery (no invite code) type SearchableOrganizationItem struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Avatar string `json:"avatar,omitempty"` - MemberCount int `json:"member_count"` - MemberLimit int `json:"member_limit"` // 0 = unlimited - ShareCount int `json:"share_count"` - IsAlreadyMember bool `json:"is_already_member"` - RequireApproval bool `json:"require_approval"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Avatar string `json:"avatar,omitempty"` + MemberCount int `json:"member_count"` + MemberLimit int `json:"member_limit"` // 0 = unlimited + ShareCount int `json:"share_count"` + IsAlreadyMember bool `json:"is_already_member"` + RequireApproval bool `json:"require_approval"` } // ListSearchableOrganizationsResponse is the response for searching discoverable organizations type ListSearchableOrganizationsResponse struct { Organizations []SearchableOrganizationItem `json:"organizations"` - Total int64 `json:"total"` + Total int64 `json:"total"` } // JoinByOrganizationIDRequest is used to join a searchable organization by ID (no invite code) type JoinByOrganizationIDRequest struct { OrganizationID string `json:"organization_id" binding:"required"` - Message string `json:"message" binding:"max=500"` // Optional message for join request - Role OrgMemberRole `json:"role"` // Optional: requested role (admin/editor/viewer); default viewer + Message string `json:"message" binding:"max=500"` // Optional message for join request + Role OrgMemberRole `json:"role"` // Optional: requested role (admin/editor/viewer); default viewer } // JoinRequestResponse represents a join request in API responses