diff --git a/src/factory/feedback/FeedbackModal.jsx b/src/factory/feedback/FeedbackModal.jsx
new file mode 100644
index 00000000..5456ca99
--- /dev/null
+++ b/src/factory/feedback/FeedbackModal.jsx
@@ -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 (
+
+
+ {/* 弹窗背景 */}
+
+

+
+
+ {/* 弹窗头部 */}
+
+
意见反馈
+
+
+
+ {/* 弹窗内容 */}
+
+ {/* 标题输入 */}
+
+
+

+
标题
+
+
+ handleInputChange('title', e.target.value)}
+ maxLength={500}
+ />
+
+ {errors.title &&
{errors.title}}
+
+
+ {/* 问题描述输入 */}
+
+
+

+
问题描述
+
+
+
+ {errors.description &&
{errors.description}}
+
+
+ {/* 附件上传 */}
+
+
+ 附件
+
+
+
+
+
+
+ 点击或拖拽文件到此区域上传
+ 文件大小不超过 100MB
+
+
+
+
+
+ {/* 弹窗底部 */}
+
+
+
+
+
+
+ );
+}
+
+export default FeedbackModal;
diff --git a/src/factory/feedback/FeedbackModal.scss b/src/factory/feedback/FeedbackModal.scss
new file mode 100644
index 00000000..a9a42be0
--- /dev/null
+++ b/src/factory/feedback/FeedbackModal.scss
@@ -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;
+ }
+}
diff --git a/src/factory/feedback/index.jsx b/src/factory/feedback/index.jsx
index e5f0117c..2c3324d6 100644
--- a/src/factory/feedback/index.jsx
+++ b/src/factory/feedback/index.jsx
@@ -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 (
{/* 顶部横幅区域 */}
@@ -301,6 +316,13 @@ function FeedbackCenter(props) {
+
+ {/* 反馈弹窗 */}
+
);
}
diff --git a/src/images/factory/feedback/icon-close-new.webp b/src/images/factory/feedback/icon-close-new.webp
new file mode 100644
index 00000000..c0b0d46f
Binary files /dev/null and b/src/images/factory/feedback/icon-close-new.webp differ
diff --git a/src/images/factory/feedback/icon-close.webp b/src/images/factory/feedback/icon-close.webp
new file mode 100644
index 00000000..c0b0d46f
Binary files /dev/null and b/src/images/factory/feedback/icon-close.webp differ
diff --git a/src/images/factory/feedback/icon-required-new.webp b/src/images/factory/feedback/icon-required-new.webp
new file mode 100644
index 00000000..73bbd823
Binary files /dev/null and b/src/images/factory/feedback/icon-required-new.webp differ
diff --git a/src/images/factory/feedback/icon-required.webp b/src/images/factory/feedback/icon-required.webp
new file mode 100644
index 00000000..73bbd823
Binary files /dev/null and b/src/images/factory/feedback/icon-required.webp differ
diff --git a/src/images/factory/feedback/modal-bg.webp b/src/images/factory/feedback/modal-bg.webp
new file mode 100644
index 00000000..90f80b72
Binary files /dev/null and b/src/images/factory/feedback/modal-bg.webp differ