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.
This commit is contained in:
@@ -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' }"
|
||||
>
|
||||
<t-option
|
||||
v-for="org in availableOrganizations"
|
||||
@@ -26,25 +28,29 @@
|
||||
:label="org.name"
|
||||
>
|
||||
<div class="org-option-content">
|
||||
<div class="org-option-header">
|
||||
<span class="org-option-name">{{ org.name }}</span>
|
||||
<t-tag v-if="org.is_owner" theme="primary" size="small" variant="light">
|
||||
{{ $t('organization.owner') }}
|
||||
</t-tag>
|
||||
<t-tag v-else-if="org.my_role" :theme="org.my_role === 'admin' ? 'warning' : 'default'" size="small" variant="light">
|
||||
{{ $t(`organization.role.${org.my_role}`) }}
|
||||
</t-tag>
|
||||
<div class="org-option-icon-wrap">
|
||||
<img src="@/assets/img/organization.svg" class="org-option-icon" alt="" aria-hidden="true" />
|
||||
</div>
|
||||
<div v-if="org.description" class="org-option-desc">{{ org.description }}</div>
|
||||
<div class="org-option-meta">
|
||||
<span class="org-meta-item">
|
||||
<t-icon name="usergroup" size="14px" />
|
||||
{{ org.member_count || 0 }} {{ $t('organization.members') }}
|
||||
</span>
|
||||
<span v-if="org.share_count !== undefined" class="org-meta-item">
|
||||
<t-icon name="share" size="14px" />
|
||||
{{ org.share_count }} {{ $t('organization.share.sharedKBs') }}
|
||||
</span>
|
||||
<div class="org-option-body">
|
||||
<div class="org-option-header">
|
||||
<span class="org-option-name">{{ org.name }}</span>
|
||||
<t-tag v-if="org.is_owner" theme="primary" size="small" variant="light">
|
||||
{{ $t('organization.owner') }}
|
||||
</t-tag>
|
||||
<t-tag v-else-if="org.my_role" :theme="org.my_role === 'admin' ? 'warning' : 'default'" size="small" variant="light">
|
||||
{{ $t(`organization.role.${org.my_role}`) }}
|
||||
</t-tag>
|
||||
</div>
|
||||
<div class="org-option-meta">
|
||||
<span class="org-meta-item">
|
||||
<t-icon name="usergroup" size="12px" />
|
||||
{{ org.member_count || 0 }} {{ $t('organization.members') }}
|
||||
</span>
|
||||
<span v-if="org.share_count !== undefined" class="org-meta-item">
|
||||
<t-icon name="share" size="12px" />
|
||||
{{ org.share_count }} {{ $t('organization.share.sharedKBs') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t-option>
|
||||
@@ -90,19 +96,27 @@
|
||||
<div v-else class="share-items">
|
||||
<div v-for="share in shares" :key="share.id" class="share-item">
|
||||
<div class="share-info">
|
||||
<img src="@/assets/img/organization.svg" class="share-org-icon" alt="" aria-hidden="true" />
|
||||
<span class="share-org-name">{{ share.organization_name }}</span>
|
||||
<t-tag :theme="share.permission === 'editor' ? 'warning' : 'default'" size="small">
|
||||
{{ share.permission === 'editor' ? $t('organization.share.permissionEditable') : $t('organization.share.permissionReadonly') }}
|
||||
</t-tag>
|
||||
</div>
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="danger"
|
||||
size="small"
|
||||
@click="handleUnshare(share)"
|
||||
>
|
||||
<t-icon name="close" />
|
||||
</t-button>
|
||||
<div class="share-actions">
|
||||
<t-tooltip :content="$t('organization.settings.editTitle')" placement="top">
|
||||
<t-button variant="text" theme="default" size="small" @click="handleGoToOrgSettings(share.organization_id)">
|
||||
<t-icon name="setting" />
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="danger"
|
||||
size="small"
|
||||
@click="handleUnshare(share)"
|
||||
>
|
||||
<t-icon name="close" />
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -294,101 +320,150 @@ function handleClose() {
|
||||
.share-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.share-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
padding: 14px 16px;
|
||||
background: var(--td-bg-color-container-hover);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--td-component-stroke);
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.share-item:hover {
|
||||
background: var(--td-bg-color-container-active);
|
||||
border-color: var(--td-component-stroke);
|
||||
}
|
||||
|
||||
.share-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.share-org-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.share-org-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// Custom option styles for organization select
|
||||
.share-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
// Custom option styles for organization select (compact)
|
||||
:deep(.t-select-option) {
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 1px 6px;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
:deep(.t-select-option:hover),
|
||||
:deep(.t-select-option.t-is-selected) {
|
||||
background: var(--td-bg-color-container-hover);
|
||||
}
|
||||
|
||||
:deep(.t-select-option__content) {
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.org-option-content {
|
||||
padding: 2px 0;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
min-width: 260px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.org-option-icon-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 82, 217, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.org-option-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.org-option-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.org-option-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
gap: 6px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.org-option-name {
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #000000e6;
|
||||
color: var(--td-text-color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.org-option-desc {
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
color: #00000066;
|
||||
line-height: 18px;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.org-option-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
color: #00000066;
|
||||
font-size: 11px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
|
||||
.org-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
// Global styles for select dropdown (outside scoped)
|
||||
.t-select__dropdown .t-select-option {
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
// Global styles for organization select dropdown (compact)
|
||||
.org-select-dropdown-popup.t-select__dropdown {
|
||||
padding: 4px 0;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.t-select__dropdown .t-select-option__content {
|
||||
white-space: normal;
|
||||
.org-select-dropdown-popup .t-select-option {
|
||||
height: auto;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 1px 6px;
|
||||
}
|
||||
|
||||
.org-select-dropdown-popup .t-select-option__content {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -248,11 +248,11 @@
|
||||
</div>
|
||||
<div class="bottom-right">
|
||||
<t-tooltip :content="kb.org_name" placement="top">
|
||||
<div class="org-source">
|
||||
<t-icon name="usergroup" size="12px" />
|
||||
<span>{{ kb.org_name }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
<div class="org-source">
|
||||
<img src="@/assets/img/organization.svg" class="org-source-icon" alt="" aria-hidden="true" />
|
||||
<span>{{ kb.org_name }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -342,7 +342,7 @@
|
||||
</div>
|
||||
</t-tooltip>
|
||||
<!-- 共享状态图标 -->
|
||||
<t-tooltip v-if="kb.share_count > 0" :content="$t('knowledgeList.sharedToOrgs', { count: kb.share_count })" placement="top">
|
||||
<t-tooltip v-if="(kb.share_count ?? 0) > 0" :content="$t('knowledgeList.sharedToOrgs', { count: kb.share_count ?? 0 })" placement="top">
|
||||
<div class="feature-badge shared">
|
||||
<t-icon name="share" size="14px" />
|
||||
</div>
|
||||
@@ -416,7 +416,7 @@
|
||||
<div class="bottom-right">
|
||||
<t-tooltip :content="shared.org_name" placement="top">
|
||||
<div class="org-source">
|
||||
<t-icon name="usergroup" size="12px" />
|
||||
<img src="@/assets/img/organization.svg" class="org-source-icon" alt="" aria-hidden="true" />
|
||||
<span>{{ shared.org_name }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
@@ -551,8 +551,8 @@
|
||||
</div>
|
||||
<div class="form-item read-only-row">
|
||||
<label class="form-label">{{ t('knowledgeList.detail.sourceOrg') }}</label>
|
||||
<div class="form-value-row">
|
||||
<t-icon name="usergroup" size="16px" class="form-value-icon" />
|
||||
<div class="form-value-row form-value-org">
|
||||
<img src="@/assets/img/organization.svg" class="form-value-org-icon" alt="" aria-hidden="true" />
|
||||
<span>{{ currentSharedKb.org_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
@@ -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' }"
|
||||
>
|
||||
<t-option
|
||||
v-for="org in availableOrganizations"
|
||||
@@ -23,25 +24,29 @@
|
||||
:label="org.name"
|
||||
>
|
||||
<div class="org-option-content">
|
||||
<div class="org-option-header">
|
||||
<span class="org-option-name">{{ org.name }}</span>
|
||||
<t-tag v-if="org.is_owner" theme="primary" size="small" variant="light">
|
||||
{{ $t('organization.owner') }}
|
||||
</t-tag>
|
||||
<t-tag v-else-if="org.my_role" :theme="org.my_role === 'admin' ? 'warning' : 'default'" size="small" variant="light">
|
||||
{{ $t(`organization.role.${org.my_role}`) }}
|
||||
</t-tag>
|
||||
<div class="org-option-icon-wrap">
|
||||
<img src="@/assets/img/organization.svg" class="org-option-icon" alt="" aria-hidden="true" />
|
||||
</div>
|
||||
<div v-if="org.description" class="org-option-desc">{{ org.description }}</div>
|
||||
<div class="org-option-meta">
|
||||
<span class="org-meta-item">
|
||||
<t-icon name="usergroup" size="14px" />
|
||||
{{ org.member_count || 0 }} {{ $t('organization.members') }}
|
||||
</span>
|
||||
<span v-if="org.share_count !== undefined" class="org-meta-item">
|
||||
<t-icon name="share" size="14px" />
|
||||
{{ org.share_count }} {{ $t('organization.share.sharedKBs') }}
|
||||
</span>
|
||||
<div class="org-option-body">
|
||||
<div class="org-option-header">
|
||||
<span class="org-option-name">{{ org.name }}</span>
|
||||
<t-tag v-if="org.is_owner" theme="primary" size="small" variant="light">
|
||||
{{ $t('organization.owner') }}
|
||||
</t-tag>
|
||||
<t-tag v-else-if="org.my_role" :theme="org.my_role === 'admin' ? 'warning' : 'default'" size="small" variant="light">
|
||||
{{ $t(`organization.role.${org.my_role}`) }}
|
||||
</t-tag>
|
||||
</div>
|
||||
<div class="org-option-meta">
|
||||
<span class="org-meta-item">
|
||||
<t-icon name="usergroup" size="12px" />
|
||||
{{ org.member_count || 0 }} {{ $t('organization.members') }}
|
||||
</span>
|
||||
<span v-if="org.share_count !== undefined" class="org-meta-item">
|
||||
<t-icon name="share" size="12px" />
|
||||
{{ org.share_count }} {{ $t('organization.share.sharedKBs') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t-option>
|
||||
@@ -87,7 +92,7 @@
|
||||
<div v-for="share in shares" :key="share.id" class="share-item">
|
||||
<div class="share-info">
|
||||
<div class="share-org">
|
||||
<t-icon name="usergroup" class="org-icon" />
|
||||
<img src="@/assets/img/organization-green.svg" class="org-icon" alt="" aria-hidden="true" />
|
||||
<span class="org-name">{{ share.organization_name }}</span>
|
||||
</div>
|
||||
<t-tag
|
||||
@@ -99,6 +104,11 @@
|
||||
</t-tag>
|
||||
</div>
|
||||
<div class="share-actions">
|
||||
<t-tooltip :content="$t('organization.settings.editTitle')" placement="top">
|
||||
<t-button variant="text" theme="default" size="small" @click="handleGoToOrgSettings(share.organization_id)">
|
||||
<t-icon name="setting" />
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
<t-select
|
||||
:value="share.permission"
|
||||
size="small"
|
||||
@@ -137,11 +147,13 @@
|
||||
import { ref, computed, watch, onMounted } 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, updateSharePermission } from '@/api/organization'
|
||||
import type { KnowledgeBaseShare } from '@/api/organization'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const orgStore = useOrganizationStore()
|
||||
|
||||
interface Props {
|
||||
@@ -258,6 +270,14 @@ async function handleUnshare(share: KnowledgeBaseShare) {
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to organization settings
|
||||
function handleGoToOrgSettings(orgId: string) {
|
||||
router.push({
|
||||
path: '/platform/organizations',
|
||||
query: { orgId }
|
||||
})
|
||||
}
|
||||
|
||||
// Watch for kbId changes
|
||||
watch(() => props.kbId, async (newKbId) => {
|
||||
if (newKbId) {
|
||||
@@ -323,14 +343,16 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.org-select {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.permission-select {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,20 +412,24 @@ onMounted(async () => {
|
||||
.shares-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.share-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
padding: 14px 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s ease;
|
||||
transition: background 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +445,9 @@ onMounted(async () => {
|
||||
gap: 8px;
|
||||
|
||||
.org-icon {
|
||||
font-size: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
@@ -434,7 +462,7 @@ onMounted(async () => {
|
||||
.share-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
|
||||
.permission-change-select {
|
||||
width: 100px;
|
||||
@@ -472,79 +500,107 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Custom option styles for organization select
|
||||
// Custom option styles for organization select (compact)
|
||||
:deep(.t-select-option) {
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 1px 6px;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
:deep(.t-select-option:hover),
|
||||
:deep(.t-select-option.t-is-selected) {
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
:deep(.t-select-option__content) {
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.org-option-content {
|
||||
padding: 2px 0;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
min-width: 260px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.org-option-icon-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 82, 217, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.org-option-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.org-option-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.org-option-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
gap: 6px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.org-option-name {
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #000000e6;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.org-option-desc {
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
color: #00000066;
|
||||
line-height: 18px;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.org-option-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
color: #00000066;
|
||||
font-size: 11px;
|
||||
color: #00000099;
|
||||
|
||||
.org-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
// Global styles for select dropdown (outside scoped)
|
||||
.t-select__dropdown .t-select-option {
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
// Global styles for organization select dropdown (compact)
|
||||
.org-select-dropdown-popup.t-select__dropdown {
|
||||
padding: 4px 0;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.t-select__dropdown .t-select-option__content {
|
||||
white-space: normal;
|
||||
.org-select-dropdown-popup .t-select-option {
|
||||
height: auto;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 1px 6px;
|
||||
}
|
||||
|
||||
.org-select-dropdown-popup .t-select-option__content {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -432,7 +432,15 @@
|
||||
</div>
|
||||
<div class="preview-detail-title-block">
|
||||
<h2 class="preview-detail-name">{{ invitePreviewData.name }}</h2>
|
||||
<p class="preview-detail-id">{{ $t('organization.join.spaceId') }}: {{ invitePreviewData.id }}</p>
|
||||
<div class="preview-detail-id-row">
|
||||
<span class="preview-detail-id-label">{{ $t('organization.join.spaceId') }}</span>
|
||||
<span class="preview-detail-id-value">{{ shortPreviewSpaceId }}</span>
|
||||
<t-tooltip :content="$t('common.copy')">
|
||||
<t-button variant="text" size="small" class="preview-detail-id-copy" @click="copyPreviewSpaceId">
|
||||
<t-icon name="file-copy" />
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
@@ -492,9 +492,8 @@
|
||||
class="shared-item"
|
||||
@click="handleShareClick(share)"
|
||||
>
|
||||
<div :class="['shared-icon', share.knowledge_base_type === 'faq' ? 'type-faq' : 'type-document']">
|
||||
<t-icon :name="share.knowledge_base_type === 'faq' ? 'chat-bubble-help' : 'folder'" size="16px" />
|
||||
<span class="badge-count">{{ share.knowledge_base_type === 'faq' ? (share.chunk_count || 0) : (share.knowledge_count || 0) }}</span>
|
||||
<div class="shared-icon shared-icon-org">
|
||||
<img src="@/assets/img/organization.svg" class="org-icon-img" alt="" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="shared-info">
|
||||
<span class="shared-name">{{ share.knowledge_base_name }}</span>
|
||||
@@ -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;
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user