From a4be3d534fe71815977a29cebfb1187880d4f8b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Mon, 4 Aug 2025 15:11:27 +0800 Subject: [PATCH] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=BB=84=E4=BB=B6=E4=BD=BF=E7=94=A8=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE=E5=9B=9E?= =?UTF-8?q?=E6=98=BE=E5=8A=9F=E8=83=BD=E7=A4=BA=E4=BE=8B=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=86=97=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/Upload/README.md | 263 ++++++++++++++-- nkebao/src/pages/login/login.tsx | 13 +- .../plan/new/steps/MessageSettings.tsx | 295 +++++------------- 3 files changed, 316 insertions(+), 255 deletions(-) diff --git a/nkebao/src/components/Upload/README.md b/nkebao/src/components/Upload/README.md index ac467f61..9920f729 100644 --- a/nkebao/src/components/Upload/README.md +++ b/nkebao/src/components/Upload/README.md @@ -1,22 +1,28 @@ # Upload 组件使用说明 -## MainImgUpload 主图封面上传组件 +## 组件概述 -### 功能特点 +本项目提供了多个专门的上传组件,所有组件都支持编辑时的数据回显功能,确保在编辑模式下能够正确显示已上传的文件。 +## 组件列表 + +### 1. MainImgUpload 主图封面上传组件 + +#### 功能特点 - 只支持上传一张图片作为主图封面 - 上传后右上角显示删除按钮 - 支持图片预览功能 - 响应式设计,适配移动端 -- 样式参考VideoUpload组件风格 +- 16:9宽高比,宽度高度自适应 +- **支持数据回显**:编辑时自动显示已上传的图片 -### 使用方法 +#### 使用方法 ```tsx -import MainImgUpload from "@/components/Upload/MainImgUpload"; +import MainImgUpload from '@/components/Upload/MainImgUpload'; const MyComponent = () => { - const [mainImage, setMainImage] = useState(""); + const [mainImage, setMainImage] = useState(''); return ( { }; ``` -### Props 参数 +#### 编辑模式数据回显 +```tsx +// 编辑模式下,传入已有的图片URL +const [mainImage, setMainImage] = useState('https://example.com/image.jpg'); -| 参数 | 类型 | 默认值 | 说明 | -| ----------- | --------------------- | ------ | ---------------- | -| value | string | '' | 当前图片URL | -| onChange | (url: string) => void | - | 图片URL变化回调 | -| disabled | boolean | false | 是否禁用 | -| className | string | - | 自定义样式类名 | -| maxSize | number | 5 | 最大文件大小(MB) | -| showPreview | boolean | true | 是否显示预览按钮 | - -### 样式特点 - -- 上传区域:200x200px 的虚线边框区域 -- 图片预览:上传后显示图片,鼠标悬停显示操作按钮 -- 删除按钮:右上角红色删除图标 -- 预览按钮:眼睛图标,点击在新窗口预览 -- 响应式:移动端自动调整尺寸 - -### 文件结构 - -``` -src/components/Upload/ -├── MainImgUpload.tsx # 主图上传组件 -├── mainImgUpload.module.scss # 主图上传样式 -├── VideoUpload.tsx # 视频上传组件 -└── index.module.scss # 通用上传样式 + ``` -### 技术实现 +### 2. ImageUpload 多图上传组件 +#### 功能特点 +- 支持多张图片上传 +- 可设置最大上传数量 +- 支持图片预览和删除 +- **支持数据回显**:编辑时自动显示已上传的图片数组 + +#### 使用方法 +```tsx +import ImageUpload from '@/components/Upload/ImageUpload/ImageUpload'; + +const MyComponent = () => { + const [images, setImages] = useState([]); + + return ( + + ); +}; +``` + +#### 编辑模式数据回显 +```tsx +// 编辑模式下,传入已有的图片URL数组 +const [images, setImages] = useState([ + 'https://example.com/image1.jpg', + 'https://example.com/image2.jpg' +]); + + +``` + +### 3. VideoUpload 视频上传组件 + +#### 功能特点 +- 支持视频文件上传 +- 支持单个或多个视频 +- 视频预览功能 +- 文件大小验证 +- **支持数据回显**:编辑时自动显示已上传的视频 + +#### 使用方法 +```tsx +import VideoUpload from '@/components/Upload/VideoUpload'; + +const MyComponent = () => { + const [videoUrl, setVideoUrl] = useState(''); + + return ( + + ); +}; +``` + +#### 编辑模式数据回显 +```tsx +// 编辑模式下,传入已有的视频URL +const [videoUrl, setVideoUrl] = useState('https://example.com/video.mp4'); + + +``` + +### 4. FileUpload 文件上传组件 + +#### 功能特点 +- 支持Excel、Word、PPT等文档文件 +- 可配置接受的文件类型 +- 文件预览和下载 +- **支持数据回显**:编辑时自动显示已上传的文件 + +#### 使用方法 +```tsx +import FileUpload from '@/components/Upload/FileUpload'; + +const MyComponent = () => { + const [fileUrl, setFileUrl] = useState(''); + + return ( + + ); +}; +``` + +#### 编辑模式数据回显 +```tsx +// 编辑模式下,传入已有的文件URL +const [fileUrl, setFileUrl] = useState('https://example.com/document.xlsx'); + + +``` + +### 5. AvatarUpload 头像上传组件 + +#### 功能特点 +- 专门的头像上传组件 +- 圆形头像显示 +- 支持删除和重新上传 +- **支持数据回显**:编辑时自动显示已上传的头像 + +#### 使用方法 +```tsx +import AvatarUpload from '@/components/Upload/AvatarUpload'; + +const MyComponent = () => { + const [avatarUrl, setAvatarUrl] = useState(''); + + return ( + + ); +}; +``` + +#### 编辑模式数据回显 +```tsx +// 编辑模式下,传入已有的头像URL +const [avatarUrl, setAvatarUrl] = useState('https://example.com/avatar.jpg'); + + +``` + +## 数据回显机制 + +### 工作原理 +所有Upload组件都通过以下机制实现数据回显: + +1. **useEffect监听value变化**:当传入的value发生变化时,自动更新内部状态 +2. **文件列表同步**:将URL转换为文件列表格式,显示已上传的文件 +3. **状态管理**:维护上传状态、文件列表等内部状态 +4. **UI更新**:根据文件列表自动更新界面显示 + +### 使用场景 +- **新增模式**:value为空或未定义,显示上传按钮 +- **编辑模式**:value包含已上传文件的URL,自动显示文件 +- **混合模式**:支持部分文件已上传,部分文件待上传 + +### 注意事项 +1. **URL格式**:确保传入的URL是有效的文件访问地址 +2. **权限验证**:确保文件URL在编辑时仍然可访问 +3. **状态同步**:value和onChange需要正确配合使用 +4. **错误处理**:组件会自动处理无效URL的显示 + +## 技术实现 + +### 核心特性 - 基于 antd Upload 组件 - 使用 antd-mobile 的 Toast 提示 - 支持 FormData 上传 - 自动处理文件验证和错误提示 - 集成项目统一的API请求封装 +- **完整的数据回显支持** + +### 文件结构 +``` +src/components/Upload/ +├── MainImgUpload/ # 主图上传组件 +├── ImageUpload/ # 多图上传组件 +├── VideoUpload/ # 视频上传组件 +├── FileUpload/ # 文件上传组件 +├── AvatarUpload/ # 头像上传组件 +└── README.md # 使用说明文档 +``` + +### 统一的数据回显模式 +所有组件都遵循相同的数据回显模式: +```tsx +// 1. 接收value属性 +interface Props { + value?: string | string[]; + onChange?: (url: string | string[]) => void; +} + +// 2. 使用useEffect监听value变化 +useEffect(() => { + if (value) { + // 将URL转换为文件列表格式 + const files = convertUrlToFileList(value); + setFileList(files); + } else { + setFileList([]); + } +}, [value]); + +// 3. 在UI中显示文件列表 +// 4. 支持编辑、删除、预览等操作 +``` diff --git a/nkebao/src/pages/login/login.tsx b/nkebao/src/pages/login/login.tsx index 1bc13773..c9d83114 100644 --- a/nkebao/src/pages/login/login.tsx +++ b/nkebao/src/pages/login/login.tsx @@ -108,18 +108,7 @@ const Login: React.FC = () => { // 根据设备数量判断跳转 if (deviceTotal > 0) { - // 有设备,跳转到首页或重定向URL - const returnUrl = searchParams.get("returnUrl"); - if (returnUrl) { - const decodedUrl = decodeURIComponent(returnUrl); - if (isLoginPage(decodedUrl)) { - navigate("/"); - } else { - window.location.href = decodedUrl; - } - } else { - navigate("/"); - } + navigate("/"); } else { // 没有设备,跳转到引导页面 navigate("/guide"); diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx index 6ce86565..dcc79795 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx @@ -1,9 +1,8 @@ -import React, { useState, useRef } from "react"; -import { Input, Button, Tabs, Modal, Alert, message } from "antd"; +import React, { useState } from "react"; +import { Input, Button, Tabs, Modal, message } from "antd"; import { PlusOutlined, CloseOutlined, - UploadOutlined, ClockCircleOutlined, MessageOutlined, PictureOutlined, @@ -14,7 +13,13 @@ import { TeamOutlined, } from "@ant-design/icons"; import styles from "./messages.module.scss"; -import { uploadFile } from "@/api/common"; +// 导入Upload组件 +import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload"; +import VideoUpload from "@/components/Upload/VideoUpload"; +import FileUpload from "@/components/Upload/FileUpload"; +import MainImgUpload from "@/components/Upload/MainImgUpload"; +// 导入GroupSelection组件 +import GroupSelection from "@/components/GroupSelection"; interface MessageContent { id: string; @@ -31,7 +36,7 @@ interface MessageContent { description?: string; address?: string; coverImage?: string; - groupId?: string; + groupIds?: string[]; // 改为数组以支持GroupSelection组件 linkUrl?: string; } @@ -58,13 +63,6 @@ const messageTypes = [ { id: "group", icon: TeamOutlined, label: "邀请入群" }, ]; -// 模拟群组数据 -const mockGroups = [ - { id: "1", name: "产品交流群1", memberCount: 156 }, - { id: "2", name: "产品交流群2", memberCount: 234 }, - { id: "3", name: "产品交流群3", memberCount: 89 }, -]; - const MessageSettings: React.FC = ({ formData, onChange, @@ -86,15 +84,6 @@ const MessageSettings: React.FC = ({ }, ]); const [isAddDayPlanOpen, setIsAddDayPlanOpen] = useState(false); - const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false); - const [selectedGroupId, setSelectedGroupId] = useState(""); - const fileInputRef = useRef(null); - const [uploadingIndex, setUploadingIndex] = useState(null); - const [uploadingType, setUploadingType] = useState< - "miniprogram" | "link" | null - >(null); - const [uploadingDay, setUploadingDay] = useState(null); - const [uploadingMsgIdx, setUploadingMsgIdx] = useState(null); // 添加新消息 const handleAddMessage = (dayIndex: number, type = "text") => { @@ -176,61 +165,6 @@ const MessageSettings: React.FC = ({ message.success(`已添加第${newDay}天的消息计划`); }; - // 选择群组 - const handleSelectGroup = (groupId: string) => { - setSelectedGroupId(groupId); - setIsGroupSelectOpen(false); - message.success( - `已选择群组:${mockGroups.find(g => g.id === groupId)?.name}`, - ); - }; - - // 触发文件选择 - const triggerUpload = ( - dayIdx: number, - msgIdx: number, - type: "miniprogram" | "link", - ) => { - setUploadingDay(dayIdx); - setUploadingMsgIdx(msgIdx); - setUploadingType(type); - setTimeout(() => { - fileInputRef.current?.click(); - }, 0); - }; - - // 处理文件上传 - const handleFileChange = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if ( - !file || - uploadingDay === null || - uploadingMsgIdx === null || - !uploadingType - ) - return; - setUploadingIndex(`${uploadingDay}-${uploadingMsgIdx}`); - try { - const url = await uploadFile(file); - // 更新对应消息的coverImage - setDayPlans(prev => { - const newPlans = [...prev]; - const msg = newPlans[uploadingDay].messages[uploadingMsgIdx]; - msg.coverImage = url; - return newPlans; - }); - message.success("上传成功"); - } catch (err) { - message.error("上传失败"); - } finally { - setUploadingIndex(null); - setUploadingType(null); - setUploadingDay(null); - setUploadingMsgIdx(null); - if (fileInputRef.current) fileInputRef.current.value = ""; - } - }; - const items = dayPlans.map((plan, dayIndex) => ({ key: plan.day.toString(), label: plan.day === 0 ? "即时消息" : `第${plan.day}天`, @@ -404,43 +338,16 @@ const MessageSettings: React.FC = ({ style={{ marginBottom: 8 }} />
- {message.coverImage ? ( -
- 封面 - -
- ) : ( - - )} + + handleUpdateMessage(dayIndex, messageIndex, { + coverImage: url, + }) + } + maxSize={5} + showPreview={true} + />
)} @@ -478,78 +385,81 @@ const MessageSettings: React.FC = ({ style={{ marginBottom: 8 }} />
- {message.coverImage ? ( -
- 封面 - -
- ) : ( - - )} + + handleUpdateMessage(dayIndex, messageIndex, { + coverImage: url, + }) + } + maxSize={5} + showPreview={true} + />
)} {/* 群邀请消息 */} {message.type === "group" && (
- + + handleUpdateMessage(dayIndex, messageIndex, { + groupIds: groupIds, + }) + } + placeholder="选择邀请入的群" + showSelectedList={true} + selectedListMaxHeight={200} + />
)} - {/* 图片/视频/文件消息 */} - {(message.type === "image" || - message.type === "video" || - message.type === "file") && ( + {/* 图片消息 */} + {message.type === "image" && (
- + count={1} + accept="image/*" + /> +
+ )} + {/* 视频消息 */} + {message.type === "video" && ( +
+ { + const videoUrl = Array.isArray(url) ? url[0] || "" : url; + handleUpdateMessage(dayIndex, messageIndex, { + content: videoUrl, + }); + }} + maxSize={50} + showPreview={true} + /> +
+ )} + {/* 文件消息 */} + {message.type === "file" && ( +
+ { + const fileUrl = Array.isArray(url) ? url[0] || "" : url; + handleUpdateMessage(dayIndex, messageIndex, { + content: fileUrl, + }); + }} + maxSize={10} + showPreview={true} + acceptTypes={["excel", "word", "ppt"]} + />
)} @@ -603,41 +513,6 @@ const MessageSettings: React.FC = ({ 添加第 {dayPlans.length} 天计划 - {/* 选择群聊弹窗 */} - setIsGroupSelectOpen(false)} - onOk={() => { - handleSelectGroup(selectedGroupId); - setIsGroupSelectOpen(false); - }} - > -
- {mockGroups.map(group => ( -
handleSelectGroup(group.id)} - > -
{group.name}
-
- 成员数:{group.memberCount} -
-
- ))} -
-
- ); };