532 lines
13 KiB
Vue
532 lines
13 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref } from 'vue';
|
||
|
||
import { $t } from '@easyflow/locales';
|
||
|
||
import { CopyDocument } from '@element-plus/icons-vue';
|
||
import { ElButton, ElCard, ElIcon, ElInput, ElMessage } from 'element-plus';
|
||
|
||
import { api } from '#/api/request';
|
||
|
||
type EndpointParam = {
|
||
location: 'body' | 'query';
|
||
name: string;
|
||
note?: string;
|
||
required?: boolean;
|
||
};
|
||
|
||
type EndpointDoc = {
|
||
hint: string;
|
||
method: 'GET' | 'POST';
|
||
params: EndpointParam[];
|
||
path: string;
|
||
};
|
||
|
||
const props = defineProps({
|
||
knowledgeId: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
collectionType: {
|
||
type: String,
|
||
default: 'DOCUMENT',
|
||
},
|
||
manageable: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
});
|
||
|
||
const createLoading = ref(false);
|
||
const generatedUrl = ref('');
|
||
const generatedExpireAt = ref('');
|
||
|
||
const apiBaseUrl = computed(() => {
|
||
const appBasePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
||
if (typeof window === 'undefined') {
|
||
return `${appBasePath}/public-api/knowledge-share`;
|
||
}
|
||
return `${window.location.origin}${appBasePath}/public-api/knowledge-share`;
|
||
});
|
||
|
||
const detailExample = computed(() => {
|
||
return [
|
||
`curl -X GET '${apiBaseUrl.value}/detail?knowledgeId=${props.knowledgeId}' \\`,
|
||
" -H 'ApiKey: 你的访问令牌'",
|
||
].join('\n');
|
||
});
|
||
|
||
const searchExample = computed(() => {
|
||
return [
|
||
`curl -G '${apiBaseUrl.value}/search' \\`,
|
||
" -H 'ApiKey: 你的访问令牌' \\",
|
||
` --data-urlencode 'knowledgeId=${props.knowledgeId}' \\`,
|
||
" --data-urlencode 'keyword=测试问题'",
|
||
].join('\n');
|
||
});
|
||
|
||
const endpointDocs = computed(() => {
|
||
const commonEndpoints: EndpointDoc[] = [
|
||
{
|
||
method: 'GET',
|
||
path: '/detail',
|
||
hint: '获取知识库详情',
|
||
params: [{ name: 'knowledgeId', location: 'query', required: true }],
|
||
},
|
||
{
|
||
method: 'GET',
|
||
path: '/search',
|
||
hint: '知识检索',
|
||
params: [
|
||
{ name: 'knowledgeId', location: 'query', required: true },
|
||
{ name: 'keyword', location: 'query', required: true },
|
||
{
|
||
name: 'retrievalMode',
|
||
location: 'query',
|
||
note: '召回方式',
|
||
},
|
||
],
|
||
},
|
||
];
|
||
const typeEndpoints: EndpointDoc[] =
|
||
props.collectionType === 'FAQ'
|
||
? [
|
||
{
|
||
method: 'GET',
|
||
path: '/faq/page',
|
||
hint: 'FAQ 分页',
|
||
params: [
|
||
{ name: 'knowledgeId', location: 'query', required: true },
|
||
{ name: 'question', location: 'query', note: '问题关键字' },
|
||
{ name: 'categoryId', location: 'query', note: '分类 ID' },
|
||
{ name: 'pageNumber', location: 'query', note: '默认 1' },
|
||
{ name: 'pageSize', location: 'query', note: '默认 10' },
|
||
],
|
||
},
|
||
{
|
||
method: 'POST',
|
||
path: '/faq/save',
|
||
hint: '新增 FAQ',
|
||
params: [
|
||
{ name: 'collectionId', location: 'body', required: true },
|
||
{ name: 'question', location: 'body', required: true },
|
||
{ name: 'answerHtml', location: 'body', required: true },
|
||
{ name: 'categoryId', location: 'body', note: '分类 ID' },
|
||
],
|
||
},
|
||
]
|
||
: [
|
||
{
|
||
method: 'GET',
|
||
path: '/document/page',
|
||
hint: '文档分页',
|
||
params: [
|
||
{ name: 'knowledgeId', location: 'query', required: true },
|
||
{ name: 'title', location: 'query', note: '文档标题' },
|
||
{ name: 'pageNumber', location: 'query', note: '默认 1' },
|
||
{ name: 'pageSize', location: 'query', note: '默认 10' },
|
||
],
|
||
},
|
||
{
|
||
method: 'POST',
|
||
path: '/document/import/commit',
|
||
hint: '导入文档',
|
||
params: [
|
||
{ name: 'knowledgeId', location: 'body', required: true },
|
||
{
|
||
name: 'previewSessionIds',
|
||
location: 'body',
|
||
required: true,
|
||
note: '预览接口返回的会话 ID 数组',
|
||
},
|
||
],
|
||
},
|
||
];
|
||
return [...commonEndpoints, ...typeEndpoints].map((item) => ({
|
||
...item,
|
||
url: `${apiBaseUrl.value}${item.path}`,
|
||
}));
|
||
});
|
||
|
||
const formatEndpointParam = (param: EndpointParam) => {
|
||
const segments = [param.location, param.required ? '必填' : '可选'];
|
||
if (param.note) {
|
||
segments.push(param.note);
|
||
}
|
||
return `${param.name}(${segments.join(',')})`;
|
||
};
|
||
|
||
const createShare = async () => {
|
||
if (!props.manageable) {
|
||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||
return;
|
||
}
|
||
createLoading.value = true;
|
||
try {
|
||
const res = await api.post('/api/v1/knowledgeShare/url/create', {
|
||
knowledgeId: props.knowledgeId,
|
||
});
|
||
if (res.errorCode === 0) {
|
||
generatedUrl.value = res.data?.shareUrl || '';
|
||
generatedExpireAt.value = res.data?.expiresAt || '';
|
||
ElMessage.success('已创建分享链接');
|
||
}
|
||
} finally {
|
||
createLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const copyGeneratedUrl = async () => {
|
||
if (!generatedUrl.value) {
|
||
ElMessage.warning('请先生成分享链接');
|
||
return;
|
||
}
|
||
await navigator.clipboard.writeText(generatedUrl.value);
|
||
ElMessage.success('已复制分享链接');
|
||
};
|
||
|
||
const copyApiExample = async (content: string) => {
|
||
await navigator.clipboard.writeText(content);
|
||
ElMessage.success('已复制调用示例');
|
||
};
|
||
|
||
const copyEndpointUrl = async (url: string) => {
|
||
await navigator.clipboard.writeText(url);
|
||
ElMessage.success('已复制接口地址');
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<div class="share-management">
|
||
<ElCard shadow="never" class="share-card">
|
||
<template #header>
|
||
<div class="share-card__header">
|
||
<div>
|
||
<div class="share-card__title">当前分享链接</div>
|
||
<div class="share-card__desc">
|
||
默认有效期 30 分钟。重新生成后旧链接立即失效。
|
||
</div>
|
||
</div>
|
||
<ElButton
|
||
type="primary"
|
||
:loading="createLoading"
|
||
:disabled="!props.manageable"
|
||
@click="createShare"
|
||
>
|
||
{{ generatedUrl ? '重新生成链接' : '生成分享链接' }}
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-if="generatedUrl" class="share-result">
|
||
<div class="share-result__row">
|
||
<ElInput v-model="generatedUrl" readonly />
|
||
<ElButton
|
||
text
|
||
circle
|
||
:icon="CopyDocument"
|
||
aria-label="复制分享链接"
|
||
title="复制分享链接"
|
||
@click="copyGeneratedUrl"
|
||
/>
|
||
</div>
|
||
<div class="share-result__meta">过期时间:{{ generatedExpireAt }}</div>
|
||
</div>
|
||
<div v-else class="share-empty">暂未生成分享链接</div>
|
||
</ElCard>
|
||
|
||
<ElCard shadow="never" class="share-card">
|
||
<template #header>
|
||
<div class="share-card__header share-card__header--stack">
|
||
<div>
|
||
<div class="share-card__title">API 调用方式</div>
|
||
<div class="share-card__desc">
|
||
使用已开通知识库分享授权的访问令牌,通过 `ApiKey` 请求头调用。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="api-doc">
|
||
<div class="api-doc__meta">
|
||
<div class="api-doc__item">
|
||
<span class="api-doc__label">接口前缀</span>
|
||
<code class="api-doc__value">{{ apiBaseUrl }}</code>
|
||
</div>
|
||
<div class="api-doc__item">
|
||
<span class="api-doc__label">请求头</span>
|
||
<code class="api-doc__value">ApiKey: 你的访问令牌</code>
|
||
</div>
|
||
<div class="api-doc__item">
|
||
<span class="api-doc__label">当前知识库 ID</span>
|
||
<code class="api-doc__value">{{ props.knowledgeId }}</code>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="api-doc__section">
|
||
<div class="api-doc__section-title">常用接口</div>
|
||
<div class="api-doc__endpoint-list">
|
||
<div
|
||
v-for="endpoint in endpointDocs"
|
||
:key="endpoint.path"
|
||
class="api-doc__endpoint"
|
||
>
|
||
<div class="api-doc__endpoint-main">
|
||
<span class="api-doc__method">{{ endpoint.method }}</span>
|
||
<code>{{ endpoint.path }}</code>
|
||
<span class="api-doc__hint">{{ endpoint.hint }}</span>
|
||
<ElButton
|
||
text
|
||
circle
|
||
:icon="CopyDocument"
|
||
class="api-doc__copy"
|
||
:aria-label="`复制 ${endpoint.path}`"
|
||
:title="`复制 ${endpoint.url}`"
|
||
@click="copyEndpointUrl(endpoint.url)"
|
||
/>
|
||
</div>
|
||
<div class="api-doc__endpoint-params">
|
||
<span class="api-doc__params-label">参数</span>
|
||
<span
|
||
v-for="param in endpoint.params"
|
||
:key="`${endpoint.path}-${param.name}`"
|
||
class="api-doc__param"
|
||
>
|
||
{{ formatEndpointParam(param) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="api-doc__section">
|
||
<div class="api-doc__section-title">详情示例</div>
|
||
<div class="api-doc__code-wrap">
|
||
<button
|
||
type="button"
|
||
class="api-doc__code-copy"
|
||
title="复制详情示例"
|
||
aria-label="复制详情示例"
|
||
@click="copyApiExample(detailExample)"
|
||
>
|
||
<ElIcon><CopyDocument /></ElIcon>
|
||
</button>
|
||
<pre class="api-doc__code">{{ detailExample }}</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="api-doc__section">
|
||
<div class="api-doc__section-title">检索示例</div>
|
||
<div class="api-doc__code-wrap">
|
||
<button
|
||
type="button"
|
||
class="api-doc__code-copy"
|
||
title="复制检索示例"
|
||
aria-label="复制检索示例"
|
||
@click="copyApiExample(searchExample)"
|
||
>
|
||
<ElIcon><CopyDocument /></ElIcon>
|
||
</button>
|
||
<pre class="api-doc__code">{{ searchExample }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ElCard>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.share-management {
|
||
display: grid;
|
||
gap: 16px;
|
||
}
|
||
|
||
.share-card {
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.share-card__header {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.share-card__header--stack {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.share-card__title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.share-card__desc,
|
||
.share-result__meta {
|
||
margin-top: 6px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.share-result {
|
||
display: grid;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.share-result__row {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
|
||
.share-empty {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.api-doc {
|
||
display: grid;
|
||
gap: 18px;
|
||
}
|
||
|
||
.api-doc__meta {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.api-doc__item {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.api-doc__label {
|
||
min-width: 84px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.api-doc__value {
|
||
padding: 4px 10px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-primary);
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.api-doc__section {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.api-doc__section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.api-doc__endpoint-list {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.api-doc__endpoint {
|
||
display: grid;
|
||
gap: 6px;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.api-doc__endpoint-main {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.api-doc__copy {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.api-doc__method {
|
||
min-width: 42px;
|
||
font-weight: 600;
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.api-doc__hint {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.api-doc__endpoint-params {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: flex-start;
|
||
flex-wrap: wrap;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.api-doc__params-label {
|
||
flex: 0 0 auto;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.api-doc__param {
|
||
padding: 2px 8px;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.api-doc__code {
|
||
margin: 0;
|
||
padding: 14px 16px;
|
||
overflow-x: auto;
|
||
font-size: 12px;
|
||
line-height: 1.7;
|
||
color: var(--el-text-color-primary);
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.api-doc__code-wrap {
|
||
position: relative;
|
||
}
|
||
|
||
.api-doc__code-copy {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
z-index: 1;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 28px;
|
||
padding: 0;
|
||
color: var(--el-text-color-secondary);
|
||
cursor: pointer;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.api-doc__code-copy:hover {
|
||
color: var(--el-color-primary);
|
||
background: var(--el-fill-color);
|
||
}
|
||
|
||
.api-doc__code-copy:focus-visible {
|
||
outline: 2px solid var(--el-color-primary-light-5);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.api-doc__code-wrap .api-doc__code {
|
||
padding-top: 40px;
|
||
}
|
||
</style>
|