UNPKG

@blocklet/ui-react

Version:

Some useful front-end web components that can be used in Blocklets.

268 lines (241 loc) 7.22 kB
/* eslint-disable @typescript-eslint/naming-convention */ /** * OrgMutateDialog - 组织创建/编辑弹窗 * * 支持两种模式: * - 创建模式:新建组织,头像必填 * - 编辑模式:编辑已有组织信息 */ import { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useReactive, useMemoizedFn } from 'ahooks'; import noop from 'lodash/noop'; import Dialog from '@arcblock/ux/lib/Dialog'; import { CircularProgress, DialogContentText, Typography, TextField, Alert, Box } from '@mui/material'; import Toast from '@arcblock/ux/lib/Toast'; import Button from '@arcblock/ux/lib/Button'; import { translate } from '@arcblock/ux/lib/Locale/util'; import { formatAxiosError } from '../../UserCenter/libs/utils'; import useOrg from './use-org'; import translations from './locales'; import AvatarUploader from './avatar-uploader'; // 操作模式 const MODE_CREATE = 'create'; const MODE_EDIT = 'edit'; export default function OrgMutateDialog({ mode = MODE_CREATE, org = null, onSuccess = noop, onCancel = noop, locale = 'en', teamDid: teamDidProp = '', prefix = '/.well-known/service', headers = noop, }) { const isEditMode = mode === MODE_EDIT; const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const { createOrg, updateOrg } = useOrg(); // 获取 teamDid:优先使用 prop,否则从 window.blocklet.did 获取 // eslint-disable-next-line no-undef const teamDid = teamDidProp || (typeof window !== 'undefined' ? window.blocklet?.did : ''); const t = useMemoizedFn((key, data = {}) => { return translate(translations, key, locale, 'en', data); }); const form = useReactive({ name: '', description: '', avatar: '', }); // 编辑模式下初始化表单数据 useEffect(() => { if (isEditMode && org) { form.name = org.name || ''; form.description = org.description || ''; form.avatar = org.avatar || ''; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isEditMode, org]); // 头像变化处理 const handleAvatarChange = useMemoizedFn((avatarPath) => { setError(''); form.avatar = avatarPath; }); // 表单验证 const validateForm = useMemoizedFn(() => { const _name = form.name.trim(); if (!_name) { setError(t('nameEmpty')); return false; } if (_name.length > 25) { setError(t('nameTooLong', { length: 25 })); return false; } const _description = form.description.trim(); if (_description.length > 255) { setError(t('descriptionTooLong', { length: 255 })); return false; } // 头像必填验证 if (!form.avatar) { setError(t('avatarEmpty')); return false; } return true; }); // 提交处理 const onSubmit = async () => { if (!validateForm()) { return; } setError(''); setLoading(true); const _name = form.name.trim(); const _description = form.description.trim(); const _avatar = form.avatar; try { if (isEditMode && org?.id) { // 编辑模式:更新组织 await updateOrg(org.id, { name: _name, description: _description, avatar: _avatar, }); } else { // 创建模式:新建组织 await createOrg({ name: _name, description: _description, avatar: _avatar, }); } onSuccess(); } catch (err) { console.error(err); const errMsg = formatAxiosError(err); setError(errMsg); Toast.error(errMsg); } finally { setLoading(false); } }; // 对话框标题 const dialogTitle = isEditMode ? t('mutate.title', { mode: t('edit') }) : t('mutate.title', { mode: t('create') }); // 提交按钮文本 const submitButtonText = isEditMode ? t('save') : t('create'); return ( <Dialog title={dialogTitle} fullWidth open onClose={onCancel} showCloseButton={false} actions={ <> <Button onClick={onCancel} color="inherit"> {t('cancel')} </Button> <Button data-cy="mutate-org-confirm" onClick={onSubmit} color="primary" disabled={loading} variant="contained" autoFocus> {loading && <CircularProgress size={16} sx={{ mr: 1 }} />} {submitButtonText} </Button> </> }> <DialogContentText component="div"> {/* 头像上传区域 */} <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 2, }}> <AvatarUploader org={isEditMode ? org : { name: form.name }} size={90} teamDid={teamDid} prefix={prefix} headers={headers} onChange={handleAvatarChange} value={form.avatar} editable /> <Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}> {t('avatar')} </Typography> </Box> {/* 表单字段 */} <Typography component="div" style={{ marginTop: 16 }}> <TextField label={t('mutate.name')} autoComplete="off" variant="outlined" name="name" data-cy="mutate-org-input-name" fullWidth autoFocus value={form.name} onChange={(e) => { setError(''); form.name = e.target.value; }} disabled={loading} required /> </Typography> <Typography component="div" style={{ marginTop: 16, marginBottom: 16 }}> <TextField label={t('mutate.description')} autoComplete="off" variant="outlined" name="description" data-cy="mutate-org-input-description" fullWidth value={form.description} onChange={(e) => { setError(''); form.description = e.target.value; }} disabled={loading} multiline rows={3} /> </Typography> </DialogContentText> {!!error && ( <Alert severity="error" style={{ width: '100%', margin: 0 }}> {error} </Alert> )} </Dialog> ); } OrgMutateDialog.propTypes = { /** 操作模式:'create' 创建 | 'edit' 编辑 */ mode: PropTypes.oneOf([MODE_CREATE, MODE_EDIT]), /** 编辑模式下的组织数据 */ org: PropTypes.shape({ id: PropTypes.string, name: PropTypes.string, description: PropTypes.string, avatar: PropTypes.string, }), onSuccess: PropTypes.func, onCancel: PropTypes.func, locale: PropTypes.string, teamDid: PropTypes.string, prefix: PropTypes.string, headers: PropTypes.func, }; // 导出模式常量供外部使用 OrgMutateDialog.MODE_CREATE = MODE_CREATE; OrgMutateDialog.MODE_EDIT = MODE_EDIT; // 保持向后兼容的别名 export { OrgMutateDialog as CreateOrgDialog };