index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <template>
  2. <div class="knowledge-retrieval-container layout-padding">
  3. <el-card shadow="never" class="h100">
  4. <el-tabs v-model="state.queryParams.Attribution" @tab-change="handleClick">
  5. <el-tab-pane label="全部" name=" "></el-tab-pane>
  6. <el-tab-pane label="中心知识库" name="中心知识库"></el-tab-pane>
  7. <el-tab-pane label="部门知识库" name="部门知识库"></el-tab-pane>
  8. </el-tabs>
  9. <el-row :gutter="20" class="h100">
  10. <el-col :xs="6" :sm="6" :md="6" :lg="4" :xl="4" class="left-container">
  11. <div class="h100 pr10">
  12. <el-tabs v-model="state.activeName" stretch @tab-change="resetNode">
  13. <el-tab-pane label="部门" name="0">
  14. <el-input v-model="filterOrg" placeholder="请输入部门名称" class="input-with-select mt10 mb10" clearable> </el-input>
  15. </el-tab-pane>
  16. <el-tab-pane label="知识分类" name="1">
  17. <el-input v-model="filterType" placeholder="请输入知识分类名称" class="input-with-select mt10 mb10" clearable> </el-input>
  18. </el-tab-pane>
  19. <el-tab-pane label="热点" name="2">
  20. <el-input v-model="filterHot" placeholder="请输入热点名称" class="input-with-select mt10 mb10" clearable> </el-input>
  21. </el-tab-pane>
  22. </el-tabs>
  23. <div style="height: calc(100% - 100px);'">
  24. <el-scrollbar>
  25. <el-tree
  26. v-show="state.activeName === '0'"
  27. :data="state.orgData"
  28. highlight-current
  29. :expand-on-click-node="false"
  30. :props="{ children: 'children', label: 'name' }"
  31. @node-click="handleNodeClick"
  32. ref="orgRef"
  33. v-loading="state.loading"
  34. :filter-node-method="filterNode"
  35. node-key="id"
  36. :style="state.activeName === '0' ? 'min-width: 100%; display: inline-block' : 'display:none'"
  37. :default-expanded-keys="state.defaultExpandedOrgKeys"
  38. >
  39. </el-tree>
  40. <el-tree
  41. :data="state.knowledgeOptions"
  42. highlight-current
  43. :expand-on-click-node="false"
  44. :props="{ children: 'children', label: 'name' }"
  45. @node-click="handleNodeClick"
  46. ref="typeRef"
  47. v-loading="state.loading"
  48. :filter-node-method="filterNodeType"
  49. node-key="id"
  50. :style="state.activeName === '1' ? 'min-width: 100%; display: inline-block' : 'display:none'"
  51. :default-expanded-keys="state.defaultExpandedTypeKeys"
  52. >
  53. </el-tree>
  54. <el-tree
  55. filterable
  56. highlight-current
  57. placeholder="请选择热点分类"
  58. :props="HotspotProps"
  59. @node-click="handleNodeClick"
  60. lazy
  61. :load="load"
  62. node-key="id"
  63. ref="hotRef"
  64. :filter-node-method="filterNodeHot"
  65. :expand-on-click-node="false"
  66. :style="state.activeName === '2' ? 'min-width: 100%; display: inline-block' : 'display:none'"
  67. >
  68. <template #default="{ data }">
  69. <span :title="data.hotSpotName">
  70. {{ data.hotSpotName }}
  71. </span>
  72. </template>
  73. </el-tree>
  74. </el-scrollbar>
  75. </div>
  76. </div>
  77. </el-col>
  78. <el-col :xs="12" :sm="12" :md="14" :lg="14" :xl="14" class="center-container">
  79. <div class="input-box">
  80. <el-select v-model="state.queryParams.RetrievalType" placeholder="请选择" style="width: 120px" @change="queryList">
  81. <el-option label="全文" :value="0" />
  82. <el-option label="标题" :value="1" />
  83. <el-option label="知识内容" :value="2" />
  84. <el-option label="摘要" :value="3" />
  85. </el-select>
  86. <div class="input-with-button w100">
  87. <div class="flex">
  88. <el-autocomplete
  89. v-model="state.queryParams.Keyword"
  90. :fetch-suggestions="querySearchAsync"
  91. placeholder="关键词"
  92. @select="handleSelect"
  93. class="mr10 w100"
  94. clearable
  95. value-key="title"
  96. >
  97. <template #default="{ item }">
  98. <template v-if="state.queryParams.RetrievalType === 0">
  99. <div class="text-no-wrap">{{ item.title }}</div>
  100. <span class="text-no-wrap color-info">{{ item.summary }}</span>
  101. </template>
  102. <template v-if="state.queryParams.RetrievalType === 1">
  103. <div class="text-no-wrap">{{ item.title }}</div>
  104. </template>
  105. <template v-if="state.queryParams.RetrievalType === 2">
  106. <div class="text-no-wrap">{{ item.title }}</div>
  107. <span class="text-no-wrap color-info">{{ item.summary }}</span>
  108. </template>
  109. <template v-if="state.queryParams.RetrievalType === 3">
  110. <span class="text-no-wrap color-info">{{ item.summary }}</span>
  111. </template>
  112. </template>
  113. </el-autocomplete>
  114. <el-button type="primary" class="btn" :loading="state.loading" @click="queryList"
  115. ><SvgIcon name="ele-Search" class="mr5" />搜索</el-button
  116. >
  117. <el-button @click="resetQuery" class="default-button"> <SvgIcon name="ele-Refresh" class="mr5" />重置</el-button>
  118. </div>
  119. <div class="flex-center-align mt5">
  120. <div style="height: 32px; line-height: 32px">排序:</div>
  121. <el-radio-group v-model="state.queryParams.Sort" @change="queryList">
  122. <el-radio label="1">浏览量</el-radio>
  123. <el-radio label="2">评分</el-radio>
  124. <el-radio label="3">创建时间</el-radio>
  125. </el-radio-group>
  126. </div>
  127. </div>
  128. </div>
  129. <div v-loading="centerLoading" class="center-container-box" style="height: calc(100% - 180px)">
  130. <template v-if="state.retrievalList.length">
  131. <el-scrollbar>
  132. <div v-for="(v, i) in state.retrievalList" :key="i" class="retrieval-content-item" @click="onPreview(v)" title="查看详情">
  133. <h4 class="mb10 text-no-wrap">{{ v.title }}</h4>
  134. <div class="text-ellipsis2">{{ v.summary }}</div>
  135. <div class="flex-center-between mt10 color-info">
  136. <div>
  137. <span class="mr10">创建部门:{{ v.creatorOrgName }}</span>
  138. <span>创建时间:{{ formatDate(v.creationTime, 'YYYY-mm-dd HH:MM:SS') }}</span>
  139. </div>
  140. <div class="flex-center-align">
  141. <span class="flex-center-align"><SvgIcon name="ele-StarFilled" size="18px" class="mr3" />{{ v.score }}</span>
  142. <span class="flex-center-align ml10"><SvgIcon name="ele-ChatDotSquare" size="16px" class="mr3" />{{ v.commentNum }}</span>
  143. <span class="flex-center-align ml10"><SvgIcon name="ele-View" size="16px" class="mr3" />{{ v.pageView }}</span>
  144. </div>
  145. </div>
  146. </div>
  147. </el-scrollbar>
  148. </template>
  149. <template v-else>
  150. <Empty type="search" description="暂无结果" />
  151. </template>
  152. </div>
  153. <pagination
  154. :total="state.total"
  155. v-model:page="state.queryParams.PageIndex"
  156. v-model:limit="state.queryParams.PageSize"
  157. @pagination="queryList"
  158. />
  159. </el-col>
  160. <el-col :xs="6" :sm="6" :md="6" :lg="6" :xl="6" class="right-container">
  161. <p class="flex-center-between pt10">
  162. <span class="font16">常用知识前10</span>
  163. <el-button link type="primary" @click="querySearchNum"><SvgIcon name="ele-Refresh" class="mr4" /> 刷新</el-button>
  164. </p>
  165. <el-divider />
  166. <p class="flex-center-between">
  167. <span>排名</span>
  168. <span>搜索频率</span>
  169. </p>
  170. <div class="top10 mt10" style="height: calc(100% - 160px)" v-loading="rightLoading">
  171. <template v-if="topList.length">
  172. <el-scrollbar>
  173. <div class="flex-center-between top10-items" v-for="(item, index) in topList" :key="item.id">
  174. <p class="flex-center-align top10-items-title" @click="onPreview(item)">
  175. {{ index + 1 }}.<el-link type="primary" :underline="false">{{ item.title }}</el-link>
  176. </p>
  177. <span class="top10-items-num">{{ item.searchNum }}</span>
  178. </div>
  179. </el-scrollbar>
  180. </template>
  181. <template v-else>
  182. <Empty />
  183. </template>
  184. </div>
  185. </el-col>
  186. </el-row>
  187. </el-card>
  188. </div>
  189. </template>
  190. <script setup lang="ts" name="knowledgeRetrieval">
  191. import { onMounted, reactive, ref, watch } from 'vue';
  192. import { useRouter } from 'vue-router';
  193. import { ElMessage } from 'element-plus';
  194. import { auth } from '/@/utils/authFunction';
  195. import { knowledgeRetrieval, searchNumList,searchNumAdd } from '/@/api/knowledge/retrieval';
  196. import { hotSpotType } from '/@/api/business/order';
  197. import { getOrgList } from '/@/api/system/organize';
  198. import { treeList } from '/@/api/knowledge/type';
  199. import { formatDate } from '/@/utils/formatTime';
  200. import { throttle } from '/@/utils/tools';
  201. const state = reactive<any>({
  202. queryParams: {
  203. // 查询条件
  204. PageIndex: 1, // 当前页
  205. PageSize: 10, // 每页条数
  206. Attribution: ' ',
  207. Keyword: null, // 关键词
  208. RetrievalType: 0, // 检索类型
  209. Sort: '1',
  210. },
  211. activeName: '0',
  212. orgData: [], // 部门
  213. knowledgeOptions: [], // 知识类型
  214. total: 0, // 总条数
  215. loading: false, // 加载状态
  216. retrievalList: [], // 检索列表
  217. });
  218. const router = useRouter(); // 路由
  219. const topList = ref<EmptyArrayType>([]); // 常用知识前10
  220. const handleClick = () => {
  221. queryList();
  222. };
  223. // 热点分类远程搜索
  224. const HotspotProps = {
  225. label: 'hotSpotFullName',
  226. children: 'children',
  227. isLeaf: 'isLeaf',
  228. };
  229. // 热点分类懒加载
  230. const load = async (node: any, resolve: any) => {
  231. if (node.isLeaf) return resolve([]);
  232. const res: any = await hotSpotType({ id: node.data.id ? node.data.id : '' });
  233. resolve(res.result);
  234. };
  235. // 三个类型的搜索
  236. const filterOrg = ref('');
  237. const orgRef = ref<RefType>();
  238. watch(filterOrg, (val) => {
  239. orgRef.value!.filter(val);
  240. });
  241. const filterNode = (value: string, data: any) => {
  242. if (!value) return true;
  243. return data.name.includes(value);
  244. };
  245. const filterType = ref('');
  246. const typeRef = ref<RefType>();
  247. watch(filterType, (val) => {
  248. typeRef.value!.filter(val);
  249. });
  250. const filterNodeType = (value: string, data: any) => {
  251. if (!value) return true;
  252. return data.name.includes(value);
  253. };
  254. const filterHot = ref('');
  255. const hotRef = ref<RefType>();
  256. watch(filterHot, (val) => {
  257. hotRef.value!.filter(val);
  258. });
  259. const filterNodeHot = (value: string, data: any) => {
  260. if (!value) return true;
  261. return data.hotSpotName.includes(value);
  262. };
  263. // 获取所有组织结构 和基础数据
  264. const getOrgListApi = async () => {
  265. state.loading = true;
  266. try {
  267. const [orgRes, treeRes] = await Promise.all([getOrgList(), treeList({ IsEnable: true })]);
  268. state.orgData = orgRes.result ?? []; //部门
  269. state.knowledgeOptions = treeRes.result ?? []; // 知识类型
  270. state.loading = false;
  271. } catch (error) {
  272. state.loading = false;
  273. }
  274. };
  275. // 点击节点
  276. const handleNodeClick = (data: any) => {
  277. switch (state.activeName) {
  278. case '0':
  279. state.queryParams.CreateOrgId = data.id;
  280. break;
  281. case '1':
  282. state.queryParams.KnowledgeTypeId = data.id;
  283. break;
  284. case '2':
  285. state.queryParams.HotspotId = data.id;
  286. break;
  287. default:
  288. break;
  289. }
  290. queryList();
  291. };
  292. // 预览
  293. const onPreview = (row: any) => {
  294. router.push({
  295. name: 'knowledgePreview',
  296. params: {
  297. id: row.id,
  298. isAddPv: 'isAddPv',
  299. tagsViewName: '知识查看',
  300. },
  301. });
  302. };
  303. // 切换tab 查询列表
  304. const rightLoading = ref(false); // 右侧加载状态
  305. // 常用知识前10
  306. const querySearchNum = () => {
  307. rightLoading.value = true;
  308. searchNumList({ Keyword: state.queryParams.Keyword })
  309. .then((res: any) => {
  310. topList.value = res.result?.items ?? [];
  311. rightLoading.value = false;
  312. })
  313. .catch(() => {
  314. rightLoading.value = false;
  315. });
  316. };
  317. const queryTitleLight = (titleInfo: string) => {
  318. return titleInfo.replace(new RegExp(state.queryParams.Keyword, 'g'), `<span class="color-danger">${state.queryParams.Keyword}</span>`);
  319. };
  320. const centerLoading = ref(false); // 中间加载状态
  321. const querySearchAsync = (queryString: string, cb: (arg: any) => void) => {
  322. if (queryString) {
  323. centerLoading.value = true;
  324. knowledgeRetrieval({
  325. ...state.queryParams,
  326. Keyword: queryString,
  327. })
  328. .then((res: any) => {
  329. centerLoading.value = false;
  330. cb(res.result?.items ?? []);
  331. })
  332. .catch(() => {
  333. centerLoading.value = false;
  334. cb([]);
  335. });
  336. } else {
  337. cb([]);
  338. }
  339. };
  340. const handleSelect = (item: Record<string, any>) => {
  341. state.queryParams.Keyword = item.title;
  342. searchNumAdd({id: item.id});
  343. queryList();
  344. };
  345. const queryList = () => {
  346. if (!auth('knowledge:retrieval')) ElMessage.error('抱歉,您没有权限知识检索');
  347. else {
  348. centerLoading.value = true;
  349. knowledgeRetrieval(state.queryParams)
  350. .then((res: any) => {
  351. state.retrievalList = res.result?.items ?? [];
  352. state.total = res.result?.total ?? 0;
  353. centerLoading.value = false;
  354. })
  355. .catch(() => {
  356. centerLoading.value = false;
  357. state.retrievalList = [];
  358. state.total = 0;
  359. });
  360. }
  361. };
  362. /** 重置按钮操作 */
  363. const resetQuery = throttle(() => {
  364. state.queryParams.PageIndex = 1;
  365. state.queryParams.PageSize = 10;
  366. state.queryParams.Keyword = null;
  367. state.queryParams.RetrievalType = 0;
  368. state.queryParams.Sort = '1';
  369. state.queryParams.Attribution = ' ';
  370. state.activeName = '0';
  371. state.queryParams.CreateOrgId = null;
  372. state.queryParams.KnowledgeTypeId = null;
  373. state.queryParams.HotspotId = null;
  374. filterOrg.value = '';
  375. filterType.value = '';
  376. filterHot.value = '';
  377. typeRef.value?.setCurrentKey(null);
  378. orgRef.value?.setCurrentKey(null);
  379. hotRef.value?.setCurrentKey(null);
  380. queryList();
  381. }, 500);
  382. // 重置选中的节点
  383. const resetNode = () => {
  384. state.queryParams.CreateOrgId = null;
  385. state.queryParams.KnowledgeTypeId = null;
  386. state.queryParams.HotspotId = null;
  387. filterOrg.value = '';
  388. filterType.value = '';
  389. filterHot.value = '';
  390. typeRef.value?.setCurrentKey(null);
  391. orgRef.value?.setCurrentKey(null);
  392. hotRef.value?.setCurrentKey(null);
  393. queryList();
  394. };
  395. onMounted(() => {
  396. getOrgListApi();
  397. queryList();
  398. querySearchNum();
  399. });
  400. </script>
  401. <style scoped lang="scss">
  402. .knowledge-retrieval-container {
  403. .left-container {
  404. border-right: 1px solid var(--el-border-color);
  405. height: 100%;
  406. }
  407. .center-container {
  408. border-right: 1px solid var(--el-border-color);
  409. height: 100%;
  410. .input-box {
  411. display: flex;
  412. }
  413. .retrieval-content {
  414. &-item {
  415. border-bottom: var(--el-border);
  416. padding: 10px 15px;
  417. margin-bottom: 10px;
  418. cursor: pointer;
  419. &:last-child {
  420. margin-bottom: 0;
  421. border: none;
  422. }
  423. &:hover {
  424. color: var(--el-color-primary);
  425. }
  426. }
  427. }
  428. }
  429. .right-container {
  430. height: 100%;
  431. .top10 {
  432. &-items {
  433. margin-bottom: 20px;
  434. &:last-child {
  435. margin-bottom: 0;
  436. }
  437. &-title {
  438. flex: 1;
  439. overflow: hidden;
  440. text-overflow: ellipsis;
  441. white-space: nowrap;
  442. }
  443. &-num {
  444. display: inline-block;
  445. width: 50px;
  446. text-align: center;
  447. }
  448. }
  449. }
  450. }
  451. :deep(.el-tree-node__content) {
  452. height: 40px;
  453. }
  454. :deep(.el-card__body) {
  455. height: 100%;
  456. }
  457. }
  458. </style>