App.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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 [documentInfo, setDocumentInfo] = useState<{ uuid: string; currentSheet: string } | null>(null);
  47. const [customTableId, setCustomTableId] = useState<string>('');
  48. const [customTableName, setCustomTableName] = useState<string>('');
  49. const loadApiList = useCallback(async () => {
  50. try {
  51. const list = await Dingdocs.script.run('getApiList');
  52. setApiList(list);
  53. } catch (error: any) {
  54. console.error('加载 API 列表失败:', error);
  55. setError(error.message || '加载 API 列表失败');
  56. }
  57. }, []);
  58. const handleAutoLogin = useCallback(async () => {
  59. try {
  60. const urlParams = new URLSearchParams(window.location.search);
  61. const corpId = urlParams.get('corpId') || '';
  62. const authResult = await Dingdocs.base.host.getAuthCode(corpId);
  63. const authCode = authResult.code;
  64. const user = await Dingdocs.script.run('login', authCode);
  65. setUserInfo(user);
  66. console.log('自动登录成功:', user);
  67. } catch (error: any) {
  68. console.error('自动登录失败:', error);
  69. setError(error.message || '登录失败');
  70. throw error;
  71. }
  72. }, []);
  73. const loadDocumentInfo = useCallback(async () => {
  74. try {
  75. const info = await Dingdocs.script.run('getDocumentInfo');
  76. setDocumentInfo({
  77. uuid: info?.uuid || '',
  78. currentSheet: info?.currentSheet || ''
  79. });
  80. setCustomTableId(info?.uuid || '');
  81. setCustomTableName(info?.currentSheet || '');
  82. } catch (error: any) {
  83. console.error('获取文档信息失败:', error);
  84. }
  85. }, []);
  86. const handleApiChange = (apiId: string) => {
  87. const api = apiList.find(item => item.id === apiId);
  88. if (api) {
  89. setCurrentApi(api);
  90. const params: Record<string, string> = {};
  91. api.apiParamAuth.forEach(param => {
  92. params[param.paramName] = param.value || '';
  93. });
  94. setApiParams(params);
  95. setResult('');
  96. } else {
  97. setCurrentApi(null);
  98. setApiParams({});
  99. setResult('');
  100. }
  101. };
  102. const handleParamChange = (paramName: string, value: string) => {
  103. setApiParams(prev => ({
  104. ...prev,
  105. [paramName]: value
  106. }));
  107. };
  108. const handleCallApi = async () => {
  109. if (!currentApi || callLoading) return;
  110. setCallLoading(true);
  111. setResult('');
  112. const apiItem = {
  113. ...currentApi,
  114. aiTableId: customTableId || currentApi.aiTableId,
  115. aiTablename: customTableName || currentApi.aiTablename,
  116. apiParamAuth: currentApi.apiParamAuth.map(p => ({
  117. paramName: p.paramName,
  118. value: apiParams[p.paramName] || p.value || ''
  119. }))
  120. };
  121. try {
  122. const response: ApiResponse = await Dingdocs.script.run('callApi', apiItem);
  123. setResult(JSON.stringify(response, null, 2));
  124. } catch (error: any) {
  125. setResult(`调用失败: ${error.message}`);
  126. } finally {
  127. setCallLoading(false);
  128. }
  129. };
  130. useEffect(() => {
  131. initView({
  132. onReady: async () => {
  133. setLoading(true);
  134. setError('');
  135. try {
  136. await handleAutoLogin();
  137. await loadApiList();
  138. loadDocumentInfo();
  139. } catch (error: any) {
  140. console.error('初始化失败:', error);
  141. setError(error.message || '初始化失败');
  142. } finally {
  143. setLoading(false);
  144. }
  145. },
  146. });
  147. }, [handleAutoLogin, loadApiList, loadDocumentInfo]);
  148. if (loading) {
  149. return (
  150. <div className="page">
  151. <div className="loading">
  152. <Spin size="small" />
  153. <Typography.Text className="loading-text">加载中...</Typography.Text>
  154. </div>
  155. </div>
  156. );
  157. }
  158. if (error) {
  159. return (
  160. <div className="page">
  161. <div className="login-container">
  162. <Card size="small" className="login-card">
  163. <Typography.Text type="danger">{error}</Typography.Text>
  164. </Card>
  165. </div>
  166. </div>
  167. );
  168. }
  169. return (
  170. <div className="page">
  171. <div className="header">
  172. <div className="header-content">
  173. <Typography.Title level={4}>API 调用测试</Typography.Title>
  174. <div className="user-info">
  175. <Typography.Text className="user-name">{userInfo?.name || userInfo?.userId || '用户'}</Typography.Text>
  176. </div>
  177. </div>
  178. </div>
  179. <div className="content">
  180. <Card size="small" className="main-card">
  181. <div className="form-item">
  182. <Typography.Text className="label">选择 API</Typography.Text>
  183. <Select
  184. className="api-select"
  185. placeholder="请选择要调用的 API 接口"
  186. onChange={handleApiChange}
  187. value={currentApi?.id || ''}
  188. >
  189. {apiList.map(api => (
  190. <Select.Option key={api.id} value={api.id}>
  191. {api.apiName}
  192. </Select.Option>
  193. ))}
  194. </Select>
  195. </div>
  196. {currentApi && (
  197. <div className="api-detail">
  198. <Card size="small" className="info-card">
  199. <div className="info-row">
  200. <Typography.Text className="info-label">API 名称:</Typography.Text>
  201. <Typography.Text strong>{currentApi.apiName}</Typography.Text>
  202. </div>
  203. <div className="info-row">
  204. <Typography.Text className="info-label">功能描述:</Typography.Text>
  205. <Typography.Text>{currentApi.description}</Typography.Text>
  206. </div>
  207. </Card>
  208. <Typography.Title level={5} className="params-title">文档信息</Typography.Title>
  209. <div className="params-container">
  210. <div className="param-item">
  211. <Typography.Text className="param-label">
  212. 文档 ID
  213. <span className="param-name">(documentId)</span>
  214. </Typography.Text>
  215. <Input
  216. className="param-input"
  217. placeholder="请输入文档 ID"
  218. value={customTableId || ''}
  219. onChange={(e) => setCustomTableId(e.target.value)}
  220. />
  221. </div>
  222. <div className="param-item">
  223. <Typography.Text className="param-label">
  224. 表格名称
  225. <span className="param-name">(tableName)</span>
  226. </Typography.Text>
  227. <Input
  228. className="param-input"
  229. placeholder="请输入表格名称"
  230. value={customTableName || ''}
  231. onChange={(e) => setCustomTableName(e.target.value)}
  232. />
  233. </div>
  234. </div>
  235. <Typography.Title level={5} className="params-title">请求参数</Typography.Title>
  236. <div className="params-container">
  237. {currentApi.apiParamAuth.map(param => (
  238. <div key={param.paramName} className="param-item">
  239. <Typography.Text className="param-label">
  240. {param.paramDesc}
  241. <span className="param-name">({param.paramName})</span>
  242. </Typography.Text>
  243. {param.valueList && param.valueList.length > 0 && param.valueList.some(v => v) ? (
  244. <Select
  245. className="param-select"
  246. placeholder="请选择"
  247. value={apiParams[param.paramName] || ''}
  248. onChange={(value) => handleParamChange(param.paramName, value)}
  249. >
  250. {param.valueList.map(val => (
  251. <Select.Option key={val} value={val}>
  252. {val}
  253. </Select.Option>
  254. ))}
  255. </Select>
  256. ) : (
  257. <Input
  258. className="param-input"
  259. placeholder={param.exampleValue || '请输入'}
  260. value={apiParams[param.paramName] || ''}
  261. onChange={(e) => handleParamChange(param.paramName, e.target.value)}
  262. />
  263. )}
  264. </div>
  265. ))}
  266. </div>
  267. <div className="action-bar">
  268. <Button
  269. type="primary"
  270. loading={callLoading}
  271. onClick={handleCallApi}
  272. className="call-btn"
  273. >
  274. 执行调用
  275. </Button>
  276. </div>
  277. {result && (
  278. <Card size="small" className="result-card">
  279. <Typography.Title level={5} className="result-title">返回结果</Typography.Title>
  280. <pre className="result-content">{result}</pre>
  281. </Card>
  282. )}
  283. </div>
  284. )}
  285. </Card>
  286. </div>
  287. </div>
  288. );
  289. }
  290. export default App;