drawer.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import React, { forwardRef, useImperativeHandle, useState, useEffect } from "react";
  2. import { Button, Drawer, Form, Input, Select, Radio, Upload, message, TreeSelect } from "antd";
  3. import { CloseOutlined, UploadOutlined, CheckCircleOutlined, LoadingOutlined, CloseCircleOutlined } from "@ant-design/icons";
  4. import { uploadTypeList, AccessType } from "./prop";
  5. import styles from "../../css/Drawer.module.css";
  6. import { getOrganizations, uploadVoiceFile, getSkillList, uploadSensors } from "../../api/index";
  7. import { AxiosResponse } from "axios";
  8. import { validateFn, validateFileLength, validateFileSize, validateFileType } from "../../util/index"
  9. const { TextArea } = Input;
  10. const { Option } = Select;
  11. // 定义 ref 的类型
  12. export interface DrawerExampleRef {
  13. showDrawer: (title: string, type?: string | undefined, item?: any) => void;
  14. }
  15. interface UploadResponse {
  16. fileUUID: string;
  17. code: number;
  18. msg: string;
  19. }
  20. interface TreeNode {
  21. deptName: React.ReactNode; // 节点的显示文本
  22. deptID: string | number; // 节点的值
  23. childNodes?: TreeNode[]; // 子节点
  24. [key: string]: any; // 其他自定义属性
  25. }
  26. const DrawerExample: React.FC = forwardRef<DrawerExampleRef, {}>((props, ref) => {
  27. const { refreshFn } = props as any;
  28. const [open, setOpen] = useState(false);
  29. const [title, setTitle] = useState("");
  30. const [departmentData, setDepartmentData] = useState<any>([]);
  31. const [skillData, setSkillData] = useState([]);
  32. const [uploadState, setUploadState] = useState(false);
  33. const [loading, setLoading] = useState(false);
  34. const [initialValues, setInitialValues] = useState({});
  35. const [disable, setDisable] = useState(false);
  36. const [onlyDisable, setOnlyDisable] = useState(false);
  37. const [form] = Form.useForm();
  38. const [pageStatus, setPageStatus] = useState("");
  39. useImperativeHandle(ref, () => ({
  40. showDrawer: (title: string, type: string | undefined, item: any) => {
  41. setTitle(title);
  42. if (type === 'edit') {
  43. setPageStatus(type);
  44. setInitialValues({
  45. streamUrl: item.streamUrl,
  46. soundSensorName: item.name,
  47. soundSensorType: item.videoType,
  48. soundSensorUrl: item.srcUrl,
  49. soundFileUUid: item.fileUuid,
  50. deptUuid: item.deptUuid,
  51. department: '',
  52. description: item.description,
  53. skillUuid: item.skillUuid,
  54. responsiblePerson: item.responsiblePerson,
  55. ip: item.ip,
  56. soundSensorUuid: item.uuid
  57. })
  58. setDisable(true)
  59. setOnlyDisable(false)
  60. } else if (type === 'show') {
  61. setPageStatus(type);
  62. setInitialValues({
  63. streamUrl: item.streamUrl,
  64. soundSensorName: item.name,
  65. soundSensorType: item.videoType,
  66. soundSensorUrl: item.srcUrl,
  67. soundFileUUid: item.fileUuid,
  68. deptUuid: item.deptUuid,
  69. department: '',
  70. description: item.description,
  71. skillUuid: item.skillUuid,
  72. responsiblePerson: item.responsiblePerson,
  73. ip: item.ip,
  74. soundSensorUuid: item.uuid
  75. })
  76. setDisable(true)
  77. setOnlyDisable(true)
  78. } else {
  79. setInitialValues({
  80. streamUrl: '',
  81. soundSensorName: '',
  82. soundSensorType: AccessType.FileUpload,
  83. soundSensorUrl: '',
  84. soundFileUUid: '',
  85. deptUuid: departmentData.length ? departmentData[0].deptID : '',
  86. department: departmentData.length ? departmentData[0].deptName : '',
  87. description: '',
  88. skillUuid: '',
  89. responsiblePerson: departmentData.length ? departmentData[0].deptName : '',
  90. ip: '',
  91. soundSensorUuid: '',
  92. })
  93. setPageStatus('');
  94. setDisable(false)
  95. setOnlyDisable(false)
  96. }
  97. // 确保 initialValues 更新完成后再打开 Drawer
  98. setTimeout(() => {
  99. setOpen(true);
  100. }, 0);
  101. }
  102. }));
  103. const customRequest = (options: any) => {
  104. const { file, onSuccess, onError, onProgress } = options;
  105. setUploadState(true);
  106. if (!validateFn(file)) {
  107. message.error({ content: '文件格式不正确' })
  108. onError({ message: "文件格式不正确" });
  109. return;
  110. }
  111. if (file.size > Number(import.meta.env.VITE_APP_FILE_SIZE) * 1024 * 1024) {
  112. message.error({ content: '文件大小超过50MB限制' })
  113. onError({ message: "文件大小超过50MB限制" });
  114. return;
  115. }
  116. const formData = new FormData();
  117. // 添加文件到 FormData(字段名需与后端约定,通常为 "file")
  118. formData.append('file', file);
  119. const fn = (percent: number) => onProgress({ percent: percent });
  120. uploadVoiceFile(formData, fn).then((res: AxiosResponse<UploadResponse>) => {
  121. const { code, msg, fileUUID } = res as any;
  122. if (code !== 200) {
  123. onError({ message: msg });
  124. return;
  125. }
  126. form.setFieldValue("soundFileUUid", fileUUID)
  127. onSuccess({ message: "文件上传成功" })
  128. message.success({
  129. content: '文件上传成功',
  130. duration: 2,
  131. })
  132. }).catch(error => {
  133. onError({ message: "文件上传失败" });
  134. });
  135. };
  136. const onClose = () => {
  137. form.resetFields();
  138. setUploadState(false);
  139. setOpen(false);
  140. };
  141. const Footer = () => {
  142. return (
  143. <div style={{ textAlign: "right" }}>
  144. <Button style={{ borderRadius: 4 }} onClick={onClose}>取消</Button>
  145. <Button style={{ marginLeft: 8, borderRadius: 4 }} type={"primary"} loading={loading} onClick={() => { form.submit() }}>确定</Button>
  146. </div>
  147. );
  148. };
  149. const onFinish = (values: any) => {
  150. if (!values) {
  151. return;
  152. }
  153. if (pageStatus === 'show') {
  154. onClose();
  155. return;
  156. }
  157. if (!values.soundFileUUid) {
  158. message.error({ content: '请确认文件是否上传完成' })
  159. return;
  160. }
  161. setLoading(true);
  162. uploadSensors(values).then((res: any) => {
  163. if (res.code === 200) {
  164. message.success("操作成功");
  165. refreshFn();
  166. }
  167. }).finally(() => {
  168. onClose();
  169. setLoading(false);
  170. });
  171. };
  172. const onSelect = (_: String, node: TreeNode) => {
  173. form.setFieldValue("responsiblePerson", node.deptName);
  174. form.setFieldValue("department", node.deptName);
  175. };
  176. const iconRender = (file: any) => {
  177. switch (file.status) {
  178. case 'uploading':
  179. return <LoadingOutlined />;
  180. case 'done':
  181. return <CheckCircleOutlined style={{ color: 'green' }} />;
  182. case 'error':
  183. return <CloseCircleOutlined style={{ color: 'red' }} />;
  184. default:
  185. return <UploadOutlined />;
  186. }
  187. };
  188. const onRemove = () => {
  189. form.setFieldValue("files", []);
  190. setUploadState(false);
  191. }
  192. useEffect(() => {
  193. // 模拟数据获取
  194. getOrganizations().then((res) => {
  195. setDepartmentData(res.data ?? []);
  196. if (res.data.length > 0) {
  197. setInitialValues({
  198. soundSensorName: '',
  199. soundSensorUuid: '',
  200. skillUuid: '',
  201. soundSensorType: AccessType.FileUpload,
  202. soundFileUUid: '',
  203. deptUuid: res.data[0].deptID,
  204. department: res.data[0].deptName,
  205. responsiblePerson: res.data[0].deptName
  206. })
  207. }
  208. });
  209. getSkillList().then((res) => {
  210. setSkillData(res.data ?? []);
  211. });
  212. }, []);
  213. useEffect(() => {
  214. if (open) {
  215. form.setFieldsValue(initialValues); // 动态更新表单值
  216. form.setFields([{ name: 'skillUuid', disabled: onlyDisable }] as any); // 动态更新字段禁用状态
  217. }
  218. }, [initialValues, open, form, onlyDisable]);
  219. return (
  220. <>
  221. <Drawer
  222. title={title}
  223. onClose={onClose}
  224. open={open}
  225. width={600}
  226. maskClosable={false}
  227. keyboard={false}
  228. loading={false}
  229. closeIcon={null}
  230. footer={Footer()}
  231. extra={<CloseOutlined onClick={onClose} />}
  232. >
  233. <Form
  234. disabled={disable}
  235. initialValues={initialValues}
  236. labelCol={{ span: 6 }}
  237. wrapperCol={{ span: 16 }}
  238. form={form}
  239. name="control-hooks"
  240. onFinish={onFinish}
  241. style={{ maxWidth: 600, fontSize: 12 }}
  242. labelAlign="left"
  243. >
  244. <Form.Item name="soundSensorUuid" hidden>
  245. <Input />
  246. </Form.Item>
  247. <Form.Item name="soundFileUUid" hidden>
  248. <Input />
  249. </Form.Item>
  250. <Form.Item name="department" hidden>
  251. <Input />
  252. </Form.Item>
  253. <Form.Item name="soundSensorName" label=" 音频传感器名称"
  254. rules={[
  255. { required: true },
  256. { pattern: /^[0-9\u4e00-\u9fa5a-zA-Z-\/#\.]+$/, message: "仅支持数字、中文、大小写英文字母、特殊字符-/#." },
  257. ]}
  258. extra={<span style={{ fontSize: 12 }}>仅支持数字、中文、大小写英文字母、特殊字符-\/#.</span>}
  259. >
  260. <Input placeholder="请输入音频传感器名称" />
  261. </Form.Item>
  262. <Form.Item name="soundSensorType" label="音频接入" rules={[{ required: true }]}>
  263. <Radio.Group block options={uploadTypeList} />
  264. </Form.Item>
  265. <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.soundSensorType !== currentValues.soundSensorType}>
  266. {({ getFieldValue }) => {
  267. return (
  268. getFieldValue("soundSensorType") === AccessType.Audio ? (
  269. <Form.Item name="soundSensorUrl" label="传感器地址" rules={[{ required: true }]}>
  270. <Input placeholder="请输入传感器地址,如 http://127.0.0.1:8080/audio" />
  271. </Form.Item>
  272. ) : null)
  273. }}
  274. </Form.Item>
  275. <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.soundSensorType !== currentValues.soundSensorType}>
  276. {({ getFieldValue }) => {
  277. return getFieldValue("soundSensorType") === AccessType.FileUpload && getFieldValue("soundSensorUuid") === "" && (
  278. <Form.Item name="files" valuePropName="fileList"
  279. getValueFromEvent={(e) => {
  280. if (Array.isArray(e)) {
  281. return e;
  282. }
  283. return e && e.fileList;
  284. }}
  285. rules={[{ required: true },
  286. { validator: (_, value) => validateFileLength(value) },
  287. { validator: (_, value) => validateFileSize(value) },
  288. { validator: (_, value) => validateFileType(value) }]}
  289. label="音频上传"
  290. >
  291. <Upload customRequest={customRequest} maxCount={1} iconRender={iconRender} onRemove={onRemove}
  292. showUploadList={{
  293. extra: ({ size = 0 }) => (
  294. <span style={{ color: '#cccccc' }}>({(size / 1024 / 1024).toFixed(2)}MB)</span>
  295. ),
  296. showRemoveIcon: true,
  297. removeIcon: <CloseOutlined style={{ color: '#cccccc' }} />,
  298. }}>
  299. <Button disabled={uploadState} icon={<UploadOutlined />}>上传文件</Button>
  300. <p className={styles.tip}>文件大小仅支持 50M 以内,支持mp4格式,仅允许上传 1 个文件</p>
  301. </Upload>
  302. </Form.Item>
  303. )
  304. }}
  305. </Form.Item>
  306. <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.soundSensorUuid !== currentValues.soundSensorUuid}>
  307. {({ getFieldValue }) =>
  308. getFieldValue("soundSensorUuid") !== "" ? (
  309. <Form.Item name="streamUrl" label="上传音频">
  310. <Input />
  311. </Form.Item>) : null
  312. }
  313. </Form.Item>
  314. <Form.Item name="skillUuid" label="配置技能" rules={[{ required: true }]}>
  315. <Select placeholder="请选择技能" allowClear disabled={onlyDisable}>
  316. {skillData.map((e: any) => {
  317. return (
  318. <Option key={e.uuid} value={e.uuid}>{e.name}</Option>
  319. );
  320. })}
  321. </Select>
  322. </Form.Item>
  323. <Form.Item name="deptUuid" label="责任部门" rules={[{ required: true }]}>
  324. <TreeSelect
  325. showSearch
  326. style={{ width: '100%' }}
  327. dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
  328. placeholder="请选择责任部门"
  329. treeDefaultExpandAll
  330. treeData={departmentData}
  331. fieldNames={{ label: 'deptName', value: 'deptID', children: 'childNodes' }}
  332. onSelect={onSelect}
  333. />
  334. </Form.Item>
  335. <Form.Item name="responsiblePerson" label="责任人" rules={[{ required: false }]}>
  336. <Input placeholder="请填写责任人" allowClear />
  337. </Form.Item>
  338. <Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.soundSensorType !== currentValues.soundSensorType}>
  339. {({ getFieldValue }) =>
  340. getFieldValue("soundSensorType") === AccessType.Audio ? (
  341. <Form.Item name="ip" label="音频传感器IP" rules={[{ required: false }]} >
  342. <Input placeholder="请输入音频传感器IP,如 127.0.0.1" />
  343. </Form.Item>
  344. ) : null
  345. }
  346. </Form.Item>
  347. <Form.Item name="description" label="备注信息" rules={[{ required: false }]}>
  348. <TextArea placeholder="请输入备注,建议长度200" maxLength={200} rows={4} showCount />
  349. </Form.Item>
  350. </Form>
  351. </Drawer>
  352. </>
  353. );
  354. });
  355. export default DrawerExample;