This commit is contained in:
黄心宇laptop 2026-03-26 09:23:29 +08:00
parent e446e46655
commit ad3d83ec8c
8 changed files with 739 additions and 3 deletions

View File

@ -0,0 +1,242 @@
import React, { useState, useCallback } from 'react';
import './FeedbackModal.scss';
import { createMemo } from '../../forums/api';
import { message, Upload } from 'antd';
import { httpUrl } from '../../forums/fetch';
//
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 [fileList, setFileList] = useState([]);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
//
const handleInputChange = useCallback((field, value) => {
setFormData(prev => ({
...prev,
[field]: value,
}));
//
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: null,
}));
}
}, [errors]);
//
const beforeUpload = useCallback((file) => {
const isLt100M = file.size / 1024 / 1024 < 100;
if (!isLt100M) {
message.error('文件大小必须小于100MB!');
}
return isLt100M;
}, []);
//
const handleFileChange = useCallback((info) => {
if (info.file.status === 'uploading' || info.file.status === 'done' || info.file.status === 'removed') {
const newFileList = info.fileList || [];
setFileList(newFileList);
}
}, []);
//
const validateForm = useCallback(() => {
const newErrors = {};
if (!formData.title.trim()) {
newErrors.title = '请输入标题';
}
if (!formData.description.trim()) {
newErrors.description = '请输入问题描述';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}, [formData]);
//
const handleSubmit = useCallback(async () => {
if (!validateForm()) return;
setIsSubmitting(true);
try {
// ID
const attachmentIdArray = fileList.map(item => {
// 使 response.id item.id
return item.response && item.response.id ? item.response.id : item.id;
}).filter(id => id); // undefined/null
// 11
const params = {
forum_id: 11,
children_forum_id: '0',
attachments: attachmentIdArray.length > 0 ? attachmentIdArray : undefined,
memo: {
subject: formData.title,
content: formData.description,
is_original: 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, fileList, validateForm, onClose, onSuccess]);
//
const handleCancel = useCallback(() => {
setFormData({ title: '', description: '' });
setFileList([]);
setErrors({});
onClose?.();
}, [onClose]);
//
const handleMaskClick = useCallback((e) => {
if (e.target === e.currentTarget) {
handleCancel();
}
}, [handleCancel]);
if (!visible) return null;
return (
<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="提升模型API调用接口的兼容性-参数错误"
value={formData.title}
onChange={(e) => handleInputChange('title', e.target.value)}
maxLength={500}
/>
</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-textarea-wrapper ${errors.description ? 'error' : ''}`}>
<textarea
className="feedback-form-textarea"
placeholder="请输入问题详情"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={4}
maxLength={500}
/>
</div>
{errors.description && <span className="feedback-form-error">{errors.description}</span>}
</div>
{/* 附件上传 */}
<div className="feedback-form-item">
<div className="feedback-form-label">
<span>附件</span>
</div>
<div className="feedback-form-upload">
<Upload.Dragger
className="feedback-upload-dragger"
action={`${httpUrl}/api/attachments.json`}
beforeUpload={beforeUpload}
onChange={handleFileChange}
fileList={fileList}
multiple={true}
>
<p className="ant-upload-drag-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L12 14M12 2L8 6M12 2L16 6" stroke="#4953e6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M2 17L2 19C2 20.1046 2.89543 21 4 21L20 21C21.1046 21 22 20.1046 22 19L22 17" stroke="#4953e6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</p>
<p className="feedback-upload-text">点击或拖拽文件到此区域上传</p>
<p className="feedback-upload-hint">文件大小不超过 100MB</p>
</Upload.Dragger>
</div>
</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"
onClick={handleSubmit}
disabled={isSubmitting}
>
{isSubmitting ? '提交中...' : '提交'}
</button>
</div>
</div>
</div>
);
}
export default FeedbackModal;

View File

@ -0,0 +1,472 @@
// 反馈中心-新增弹窗样式
// 遵循 factory 目录下的 SCSS 规范
// 颜色变量
$primary-color: #4953e6;
$primary-hover: #3a42d1;
$text-primary: #091221;
$text-secondary: #223452;
$text-muted: #9494b4;
$border-color: #cfd5e8;
$border-focus: #4953e6;
$bg-white: #ffffff;
$bg-mask: rgba(0, 0, 0, 0.4);
$error-color: #ff4d4f;
// 尺寸变量
$modal-width: 900px;
$modal-height: 551px;
$content-width: 860px;
$content-height: 446px;
$border-radius-modal: 30px;
$border-radius-content: 30px;
$border-radius-input: 8px;
$border-radius-btn: 9px;
// 动画变量
$transition-duration: 0.3s;
// 蒙层
.feedback-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: $bg-mask;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn $transition-duration ease;
}
// 弹窗容器
.feedback-modal {
position: relative;
width: $modal-width;
height: $modal-height;
border-radius: $border-radius-modal;
overflow: hidden;
animation: slideUp $transition-duration ease;
}
// 弹窗背景
.feedback-modal-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
// 添加渐变叠加层以增强文字可读性
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
180deg,
rgba(73, 83, 230, 0.1) 0%,
rgba(73, 83, 230, 0.05) 100%
);
}
}
// 弹窗头部
.feedback-modal-header {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 30px;
height: 81px;
}
// 弹窗标题
.feedback-modal-title {
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 20px;
font-weight: 500;
color: $bg-white;
text-align: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
// 关闭按钮
.feedback-modal-close {
position: absolute;
right: 30px;
top: 50%;
transform: translateY(-50%);
width: 30px;
height: 30px;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: transform $transition-duration ease;
img {
width: 20px;
height: 20px;
filter: brightness(0) invert(1);
}
&:hover {
transform: translateY(-50%) rotate(90deg);
}
}
// 弹窗内容区域
.feedback-modal-content {
position: relative;
z-index: 1;
width: $content-width;
height: $content-height;
margin: 0 auto;
background-color: $bg-white;
border-radius: $border-radius-content;
padding: 40px;
display: flex;
flex-direction: column;
gap: 24px;
}
// 表单项
.feedback-form-item {
display: flex;
flex-direction: column;
gap: 8px;
}
// 表单标签
.feedback-form-label {
display: flex;
align-items: center;
gap: 4px;
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 14px;
color: $text-primary;
line-height: 1.5;
.required-icon {
width: 10px;
height: 10px;
}
}
// 输入框容器
.feedback-form-input-wrapper,
.feedback-form-textarea-wrapper {
width: 100%;
border: 1px solid $border-color;
border-radius: $border-radius-input;
transition: border-color $transition-duration ease, box-shadow $transition-duration ease;
&:hover {
border-color: darken($border-color, 10%);
}
&:focus-within {
border-color: $border-focus;
box-shadow: 0 0 0 2px rgba($border-focus, 0.1);
}
&.error {
border-color: $error-color;
box-shadow: 0 0 0 2px rgba($error-color, 0.1);
}
}
// 输入框
.feedback-form-input {
width: 100%;
height: 36px;
padding: 8px 16px;
border: none;
background: transparent;
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 14px;
color: $text-primary;
line-height: 1.5;
outline: none;
&::placeholder {
color: $text-muted;
}
}
// 文本域
.feedback-form-textarea {
width: 100%;
min-height: 80px;
padding: 12px 16px;
border: none;
background: transparent;
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 14px;
color: $text-primary;
line-height: 1.6;
outline: none;
resize: vertical;
&::placeholder {
color: $text-muted;
}
}
// 错误提示
.feedback-form-error {
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 12px;
color: $error-color;
line-height: 1.4;
}
// 上传区域
.feedback-form-upload {
width: 100%;
}
// Ant Design Upload.Dragger 样式覆盖
.feedback-upload-dragger {
.ant-upload {
padding: 20px !important;
background: #fafafa !important;
border: 1px dashed $border-color !important;
border-radius: 8px !important;
transition: all $transition-duration ease;
&:hover {
border-color: $primary-color !important;
background: rgba($primary-color, 0.02) !important;
}
&.ant-upload-drag-hover {
border-color: $primary-color !important;
background: rgba($primary-color, 0.05) !important;
}
}
.ant-upload-drag-icon {
margin-bottom: 12px;
svg {
display: block;
margin: 0 auto;
}
}
.feedback-upload-text {
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 14px;
color: $text-primary;
line-height: 1.5;
margin-bottom: 4px;
}
.feedback-upload-hint {
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 12px;
color: $text-muted;
line-height: 1.4;
}
// 文件列表样式
.ant-upload-list {
margin-top: 12px;
.ant-upload-list-item {
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 13px;
&:hover {
background-color: rgba($primary-color, 0.02);
}
}
}
}
// 隐藏文件输入
.feedback-form-file-input {
display: none;
}
// 上传按钮旧版保留兼容性
.feedback-form-upload-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
border: 1px solid $primary-color;
border-radius: 4px;
background-color: transparent;
cursor: pointer;
transition: all $transition-duration ease;
.upload-icon {
width: 14px;
height: 14px;
color: $primary-color;
}
span {
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 14px;
color: $primary-color;
line-height: 1.4;
}
&:hover {
background-color: rgba($primary-color, 0.05);
}
}
// 文件名旧版保留兼容性
.feedback-form-file-name {
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 13px;
color: $text-secondary;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 弹窗底部
.feedback-modal-footer {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 20px 0;
}
// 按钮基础样式
.feedback-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 101px;
height: 36px;
padding: 0 24px;
border-radius: $border-radius-btn;
font-family: 'Alibaba PuHuiTi', 'PingFang SC', sans-serif;
font-size: 14px;
line-height: 1.4;
cursor: pointer;
transition: all $transition-duration ease;
border: none;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
// 取消按钮
.feedback-btn-cancel {
background-color: transparent;
border: 1px solid $text-primary;
color: $text-primary;
&:hover:not(:disabled) {
background-color: rgba($text-primary, 0.05);
}
}
// 提交按钮
.feedback-btn-submit {
background-color: $text-primary;
color: $bg-white;
&:hover:not(:disabled) {
background-color: lighten($text-primary, 10%);
}
}
// 动画定义
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// 响应式适配
@media screen and (max-width: 992px) {
.feedback-modal {
width: 90vw;
max-width: 600px;
height: auto;
max-height: 90vh;
overflow-y: auto;
}
.feedback-modal-content {
width: auto;
height: auto;
margin: 0 20px;
padding: 24px;
}
.feedback-modal-header {
padding: 20px 24px;
height: auto;
}
.feedback-modal-close {
right: 20px;
}
}
@media screen and (max-width: 576px) {
.feedback-modal-content {
padding: 20px;
gap: 16px;
}
.feedback-modal-footer {
flex-direction: column;
gap: 12px;
padding: 16px 20px;
.feedback-btn {
width: 100%;
}
}
.feedback-form-upload {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import './index.scss';
import { Link } from 'react-router-dom';
import { TPMIndexHOC } from "../../modules/tpm/TPMIndexHOC";
import FeedbackModal from './FeedbackModal';
//
import headerBg from '../../images/factory/feedback/header-bg.webp';
@ -134,6 +135,7 @@ const mockAllFeedback = [
function FeedbackCenter(props) {
const [hotFeedback, setHotFeedback] = useState(mockHotFeedback);
const [allFeedback, setAllFeedback] = useState(mockAllFeedback);
const [modalVisible, setModalVisible] = useState(false);
const contentRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
@ -160,15 +162,28 @@ function FeedbackCenter(props) {
};
}, []);
//
const handleFeedbackClick = () => {
const { user, history } = props;
const { user } = props;
if (user && user.login) {
history.push('/sf/feedback/new');
setModalVisible(true);
} else {
history.push(`/login?go_page=/sf/feedback/new`);
const { history } = props;
history.push(`/login?go_page=/sf/feedback`);
}
};
//
const handleModalClose = () => {
setModalVisible(false);
};
//
const handleModalSuccess = (ret) => {
//
console.log('反馈提交成功:', ret);
};
return (
<div className="feedback-center" ref={contentRef}>
{/* 顶部横幅区域 */}
@ -301,6 +316,13 @@ function FeedbackCenter(props) {
</div>
</div>
</div>
{/* 反馈弹窗 */}
<FeedbackModal
visible={modalVisible}
onClose={handleModalClose}
onSuccess={handleModalSuccess}
/>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB