App.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. /*global Dingdocs*/
  2. import { useEffect, useState, useCallback } from 'react';
  3. import { initView } from 'dingtalk-docs-cool-app';
  4. import { Typography, Button, Card, Select, Input, Spin } from 'dingtalk-design-desktop';
  5. import './style.css';
  6. interface ApiParam {
  7. paramName: string;
  8. paramDesc: string;
  9. exampleValue?: string;
  10. valueList?: string[];
  11. value?: string;
  12. }
  13. interface ApiItem {
  14. id: string;
  15. apiName: string;
  16. url: string;
  17. description: string;
  18. aiTableId: string;
  19. aiTablename: string;
  20. apiParamAuth: ApiParam[];
  21. }
  22. interface ApiResponse {
  23. code: number;
  24. msg: string;
  25. data?: any;
  26. }
  27. interface UserInfo {
  28. userId: string;
  29. name?: string;
  30. avatar?: string;
  31. mobile?: string;
  32. jobnumber?: string;
  33. department?: number[];
  34. permissions?: string[];
  35. [key: string]: any;
  36. }
  37. function App() {
  38. const [loading, setLoading] = useState<boolean>(true);
  39. const [apiList, setApiList] = useState<ApiItem[]>([]);
  40. const [currentApi, setCurrentApi] = useState<ApiItem | null>(null);
  41. const [apiParams, setApiParams] = useState<Record<string, string>>({});
  42. const [result, setResult] = useState<string>('');
  43. const [callLoading, setCallLoading] = useState<boolean>(false);
  44. const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
  45. const [error, setError] = useState<string>('');
  46. const [customTableId, setCustomTableId] = useState<string>('');
  47. const [customTableName, setCustomTableName] = useState<string>('');
  48. const loadApiList = useCallback(async () => {
  49. try {
  50. const list = await Dingdocs.script.run('getApiList');
  51. setApiList(list);
  52. } catch (error: any) {
  53. console.error('加载 API 列表失败:', error);
  54. setError(error.message || '加载 API 列表失败');
  55. }
  56. }, []);
  57. const handleConfigPermission = useCallback(async (token: string) => {
  58. try {
  59. const currentUrl = window.location.href.split('#')[0];
  60. const response = await fetch(`https://openapi.julefood.cn:8082/api/sys_user/jsapi/config?url=${encodeURIComponent(currentUrl)}`, {
  61. headers: {
  62. 'Authorization': `Bearer ${token}`,
  63. 'token': token
  64. }
  65. });
  66. const config = await response.json();
  67. if (config.code !== 200) {
  68. return;
  69. }
  70. const result = config.result || {};
  71. if (!result.agentId || !result.corpId || !result.signature) {
  72. return;
  73. }
  74. await Dingdocs.base.host.configPermission(
  75. result.agentId,
  76. result.corpId,
  77. result.timeStamp,
  78. result.nonceStr,
  79. result.signature,
  80. ['DingdocsScript.base.readWriteAll']
  81. );
  82. } catch (error: any) {
  83. console.error('权限配置失败:', error);
  84. }
  85. }, []);
  86. const handleAutoLogin = useCallback(async () => {
  87. try {
  88. const urlParams = new URLSearchParams(window.location.search);
  89. const corpId = urlParams.get('corpId') || '';
  90. const authResult = await Dingdocs.base.host.getAuthCode(corpId);
  91. const authCode = authResult.code;
  92. const user = await Dingdocs.script.run('login', authCode);
  93. setUserInfo(user);
  94. const token = await Dingdocs.script.run('getToken');
  95. await handleConfigPermission(token);
  96. } catch (error: any) {
  97. console.error('自动登录失败:', error);
  98. setError(error.message || '登录失败');
  99. throw error;
  100. }
  101. }, [handleConfigPermission]);
  102. const loadDocumentInfo = useCallback(async () => {
  103. try {
  104. const info = await Dingdocs.script.run('getDocumentInfo');
  105. setCustomTableId(info?.uuid || '');
  106. setCustomTableName(info?.currentSheet || '');
  107. } catch (error: any) {
  108. console.error('获取文档信息失败:', error);
  109. }
  110. }, []);
  111. const handleApiChange = (apiId: string) => {
  112. const api = apiList.find(item => item.id === apiId);
  113. if (api) {
  114. setCurrentApi(api);
  115. const params: Record<string, string> = {};
  116. api.apiParamAuth.forEach(param => {
  117. params[param.paramName] = param.value || '';
  118. });
  119. setApiParams(params);
  120. setResult('');
  121. } else {
  122. setCurrentApi(null);
  123. setApiParams({});
  124. setResult('');
  125. }
  126. };
  127. const handleParamChange = (paramName: string, value: string) => {
  128. setApiParams(prev => ({
  129. ...prev,
  130. [paramName]: value
  131. }));
  132. };
  133. const handleCallApi = async () => {
  134. if (!currentApi || callLoading) return;
  135. setCallLoading(true);
  136. setResult('');
  137. const apiItem = {
  138. ...currentApi,
  139. aiTableId: customTableId || currentApi.aiTableId,
  140. aiTablename: customTableName || currentApi.aiTablename,
  141. apiParamAuth: currentApi.apiParamAuth.map(p => ({
  142. paramName: p.paramName,
  143. value: apiParams[p.paramName] || p.value || ''
  144. }))
  145. };
  146. try {
  147. const response: ApiResponse = await Dingdocs.script.run('callApi', apiItem);
  148. setResult(JSON.stringify(response, null, 2));
  149. } catch (error: any) {
  150. setResult(`调用失败: ${error.message}`);
  151. } finally {
  152. setCallLoading(false);
  153. }
  154. };
  155. useEffect(() => {
  156. initView({
  157. onReady: async () => {
  158. setLoading(true);
  159. setError('');
  160. try {
  161. await handleAutoLogin();
  162. await loadApiList();
  163. loadDocumentInfo();
  164. } catch (error: any) {
  165. console.error('初始化失败:', error);
  166. setError(error.message || '初始化失败');
  167. } finally {
  168. setLoading(false);
  169. }
  170. },
  171. });
  172. if (typeof Dingdocs !== 'undefined' && Dingdocs?.base?.event?.onSelectionChanged) {
  173. let currentSheetId = null;
  174. Dingdocs.base.event.onSelectionChanged((event: any) => {
  175. if (event.sheetId !== currentSheetId) {
  176. currentSheetId = event.sheetId;
  177. console.log('Sheet 切换到:', currentSheetId);
  178. const sheet = (Dingdocs.base as any).getSheet?.(currentSheetId);
  179. if (sheet) {
  180. const sheetName = sheet.getName?.();
  181. if (sheetName) {
  182. setCustomTableName(sheetName);
  183. console.log('表格名称已更新:', sheetName);
  184. }
  185. }
  186. }
  187. });
  188. }
  189. }, [handleAutoLogin, loadApiList, loadDocumentInfo]);
  190. if (loading) {
  191. return (
  192. <div className="page">
  193. <div className="loading">
  194. <Spin size="small" />
  195. <Typography.Text className="loading-text">加载中...</Typography.Text>
  196. </div>
  197. </div>
  198. );
  199. }
  200. if (error) {
  201. return (
  202. <div className="page">
  203. <div className="login-container">
  204. <Card size="small" className="login-card">
  205. <Typography.Text type="danger">{error}</Typography.Text>
  206. </Card>
  207. </div>
  208. </div>
  209. );
  210. }
  211. return (
  212. <div className="page">
  213. <div className="header">
  214. <div className="header-content">
  215. <Typography.Title level={4}>API 调用测试</Typography.Title>
  216. <div className="user-info">
  217. <Typography.Text className="user-name">{userInfo?.name || userInfo?.userId || '用户'}</Typography.Text>
  218. </div>
  219. </div>
  220. </div>
  221. <div className="content">
  222. <Card size="small" className="main-card">
  223. <div className="form-item">
  224. <Typography.Text className="label">选择 API</Typography.Text>
  225. <Select
  226. className="api-select"
  227. placeholder="请选择要调用的 API 接口"
  228. onChange={handleApiChange}
  229. value={currentApi?.id || ''}
  230. >
  231. {apiList.map(api => (
  232. <Select.Option key={api.id} value={api.id}>
  233. {api.apiName}
  234. </Select.Option>
  235. ))}
  236. </Select>
  237. </div>
  238. {currentApi && (
  239. <div className="api-detail">
  240. <Card size="small" className="info-card">
  241. <div className="info-row">
  242. <Typography.Text className="info-label">API 名称:</Typography.Text>
  243. <Typography.Text strong>{currentApi.apiName}</Typography.Text>
  244. </div>
  245. <div className="info-row">
  246. <Typography.Text className="info-label">功能描述:</Typography.Text>
  247. <Typography.Text>{currentApi.description}</Typography.Text>
  248. </div>
  249. </Card>
  250. <Typography.Title level={5} className="params-title">文档信息</Typography.Title>
  251. <div className="params-container">
  252. <div className="param-item">
  253. <Typography.Text className="param-label">
  254. 文档 ID
  255. <span className="param-name">(documentId)</span>
  256. </Typography.Text>
  257. <Input
  258. className="param-input"
  259. placeholder="请输入文档 ID"
  260. value={customTableId || ''}
  261. onChange={(e) => setCustomTableId(e.target.value)}
  262. />
  263. </div>
  264. <div className="param-item">
  265. <Typography.Text className="param-label">
  266. 表格名称
  267. <span className="param-name">(tableName)</span>
  268. </Typography.Text>
  269. <Input
  270. className="param-input"
  271. placeholder="请输入表格名称"
  272. value={customTableName || ''}
  273. onChange={(e) => setCustomTableName(e.target.value)}
  274. />
  275. </div>
  276. </div>
  277. <Typography.Title level={5} className="params-title">请求参数</Typography.Title>
  278. <div className="params-container">
  279. {currentApi.apiParamAuth.map(param => (
  280. <div key={param.paramName} className="param-item">
  281. <Typography.Text className="param-label">
  282. {param.paramDesc}
  283. <span className="param-name">({param.paramName})</span>
  284. </Typography.Text>
  285. {param.valueList && param.valueList.length > 0 && param.valueList.some(v => v) ? (
  286. <Select
  287. className="param-select"
  288. placeholder="请选择"
  289. value={apiParams[param.paramName] || ''}
  290. onChange={(value) => handleParamChange(param.paramName, value)}
  291. >
  292. {param.valueList.map(val => (
  293. <Select.Option key={val} value={val}>
  294. {val}
  295. </Select.Option>
  296. ))}
  297. </Select>
  298. ) : (
  299. <Input
  300. className="param-input"
  301. placeholder={param.exampleValue || '请输入'}
  302. value={apiParams[param.paramName] || ''}
  303. onChange={(e) => handleParamChange(param.paramName, e.target.value)}
  304. />
  305. )}
  306. </div>
  307. ))}
  308. </div>
  309. <div className="action-bar">
  310. <Button
  311. type="primary"
  312. loading={callLoading}
  313. onClick={handleCallApi}
  314. className="call-btn"
  315. >
  316. 执行调用
  317. </Button>
  318. </div>
  319. {result && (
  320. <Card size="small" className="result-card">
  321. <Typography.Title level={5} className="result-title">返回结果</Typography.Title>
  322. <pre className="result-content">{result}</pre>
  323. </Card>
  324. )}
  325. </div>
  326. )}
  327. </Card>
  328. </div>
  329. </div>
  330. );
  331. }
  332. export default App;