index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. import { ChatContext } from '@/app/chat-context';
  2. import {
  3. apiInterceptors,
  4. delApp,
  5. getAppAdmins,
  6. getAppList,
  7. newDialogue,
  8. publishApp,
  9. unPublishApp,
  10. updateAppAdmins,
  11. } from '@/client/api';
  12. import BlurredCard, { ChatButton, InnerDropdown } from '@/new-components/common/blurredCard';
  13. import ConstructLayout from '@/new-components/layout/Construct';
  14. import { IApp } from '@/types/app';
  15. import { BulbOutlined, DingdingOutlined, PlusOutlined, SearchOutlined, WarningOutlined } from '@ant-design/icons';
  16. import { useDebounceFn, useRequest } from 'ahooks';
  17. import { App, Avatar, Button, Input, Modal, Pagination, Popover, Segmented, SegmentedProps, Select, Spin, Tag } from 'antd';
  18. import copy from 'copy-to-clipboard';
  19. import moment from 'moment';
  20. import { useRouter } from 'next/router';
  21. import { useCallback, useContext, useEffect, useRef, useState } from 'react';
  22. import { useTranslation } from 'react-i18next';
  23. import CreateAppModal from './components/create-app-modal';
  24. type TabKey = 'all' | 'published' | 'unpublished';
  25. type ModalType = 'edit' | 'add';
  26. export default function AppContent() {
  27. const { t } = useTranslation();
  28. const [open, setOpen] = useState<boolean>(false);
  29. const [spinning, setSpinning] = useState<boolean>(false);
  30. const [activeKey, setActiveKey] = useState<TabKey>('all');
  31. const [apps, setApps] = useState<IApp[]>([]);
  32. const [modalType, setModalType] = useState<ModalType>('add');
  33. const { model, setAgent: setAgentToChat, setCurrentDialogInfo } = useContext(ChatContext);
  34. const router = useRouter();
  35. const { openModal = '' } = router.query;
  36. const [filterValue, setFilterValue] = useState('');
  37. const [curApp] = useState<IApp>();
  38. const [adminOpen, setAdminOpen] = useState<boolean>(false);
  39. const [admins, setAdmins] = useState<string[]>([]);
  40. // 分页信息
  41. const totalRef = useRef<{
  42. current_page: number;
  43. total_count: number;
  44. total_page: number;
  45. }>();
  46. // 区分是单击还是双击
  47. const [clickTimeout, setClickTimeout] = useState(null);
  48. const { message } = App.useApp();
  49. const handleCreate = () => {
  50. setModalType('add');
  51. setOpen(true);
  52. localStorage.removeItem('new_app_info');
  53. };
  54. const handleEdit = (app: any) => {
  55. localStorage.setItem('new_app_info', JSON.stringify({ ...app, isEdit: true }));
  56. router.push(`/construct/app/extra`);
  57. };
  58. const getListFiltered = useCallback(() => {
  59. let published = undefined;
  60. if (activeKey === 'published') {
  61. published = 'true';
  62. }
  63. if (activeKey === 'unpublished') {
  64. published = 'false';
  65. }
  66. initData({ app_name: filterValue, published });
  67. // eslint-disable-next-line react-hooks/exhaustive-deps
  68. }, [activeKey, filterValue]);
  69. const handleTabChange = (activeKey: string) => {
  70. setActiveKey(activeKey as TabKey);
  71. };
  72. // 发布或取消发布应用
  73. const { run: operate } = useRequest(
  74. async (app: IApp) => {
  75. if (app.published === 'true') {
  76. return await apiInterceptors(unPublishApp(app.app_code));
  77. } else {
  78. return await apiInterceptors(publishApp(app.app_code));
  79. }
  80. },
  81. {
  82. manual: true,
  83. onSuccess: data => {
  84. if (data[2]?.success) {
  85. message.success('操作成功');
  86. }
  87. getListFiltered();
  88. },
  89. },
  90. );
  91. const initData = useDebounceFn(
  92. async params => {
  93. setSpinning(true);
  94. const obj: any = {
  95. page: 1,
  96. page_size: 12,
  97. ...params,
  98. };
  99. const [error, data] = await apiInterceptors(getAppList(obj));
  100. if (error) {
  101. setSpinning(false);
  102. return;
  103. }
  104. if (!data) return;
  105. console.log(data?.app_list);
  106. setApps(data?.app_list || []);
  107. totalRef.current = {
  108. current_page: data?.current_page || 1,
  109. total_count: data?.total_count || 0,
  110. total_page: data?.total_page || 0,
  111. };
  112. setSpinning(false);
  113. },
  114. {
  115. wait: 500,
  116. },
  117. ).run;
  118. const showDeleteConfirm = (app: IApp) => {
  119. Modal.confirm({
  120. title: t('Tips'),
  121. icon: <WarningOutlined />,
  122. content: `do you want delete the application?`,
  123. okText: 'Yes',
  124. okType: 'danger',
  125. cancelText: 'No',
  126. async onOk() {
  127. await apiInterceptors(delApp({ app_code: app.app_code }));
  128. getListFiltered();
  129. },
  130. });
  131. };
  132. useEffect(() => {
  133. if (openModal) {
  134. setModalType('add');
  135. setOpen(true);
  136. }
  137. }, [openModal]);
  138. const languageMap = {
  139. en: t('English'),
  140. zh: t('Chinese'),
  141. };
  142. const handleChat = async (app: IApp) => {
  143. // 原生应用跳转
  144. if (app.team_mode === 'native_app') {
  145. const { chat_scene = '' } = app.team_context;
  146. const [, res] = await apiInterceptors(newDialogue({ chat_mode: chat_scene }));
  147. if (res) {
  148. setCurrentDialogInfo?.({
  149. chat_scene: res.chat_mode,
  150. app_code: app.app_code,
  151. });
  152. localStorage.setItem(
  153. 'cur_dialog_info',
  154. JSON.stringify({
  155. chat_scene: res.chat_mode,
  156. app_code: app.app_code,
  157. }),
  158. );
  159. router.push(`/chat?scene=${chat_scene}&id=${res.conv_uid}${model ? `&model=${model}` : ''}`);
  160. }
  161. } else {
  162. // 自定义应用
  163. const [, res] = await apiInterceptors(newDialogue({ chat_mode: 'chat_agent' }));
  164. if (res) {
  165. setCurrentDialogInfo?.({
  166. chat_scene: res.chat_mode,
  167. app_code: app.app_code,
  168. });
  169. localStorage.setItem(
  170. 'cur_dialog_info',
  171. JSON.stringify({
  172. chat_scene: res.chat_mode,
  173. app_code: app.app_code,
  174. }),
  175. );
  176. setAgentToChat?.(app.app_code);
  177. router.push(`/chat/?scene=chat_agent&id=${res.conv_uid}${model ? `&model=${model}` : ''}`);
  178. }
  179. }
  180. };
  181. const items: SegmentedProps['options'] = [
  182. {
  183. value: 'all',
  184. label: t('apps'),
  185. },
  186. {
  187. value: 'published',
  188. label: t('published'),
  189. },
  190. {
  191. value: 'unpublished',
  192. label: t('unpublished'),
  193. },
  194. ];
  195. const onSearch = async (e: any) => {
  196. const v = e.target.value;
  197. setFilterValue(v);
  198. };
  199. // 获取应用权限列表
  200. const { run: getAdmins, loading } = useRequest(
  201. async (appCode: string) => {
  202. const [, res] = await apiInterceptors(getAppAdmins(appCode));
  203. return res ?? [];
  204. },
  205. {
  206. manual: true,
  207. onSuccess: data => {
  208. setAdmins(data);
  209. },
  210. },
  211. );
  212. // 更新应用权限
  213. const { run: updateAdmins, loading: adminLoading } = useRequest(
  214. async (params: { app_code: string; admins: string[] }) => await apiInterceptors(updateAppAdmins(params)),
  215. {
  216. manual: true,
  217. onSuccess: () => {
  218. message.success('更新成功');
  219. },
  220. },
  221. );
  222. const handleChange = async (value: string[]) => {
  223. setAdmins(value);
  224. await updateAdmins({
  225. app_code: curApp?.app_code || '',
  226. admins: value,
  227. });
  228. await initData();
  229. };
  230. useEffect(() => {
  231. if (curApp) {
  232. getAdmins(curApp.app_code);
  233. }
  234. }, [curApp, getAdmins]);
  235. useEffect(() => {
  236. getListFiltered();
  237. }, [getListFiltered]);
  238. // 单击复制分享钉钉链接
  239. const shareDingding = (item: IApp) => {
  240. if (clickTimeout) {
  241. clearTimeout(clickTimeout);
  242. setClickTimeout(null);
  243. }
  244. const timeoutId = setTimeout(() => {
  245. const mobileUrl = `${location.origin}/mobile/chat/?chat_scene=${item?.team_context?.chat_scene || 'chat_agent'}&app_code=${item.app_code}`;
  246. const dingDingUrl = `dingtalk://dingtalkclient/page/link?url=${encodeURIComponent(mobileUrl)}&pc_slide=true`;
  247. const result = copy(dingDingUrl);
  248. if (result) {
  249. message.success('复制成功');
  250. } else {
  251. message.error('复制失败');
  252. }
  253. setClickTimeout(null);
  254. }, 300); // 双击时间间隔
  255. setClickTimeout(timeoutId as any);
  256. };
  257. // 双击直接打开钉钉
  258. const openDingding = (item: IApp) => {
  259. if (clickTimeout) {
  260. clearTimeout(clickTimeout);
  261. setClickTimeout(null);
  262. }
  263. const mobileUrl = `${location.origin}/mobile/chat/?chat_scene=${item?.team_context?.chat_scene || 'chat_agent'}&app_code=${item.app_code}`;
  264. const dingDingUrl = `dingtalk://dingtalkclient/page/link?url=${encodeURIComponent(mobileUrl)}&pc_slide=true`;
  265. window.open(dingDingUrl);
  266. };
  267. return (
  268. <ConstructLayout>
  269. <Spin spinning={spinning}>
  270. <div className='h-screen w-full px-4 bg-[#fff] overflow-y-auto mt-0'>
  271. <div className='mt-2 rounded-[10px] flex h-16 justify-between items-center px-2'>
  272. <Input
  273. variant='filled'
  274. prefix={<SearchOutlined />}
  275. placeholder={t('please_enter_the_keywords')}
  276. onChange={onSearch}
  277. onPressEnter={onSearch}
  278. allowClear
  279. className='w-[400px] h-[40px]
  280. border-1 border-[#d1d1d1]
  281. backdrop-filter
  282. backdrop-blur-lg
  283. dark:border-[#6f7f95]
  284. dark:bg-[#6f7f95]
  285. dark:bg-opacity-60'
  286. />
  287. <span className='flex gap-2 items-center'>
  288. <Avatar className='bg-gradient-to-tr from-[#31afff] to-[#1677ff] cursor-pointer'>
  289. </Avatar>
  290. <span
  291. >
  292. admin
  293. </span>
  294. </span>
  295. </div>
  296. <div className='rounded-[10px] h-full mt-4 relative bg-slate-200 p-4 border-1 mb-2'>
  297. <div className='flex justify-between items-center'>
  298. <Segmented
  299. className='backdrop-filter h-10 backdrop-blur-lg bg-white bg-opacity-30 border border-white rounded-lg shadow p-1 dark:border-[#6f7f95] dark:bg-[#6f7f95] dark:bg-opacity-60'
  300. options={items as any}
  301. onChange={handleTabChange}
  302. value={activeKey}
  303. />
  304. <Button
  305. className='border-none text-white bg-button-gradient flex items-center'
  306. onClick={handleCreate}
  307. >
  308. {'创建应用'}
  309. </Button>
  310. </div>
  311. <div className='w-full flex flex-wrap pb-12 mx-[-8px]'>
  312. {apps.map(item => {
  313. return (
  314. <BlurredCard
  315. key={item.app_code}
  316. code={item.app_code}
  317. name={item.app_name}
  318. description={item.app_describe}
  319. RightTop={
  320. <div className='flex items-center gap-2'>
  321. <Popover
  322. content={
  323. <div className='flex flex-col gap-2'>
  324. <div className='flex items-center gap-2'>
  325. <BulbOutlined
  326. style={{
  327. color: 'rgb(252,204,96)',
  328. fontSize: 12,
  329. }}
  330. />
  331. <span className='text-sm text-gray-500'>{t('copy_url')}</span>
  332. </div>
  333. <div className='flex items-center gap-2'>
  334. <BulbOutlined
  335. style={{
  336. color: 'rgb(252,204,96)',
  337. fontSize: 12,
  338. }}
  339. />
  340. <span className='text-sm text-gray-500'>{t('double_click_open')}</span>
  341. </div>
  342. </div>
  343. }
  344. >
  345. <DingdingOutlined
  346. className='cursor-pointer text-[#0069fe] hover:bg-white hover:dark:bg-black p-2 rounded-md'
  347. onClick={() => shareDingding(item)}
  348. onDoubleClick={() => openDingding(item)}
  349. />
  350. </Popover>
  351. <InnerDropdown
  352. menu={{
  353. items: [
  354. {
  355. key: 'publish',
  356. label: (
  357. <span
  358. onClick={e => {
  359. e.stopPropagation();
  360. operate(item);
  361. }}
  362. >
  363. {item.published === 'true' ? t('unpublish') : t('publish')}
  364. </span>
  365. ),
  366. },
  367. {
  368. key: 'del',
  369. label: (
  370. <span
  371. className='text-red-400'
  372. onClick={e => {
  373. e.stopPropagation();
  374. showDeleteConfirm(item);
  375. }}
  376. >
  377. {t('Delete')}
  378. </span>
  379. ),
  380. },
  381. ],
  382. }}
  383. />
  384. </div>
  385. }
  386. Tags={
  387. <div>
  388. <Tag>{languageMap[item.language]}</Tag>
  389. <Tag>{item.team_mode}</Tag>
  390. <Tag>{item.published === 'true' ? t('published') : t('unpublished')}</Tag>
  391. </div>
  392. }
  393. rightTopHover={false}
  394. LeftBottom={
  395. <div className='flex gap-2'>
  396. <span>{item.owner_name}</span>
  397. <span>•</span>
  398. {item?.updated_at && <span>{moment(item?.updated_at).fromNow() + ' ' + t('update')}</span>}
  399. </div>
  400. }
  401. RightBottom={
  402. <ChatButton
  403. onClick={() => {
  404. handleChat(item);
  405. }}
  406. />
  407. }
  408. onClick={() => {
  409. handleEdit(item);
  410. }}
  411. scene={item?.team_context?.chat_scene || 'chat_agent'}
  412. />
  413. );
  414. })}
  415. <div className='w-full flex justify-end shrink-0 pb-12'>
  416. <Pagination
  417. total={totalRef.current?.total_count || 0}
  418. pageSize={12}
  419. current={totalRef.current?.current_page}
  420. onChange={async (page, _page_size) => {
  421. await initData({ page });
  422. }}
  423. />
  424. </div>
  425. </div>
  426. </div>
  427. {open && (
  428. <CreateAppModal
  429. open={open}
  430. onCancel={() => {
  431. setOpen(false);
  432. }}
  433. refresh={initData}
  434. type={modalType}
  435. />
  436. )}
  437. </div>
  438. </Spin>
  439. <Modal title='权限管理' open={adminOpen} onCancel={() => setAdminOpen(false)} footer={null}>
  440. <Spin spinning={loading}>
  441. <div className='py-4'>
  442. <div className='mb-1'>管理员(工号,去前缀0):</div>
  443. <Select
  444. mode='tags'
  445. value={admins}
  446. style={{ width: '100%' }}
  447. onChange={handleChange}
  448. tokenSeparators={[',']}
  449. options={admins?.map((item: string) => ({
  450. label: item,
  451. value: item,
  452. }))}
  453. loading={adminLoading}
  454. />
  455. </div>
  456. </Spin>
  457. </Modal>
  458. </ConstructLayout>
  459. );
  460. }