260 lines
8.4 KiB
JavaScript
260 lines
8.4 KiB
JavaScript
import React, { useState, useCallback } from 'react';
|
||
import './FeedbackModal.scss';
|
||
import { createMemo } from '../../forums/api';
|
||
import { message, Upload, Icon, Button } from 'antd';
|
||
import { httpUrl } from '../../forums/fetch';
|
||
import RichEditor from '../../forge/Component/RichEditor';
|
||
import cookie from 'react-cookies';
|
||
import '../common.scss'
|
||
import { feedBackId } from '../../forums/static'
|
||
|
||
// 导入图片资源
|
||
import modalBg from '../../images/factory/feedback/modal-bg.webp';
|
||
import iconClose from '../../images/factory/feedback/icon-close-new.webp';
|
||
import iconRequired from '../../images/factory/feedback/icon-required-new.webp';
|
||
|
||
/**
|
||
* 反馈中心-新增弹窗组件
|
||
* @param {boolean} visible - 弹窗显示状态
|
||
* @param {function} onClose - 关闭弹窗回调
|
||
* @param {function} onSuccess - 提交成功回调
|
||
*/
|
||
function FeedbackModal({ visible, onClose, onSuccess }) {
|
||
const [formData, setFormData] = useState({
|
||
title: '',
|
||
description: '',
|
||
});
|
||
const [errors, setErrors] = useState({});
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [fileList, setFileList] = useState([]);
|
||
|
||
// 处理输入变化
|
||
const handleInputChange = useCallback((field, value) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[field]: value,
|
||
}));
|
||
// 清除对应字段的错误
|
||
if (errors[field]) {
|
||
setErrors(prev => ({
|
||
...prev,
|
||
[field]: null,
|
||
}));
|
||
}
|
||
}, [errors]);
|
||
|
||
// 文件上传前校验
|
||
const beforeUpload = (file) => {
|
||
const isLt100M = file.size / 1024 / 1024 < 100;
|
||
if (!isLt100M) {
|
||
message.error('文件大小必须小于100MB!');
|
||
}
|
||
return isLt100M;
|
||
};
|
||
|
||
// 文件列表变化
|
||
const handleFileChange = (info) => {
|
||
if (info.file.status === 'uploading' || info.file.status === 'done' || info.file.status === 'removed') {
|
||
let fileList = info.fileList || [];
|
||
|
||
// 处理后端返回格式与Ant Design Upload预期格式不匹配的问题
|
||
// 后端返回 { status: 1, message: "成功", id: 123 } 格式
|
||
// Ant Design 期望 response 中包含能识别的成功标识
|
||
fileList = fileList.map(file => {
|
||
if (file.response && file.response.status === 1 && file.status === 'uploading') {
|
||
return {
|
||
...file,
|
||
status: 'done',
|
||
};
|
||
}
|
||
return file;
|
||
});
|
||
|
||
setFileList(fileList);
|
||
}
|
||
};
|
||
|
||
// 验证表单
|
||
const validateForm = useCallback(() => {
|
||
const newErrors = {};
|
||
if (!formData.title.trim()) {
|
||
newErrors.title = '请输入标题';
|
||
}
|
||
|
||
if (!formData.description) {
|
||
newErrors.description = '请输入问题描述';
|
||
} else {
|
||
if (formData.description === '<p><br></p>') {
|
||
newErrors.description = '请输入问题描述';
|
||
}
|
||
}
|
||
|
||
setErrors(newErrors);
|
||
return Object.keys(newErrors).length === 0;
|
||
}, [formData]);
|
||
|
||
// 处理提交
|
||
const handleSubmit = useCallback(async () => {
|
||
if (!validateForm()) return;
|
||
|
||
setIsSubmitting(true);
|
||
try {
|
||
// 构建附件ID数组,参考forums/edit的实现
|
||
const attachmentIdArray = fileList.map(item =>
|
||
item.response && item.response.id ? item.response.id : item.id ? item.id : item
|
||
);
|
||
|
||
// 构建接口参数,主题板块默认填12
|
||
const params = {
|
||
forum_id: feedBackId,
|
||
attachments: attachmentIdArray,
|
||
memo: {
|
||
subject: formData.title,
|
||
content: formData.description,
|
||
is_original: 1,
|
||
tag_id: 1
|
||
},
|
||
};
|
||
|
||
const ret = await createMemo(params);
|
||
|
||
if (ret.status === 1) {
|
||
message.success(ret.message || '提交成功');
|
||
// 提交成功后重置表单并关闭
|
||
setFormData({ title: '', description: '' });
|
||
setFileList([]);
|
||
onClose?.();
|
||
onSuccess?.(ret);
|
||
} else {
|
||
message.error(ret.message || '提交失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('提交反馈失败:', error);
|
||
message.error('提交失败,请稍后重试');
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
}, [formData, validateForm, fileList, onClose, onSuccess]);
|
||
|
||
// 处理取消
|
||
const handleCancel = useCallback(() => {
|
||
setFormData({ title: '', description: '' });
|
||
setErrors({});
|
||
setFileList([]);
|
||
onClose?.();
|
||
}, [onClose]);
|
||
|
||
// 点击蒙层关闭(已禁用,仅通过X按钮和取消按钮关闭)
|
||
const handleMaskClick = useCallback((e) => {
|
||
// 点击蒙层不关闭弹窗
|
||
}, []);
|
||
|
||
if (!visible) return null;
|
||
|
||
return (
|
||
<div className="feedback-modal-wrap">
|
||
<div className="feedback-modal-mask" onClick={handleMaskClick}>
|
||
<div className="feedback-modal">
|
||
{/* 弹窗背景 */}
|
||
<div className="feedback-modal-bg">
|
||
<img src={modalBg} alt="" />
|
||
</div>
|
||
|
||
{/* 弹窗头部 */}
|
||
<div className="feedback-modal-header">
|
||
<h2 className="feedback-modal-title">意见反馈</h2>
|
||
<button
|
||
className="feedback-modal-close"
|
||
onClick={handleCancel}
|
||
aria-label="关闭"
|
||
>
|
||
<img src={iconClose} alt="关闭" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* 弹窗内容 */}
|
||
<div className="feedback-modal-content">
|
||
{/* 标题输入 */}
|
||
<div className="feedback-form-item">
|
||
<div className="feedback-form-label">
|
||
<img src={iconRequired} alt="必填" className="required-icon" />
|
||
<span>标题</span>
|
||
</div>
|
||
<div className={`feedback-form-input-wrapper ${errors.title ? 'error' : ''}`}>
|
||
<input
|
||
type="text"
|
||
className="feedback-form-input"
|
||
placeholder="请输入标题"
|
||
value={formData.title}
|
||
onChange={(e) => handleInputChange('title', e.target.value)}
|
||
maxLength={200}
|
||
/>
|
||
</div>
|
||
{errors.title && <span className="feedback-form-error">{errors.title}</span>}
|
||
</div>
|
||
|
||
{/* 问题描述输入 */}
|
||
<div className="feedback-form-item">
|
||
<div className="feedback-form-label">
|
||
<img src={iconRequired} alt="必填" className="required-icon" />
|
||
<span>问题描述</span>
|
||
</div>
|
||
<div className="feedback-form-rich">
|
||
<RichEditor httpUrl={httpUrl} uploadUrl={`${httpUrl}/api/attachments.json`} videoUploadUrl={`${httpUrl}/api/attachments.json`} height={'270px'} setValue={(value) => { handleInputChange('description', value) }} />
|
||
</div>
|
||
{errors.description && <span className="feedback-form-error">{errors.description}</span>}
|
||
</div>
|
||
|
||
{/* 附件上传 - 参考forums/edit配置 */}
|
||
<div className="feedback-form-item">
|
||
<div className="feedback-form-label">
|
||
<span>附件</span>
|
||
</div>
|
||
<div className="feedback-form-upload">
|
||
<Upload
|
||
action={`${httpUrl}/api/attachments.json`}
|
||
beforeUpload={beforeUpload}
|
||
onChange={handleFileChange}
|
||
fileList={fileList}
|
||
className="feedback-upload-dragger"
|
||
withCredentials={true}
|
||
headers={{ Authorization: cookie.load('autologin_trustie') }}
|
||
>
|
||
{
|
||
fileList.length < 3 && <>
|
||
<Button className="uploadBtn">
|
||
<Icon type="upload" /> 选择文件
|
||
</Button>
|
||
<span className={"ml10 color-ooo"}>(单个文件最大100M,最多3个附件)</span></>
|
||
}
|
||
|
||
</Upload>
|
||
</div>
|
||
</div>
|
||
<div className="feedback-modal-footer">
|
||
<button
|
||
className="feedback-btn feedback-btn-cancel"
|
||
onClick={handleCancel}
|
||
disabled={isSubmitting}
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
className="feedback-btn feedback-btn-submit shiny-button"
|
||
onClick={handleSubmit}
|
||
disabled={isSubmitting}
|
||
>
|
||
{isSubmitting ? '提交中...' : '提交'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default FeedbackModal;
|