index.tsx 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250
  1. import * as React from 'react';
  2. import { observer } from 'mobx-react';
  3. import {
  4. List,
  5. Button,
  6. Divider,
  7. Flex,
  8. Layout,
  9. Empty,
  10. Image,
  11. Modal,
  12. Tag,
  13. message,
  14. Tooltip,
  15. Select,
  16. Form,
  17. Space,
  18. Row,
  19. Col,
  20. Input, Cascader, Card, Spin
  21. } from 'antd';
  22. import {
  23. PlusOutlined,
  24. FileOutlined,
  25. SettingOutlined,
  26. DeleteOutlined,
  27. StepForwardOutlined,
  28. SearchOutlined,
  29. ReloadOutlined,
  30. BookOutlined,
  31. TeamOutlined,
  32. AppstoreOutlined,
  33. EditOutlined, CloseOutlined, BulbOutlined, UpOutlined, DownOutlined
  34. } from '@ant-design/icons';
  35. import { apis } from '@/apis';
  36. import './style.less';
  37. import { PaginationConfig } from 'antd/es/pagination';
  38. import router from '@/router';
  39. import LocalStorage from '@/LocalStorage';
  40. import { create } from 'domain';
  41. import audit from '../../audit';
  42. import { set } from 'mobx';
  43. import IconSvg from "@/assets/public/icon.svg";
  44. import dayjs from 'dayjs';
  45. import UpdateNotification from '@/help/components/UpdateNotification';
  46. import { CURRENT_VERSION } from '@/help/components/UpdateNotification/version';
  47. import { useLocation } from 'react-router-dom';
  48. const { Header, Footer, Sider, Content } = Layout;
  49. const { Option } = Select;
  50. const FormItem = Form.Item;
  51. const headerStyle: React.CSSProperties = {
  52. textAlign: 'center',
  53. height: 24,
  54. paddingInline: 48,
  55. lineHeight: '30px',
  56. backgroundColor: '#fff',
  57. };
  58. const contentStyle: React.CSSProperties = {
  59. textAlign: 'center',
  60. lineHeight: '40px',
  61. backgroundColor: '#fff',
  62. };
  63. const siderStyle: React.CSSProperties = {
  64. paddingLeft: 30,
  65. paddingTop: 30,
  66. height: 80,
  67. backgroundColor: '#fff',
  68. };
  69. const footerStyle: React.CSSProperties = {
  70. textAlign: 'center',
  71. color: '#fff',
  72. height: 24,
  73. backgroundColor: '#4096ff',
  74. };
  75. const layoutStyle = {
  76. borderRadius: 8,
  77. overflow: 'hidden',
  78. width: 'calc(10% - 8px)',
  79. maxWidth: 'calc(20% - 8px)',
  80. };
  81. const QuestionAnswerList: React.FC = () => {
  82. const [form] = Form.useForm();
  83. const location = useLocation();
  84. interface Item {
  85. name: string,
  86. desc: string,
  87. appId: number,
  88. createBy: string,
  89. typeId: string;
  90. status: string;
  91. comment: string;
  92. auditStatus: string;
  93. projectName: string;
  94. updateTime: string;
  95. };
  96. interface PageInfo {
  97. pageNumber: number,
  98. pageSize: number,
  99. total: number,
  100. };
  101. type AppTypeList = {
  102. label: string,
  103. value: string,
  104. }[];
  105. type ProjectTypeList = {
  106. label: string,
  107. value: string,
  108. }[];
  109. const [listLoading, setListLoading] = React.useState(false);
  110. const [list, setList] = React.useState<Item[]>([]);
  111. const [originalList, setOriginalList] = React.useState<Item[]>([]); // 保存原始列表数据,用于前端过滤
  112. const [page, setPage] = React.useState<PageInfo>({
  113. pageNumber: 1,
  114. pageSize: 10,
  115. total: 0,
  116. });
  117. const [appCount, setAppCount] = React.useState<string>();
  118. const [knowCount, setKnowCount] = React.useState<string>();
  119. const { Header, Footer, Sider, Content } = Layout;
  120. const [appTypeList, setAppTypeList] = React.useState<AppTypeList>([]);
  121. const [createFlag, setCreateFlag] = React.useState(false);
  122. const [deleteFlag, setDeleteFlag] = React.useState(false);
  123. const [updateFlag, setUpdateFlag] = React.useState(false);
  124. const [userInfoAll, setUserInfoAll] = React.useState<any>({});
  125. const [projectList, setProjectList] = React.useState<ProjectTypeList>([]);
  126. const [appProjectList, setAppProjectList] = React.useState<AppTypeList>([]);
  127. const [showSubPanel, setShowSubPanel] = React.useState(false);
  128. const [selectedType, setSelectedType] = React.useState<string | null>('全部');
  129. const wrapperRef = React.useRef<HTMLDivElement>(null);
  130. const selectRef = React.useRef<any>(null);
  131. const [levelTypeList, setLevelTypeList] = React.useState<AppTypeList>([]);
  132. // 新手引导整体可见性(持久化到 localStorage)
  133. const [showGuide, setShowGuide] = React.useState<boolean>(() => localStorage.getItem('appGuideHidden') !== 'true');
  134. // 新手引导展开/折叠状态(持久化到 localStorage),默认展开
  135. const [isGuideExpanded, setIsGuideExpanded] = React.useState<boolean>(() => {
  136. const saved = localStorage.getItem('appGuideExpanded');
  137. // 如果 localStorage 中没有值,默认展开(返回 true)
  138. return saved === null ? true : saved === 'true';
  139. });
  140. const toggleGuide = () => {
  141. setIsGuideExpanded(prev => {
  142. const newValue = !prev;
  143. // 保存到 localStorage
  144. localStorage.setItem('appGuideExpanded', String(newValue));
  145. return newValue;
  146. });
  147. };
  148. // 搜索输入框展开状态
  149. const [isSearchExpanded, setIsSearchExpanded] = React.useState(false);
  150. const searchWrapperRef = React.useRef<HTMLDivElement>(null);
  151. const searchInputRef = React.useRef<any>(null);
  152. // 标记是否正在重置,避免触发 useEffect 循环
  153. const isResettingRef = React.useRef(false);
  154. const appApi = {
  155. fetchList: async (typeId: any, projectId: any, forceRefresh: boolean = false, pageNumber?: number) => {
  156. const keyword = form.getFieldValue('keyword');
  157. const currentPageNumber = pageNumber !== undefined ? pageNumber : page.pageNumber;
  158. // 如果有关键字且不是强制刷新,直接从现有数据筛选,不请求接口
  159. if (keyword && keyword.trim() !== '' && !forceRefresh) {
  160. applyFrontendFilter(originalList, keyword, currentPageNumber);
  161. return;
  162. }
  163. setListLoading(true);
  164. try {
  165. const userInfo = LocalStorage.getUserInfo();
  166. const userId = (userInfo?.id ?? '').toString();
  167. const res = await apis.fetchTakaiAppList({
  168. pageSize: page.pageSize,
  169. pageNumber: currentPageNumber,
  170. userId: userId,
  171. typeId: typeId,
  172. projectId: projectId?.toString(),
  173. keyword: '', // 不传关键字给后端
  174. name: '', // 不传name给后端
  175. })
  176. const list = res.rows.map((item: any) => {
  177. return {
  178. name: item.name,
  179. desc: item.desc,
  180. appId: item.appId,
  181. createBy: item.createBy,
  182. typeId: item.typeId,
  183. status: item.status,
  184. comment: item.comment,
  185. auditStatus: item.auditStatus,
  186. projectName: item.projectName,
  187. updateTime: item.updateTime
  188. }
  189. });
  190. const c = LocalStorage.getStatusFlag('deepseek:application:create');
  191. const u = LocalStorage.getStatusFlag('deepseek:application:delete');
  192. const filteredList = list.filter((item: any) => {
  193. // 如果有 createFlag 或 updateFlag 权限,显示所有数据
  194. if (c || u) {
  195. return true;
  196. }
  197. // 没有权限时排除 status='5' 的数据
  198. return item.status !== '5';
  199. });
  200. // 保存原始数据
  201. setOriginalList(filteredList);
  202. // 没有关键字时,显示当前页的数据
  203. setList(filteredList);
  204. setPage({
  205. pageNumber: currentPageNumber,
  206. pageSize: page.pageSize,
  207. total: res.total,
  208. });
  209. } catch (error) {
  210. console.error(error);
  211. } finally {
  212. setListLoading(false);
  213. }
  214. },
  215. auditApplication: async (appId: string, userId: string) => {
  216. const res = await apis.auditTakaiApplicationLibApi(appId, userId);
  217. if (res.data === 9) {
  218. message.error('您没有添加审核人');
  219. }
  220. await appApi.fetchList(null, null);
  221. }
  222. };
  223. // 立即使用应用
  224. const useNowAppLication = (appId: number) => {
  225. const userInfo = LocalStorage.getUserInfo();
  226. const getToken = LocalStorage.getToken();
  227. // const baseUrl = import.meta.env.VITE_JUMP_URL;
  228. const baseUrl = 'https://llm.jkec.info:11432/#/knowledgeChat?showMenu=false&chatMode=LOCAL'
  229. window.open(`${baseUrl}&appId=${appId}&userId=${userInfo?.id}&nickName=${userInfo?.name}&token=${getToken}`, '_blank');
  230. }
  231. // 删除应用
  232. const delApplication = async (appId: string) => {
  233. try {
  234. await apis.deleteTakaiApplicationApi(appId);
  235. await appApi.fetchList(null, null);
  236. } catch (error) {
  237. console.error(error);
  238. }
  239. }
  240. const indexApi = {
  241. fetchIndex: async (typeId: any, projectId: any, pageNumber?: number) => {
  242. try {
  243. const userInfo = LocalStorage.getUserInfo();
  244. const userId = (userInfo?.id ?? '').toString();
  245. const keyword = form.getFieldValue('keyword');
  246. const currentPageNumber = pageNumber !== undefined ? pageNumber : page.pageNumber;
  247. const res = await apis.fetchTakaiIndexCount({
  248. pageSize: page.pageSize,
  249. pageNumber: currentPageNumber,
  250. userId: userId,
  251. typeId: typeId,
  252. projectId: projectId?.toString(),
  253. keyword: keyword,
  254. })
  255. setAppCount(res.data.applicationCount);
  256. setKnowCount(res.data.knowledgeCount);
  257. } catch (error) {
  258. console.error(error);
  259. } finally {
  260. setListLoading(false);
  261. }
  262. }
  263. };
  264. // 获取应用类型
  265. const appTypeApi = {
  266. fetchAppType: async () => {
  267. try {
  268. // 1. 先请求应用列表,获取项目名称数据
  269. const userInfo = LocalStorage.getUserInfo();
  270. const userId = (userInfo?.id ?? '1').toString();
  271. const appListRes = await apis.fetchTakaiAppList({
  272. keyword: '',
  273. name: '',
  274. pageNumber: 1,
  275. pageSize: 1000,
  276. typeId: '41',
  277. userId: userId,
  278. projectId: null
  279. });
  280. // 提取所有的 projectName 并去重
  281. const projectNames = appListRes.rows.map((item: any) => item.projectName).filter(Boolean);
  282. const uniqueProjectNames = new Set(projectNames);
  283. // 2. 请求应用类型列表
  284. const res = await apis.fetchTakaiAppTypeList('app_type');
  285. // 3. 对比数据,保留所有类型,但标记是否有对应的项目数据
  286. const list = res.data.map((item: any) => {
  287. return {
  288. label: item.dictLabel,
  289. value: item.dictCode,
  290. }
  291. });
  292. // 如果没有项目级应用的数据,过滤掉"项目级应用"类型
  293. const filteredList = list.filter((item: any) => {
  294. if (item.label === '项目级应用') {
  295. return uniqueProjectNames.size > 0;
  296. }
  297. return true;
  298. });
  299. setAppTypeList(filteredList);
  300. } catch (error: any) {
  301. console.error(error);
  302. }
  303. },
  304. };
  305. // 项目级应用下的类型
  306. const appProTypeApi = {
  307. fetchAppProType: async () => {
  308. try {
  309. const res = await apis.fetchTakaiAppTypeList('projectTree');
  310. // const list: AppTypeList = res.data;
  311. const list: AppTypeList = res.data?.reduce((acc: any, item: any) => {
  312. if (item.children.length > 0) {
  313. item.children.forEach((val: any) => {
  314. acc.push({
  315. label: val.label,
  316. value: `${val.value}`,
  317. })
  318. })
  319. }
  320. return acc;
  321. }, []);
  322. setAppProjectList(list);
  323. // const res = await apis.fetchTakaiAppTypeList('projectTree');
  324. // const list = res.data.map((item: any) => {
  325. // return {
  326. // label: item.dictLabel,
  327. // value: item.dictCode,
  328. // }
  329. // });
  330. // setAppProjectList(list);
  331. } catch (error: any) {
  332. console.error(error);
  333. }
  334. },
  335. };
  336. const projectApi = {
  337. fetchProject: async () => {
  338. try {
  339. const res = await apis.fetchTakaiProjectLibApi();
  340. const list = res.data.map((item: any) => {
  341. return {
  342. label: item.projectName,
  343. value: item.projectId,
  344. }
  345. });
  346. setProjectList(list);
  347. } catch (error: any) {
  348. console.error(error);
  349. }
  350. },
  351. };
  352. // 获取应用类型
  353. const levelTypeApi = {
  354. fetchLevelAppType: async () => {
  355. try {
  356. const res = await apis.fetchTakaiAppTypeList('project_type');
  357. const list = res.data.map((item: any) => {
  358. return {
  359. label: item.dictLabel,
  360. value: item.dictCode,
  361. }
  362. });
  363. setLevelTypeList(list);
  364. } catch (error: any) {
  365. console.error(error);
  366. }
  367. },
  368. };
  369. const init = async () => {
  370. await appTypeApi.fetchAppType();
  371. await projectApi.fetchProject();
  372. await appProTypeApi.fetchAppProType();
  373. await levelTypeApi.fetchLevelAppType();
  374. // 检查是否从导航跳转过来,需要选中"项目级应用"
  375. const shouldSelectProjectApp = location.state?.selectProjectApp;
  376. if (!shouldSelectProjectApp) {
  377. // 默认加载全部数据
  378. await appApi.fetchList(null, null, true); // 强制刷新
  379. await indexApi.fetchIndex(null, null);
  380. // 设置默认选择"全部"
  381. form.setFieldsValue({ typeId: '全部' });
  382. }
  383. // 如果需要选中项目级应用,会在 useEffect 中处理
  384. }
  385. React.useEffect(() => {
  386. // 如果正在重置,跳过执行,避免循环
  387. if (isResettingRef.current) {
  388. isResettingRef.current = false;
  389. return;
  390. }
  391. setCreateFlag(LocalStorage.getStatusFlag('deepseek:application:create'));
  392. setDeleteFlag(LocalStorage.getStatusFlag('deepseek:application:delete'));
  393. setUpdateFlag(LocalStorage.getStatusFlag('deepseek:application:update'));
  394. setUserInfoAll(LocalStorage.getUserInfo());
  395. init();
  396. }, [page.pageSize, page.pageNumber])
  397. // 监听 appTypeList 变化,处理从导航跳转过来选中"项目级应用"的逻辑
  398. React.useEffect(() => {
  399. const shouldSelectProjectApp = location.state?.selectProjectApp;
  400. if (shouldSelectProjectApp && appTypeList.length > 0) {
  401. // 找到"项目级应用"的配置
  402. const projectAppType = appTypeList.find(item => item.label === '项目级应用');
  403. if (projectAppType) {
  404. const projectAppValue = projectAppType.value;
  405. setSelectedType(projectAppValue);
  406. form.setFieldsValue({ typeId: projectAppValue });
  407. // 如果是项目级应用(value为'41'),显示子面板
  408. if (projectAppValue === '41') {
  409. setShowSubPanel(true);
  410. }
  411. // 加载对应的数据
  412. appApi.fetchList(projectAppValue, null, true);
  413. indexApi.fetchIndex(projectAppValue, null);
  414. // 清除 location.state,避免重复触发
  415. window.history.replaceState({}, document.title);
  416. }
  417. }
  418. }, [appTypeList, location.state])
  419. // 监听 selectedType 变化,通知 Header 组件更新选中状态
  420. React.useEffect(() => {
  421. if (appTypeList.length > 0) {
  422. const projectAppType = appTypeList.find(item => item.label === '项目级应用');
  423. const isProjectApp = projectAppType && selectedType === projectAppType.value;
  424. // 触发自定义事件通知 Header 组件
  425. const event = new CustomEvent('projectAppActiveChange', {
  426. detail: { isActive: isProjectApp }
  427. });
  428. window.dispatchEvent(event);
  429. }
  430. }, [selectedType, appTypeList])
  431. const paginationConfig: PaginationConfig = {
  432. // 显示数据总量
  433. showTotal: (total: number) => {
  434. return `共 ${total} 条`;
  435. },
  436. // 展示分页条数切换
  437. showSizeChanger: true,
  438. // 指定每页显示条数
  439. // pageSizeOptions: ['2', '20', '50', '100'],
  440. // 快速跳转至某页
  441. showQuickJumper: true,
  442. current: page.pageNumber,
  443. pageSize: page.pageSize,
  444. total: page.total,
  445. onChange: (pageNumber, pageSize) => {
  446. const keyword = form.getFieldValue('keyword');
  447. // 根据是否有搜索关键字,决定使用哪个数据源
  448. const dataSource = keyword && keyword.trim() !== ''
  449. ? originalList.filter((item: Item) =>
  450. item.name && item.name.toLowerCase().includes(keyword.toLowerCase().trim())
  451. )
  452. : originalList;
  453. // 获取新页的数据
  454. const startIndex = (pageNumber - 1) * pageSize;
  455. const endIndex = startIndex + pageSize;
  456. const paginatedData = dataSource.slice(startIndex, endIndex);
  457. setList(paginatedData);
  458. setPage({
  459. pageNumber: pageNumber,
  460. pageSize: pageSize,
  461. total: dataSource.length,
  462. });
  463. },
  464. };
  465. // 前端模糊查询过滤函数
  466. const applyFrontendFilter = (dataList: Item[], keyword: string | undefined, currentPageNumber: number = 1) => {
  467. if (!keyword || keyword.trim() === '') {
  468. // 如果没有关键字,显示所有数据
  469. const paginatedData = getPaginatedData(dataList, currentPageNumber, page.pageSize);
  470. setList(paginatedData);
  471. setPage(prev => ({
  472. ...prev,
  473. total: dataList.length,
  474. pageNumber: currentPageNumber,
  475. }));
  476. return;
  477. }
  478. // 对标题进行模糊匹配(不区分大小写)
  479. const filtered = dataList.filter((item: Item) => {
  480. return item.name && item.name.toLowerCase().includes(keyword.toLowerCase().trim());
  481. });
  482. // 计算过滤后的总页数
  483. const totalPages = Math.ceil(filtered.length / page.pageSize);
  484. // 如果当前页码超出了总页数,调整到最后一页(至少为1)
  485. const adjustedPageNumber = Math.min(currentPageNumber, Math.max(1, totalPages));
  486. // 获取调整后页码的数据
  487. const paginatedData = getPaginatedData(filtered, adjustedPageNumber, page.pageSize);
  488. setList(paginatedData);
  489. setPage(prev => ({
  490. ...prev,
  491. total: filtered.length,
  492. pageNumber: adjustedPageNumber,
  493. }));
  494. };
  495. // 获取分页数据
  496. const getPaginatedData = (dataList: Item[], pageNumber: number, pageSize: number) => {
  497. const startIndex = (pageNumber - 1) * pageSize;
  498. const endIndex = startIndex + pageSize;
  499. return dataList.slice(startIndex, endIndex);
  500. };
  501. // 点击查询
  502. const handleClickSearch = async () => {
  503. form.validateFields().then(async (values) => {
  504. const keyword = values.keyword;
  505. // 如果有关键字,直接从现有数据中筛选,不请求接口
  506. if (keyword && keyword.trim() !== '') {
  507. // 模糊查询:从现有数据中筛选,保持当前页码
  508. applyFrontendFilter(originalList, keyword, page.pageNumber);
  509. return;
  510. }
  511. // 如果没有关键字,正常查询接口
  512. if (values.proTypeId) {
  513. values.typeId = values.proTypeId;
  514. }
  515. if (values.typeId === '全部') {
  516. values.typeId = null;
  517. }
  518. if (values.projectId instanceof Array && values.projectId.length == 2) {
  519. values.projectId = values.projectId[1];
  520. }
  521. // 重置到第一页
  522. setPage(prev => ({ ...prev, pageNumber: 1 }));
  523. await indexApi.fetchIndex(values.typeId, values.projectId);
  524. await appApi.fetchList(values.typeId, values.projectId, true); // 强制刷新
  525. }).catch((error) => {
  526. console.error(error);
  527. });
  528. };
  529. // 点击重置
  530. const handleClickReset = async () => {
  531. form.resetFields();
  532. setShowSubPanel(false);
  533. setSelectedType('全部'); // 重置为"全部"
  534. // 标记正在重置,避免触发 useEffect 循环
  535. isResettingRef.current = true;
  536. // 先更新 page 到第一页
  537. setPage({
  538. pageNumber: 1,
  539. pageSize: 10,
  540. total: 0,
  541. });
  542. // 调用接口获取数据,传递 pageNumber: 1 确保返回第一页
  543. await appApi.fetchList(null, null, true, 1); // 强制刷新,第一页
  544. await indexApi.fetchIndex(null, null, 1); // 第一页
  545. };
  546. /** 点击外部关闭面板 */
  547. React.useEffect(() => {
  548. const handleClickOutside = (event: MouseEvent) => {
  549. if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
  550. setShowSubPanel(false);
  551. }
  552. };
  553. document.addEventListener('mousedown', handleClickOutside, true);
  554. return () => {
  555. document.removeEventListener('mousedown', handleClickOutside, true);
  556. };
  557. }, []);
  558. /** 点击外部关闭搜索框 */
  559. React.useEffect(() => {
  560. const handleClickOutside = (event: MouseEvent) => {
  561. if (searchWrapperRef.current && !searchWrapperRef.current.contains(event.target as Node)) {
  562. setIsSearchExpanded(false);
  563. }
  564. };
  565. if (isSearchExpanded) {
  566. document.addEventListener('mousedown', handleClickOutside, true);
  567. return () => {
  568. document.removeEventListener('mousedown', handleClickOutside, true);
  569. };
  570. }
  571. }, [isSearchExpanded]);
  572. // 展开搜索框并聚焦输入框
  573. const handleExpandSearch = () => {
  574. setIsSearchExpanded(true);
  575. // 延迟聚焦,确保输入框已渲染
  576. setTimeout(() => {
  577. const input = searchInputRef.current?.input || searchInputRef.current;
  578. if (input) {
  579. input.focus();
  580. input.select(); // 选中输入框中的文本
  581. }
  582. }, 150);
  583. };
  584. const handleAppTypeChange = (value: string) => {
  585. if (value === '41') {
  586. // 如果是项目级应用,切换面板状态
  587. // setShowSubPanel(prev => !prev);
  588. setShowSubPanel(true);
  589. } else {
  590. // 其他选项,隐藏面板
  591. setShowSubPanel(false);
  592. }
  593. setSelectedType(value);
  594. form.setFieldsValue({ typeId: value });
  595. // 自动提交逻辑
  596. if (value === '全部') {
  597. // 全部选项,传递null给后端
  598. appApi.fetchList(null, null, true); // 强制刷新
  599. indexApi.fetchIndex(null, null);
  600. } else {
  601. // 其他选项,传递对应的typeId
  602. appApi.fetchList(value, null, true); // 强制刷新
  603. indexApi.fetchIndex(value, null);
  604. }
  605. };
  606. const handleAppProTypeChange = (value: string) => {
  607. console.log(value, 'valuevalue');
  608. setSelectedType(value);
  609. form.setFieldsValue({ typeId: value });
  610. };
  611. return (
  612. <div>
  613. {/* 更新通知弹窗 - 只在此页面显示 */}
  614. <UpdateNotification version={CURRENT_VERSION} />
  615. <div style={{ padding: '16px 20px', display: 'flex' }}>
  616. <Form
  617. form={form}
  618. layout='inline'
  619. colon={false}
  620. style={{ flex: 1 }}
  621. >
  622. <div>
  623. {/* 主选择器 - 修改为按钮组形式 */}
  624. <FormItem
  625. name="typeId"
  626. style={{ marginBottom: 0 }}
  627. >
  628. <div className="filter-button-group">
  629. {/* 全部按钮 */}
  630. <Button
  631. key="全部"
  632. type={selectedType === '全部' ? 'primary' : 'default'}
  633. size="small"
  634. onClick={() => handleAppTypeChange('全部')}
  635. >
  636. 全部
  637. </Button>
  638. {/* 动态应用类型按钮 */}
  639. {appTypeList.map(item => {
  640. // 根据label匹配对应的图标
  641. let icon = null;
  642. const isSelected = selectedType === item.value;
  643. if (item.label === '专业知识') {
  644. icon = <BookOutlined style={{ fontSize: '14px', marginRight: '0' }} />;
  645. } else if (item.label === '职能管理') {
  646. icon = <TeamOutlined style={{ fontSize: '14px', marginRight: '0' }} />;
  647. } else if (item.label === '项目级应用') {
  648. icon = <AppstoreOutlined style={{ fontSize: '14px', marginRight: '0' }} />;
  649. }
  650. return (
  651. <Button
  652. key={item.value}
  653. type={isSelected ? 'primary' : 'default'}
  654. size="small"
  655. onClick={() => handleAppTypeChange(item.value)}
  656. >
  657. {icon}
  658. {item.label}
  659. </Button>
  660. );
  661. })}
  662. </div>
  663. </FormItem>
  664. {/* 子选项面板 */}
  665. {showSubPanel && selectedType === '41' && (
  666. <FormItem
  667. label='类型'
  668. name='proTypeId'
  669. rules={[{ required: true, message: '类型不能为空' }]}
  670. >
  671. <Select
  672. placeholder='请选择'
  673. allowClear
  674. style={{ width: 200 }}
  675. popupMatchSelectWidth={200}
  676. onChange={(value) => {
  677. // 项目类型选择器自动提交逻辑
  678. const currentProjectId = form.getFieldValue('projectId');
  679. appApi.fetchList(value, currentProjectId, true); // 强制刷新
  680. indexApi.fetchIndex(value, currentProjectId);
  681. }}
  682. >
  683. {
  684. appProjectList.map((item, index) => {
  685. return <Option value={item.value} key={index} title={item.label}>
  686. <div style={{
  687. overflow: 'hidden',
  688. textOverflow: 'ellipsis',
  689. whiteSpace: 'nowrap',
  690. width: '100%'
  691. }}>
  692. {item.label}
  693. </div>
  694. </Option>
  695. })
  696. }
  697. </Select>
  698. </FormItem>
  699. )}
  700. </div>
  701. {/* {
  702. appProjectList.map((subItem, index) => (
  703. <div key={index}>
  704. {subItem.label}
  705. </div>
  706. ))
  707. } */ }
  708. {appTypeList.find(item => item.value === selectedType)?.label === '项目级应用' && (
  709. <FormItem name='projectId'>
  710. <Cascader
  711. options={appProjectList}
  712. placeholder="请选择项目"
  713. showSearch
  714. style={{ width: 200 }}
  715. maxTagTextLength={10}
  716. popupClassName="cascader-dropdown-fixed-width"
  717. dropdownRender={(menus) => (
  718. <div style={{ maxWidth: 200 }}>
  719. {menus}
  720. </div>
  721. )}
  722. onChange={(value) => {
  723. // 项目选择器自动提交逻辑
  724. const currentProTypeId = form.getFieldValue('proTypeId');
  725. let projectId: any = value;
  726. // 如果是数组且长度为2,取第二个元素(项目ID)
  727. if (projectId instanceof Array && projectId.length === 2) {
  728. projectId = projectId[1];
  729. }
  730. appApi.fetchList(currentProTypeId, projectId, true); // 强制刷新
  731. indexApi.fetchIndex(currentProTypeId, projectId);
  732. }}
  733. />
  734. </FormItem>
  735. )}
  736. {/*<Select*/}
  737. {/* placeholder='请选择项目'*/}
  738. {/* allowClear*/}
  739. {/* onChange={(value) => {*/}
  740. {/* // 项目选择器自动提交逻辑*/}
  741. {/* const currentTypeId = form.getFieldValue('typeId');*/}
  742. {/* const typeId = currentTypeId === '全部' ? null : currentTypeId;*/}
  743. {/* appApi.fetchList(typeId, value);*/}
  744. {/* indexApi.fetchIndex(typeId, value);*/}
  745. {/* }}*/}
  746. {/*>*/}
  747. {/* {*/}
  748. {/* projectList.map((item, index) => {*/}
  749. {/* return <Option value={item.value} key={index}>*/}
  750. {/* {item.label}*/}
  751. {/* </Option>*/}
  752. {/* })*/}
  753. {/* }*/}
  754. {/*</Select>*/}
  755. {/* </FormItem> */}
  756. <FormItem>
  757. <Space size={12}>
  758. {/* <div
  759. className={`search-expand-wrapper ${isSearchExpanded ? 'expanded' : ''}`}
  760. ref={searchWrapperRef}
  761. >
  762. <div className={`search-input-container ${isSearchExpanded ? 'expanded' : ''}`}>
  763. <FormItem name='keyword' style={{ marginBottom: 0 }}>
  764. <Input
  765. ref={searchInputRef}
  766. className="search-input"
  767. placeholder="请输入关键字"
  768. allowClear
  769. onPressEnter={handleClickSearch}
  770. onFocus={() => setIsSearchExpanded(true)}
  771. />
  772. </FormItem>
  773. </div>
  774. <Button
  775. type='primary'
  776. className="search-button-blue"
  777. onClick={() => {
  778. if (isSearchExpanded) {
  779. handleClickSearch();
  780. } else {
  781. handleExpandSearch();
  782. }
  783. }}
  784. icon={<SearchOutlined />}
  785. />
  786. </div> */}
  787. <Tooltip title="重置">
  788. <Button
  789. shape="circle"
  790. icon={<ReloadOutlined />}
  791. onClick={handleClickReset}
  792. />
  793. </Tooltip>
  794. </Space>
  795. </FormItem>
  796. {/* {
  797. createFlag && (
  798. <div style={ { marginLeft: 'auto' } }>
  799. <Button type='primary'
  800. icon={ <PlusOutlined /> }
  801. onClick={ () => {
  802. router.navigate( { pathname: '/deepseek/questionAnswer/create' } );
  803. } }>创建问答应用</Button>
  804. </div>
  805. )
  806. }
  807. */}
  808. {/* 创建按钮已移至面包屑组件 */}
  809. </Form>
  810. </div>
  811. {/* 新手使用提示区块 */}
  812. {showGuide && (
  813. <div style={{ padding: '12px 20px 12px 20px' }}>
  814. <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: isGuideExpanded ? 8 : 0 }}>
  815. <div style={{ fontSize: 13, color: '#333', display: 'flex', alignItems: 'center', gap: 6 }}>
  816. <BulbOutlined style={{ color: '#faad14' }} />
  817. 提示:如何创建并发布自己的RAG(增强检索生成)应用?
  818. </div>
  819. <div onClick={toggleGuide} style={{ color: '#999', cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
  820. {isGuideExpanded ? <UpOutlined /> : <DownOutlined />}
  821. </div>
  822. </div>
  823. {isGuideExpanded && (
  824. <Row gutter={12}>
  825. <Col xs={24} sm={24} md={12} lg={8}>
  826. <Card
  827. size="small"
  828. bordered={false}
  829. style={{
  830. background: 'linear-gradient(90deg, #e6f4ff 0%, #f0f7ff 100%)',
  831. boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
  832. borderRadius: 8,
  833. height: '100%'
  834. }}
  835. bodyStyle={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 80 }}
  836. title={<span style={{ fontWeight: 600 }}>创建或选用公共知识库</span>}
  837. >
  838. <div style={{ color: '#666', fontSize: 12 }}>选择公共知识库,或点击知识库菜单栏,创建自己的知识库。</div>
  839. <div style={{ textAlign: 'right', color: '#1677ff', fontWeight: 600 }}>step 1</div>
  840. </Card>
  841. </Col>
  842. <Col xs={24} sm={24} md={12} lg={8}>
  843. <Card
  844. size="small"
  845. bordered={false}
  846. style={{
  847. background: 'linear-gradient(90deg, #e6fffb 0%, #f0fffe 100%)',
  848. boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
  849. borderRadius: 8,
  850. height: '100%'
  851. }}
  852. bodyStyle={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 80 }}
  853. title={<span style={{ fontWeight: 600 }}>参数配置</span>}
  854. >
  855. <div style={{ color: '#666', fontSize: 12 }}>点击右上角,创建应用,选择模型与知识范围,配置Prompt、检索策略并调试。</div>
  856. <div style={{ textAlign: 'right', color: '#1677ff', fontWeight: 600 }}>step 2</div>
  857. </Card>
  858. </Col>
  859. <Col xs={24} sm={24} md={12} lg={8}>
  860. <Card
  861. size="small"
  862. bordered={false}
  863. style={{
  864. background: 'linear-gradient(90deg, #fff7e6 0%, #fffaf0 100%)',
  865. boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
  866. borderRadius: 8,
  867. height: '100%'
  868. }}
  869. bodyStyle={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 80 }}
  870. title={<span style={{ fontWeight: 600 }}>发布应用</span>}
  871. >
  872. <div style={{ color: '#666', fontSize: 12 }}>发布后可在相应的项目内使用。</div>
  873. <div style={{ textAlign: 'right', color: '#1677ff', fontWeight: 600 }}>step 3</div>
  874. </Card>
  875. </Col>
  876. </Row>
  877. )}
  878. </div>
  879. )}
  880. {
  881. listLoading ? (
  882. <div style={{
  883. display: 'flex',
  884. justifyContent: 'center',
  885. alignItems: 'center',
  886. minHeight: '400px',
  887. padding: '50px 0'
  888. }}>
  889. <Spin size="large" tip="加载中..." />
  890. </div>
  891. ) : list.length ? (
  892. <div className='questionAnswerList'>
  893. {/*<div style={ { overflow: 'auto' } }>*/}
  894. {/* <Flex gap="middle" wrap>*/}
  895. {/* <Layout style={ layoutStyle }>*/}
  896. {/* <Sider width="25%" style={ siderStyle }>*/}
  897. {/* <FileOutlined />*/}
  898. {/* </Sider>*/}
  899. {/* <Layout>*/}
  900. {/* <Header style={ headerStyle }>问答应用总数</Header>*/}
  901. {/* <Content style={ contentStyle }>{ appCount }个</Content>*/}
  902. {/* </Layout>*/}
  903. {/* </Layout>*/}
  904. {/* <Layout style={ layoutStyle }>*/}
  905. {/* <Sider width="25%" style={ siderStyle }>*/}
  906. {/* <FileOutlined />*/}
  907. {/* </Sider>*/}
  908. {/* <Layout>*/}
  909. {/* <Header style={ headerStyle }>知识库总数</Header>*/}
  910. {/* <Content style={ contentStyle }>{ knowCount } 个</Content>*/}
  911. {/* </Layout>*/}
  912. {/* </Layout>*/}
  913. {/* </Flex>*/}
  914. {/*</div>*/}
  915. {/*<div style={ { display: 'flex', justifyContent: 'space-between', padding: '16px 20px' } }>*/}
  916. {/* <div>所有问答应用</div>*/}
  917. {/* /!*{*!/*/}
  918. {/* /!* createFlag &&*!/*/}
  919. {/* /!* <Button type='primary'*!/*/}
  920. {/* /!* icon={ <PlusOutlined /> }*!/*/}
  921. {/* /!* onClick={ () => {*!/*/}
  922. {/* /!* router.navigate( { pathname: '/deepseek/questionAnswer/create' } );*!/*/}
  923. {/* /!* } }>创建问答应用</Button>*!/*/}
  924. {/* /!*}*!/*/}
  925. {/*</div>*/}
  926. <div className='applicationList'>
  927. <List grid={{
  928. gutter: 16,
  929. xs: 1,
  930. sm: 1,
  931. md: 2,
  932. lg: 2,
  933. xl: 3,
  934. xxl: 4, // 展示的列数
  935. }}
  936. dataSource={list}
  937. renderItem={(item) => (
  938. <List.Item>
  939. <div className='card'>
  940. <div style={{
  941. display: 'flex',
  942. justifyContent: 'space-between',
  943. alignItems: 'center',
  944. overflow: 'hidden',
  945. }}>
  946. <div style={{ display: 'flex', alignItems: 'center', overflow: 'auto' }}>
  947. <div style={{ marginRight: 10, overflow: 'auto' }}>
  948. <Image
  949. width={32}
  950. height={32}
  951. src={IconSvg}
  952. preview={false}
  953. />
  954. </div>
  955. {/*<div style={ { overflow: 'auto' } }>*/}
  956. {/* { item.name }*/}
  957. {/*</div>*/}
  958. <div style={{
  959. display: 'flex',
  960. flexDirection: 'column',
  961. justifyContent: 'center',
  962. overflow: 'hidden',
  963. maxWidth: '85%',
  964. height: '100%'
  965. }}>
  966. <Tooltip title={item.name} placement="top">
  967. <div style={{
  968. lineHeight: '18px',
  969. fontSize: 14,
  970. fontWeight: 500,
  971. overflow: 'hidden',
  972. textOverflow: 'ellipsis',
  973. whiteSpace: 'nowrap',
  974. // maxWidth: '200px', // 可以根据需要调整宽度
  975. cursor: 'pointer'
  976. }}>
  977. {item.name.length > 20 ? `${item.name.substring(0, 30)}...` : item.name}
  978. </div>
  979. </Tooltip>
  980. <Space size={4} style={{ lineHeight: '18px' }}>
  981. <span style={{
  982. color: '#999',
  983. fontSize: 12,
  984. margin: 0
  985. }}>ID:{item.appId}</span>
  986. <Divider type="vertical" style={{ color: '999', margin: 0, height: 12 }} />
  987. <span
  988. style={ {
  989. color: '#999',
  990. fontSize: 12,
  991. display: 'inline-block',
  992. maxWidth: '100%', // 限制最大宽度
  993. flex: 1, // 使span占据剩余空间
  994. whiteSpace: 'nowrap', // 禁止换行
  995. overflow: 'hidden', // 隐藏溢出内容
  996. textOverflow: 'ellipsis', // 显示省略号
  997. verticalAlign: 'middle' // 垂直居中
  998. } }
  999. title={ item.projectName } // 鼠标悬停时显示完整名称
  1000. >
  1001. {item.projectName}
  1002. </span>
  1003. {/*<span style={{ color: '#999', fontSize: 12 }}>*/}
  1004. {/* {*/}
  1005. {/* item.projectName && item.projectName.length > 8 ?*/}
  1006. {/* `${item.projectName.substring(0, 8)}...` :*/}
  1007. {/* item.projectName*/}
  1008. {/* }*/}
  1009. {/*</span>*/}
  1010. </Space>
  1011. </div>
  1012. </div>
  1013. <div>
  1014. <>
  1015. {/*{ item.projectName } 移动599 line*/}
  1016. {
  1017. (item.status !== null && item.status !== '3') &&
  1018. < Tag style={{
  1019. // marginLeft: 16,
  1020. width: 65,
  1021. color: '#fff',
  1022. height: 24,
  1023. backgroundColor: item.status === '1' ? '#D26900' : item.status === '2' ? '#408080' : item.auditStatus === '4' ? '#CE0000' : item.status === '5' ? '#5151A2' : ''
  1024. }}>
  1025. {item.status === '1' ? '待审核' : item.status === '2' ? '审核中' : item.auditStatus === '4' ? '审核拒绝' : item.status === '5' ? '待提交' : '未知'}
  1026. </Tag>
  1027. }
  1028. {
  1029. (item.auditStatus === '4') &&
  1030. <Tooltip title={item.comment}>
  1031. {
  1032. item.comment !== '' && item.comment !== null && item.comment.length > 10 ?
  1033. item.comment.substring(0, 10) + '......' :
  1034. item.comment
  1035. }
  1036. </Tooltip>
  1037. }
  1038. </>
  1039. </div>
  1040. </div>
  1041. <Divider plain style={{ margin: '16px 0' }}></Divider>
  1042. <div className='desc'>
  1043. {
  1044. item.desc !== '' && item.desc !== null && item.desc.length > 40 ? item.desc.substring(0, 40) + '......' : item.desc
  1045. }
  1046. </div>
  1047. <div style={{
  1048. display: 'flex',
  1049. justifyContent: 'space-between',
  1050. alignItems: 'center'
  1051. }}>
  1052. <span style={{
  1053. color: '#999',
  1054. fontSize: 12,
  1055. margin: 0
  1056. }}>更新时间: {dayjs(item.updateTime).format("YYYY-MM-DD HH:mm:ss")}</span>
  1057. </div>
  1058. <div style={{
  1059. display: 'flex',
  1060. justifyContent: 'space-between',
  1061. alignItems: 'flex-end',
  1062. paddingTop: 3,
  1063. // overflow: 'auto',
  1064. // paddingTop: 16,
  1065. // height: '100%'
  1066. }}>
  1067. <div style={{
  1068. overflow: 'auto'
  1069. }}>
  1070. {
  1071. (item.status === '5' || item.status === '4' || item.status === '3' || item.status === '' || item.status === null) &&
  1072. <>
  1073. {
  1074. (updateFlag || userInfoAll.id === item.createBy) &&
  1075. <a
  1076. className="action-button"
  1077. style={{ marginRight: 16 }}
  1078. onClick={() => {
  1079. router.navigate({ pathname: '/deepseek/questionAnswer/modify' }, { state: { id: item.appId } });
  1080. }}>
  1081. <EditOutlined /> 编辑
  1082. </a>
  1083. }
  1084. {
  1085. (deleteFlag || userInfoAll.id === item.createBy) &&
  1086. <a className='delete-button' onClick={() => {
  1087. Modal.confirm({
  1088. title: '删除',
  1089. content: `确定删除应用: ` + item.name + ` 吗?`,
  1090. okType: 'danger',
  1091. onOk: async () => {
  1092. await delApplication(item.appId.toString());
  1093. }
  1094. });
  1095. }}>
  1096. <DeleteOutlined /> 删除
  1097. </a>
  1098. }
  1099. </>
  1100. }
  1101. {
  1102. createFlag && item.status === '5' &&
  1103. <a className="action-button" style={{ marginLeft: 16 }} onClick={() => {
  1104. Modal.confirm({
  1105. title: '提交审核',
  1106. content: `确认提交审核应用: ` + item.name + `吗?`,
  1107. okType: 'danger',
  1108. onOk: async () => {
  1109. const userInfo = LocalStorage.getUserInfo();
  1110. const userId = (userInfo?.id ?? '').toString();
  1111. appApi.auditApplication(item.appId.toString(), userId);
  1112. }
  1113. });
  1114. }}>
  1115. <StepForwardOutlined /> 提交审核
  1116. </a>
  1117. }
  1118. <Button size='small' style={{
  1119. background: 'transparent',
  1120. border: '1px solid #1677ff',
  1121. color: '#1677ff',
  1122. marginTop: '2px',
  1123. marginLeft: createFlag && item.status === '5' || (item.status === '5' || item.status === '4' || item.status === '3' || item.status === '' || item.status === null) && updateFlag||userInfoAll.id === item.createBy ? '10px' : 0,
  1124. fontSize: 12,
  1125. }} type="primary" variant="outlined" onClick={() => { useNowAppLication(item.appId) }}>立即使用</Button>
  1126. </div>
  1127. <div>
  1128. <Tag
  1129. style={{
  1130. // padding: '4px 8px',
  1131. marginRight: 0,
  1132. fontSize: 12,
  1133. fontWeight: 600,
  1134. background: '#f8f8f8',
  1135. // border: '1px solid #d9d9d9'
  1136. }}
  1137. >
  1138. {
  1139. appTypeList
  1140. .find(item1 => item1.value.toString() === item.typeId)?.label || levelTypeList.find(item2 => item2.value.toString() === item.typeId)?.label || '未分类'
  1141. }
  1142. </Tag>
  1143. </div>
  1144. </div>
  1145. </div>
  1146. </List.Item>
  1147. )}
  1148. pagination={paginationConfig} // 分页
  1149. />
  1150. </div>
  1151. </div>
  1152. ) : (
  1153. <div>
  1154. {/* {
  1155. createFlag &&
  1156. <Button type='primary'
  1157. icon={ <PlusOutlined /> }
  1158. onClick={ () => {
  1159. router.navigate( { pathname: '/deepseek/questionAnswer/create' } );
  1160. } }>创建问答应用</Button>
  1161. } */}
  1162. <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
  1163. </div>
  1164. )
  1165. }
  1166. </div>
  1167. )
  1168. };
  1169. export default observer(QuestionAnswerList);