index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. <template>
  2. <div class="ai-page ai-kb">
  3. <!-- 内容卡:标题 + 摘要 + 搜索 + 操作 + 知识库卡片网格(工具栏合并进卡头,版式对齐研判方案页) -->
  4. <section class="ai-kb__panel">
  5. <div class="ai-kb__panel-head">
  6. <span class="ai-kb__card-title">知识库</span>
  7. <span class="ai-kb__summary">{{ summary }}</span>
  8. <label class="ai-kb__search">
  9. <Icon icon="mdi:magnify" :size="14" />
  10. <input v-model="keyword" type="text" placeholder="输入知识库名称搜索" />
  11. </label>
  12. <span class="ai-kb__spacer"></span>
  13. <span class="ai-kb__panel-count">共 {{ filteredBases.length }} 个</span>
  14. <div class="ai-kb__actions">
  15. <a-button :loading="loading" @click="load">
  16. <template #icon><Icon icon="mdi:refresh" :size="14" /></template>
  17. 刷新
  18. </a-button>
  19. <a-button type="primary" @click="openCreate">
  20. <template #icon><Icon icon="mdi:plus" :size="14" /></template>
  21. 新建知识库
  22. </a-button>
  23. </div>
  24. </div>
  25. <div class="ai-kb__panel-body">
  26. <a-spin :spinning="loading">
  27. <!-- 卡片混合展示(内置在前),卡片头标签区分内置 / 自定义 -->
  28. <div v-if="filteredBases.length" class="ai-kb__grid">
  29. <article v-for="kb in filteredBases" :key="kb.id" class="ai-kb__card">
  30. <div class="ai-kb__card-head">
  31. <span class="ai-kb__card-icon">
  32. <Icon icon="mdi:bookshelf" :size="17" />
  33. </span>
  34. <div class="ai-kb__card-heading">
  35. <div class="ai-kb__name-row">
  36. <span class="ai-kb__name">{{ kb.name }}</span>
  37. <span class="ai-kb__chip" :class="{ 'is-primary': !kb.builtin }">
  38. {{ kb.builtin ? '内置' : '自定义' }}
  39. </span>
  40. </div>
  41. <span class="ai-kb__card-sub">{{ kb.builtin ? '系统内置知识库(管理员维护)' : '我的知识库' }}</span>
  42. </div>
  43. </div>
  44. <p class="ai-kb__desc">{{ kb.description || '暂无描述' }}</p>
  45. <div class="ai-kb__chips">
  46. <span class="ai-kb__chip">
  47. <i class="ai-kb__chip-dot" style="background: #0969da"></i>
  48. 我的文档 {{ kb.myDocCount }} 篇
  49. </span>
  50. <span v-if="kb.myPendingCount > 0" class="ai-kb__chip">
  51. <i class="ai-kb__chip-dot" style="background: #d97706"></i>
  52. {{ kb.myPendingCount }} 篇待索引
  53. </span>
  54. </div>
  55. <div class="ai-kb__card-ops">
  56. <!-- 「管理文档」= 主操作:打开我的上传文档弹窗(列表/上传/下载按上传者隔离) -->
  57. <button class="ai-kb__run" type="button" @click="openDocs(kb)">
  58. 管理文档
  59. <Icon icon="mdi:arrow-right" :size="13" />
  60. </button>
  61. <span class="ai-kb__card-ops-sp"></span>
  62. <!-- 查看详情:内置/自定义都可看(只读弹框) -->
  63. <a-tooltip title="查看详情">
  64. <button class="ai-kb__iconbtn" type="button" aria-label="查看知识库详情" @click="openDetail(kb)">
  65. <Icon icon="mdi:eye-outline" :size="15" />
  66. </button>
  67. </a-tooltip>
  68. <!-- 内置只可查看/绑定(编辑/删除入口根本不渲染,后端另有双重校验) -->
  69. <template v-if="!kb.builtin">
  70. <a-tooltip title="编辑知识库">
  71. <button class="ai-kb__iconbtn" type="button" aria-label="编辑知识库" @click="openEdit(kb)">
  72. <Icon icon="mdi:pencil-outline" :size="15" />
  73. </button>
  74. </a-tooltip>
  75. <a-tooltip title="删除知识库">
  76. <button class="ai-kb__iconbtn is-danger" type="button" aria-label="删除知识库" @click="onDelete(kb)">
  77. <Icon icon="mdi:trash-can-outline" :size="15" />
  78. </button>
  79. </a-tooltip>
  80. </template>
  81. </div>
  82. </article>
  83. </div>
  84. <EmptyState
  85. v-else-if="!loading"
  86. :title="bases.length ? '没有匹配的知识库' : '还没有知识库'"
  87. :description="
  88. bases.length
  89. ? '换个知识库名称试试,或清空搜索条件查看全部。'
  90. : '点击右上角「新建知识库」创建第一个;内置知识库对所有用户可见。'
  91. "
  92. :min-height="300"
  93. description-width="min(100%, 520px)"
  94. />
  95. </a-spin>
  96. </div>
  97. </section>
  98. <!-- 新建/编辑:小弹框(名称 + 描述) -->
  99. <BasicModal
  100. :open="formOpen"
  101. :title="editing ? '编辑知识库' : '新建知识库'"
  102. :width="520"
  103. :confirm-loading="submitting"
  104. ok-text="保存"
  105. cancel-text="取消"
  106. @ok="submit"
  107. @cancel="formOpen = false"
  108. >
  109. <a-form layout="vertical">
  110. <a-form-item label="知识库名称" required>
  111. <a-input v-model:value="form.name" placeholder="如:法规与量刑指引" :maxlength="100" />
  112. </a-form-item>
  113. <a-form-item label="描述">
  114. <a-textarea
  115. v-model:value="form.description"
  116. :rows="3"
  117. :maxlength="500"
  118. placeholder="一句话说明这个知识库收录什么内容(可选)"
  119. />
  120. </a-form-item>
  121. </a-form>
  122. </BasicModal>
  123. <!-- 知识库详情(只读):卡片眼睛图标打开 -->
  124. <BasicModal
  125. :open="detailOpen"
  126. :title="detail ? `知识库详情:${detail.name}` : '知识库详情'"
  127. :width="640"
  128. :show-ok-btn="false"
  129. cancel-text="关闭"
  130. @cancel="detailOpen = false"
  131. >
  132. <div v-if="detail" class="akb-detail">
  133. <div class="akb-detail__head">
  134. <span class="akb-detail__badge" :class="detail.builtin ? 'is-builtin' : 'is-custom'">
  135. {{ detail.builtin ? '内置' : '自定义' }}
  136. </span>
  137. <span class="akb-detail__name">{{ detail.name }}</span>
  138. </div>
  139. <p class="akb-detail__desc">{{ detail.description || '暂无描述' }}</p>
  140. <section class="akb-detail__panel">
  141. <div class="akb-detail__label">使用方式</div>
  142. <p class="akb-detail__text">
  143. 在「研判方案」新建/编辑时绑定本知识库,对话中模型即可用 search_knowledge
  144. 工具按需检索库内知识(含来源文档名)。文档按上传者隔离:管理文档只显示自己上传的文件。
  145. </p>
  146. </section>
  147. <section class="akb-detail__panel">
  148. <div class="akb-detail__label">我的文档</div>
  149. <p class="akb-detail__text">
  150. {{ detail.myDocCount }} 篇<template v-if="detail.myPendingCount > 0">
  151. ,其中 {{ detail.myPendingCount }} 篇待索引</template
  152. >。{{ detail.builtin ? '内置库的文档由管理员维护,普通用户清单不外露(检索可用)。' : '' }}
  153. </p>
  154. </section>
  155. </div>
  156. </BasicModal>
  157. <!-- 我的上传文档(列表/上传/下载按上传者隔离) -->
  158. <KnowledgeDocModal v-model:open="docsOpen" :kb="docsKb" />
  159. </div>
  160. </template>
  161. <script lang="ts" setup name="AiKnowledgeIndex">
  162. /**
  163. * 知识库管理页:卡片混合列表,卡片头标签区分「内置 / 自定义」—— 版式对齐研判方案页(aiExpert)。
  164. *
  165. * 对接 KnowledgeBaseController(/knowledge-bases/**,Result 包装):列表本身就是「内置 + 自己的」
  166. * (后端过滤,内置在前),页面不再分组,只靠卡片角标区分来源。
  167. *
  168. * 操作语义:
  169. * - 管理文档(主操作):打开「我的上传文档」弹窗 —— 文档按上传者隔离,
  170. * 内置库普通用户打开也是空列表(管理员维护的文档清单不外露,检索不受影响);
  171. * - 编辑/删除:仅自建卡片出现,内置的按钮根本不渲染(后端另有双重校验)。
  172. */
  173. import { computed, onMounted, reactive, ref } from 'vue';
  174. import { Button, Form, FormItem, Input, Modal, Spin, Textarea, Tooltip } from 'ant-design-vue';
  175. import Icon from '@/core/components/Icon/src/Icon.vue';
  176. import { useMessage } from '@/core/hooks/web/useMessage';
  177. import { BasicModal } from '@/core/components/Modal';
  178. import { EmptyState } from '@/core/components/EmptyState';
  179. import * as knowledgeApi from '../../api/knowledgeApi';
  180. import type { KnowledgeBase, SaveKnowledgeBaseParams } from '../../api/types';
  181. import KnowledgeDocModal from '../../components/KnowledgeDocModal.vue';
  182. const AButton = Button;
  183. const ASpin = Spin;
  184. const ATooltip = Tooltip;
  185. const AForm = Form;
  186. const AFormItem = FormItem;
  187. const AInput = Input;
  188. const ATextarea = Textarea;
  189. const { showMessage } = useMessage();
  190. const bases = ref<KnowledgeBase[]>([]);
  191. const loading = ref(false);
  192. /** 新建/编辑(名称 + 描述) */
  193. const formOpen = ref(false);
  194. const submitting = ref(false);
  195. const editing = ref<KnowledgeBase | null>(null);
  196. const form = reactive({ name: '', description: '' });
  197. /** 知识库详情(只读弹框) */
  198. const detailOpen = ref(false);
  199. const detail = ref<KnowledgeBase | null>(null);
  200. /** 我的上传文档弹窗 */
  201. const docsOpen = ref(false);
  202. const docsKb = ref<KnowledgeBase | null>(null);
  203. /** 知识库名称搜索(纯前端过滤,不改接口与加载逻辑) */
  204. const keyword = ref('');
  205. const filteredBases = computed(() => {
  206. const k = keyword.value.trim().toLowerCase();
  207. if (!k) return bases.value;
  208. return bases.value.filter((kb) => (kb.name || '').toLowerCase().includes(k));
  209. });
  210. const builtinBases = computed(() => bases.value.filter((kb) => kb.builtin));
  211. const mineBases = computed(() => bases.value.filter((kb) => !kb.builtin));
  212. const summary = computed(() => {
  213. const parts: string[] = [];
  214. if (builtinBases.value.length) parts.push(`内置 ${builtinBases.value.length}`);
  215. if (mineBases.value.length) parts.push(`我的 ${mineBases.value.length}`);
  216. return parts.length ? parts.join(' · ') : '尚未创建知识库';
  217. });
  218. onMounted(load);
  219. async function load() {
  220. loading.value = true;
  221. try {
  222. bases.value = (await knowledgeApi.listKnowledgeBases()) || [];
  223. } catch (e: any) {
  224. showMessage(e?.message || '加载知识库列表失败', 'error');
  225. } finally {
  226. loading.value = false;
  227. }
  228. }
  229. function openCreate() {
  230. editing.value = null;
  231. form.name = '';
  232. form.description = '';
  233. formOpen.value = true;
  234. }
  235. function openEdit(kb: KnowledgeBase) {
  236. editing.value = kb;
  237. form.name = kb.name;
  238. form.description = kb.description || '';
  239. formOpen.value = true;
  240. }
  241. async function submit() {
  242. const name = form.name.trim();
  243. if (!name) {
  244. showMessage('请输入知识库名称', 'error');
  245. return;
  246. }
  247. const payload: SaveKnowledgeBaseParams = { name, description: form.description?.trim() || '' };
  248. submitting.value = true;
  249. try {
  250. if (editing.value) {
  251. await knowledgeApi.updateKnowledgeBase(editing.value.id, payload);
  252. } else {
  253. await knowledgeApi.createKnowledgeBase(payload);
  254. }
  255. showMessage('已保存', 'success');
  256. formOpen.value = false;
  257. await load();
  258. } catch (e: any) {
  259. showMessage(e?.message || '保存失败', 'error');
  260. } finally {
  261. submitting.value = false;
  262. }
  263. }
  264. function openDetail(kb: KnowledgeBase) {
  265. detail.value = kb;
  266. detailOpen.value = true;
  267. }
  268. function openDocs(kb: KnowledgeBase) {
  269. docsKb.value = kb;
  270. docsOpen.value = true;
  271. }
  272. function onDelete(kb: KnowledgeBase) {
  273. Modal.confirm({
  274. title: '删除知识库',
  275. content: `确认删除「${kb.name}」?库内全部文档(含文件与向量索引)、下载记录会一并清理,绑定它的研判方案将自动解绑。`,
  276. okText: '删除',
  277. okType: 'primary',
  278. okButtonProps: { danger: true },
  279. cancelText: '取消',
  280. onOk: async () => {
  281. try {
  282. await knowledgeApi.deleteKnowledgeBase(kb.id);
  283. showMessage('已删除', 'success');
  284. await load();
  285. } catch (err: any) {
  286. showMessage(err?.message || '删除失败', 'error');
  287. }
  288. },
  289. });
  290. }
  291. </script>
  292. <style lang="less" scoped>
  293. /* ======================================================================
  294. * 版式对齐研判方案页(aiExpert):页底 #f4f6fa · 白卡圆角 14 · 描边 #e7ebf3
  295. * 标题蓝条 · 主按钮 #2a50ec · 卡片网格 auto-fill 320px
  296. * ==================================================================== */
  297. .ai-page.ai-kb {
  298. display: flex;
  299. flex-direction: column;
  300. gap: 8px;
  301. width: 100%;
  302. height: 100%;
  303. min-height: 0;
  304. margin: 0;
  305. padding: 8px 10px;
  306. background: #f4f6fa;
  307. border: 0;
  308. border-radius: 0;
  309. box-sizing: border-box;
  310. overflow: hidden;
  311. }
  312. .ai-kb {
  313. &__card-title {
  314. position: relative;
  315. flex: 0 0 auto;
  316. padding-left: 11px;
  317. color: #111827;
  318. font-size: 15px;
  319. font-weight: 600;
  320. line-height: 22px;
  321. white-space: nowrap;
  322. &::before {
  323. content: '';
  324. position: absolute;
  325. top: 4px;
  326. bottom: 4px;
  327. left: 0;
  328. width: 3px;
  329. background: #2a50ec;
  330. border-radius: 2px;
  331. }
  332. }
  333. &__summary {
  334. max-width: 200px;
  335. overflow: hidden;
  336. color: #98a2b3;
  337. font-size: 11.5px;
  338. white-space: nowrap;
  339. text-overflow: ellipsis;
  340. }
  341. &__search {
  342. display: flex;
  343. flex: 0 1 auto;
  344. align-items: center;
  345. gap: 8px;
  346. width: 240px;
  347. min-width: 160px;
  348. height: 32px;
  349. padding: 0 11px;
  350. color: #98a2b3;
  351. background: #fff;
  352. border: 1px solid #e7ebf3;
  353. border-radius: 8px;
  354. cursor: text;
  355. transition:
  356. border-color 0.15s ease,
  357. box-shadow 0.15s ease;
  358. &:focus-within {
  359. border-color: #2a50ec;
  360. box-shadow: 0 0 0 2px rgb(42 80 236 / 12%);
  361. }
  362. input {
  363. flex: 1;
  364. min-width: 0;
  365. color: #111827;
  366. font-size: 12px;
  367. font-family: inherit;
  368. background: transparent;
  369. border: 0;
  370. outline: none;
  371. &::placeholder {
  372. color: #98a2b3;
  373. }
  374. }
  375. }
  376. &__spacer {
  377. flex: 1 1 auto;
  378. }
  379. &__actions {
  380. display: flex;
  381. flex: 0 0 auto;
  382. align-items: center;
  383. gap: 8px;
  384. :deep(.ant-btn) {
  385. height: 30px;
  386. padding: 0 14px;
  387. color: #5b6577;
  388. font-size: 12px;
  389. font-weight: 500;
  390. background: #fff;
  391. border: 1px solid #e7ebf3;
  392. border-radius: 8px;
  393. box-shadow: none;
  394. &:hover,
  395. &:focus {
  396. color: #2a50ec;
  397. background: #f8faff;
  398. border-color: #bfcdfb;
  399. }
  400. }
  401. :deep(.ant-btn-primary) {
  402. color: #fff;
  403. background: #2a50ec;
  404. border-color: #2a50ec;
  405. &:hover,
  406. &:focus {
  407. color: #fff;
  408. background: #4b6bf0;
  409. border-color: #4b6bf0;
  410. }
  411. &:active {
  412. background: #1e3ed0;
  413. border-color: #1e3ed0;
  414. }
  415. }
  416. :deep(.ant-btn[disabled]) {
  417. color: rgb(0 0 0 / 25%);
  418. background: #f5f5f5;
  419. border-color: #e7ebf3;
  420. }
  421. }
  422. &__panel {
  423. display: flex;
  424. flex: 1 1 auto;
  425. flex-direction: column;
  426. gap: 0;
  427. min-height: 0;
  428. padding: 0;
  429. background: #fff;
  430. border: 1px solid #e7ebf3;
  431. border-radius: 14px;
  432. box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
  433. overflow: hidden;
  434. }
  435. &__panel-head {
  436. display: flex;
  437. flex: 0 0 auto;
  438. align-items: center;
  439. gap: 10px;
  440. min-height: 52px;
  441. padding: 10px 14px;
  442. border-bottom: 1px solid #eff2f7;
  443. box-sizing: border-box;
  444. }
  445. &__panel-count {
  446. color: #98a2b3;
  447. font-size: 11.5px;
  448. white-space: nowrap;
  449. font-variant-numeric: tabular-nums;
  450. }
  451. &__panel-body {
  452. flex: 1 1 auto;
  453. min-height: 0;
  454. padding: 14px;
  455. overflow-y: auto;
  456. overscroll-behavior: contain;
  457. box-sizing: border-box;
  458. }
  459. &__grid {
  460. display: grid;
  461. grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  462. gap: 12px;
  463. }
  464. &__card {
  465. position: relative;
  466. display: flex;
  467. flex-direction: column;
  468. gap: 10px;
  469. padding: 14px;
  470. background: #fff;
  471. border: 1px solid #e7ebf3;
  472. border-radius: 12px;
  473. box-sizing: border-box;
  474. transition:
  475. border-color 0.15s ease,
  476. box-shadow 0.15s ease;
  477. &:hover {
  478. border-color: #bfcdfb;
  479. box-shadow: 0 4px 14px rgb(16 24 40 / 8%);
  480. transform: none;
  481. }
  482. }
  483. &__card-head {
  484. display: flex;
  485. align-items: flex-start;
  486. gap: 10px;
  487. }
  488. &__card-icon {
  489. display: flex;
  490. flex: 0 0 auto;
  491. align-items: center;
  492. justify-content: center;
  493. width: 34px;
  494. height: 34px;
  495. color: #2a50ec;
  496. background: rgb(42 80 236 / 8%);
  497. border-radius: 9px;
  498. }
  499. &__card-heading {
  500. display: flex;
  501. flex: 1 1 auto;
  502. flex-direction: column;
  503. gap: 4px;
  504. min-width: 0;
  505. }
  506. &__name-row {
  507. display: flex;
  508. align-items: center;
  509. gap: 6px;
  510. min-width: 0;
  511. }
  512. &__name {
  513. flex: 1 1 auto;
  514. min-width: 0;
  515. overflow: hidden;
  516. color: #111827;
  517. font-size: 14px;
  518. font-weight: 600;
  519. line-height: 1.5;
  520. letter-spacing: 0;
  521. white-space: nowrap;
  522. text-overflow: ellipsis;
  523. }
  524. &__card-sub {
  525. overflow: hidden;
  526. color: #98a2b3;
  527. font-size: 11.5px;
  528. white-space: nowrap;
  529. text-overflow: ellipsis;
  530. }
  531. &__chip {
  532. display: inline-flex;
  533. flex: 0 0 auto;
  534. align-items: center;
  535. gap: 5px;
  536. max-width: 100%;
  537. height: 20px;
  538. padding: 0 7px;
  539. overflow: hidden;
  540. color: #5b6577;
  541. font-size: 11px;
  542. font-weight: 500;
  543. line-height: 18px;
  544. white-space: nowrap;
  545. text-overflow: ellipsis;
  546. background: #f4f6fa;
  547. border: 1px solid #e7ebf3;
  548. border-radius: 6px;
  549. &.is-primary {
  550. color: #2a50ec;
  551. background: rgb(42 80 236 / 7%);
  552. border-color: rgb(42 80 236 / 18%);
  553. }
  554. }
  555. &__chip-dot {
  556. flex: 0 0 auto;
  557. width: 5px;
  558. height: 5px;
  559. border-radius: 50%;
  560. }
  561. &__desc {
  562. display: -webkit-box;
  563. min-height: calc(12.5px * 1.7 * 2);
  564. margin: 0;
  565. overflow: hidden;
  566. color: #5b6577;
  567. font-size: 12.5px;
  568. line-height: 1.7;
  569. -webkit-box-orient: vertical;
  570. -webkit-line-clamp: 3;
  571. }
  572. &__chips {
  573. display: flex;
  574. flex-wrap: wrap;
  575. gap: 6px;
  576. }
  577. &__card-ops {
  578. display: flex;
  579. align-items: center;
  580. gap: 6px;
  581. margin-top: auto;
  582. padding-top: 10px;
  583. border-top: 1px solid #eff2f7;
  584. }
  585. &__card-ops-sp {
  586. flex: 1 1 auto;
  587. }
  588. &__run {
  589. display: inline-flex;
  590. align-items: center;
  591. gap: 5px;
  592. height: 26px;
  593. padding: 0 10px;
  594. color: #fff;
  595. font-size: 11.5px;
  596. font-weight: 500;
  597. font-family: inherit;
  598. white-space: nowrap;
  599. background: #2a50ec;
  600. border: 1px solid #2a50ec;
  601. border-radius: 6px;
  602. cursor: pointer;
  603. transition:
  604. background-color 0.15s ease,
  605. border-color 0.15s ease;
  606. &:hover {
  607. background: #4b6bf0;
  608. border-color: #4b6bf0;
  609. }
  610. }
  611. &__iconbtn {
  612. display: inline-flex;
  613. align-items: center;
  614. justify-content: center;
  615. width: 26px;
  616. height: 26px;
  617. padding: 0;
  618. color: #98a2b3;
  619. background: #fff;
  620. border: 1px solid #e7ebf3;
  621. border-radius: 6px;
  622. cursor: pointer;
  623. transition:
  624. color 0.15s ease,
  625. background-color 0.15s ease,
  626. border-color 0.15s ease;
  627. &:hover {
  628. color: #2a50ec;
  629. background: #f8faff;
  630. border-color: #bfcdfb;
  631. }
  632. &.is-danger:hover {
  633. color: #e5484d;
  634. background: #fff5f5;
  635. border-color: rgb(229 72 77 / 35%);
  636. }
  637. }
  638. }
  639. /* ── 知识库详情弹框(只读):头 + 描述 + 使用方式 ── */
  640. .akb-detail {
  641. display: flex;
  642. flex-direction: column;
  643. gap: 12px;
  644. &__head {
  645. display: flex;
  646. align-items: center;
  647. gap: 8px;
  648. }
  649. &__badge {
  650. flex: 0 0 auto;
  651. padding: 0 7px;
  652. font-size: 11px;
  653. line-height: 18px;
  654. border-radius: 99px;
  655. &.is-builtin {
  656. color: #5b6577;
  657. background: #f4f6fa;
  658. }
  659. &.is-custom {
  660. color: #2a50ec;
  661. background: rgb(42 80 236 / 8%);
  662. }
  663. }
  664. &__name {
  665. overflow: hidden;
  666. color: #111827;
  667. font-size: 15px;
  668. font-weight: 700;
  669. white-space: nowrap;
  670. text-overflow: ellipsis;
  671. }
  672. &__desc {
  673. margin: 0;
  674. color: #5b6577;
  675. font-size: 13px;
  676. line-height: 1.7;
  677. }
  678. &__panel {
  679. display: flex;
  680. flex-direction: column;
  681. gap: 8px;
  682. padding: 12px 14px;
  683. background: #fff;
  684. border: 1px solid #e7ebf3;
  685. border-radius: 10px;
  686. }
  687. &__label {
  688. color: #111827;
  689. font-size: 12.5px;
  690. font-weight: 700;
  691. }
  692. &__text {
  693. margin: 0;
  694. color: #5b6577;
  695. font-size: 12.5px;
  696. line-height: 1.7;
  697. }
  698. }
  699. @media (max-width: 1180px) {
  700. .ai-kb__summary {
  701. display: none;
  702. }
  703. }
  704. </style>