feat: 统一后台与用户端图片与文件上传交互,支持点击放大预览、删除与多图批量多选上传
这个提交包含在:
@@ -0,0 +1,155 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Upload,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Message,
|
||||
} from '@arco-design/web-react';
|
||||
import type { UploadItem } from '@arco-design/web-react/es/Upload';
|
||||
import {
|
||||
IconFile,
|
||||
IconDelete,
|
||||
IconEye,
|
||||
IconUpload,
|
||||
} from '@arco-design/web-react/icon';
|
||||
|
||||
export interface FileUploaderProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
accept?: string;
|
||||
disabled?: boolean;
|
||||
action?: string;
|
||||
btnText?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ACTION = '/api/admin/base/comm/upload';
|
||||
|
||||
function parseUploadedUrl(response: unknown): string {
|
||||
if (!response || typeof response !== 'object') {
|
||||
return '';
|
||||
}
|
||||
const rec = response as Record<string, unknown>;
|
||||
if (typeof rec.data === 'string' && rec.data) {
|
||||
return rec.data;
|
||||
}
|
||||
if (typeof rec.url === 'string' && rec.url) {
|
||||
return rec.url;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export const FileUploader: React.FC<FileUploaderProps> = ({
|
||||
value = '',
|
||||
onChange,
|
||||
accept = '.pdf',
|
||||
disabled = false,
|
||||
action = DEFAULT_ACTION,
|
||||
btnText = 'Upload File',
|
||||
}) => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const handleUploadChange = useCallback(
|
||||
(_fileList: UploadItem[], currentFile: UploadItem) => {
|
||||
if (currentFile.status === 'uploading') {
|
||||
setUploading(true);
|
||||
} else if (currentFile.status === 'done') {
|
||||
setUploading(false);
|
||||
const uploadedUrl = parseUploadedUrl(currentFile.response);
|
||||
if (uploadedUrl) {
|
||||
Message.success('File uploaded successfully');
|
||||
onChange?.(uploadedUrl);
|
||||
} else {
|
||||
Message.error('Upload failed: missing url');
|
||||
}
|
||||
} else if (currentFile.status === 'error') {
|
||||
setUploading(false);
|
||||
Message.error('Upload failed');
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!disabled) {
|
||||
onChange?.('');
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
const handlePreview = useCallback(() => {
|
||||
if (value) {
|
||||
window.open(value, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const fileName = value ? value.split('/').pop() || 'document.pdf' : '';
|
||||
|
||||
return (
|
||||
<div style={{ display: 'inline-block', width: '100%' }}>
|
||||
{value ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid var(--color-border-2, #E3E7ED)',
|
||||
backgroundColor: 'var(--color-fill-2, #F7F8FA)',
|
||||
}}
|
||||
>
|
||||
<Space size={8} style={{ overflow: 'hidden' }}>
|
||||
<IconFile style={{ fontSize: 18, color: 'var(--color-primary-6, #0B5CFF)' }} />
|
||||
<Typography.Text
|
||||
ellipsis
|
||||
style={{ maxWidth: 260, fontSize: 13, color: 'var(--color-text-1, #1D2129)' }}
|
||||
>
|
||||
{fileName}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
<Space size={4}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconEye />}
|
||||
onClick={handlePreview}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{!disabled && (
|
||||
<Button
|
||||
type="text"
|
||||
status="danger"
|
||||
size="small"
|
||||
icon={<IconDelete />}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
) : (
|
||||
<Upload
|
||||
action={action}
|
||||
accept={accept}
|
||||
showUploadList={false}
|
||||
disabled={disabled}
|
||||
onChange={handleUploadChange}
|
||||
>
|
||||
<Button
|
||||
type="outline"
|
||||
size="small"
|
||||
loading={uploading}
|
||||
disabled={disabled}
|
||||
icon={<IconUpload />}
|
||||
>
|
||||
{btnText}
|
||||
</Button>
|
||||
</Upload>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUploader;
|
||||
@@ -0,0 +1,239 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Upload,
|
||||
Image,
|
||||
Space,
|
||||
Button,
|
||||
Message,
|
||||
Modal,
|
||||
} from '@arco-design/web-react';
|
||||
import type { UploadItem } from '@arco-design/web-react/es/Upload';
|
||||
import {
|
||||
IconPlus,
|
||||
IconEye,
|
||||
IconDelete,
|
||||
IconUpload,
|
||||
} from '@arco-design/web-react/icon';
|
||||
|
||||
export interface ImageUploaderProps {
|
||||
value?: string | string[];
|
||||
onChange?: (value: string | string[]) => void;
|
||||
multiple?: boolean;
|
||||
maxCount?: number;
|
||||
accept?: string;
|
||||
disabled?: boolean;
|
||||
action?: string;
|
||||
tip?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ACTION = '/api/admin/base/comm/upload';
|
||||
|
||||
function parseUploadedUrl(response: unknown): string {
|
||||
if (!response || typeof response !== 'object') {
|
||||
return '';
|
||||
}
|
||||
const rec = response as Record<string, unknown>;
|
||||
if (typeof rec.data === 'string' && rec.data) {
|
||||
return rec.data;
|
||||
}
|
||||
if (typeof rec.url === 'string' && rec.url) {
|
||||
return rec.url;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
multiple = false,
|
||||
maxCount = 9,
|
||||
accept = 'image/*',
|
||||
disabled = false,
|
||||
action = DEFAULT_ACTION,
|
||||
tip,
|
||||
}) => {
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewSrc, setPreviewSrc] = useState('');
|
||||
|
||||
const currentList = useMemo<string[]>(() => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter((v): v is string => typeof v === 'string' && v.trim() !== '');
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? [trimmed] : [];
|
||||
}, [value]);
|
||||
|
||||
const handlePreview = useCallback((url: string) => {
|
||||
setPreviewSrc(url);
|
||||
setPreviewVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(indexToDelete: number) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (multiple) {
|
||||
const nextList = currentList.filter((_, idx) => idx !== indexToDelete);
|
||||
onChange?.(nextList);
|
||||
} else {
|
||||
onChange?.('');
|
||||
}
|
||||
},
|
||||
[currentList, disabled, multiple, onChange]
|
||||
);
|
||||
|
||||
const handleUploadChange = useCallback(
|
||||
(_fileList: UploadItem[], currentFile: UploadItem) => {
|
||||
if (currentFile.status === 'done') {
|
||||
const uploadedUrl = parseUploadedUrl(currentFile.response);
|
||||
if (uploadedUrl) {
|
||||
Message.success('Upload success');
|
||||
if (multiple) {
|
||||
const nextList = [...currentList, uploadedUrl].slice(0, maxCount);
|
||||
onChange?.(nextList);
|
||||
} else {
|
||||
onChange?.(uploadedUrl);
|
||||
}
|
||||
} else {
|
||||
Message.error('Upload failed: missing url');
|
||||
}
|
||||
} else if (currentFile.status === 'error') {
|
||||
Message.error('Upload failed');
|
||||
}
|
||||
},
|
||||
[currentList, maxCount, multiple, onChange]
|
||||
);
|
||||
|
||||
const canUploadMore = multiple ? currentList.length < maxCount : currentList.length === 0;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'inline-block', width: '100%' }}>
|
||||
<Space wrap size={12} align="center">
|
||||
{/* 多图画廊预览组 */}
|
||||
<Image.PreviewGroup
|
||||
srcList={currentList}
|
||||
visible={previewVisible}
|
||||
onVisibleChange={(visible) => setPreviewVisible(visible)}
|
||||
current={currentList.indexOf(previewSrc) >= 0 ? currentList.indexOf(previewSrc) : 0}
|
||||
>
|
||||
{currentList.map((url, idx) => (
|
||||
<div
|
||||
key={`${url}-${idx}`}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
border: '1px solid var(--color-border-2, #E3E7ED)',
|
||||
backgroundColor: 'var(--color-fill-2, #F7F8FA)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
alt="thumb"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.45)',
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.opacity = '1';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.opacity = '0';
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="mini"
|
||||
style={{ color: '#fff' }}
|
||||
icon={<IconEye />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePreview(url);
|
||||
}}
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="mini"
|
||||
style={{ color: '#ff4d4f' }}
|
||||
icon={<IconDelete />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(idx);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Image.PreviewGroup>
|
||||
|
||||
{/* 上传触发按钮 */}
|
||||
{canUploadMore && !disabled && (
|
||||
<Upload
|
||||
action={action}
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
showUploadList={false}
|
||||
onChange={handleUploadChange}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: 12,
|
||||
border: '1px dashed var(--color-border-3, #C9CDD4)',
|
||||
backgroundColor: 'var(--color-fill-1, #F7F8FA)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<IconPlus style={{ fontSize: 20, color: 'var(--color-text-3, #86909C)' }} />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
color: 'var(--color-text-3, #86909C)',
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</span>
|
||||
</div>
|
||||
</Upload>
|
||||
)}
|
||||
</Space>
|
||||
{tip && (
|
||||
<div style={{ marginTop: 6, fontSize: 12, color: 'var(--color-text-3, #86909C)' }}>
|
||||
{tip}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploader;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ImageUploader';
|
||||
export * from './FileUploader';
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Space,
|
||||
Grid,
|
||||
} from '@arco-design/web-react';
|
||||
import { ImageUploader } from '@/components/Upload';
|
||||
import type { PaginationProps, TableColumnProps } from '@arco-design/web-react';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
@@ -528,7 +529,7 @@ export default function CardArticlesPage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t['cardArticles.field.coverImage']} field="coverImage">
|
||||
<Input placeholder="https://cdn.omnicard.ai/articles/cover.jpg" />
|
||||
<ImageUploader />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t['cardArticles.field.content']}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Card,
|
||||
Tabs,
|
||||
} from '@arco-design/web-react';
|
||||
import { ImageUploader } from '@/components/Upload';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import { getCssVariables } from '@/style/tokens';
|
||||
import type {
|
||||
@@ -230,20 +231,26 @@ function CardBuilderPage() {
|
||||
};
|
||||
}, [hasId, id, t]);
|
||||
|
||||
const lastSelectedIdRef = useRef<string | number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedBlock) {
|
||||
form.resetFields();
|
||||
lastSelectedIdRef.current = null;
|
||||
return;
|
||||
}
|
||||
form.setFieldsValue({
|
||||
title: selectedBlock.title,
|
||||
content: selectedBlock.content,
|
||||
mediaUrl: selectedBlock.mediaUrl,
|
||||
linkUrl: selectedBlock.linkUrl,
|
||||
colSpan: selectedBlock.colSpan,
|
||||
rowSpan: selectedBlock.rowSpan,
|
||||
type: selectedBlock.type,
|
||||
});
|
||||
if (lastSelectedIdRef.current !== selectedBlock.id) {
|
||||
lastSelectedIdRef.current = selectedBlock.id;
|
||||
form.setFieldsValue({
|
||||
title: selectedBlock.title,
|
||||
content: selectedBlock.content,
|
||||
mediaUrl: selectedBlock.mediaUrl,
|
||||
linkUrl: selectedBlock.linkUrl,
|
||||
colSpan: selectedBlock.colSpan,
|
||||
rowSpan: selectedBlock.rowSpan,
|
||||
type: selectedBlock.type,
|
||||
});
|
||||
}
|
||||
}, [form, selectedBlock]);
|
||||
|
||||
const handleAdd = useCallback(
|
||||
@@ -487,7 +494,7 @@ function CardBuilderPage() {
|
||||
field="mediaUrl"
|
||||
label={t['cardBuilder.field.mediaUrl']}
|
||||
>
|
||||
<Input />
|
||||
<ImageUploader />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="linkUrl"
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '@arco-design/web-react';
|
||||
import type { TableColumnProps, PaginationProps } from '@arco-design/web-react';
|
||||
import { IconPlus, IconSearch, IconRefresh, IconFile, IconUpload } from '@arco-design/web-react/icon';
|
||||
import { ImageUploader, FileUploader } from '@/components/Upload';
|
||||
import axios from 'axios';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
@@ -302,12 +303,13 @@ export default function CardCasesPage(): React.ReactElement {
|
||||
width: 110,
|
||||
render: (url?: string) =>
|
||||
url ? (
|
||||
<img
|
||||
<Image
|
||||
src={url}
|
||||
alt="cover"
|
||||
width={64}
|
||||
height={40}
|
||||
preview
|
||||
style={{
|
||||
width: 64,
|
||||
height: 40,
|
||||
objectFit: 'cover',
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${lightColors.border}`,
|
||||
@@ -570,44 +572,14 @@ export default function CardCasesPage(): React.ReactElement {
|
||||
</div>
|
||||
|
||||
<Form.Item field="coverImage" label={t['cases.coverImage']}>
|
||||
<Input
|
||||
placeholder={t['cases.coverImagePlaceholder']}
|
||||
addAfter={
|
||||
<Upload
|
||||
action="/api/common/upload"
|
||||
showUploadList={false}
|
||||
onChange={(_, file) => {
|
||||
if (file.response?.url) {
|
||||
editForm.setFieldsValue({ coverImage: file.response.url });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="text" size="mini" icon={<IconUpload />}>
|
||||
Upload
|
||||
</Button>
|
||||
</Upload>
|
||||
}
|
||||
<ImageUploader
|
||||
tip={t['cases.uploadHint']}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item field="pdfUrl" label={t['cases.pdfUrl']}>
|
||||
<Input
|
||||
placeholder={t['cases.pdfUrlPlaceholder']}
|
||||
addAfter={
|
||||
<Upload
|
||||
action="/api/common/upload"
|
||||
showUploadList={false}
|
||||
onChange={(_, file) => {
|
||||
if (file.response?.url) {
|
||||
editForm.setFieldsValue({ pdfUrl: file.response.url });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="text" size="mini" icon={<IconUpload />}>
|
||||
Upload
|
||||
</Button>
|
||||
</Upload>
|
||||
}
|
||||
<FileUploader
|
||||
btnText={t['cases.uploadText']}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Typography,
|
||||
Image,
|
||||
} from '@arco-design/web-react';
|
||||
import { ImageUploader } from '@/components/Upload';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import { lightColors } from '@/style/tokens';
|
||||
@@ -279,9 +280,8 @@ function TeamBrandKitPage() {
|
||||
field="logoUrl"
|
||||
label={t['teamBrandKit.form.logoUrl']}
|
||||
>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder={t['teamBrandKit.form.logoUrl.placeholder']}
|
||||
<ImageUploader
|
||||
tip={t['teamBrandKit.form.logoUrl.placeholder']}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Avatar,
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
Tag,
|
||||
Skeleton,
|
||||
Link,
|
||||
Image,
|
||||
Space,
|
||||
} from '@arco-design/web-react';
|
||||
import { IconCamera, IconPlus } from '@arco-design/web-react/icon';
|
||||
import { IconCamera, IconPlus, IconEye, IconDelete } from '@arco-design/web-react/icon';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import locale from './locale';
|
||||
import styles from './style/header.module.less';
|
||||
@@ -17,19 +19,34 @@ export default function Info({
|
||||
userInfo = {},
|
||||
loading,
|
||||
}: {
|
||||
userInfo: any;
|
||||
userInfo: Record<string, unknown>;
|
||||
loading: boolean;
|
||||
}) {
|
||||
const t = useLocale(locale);
|
||||
|
||||
const [avatar, setAvatar] = useState('');
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
|
||||
function onAvatarChange(_, file) {
|
||||
setAvatar(file.originFile ? URL.createObjectURL(file.originFile) : '');
|
||||
function onAvatarChange(_: unknown, file: { originFile?: File; response?: { data?: string; url?: string } }) {
|
||||
if (file.response?.data) {
|
||||
setAvatar(file.response.data);
|
||||
} else if (file.response?.url) {
|
||||
setAvatar(file.response.url);
|
||||
} else if (file.originFile) {
|
||||
setAvatar(URL.createObjectURL(file.originFile));
|
||||
} else {
|
||||
setAvatar('');
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
setAvatar('');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setAvatar(userInfo.avatar);
|
||||
if (typeof userInfo.avatar === 'string') {
|
||||
setAvatar(userInfo.avatar);
|
||||
}
|
||||
}, [userInfo]);
|
||||
|
||||
const loadingImg = (
|
||||
@@ -43,19 +60,59 @@ export default function Info({
|
||||
const loadingNode = <Skeleton text={{ rows: 1 }} animation />;
|
||||
return (
|
||||
<div className={styles['info-wrapper']}>
|
||||
<Upload showUploadList={false} onChange={onAvatarChange}>
|
||||
{loading ? (
|
||||
loadingImg
|
||||
) : (
|
||||
<Avatar
|
||||
size={100}
|
||||
triggerIcon={<IconCamera />}
|
||||
className={styles['info-avatar']}
|
||||
>
|
||||
{avatar ? <img src={avatar} /> : <IconPlus />}
|
||||
</Avatar>
|
||||
)}
|
||||
</Upload>
|
||||
{loading ? (
|
||||
loadingImg
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Avatar
|
||||
size={100}
|
||||
className={styles['info-avatar']}
|
||||
>
|
||||
{avatar ? <img src={avatar} alt="avatar" /> : <IconPlus />}
|
||||
</Avatar>
|
||||
</div>
|
||||
<Space size={8}>
|
||||
<Upload
|
||||
action="/api/admin/base/comm/upload"
|
||||
showUploadList={false}
|
||||
onChange={onAvatarChange}
|
||||
>
|
||||
<Button size="mini" type="outline" icon={<IconCamera />}>
|
||||
Upload
|
||||
</Button>
|
||||
</Upload>
|
||||
{avatar && (
|
||||
<>
|
||||
<Button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon={<IconEye />}
|
||||
onClick={() => setPreviewVisible(true)}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
type="text"
|
||||
status="danger"
|
||||
icon={<IconDelete />}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
{avatar && (
|
||||
<Image.Preview
|
||||
src={avatar}
|
||||
visible={previewVisible}
|
||||
onVisibleChange={setPreviewVisible}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Descriptions
|
||||
className={styles['info-content']}
|
||||
column={2}
|
||||
|
||||
在新工单中引用
屏蔽一个用户