Merge branch 'yongpxu-dev' into yongxu-dev3
# Conflicts: # nkebao/vite.config.ts resolved by yongpxu-dev version
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
# 基础环境变量示例
|
||||
VITE_API_BASE_URL=http://www.yishi.com
|
||||
# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
|
||||
# VITE_API_BASE_URL=http://www.yishi.com
|
||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
VITE_APP_TITLE=Nkebao Base
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ module.exports = {
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:prettier/recommended", // 这个配置会自动处理大部分冲突
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
@@ -32,8 +32,29 @@ module.exports = {
|
||||
"eol-last": "off",
|
||||
"no-empty": "warn",
|
||||
"prefer-const": "warn",
|
||||
// 移除与Prettier冲突的规则
|
||||
// 确保与 Prettier 完全兼容
|
||||
"comma-dangle": "off",
|
||||
"comma-spacing": "off",
|
||||
"comma-style": "off",
|
||||
"object-curly-spacing": "off",
|
||||
"array-bracket-spacing": "off",
|
||||
indent: "off",
|
||||
quotes: "off",
|
||||
semi: "off",
|
||||
"arrow-parens": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"max-len": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"space-before-blocks": "off",
|
||||
"keyword-spacing": "off",
|
||||
"space-infix-ops": "off",
|
||||
"space-in-parens": "off",
|
||||
"space-in-brackets": "off",
|
||||
"object-property-newline": "off",
|
||||
"array-element-newline": "off",
|
||||
"function-paren-newline": "off",
|
||||
"object-curly-newline": "off",
|
||||
"array-bracket-newline": "off",
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"trailingComma": "all",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
@@ -10,4 +10,4 @@
|
||||
"arrowParens": "avoid",
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "as-needed"
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build": "vite build",
|
||||
"build:check": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,scss,css}\"",
|
||||
|
||||
@@ -1,150 +1,112 @@
|
||||
# Upload 组件
|
||||
# Upload 上传组件
|
||||
|
||||
通用的文件上传组件,基于 Ant Design 的 Upload 组件封装。
|
||||
基于 antd-mobile 的 ImageUploader 组件封装的上传组件,支持图片上传、预览、删除等功能。
|
||||
|
||||
## 组件列表
|
||||
## 功能特性
|
||||
|
||||
### UploadComponent
|
||||
- ✅ 支持单张/多张图片上传
|
||||
- ✅ 文件类型和大小验证
|
||||
- ✅ 上传进度显示
|
||||
- ✅ 图片预览功能
|
||||
- ✅ 删除确认
|
||||
- ✅ 数量限制
|
||||
- ✅ 编辑和新增状态支持
|
||||
- ✅ 响应式设计
|
||||
|
||||
通用的图片/文件上传组件,支持多文件上传。
|
||||
## 使用方法
|
||||
|
||||
#### 使用方法
|
||||
### 基础用法
|
||||
|
||||
```tsx
|
||||
import UploadComponent from '@/components/Upload';
|
||||
import React, { useState } from "react";
|
||||
import UploadComponent from "@/components/Upload";
|
||||
|
||||
// 基础用法
|
||||
<UploadComponent
|
||||
value={imageUrls}
|
||||
onChange={setImageUrls}
|
||||
count={9}
|
||||
accept="image/*"
|
||||
/>
|
||||
const MyComponent = () => {
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
|
||||
// 单文件上传
|
||||
<UploadComponent
|
||||
value={[singleImage]}
|
||||
onChange={(urls) => setSingleImage(urls[0])}
|
||||
count={1}
|
||||
listType="picture-card"
|
||||
/>
|
||||
return (
|
||||
<UploadComponent
|
||||
value={images}
|
||||
onChange={setImages}
|
||||
count={5}
|
||||
accept="image/*"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| value | 已上传的文件URL数组 | `string[]` | `[]` |
|
||||
| onChange | 文件列表变化时的回调 | `(urls: string[]) => void` | - |
|
||||
| count | 最大上传数量 | `number` | `9` |
|
||||
| accept | 接受的文件类型 | `string` | `"image/*"` |
|
||||
| listType | 上传列表的内建样式 | `"text" \| "picture" \| "picture-card"` | `"picture-card"` |
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| className | 自定义样式类名 | `string` | - |
|
||||
|
||||
### VideoUpload
|
||||
|
||||
专门的视频上传组件,支持单文件上传。
|
||||
|
||||
#### 使用方法
|
||||
### 编辑模式
|
||||
|
||||
```tsx
|
||||
import VideoUpload from '@/components/Upload/VideoUpload';
|
||||
const EditComponent = () => {
|
||||
const [images, setImages] = useState<string[]>([
|
||||
"https://example.com/image1.jpg",
|
||||
"https://example.com/image2.jpg",
|
||||
]);
|
||||
|
||||
<VideoUpload
|
||||
value={videoUrl}
|
||||
onChange={setVideoUrl}
|
||||
disabled={false}
|
||||
/>
|
||||
return (
|
||||
<UploadComponent
|
||||
value={images}
|
||||
onChange={setImages}
|
||||
count={9}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Props
|
||||
### 禁用状态
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| value | 已上传的视频URL | `string` | `""` |
|
||||
| onChange | 视频URL变化时的回调 | `(url: string) => void` | - |
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| className | 自定义样式类名 | `string` | - |
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 上传接口
|
||||
|
||||
- **接口地址**: `/v1/attachment/upload`
|
||||
- **请求方式**: `POST`
|
||||
- **请求格式**: `multipart/form-data`
|
||||
- **文件字段名**: `file`
|
||||
|
||||
### 响应格式
|
||||
|
||||
组件会自动处理以下响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": "https://example.com/file.jpg",
|
||||
"message": "success"
|
||||
}
|
||||
```tsx
|
||||
<UploadComponent value={images} onChange={setImages} disabled={true} />
|
||||
```
|
||||
|
||||
或者:
|
||||
## API
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"url": "https://example.com/file.jpg"
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
### Props
|
||||
|
||||
### 文件限制
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------- | -------------- | -------------------------- | ----------- |
|
||||
| value | 图片URL数组 | `string[]` | `[]` |
|
||||
| onChange | 图片变化回调 | `(urls: string[]) => void` | - |
|
||||
| count | 最大上传数量 | `number` | `9` |
|
||||
| accept | 接受的文件类型 | `string` | `"image/*"` |
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| className | 自定义类名 | `string` | - |
|
||||
|
||||
- **图片文件**: 最大 5MB
|
||||
- **视频文件**: 最大 50MB
|
||||
- **支持格式**: 根据 `accept` 属性设置
|
||||
### 事件
|
||||
|
||||
### 状态管理
|
||||
|
||||
组件内部管理以下状态:
|
||||
|
||||
- `uploading`: 文件上传中
|
||||
- `done`: 文件上传完成
|
||||
- `error`: 文件上传失败
|
||||
- `removed`: 文件已删除
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| -------- | ------------------ | -------------------------- |
|
||||
| onChange | 图片列表变化时触发 | `(urls: string[]) => void` |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API 集成**: 组件使用 Ant Design Upload 的 `action` 属性,直接调用 `/v1/attachment/upload` 接口
|
||||
2. **文件验证**: 在 `beforeUpload` 中进行文件类型和大小验证
|
||||
3. **状态同步**: 组件会自动同步 `value` 和内部 `fileList` 状态
|
||||
4. **错误处理**: 上传失败时会显示错误提示并自动清理失败的文件
|
||||
5. **进度显示**: 上传过程中会显示加载状态和进度指示
|
||||
1. **文件大小限制**: 默认限制为 5MB
|
||||
2. **文件类型**: 默认只接受图片文件
|
||||
3. **上传接口**: 使用 `/v1/attachment/upload` 接口
|
||||
4. **认证**: 自动携带 token 进行认证
|
||||
5. **预览**: 点击图片可预览
|
||||
6. **删除**: 删除图片会有确认提示
|
||||
|
||||
## 样式定制
|
||||
|
||||
组件使用 SCSS Modules,可以通过修改 `index.module.scss` 文件来自定义样式:
|
||||
组件支持通过 CSS 模块进行样式定制:
|
||||
|
||||
```scss
|
||||
.upload-container {
|
||||
// 容器样式
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
// 上传按钮样式
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
// 图标样式
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
// 文字样式
|
||||
}
|
||||
|
||||
.uploading {
|
||||
// 上传中状态样式
|
||||
.uploadContainer {
|
||||
// 自定义样式
|
||||
:global {
|
||||
.adm-image-uploader {
|
||||
// 覆盖 antd-mobile 默认样式
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 文件类型不匹配时会显示错误提示
|
||||
- 文件大小超限时会显示错误提示
|
||||
- 上传失败时会显示错误提示
|
||||
- 网络错误时会显示错误提示
|
||||
|
||||
@@ -1,75 +1,108 @@
|
||||
.upload-container {
|
||||
.uploadContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
// 自定义上传组件样式
|
||||
:global {
|
||||
.adm-image-uploader {
|
||||
.adm-image-uploader-upload-button {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.adm-image-uploader-upload-button-icon {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.adm-image-uploader-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.uploading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.adm-image-uploader-item-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
color: #1677ff;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.adm-image-uploader-item-delete {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 覆盖antd默认样式
|
||||
:global {
|
||||
.ant-upload-list-picture-card {
|
||||
.ant-upload-list-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
.adm-image-uploader-item-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
// 禁用状态
|
||||
.uploadContainer.disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
.uploadContainer.error {
|
||||
:global {
|
||||
.adm-image-uploader-upload-button {
|
||||
border-color: #ff4d4f;
|
||||
background: #fff2f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.uploadContainer {
|
||||
:global {
|
||||
.adm-image-uploader {
|
||||
.adm-image-uploader-upload-button,
|
||||
.adm-image-uploader-item {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.adm-image-uploader-upload-button-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Upload, message } from "antd";
|
||||
import { LoadingOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import type { UploadProps, UploadFile } from "antd/es/upload/interface";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ImageUploader, Toast, Dialog } from "antd-mobile";
|
||||
import type { ImageUploadItem } from "antd-mobile/es/components/image-uploader";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface UploadComponentProps {
|
||||
@@ -9,7 +8,6 @@ interface UploadComponentProps {
|
||||
onChange?: (urls: string[]) => void;
|
||||
count?: number; // 最大上传数量
|
||||
accept?: string; // 文件类型
|
||||
listType?: "text" | "picture" | "picture-card";
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
@@ -19,21 +17,17 @@ const UploadComponent: React.FC<UploadComponentProps> = ({
|
||||
onChange,
|
||||
count = 9,
|
||||
accept = "image/*",
|
||||
listType = "picture-card",
|
||||
disabled = false,
|
||||
className,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [fileList, setFileList] = useState<ImageUploadItem[]>([]);
|
||||
|
||||
// 将value转换为fileList格式
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (value && value.length > 0) {
|
||||
const files = value.map((url, index) => ({
|
||||
url: url || "",
|
||||
uid: `file-${index}`,
|
||||
name: `file-${index}`,
|
||||
status: "done" as const,
|
||||
url: url || "", // 确保 URL 不为 undefined
|
||||
}));
|
||||
setFileList(files);
|
||||
} else {
|
||||
@@ -43,89 +37,97 @@ const UploadComponent: React.FC<UploadComponentProps> = ({
|
||||
|
||||
// 文件验证
|
||||
const beforeUpload = (file: File) => {
|
||||
// 检查文件类型
|
||||
const isValidType = file.type.startsWith(accept.replace("*", ""));
|
||||
if (!isValidType) {
|
||||
message.error(`只能上传${accept}格式的文件!`);
|
||||
return false;
|
||||
Toast.show(`只能上传${accept}格式的文件!`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查文件大小 (5MB)
|
||||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||
if (!isLt5M) {
|
||||
message.error("文件大小不能超过5MB!");
|
||||
return false;
|
||||
Toast.show("文件大小不能超过5MB!");
|
||||
return null;
|
||||
}
|
||||
|
||||
return true; // 允许上传
|
||||
return file;
|
||||
};
|
||||
|
||||
// 上传函数
|
||||
const upload = async (file: File): Promise<{ url: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/v1/attachment/upload`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
Toast.show("上传成功");
|
||||
return { url: result.data.url || result.data };
|
||||
} else {
|
||||
throw new Error(result.msg || "上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show("上传失败,请重试");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文件变化
|
||||
const handleChange: UploadProps["onChange"] = info => {
|
||||
// 更新 fileList,确保所有 URL 都是字符串
|
||||
const updatedFileList = info.fileList.map(file => ({
|
||||
...file,
|
||||
url:
|
||||
file.url ||
|
||||
file.response?.data ||
|
||||
file.response?.url ||
|
||||
file.response ||
|
||||
"",
|
||||
}));
|
||||
const handleChange = (files: ImageUploadItem[]) => {
|
||||
setFileList(files);
|
||||
|
||||
setFileList(updatedFileList);
|
||||
// 提取URL数组并传递给父组件
|
||||
const urls = files
|
||||
.map(file => file.url)
|
||||
.filter(url => Boolean(url)) as string[];
|
||||
|
||||
// 处理上传状态
|
||||
if (info.file.status === "uploading") {
|
||||
setLoading(true);
|
||||
} else if (info.file.status === "done") {
|
||||
setLoading(false);
|
||||
message.success("上传成功");
|
||||
console.log(info.file.response);
|
||||
// 从响应中获取上传后的URL
|
||||
const uploadedUrl = info.file.response?.data?.url || "";
|
||||
if (uploadedUrl) {
|
||||
onChange?.([uploadedUrl]);
|
||||
}
|
||||
} else if (info.file.status === "error") {
|
||||
setLoading(false);
|
||||
message.error("上传失败,请重试");
|
||||
} else if (info.file.status === "removed") {
|
||||
// 文件被删除
|
||||
const urls = updatedFileList
|
||||
.map(f => f.url || "")
|
||||
.filter(Boolean) as string[];
|
||||
onChange?.(urls);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
const newFileList = fileList.filter(f => f.uid !== file.uid);
|
||||
setFileList(newFileList);
|
||||
const urls = newFileList.map(f => f.url || "").filter(Boolean) as string[];
|
||||
onChange?.(urls);
|
||||
return true;
|
||||
};
|
||||
|
||||
const action = import.meta.env.VITE_API_BASE_URL + "/v1/attachment/upload";
|
||||
// 删除确认
|
||||
const handleDelete = () => {
|
||||
return Dialog.confirm({
|
||||
content: "确定要删除这张图片吗?",
|
||||
});
|
||||
};
|
||||
|
||||
// 数量超出限制
|
||||
const handleCountExceed = (exceed: number) => {
|
||||
Toast.show(`最多选择 ${count} 张图片,你多选了 ${exceed} 张`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Upload
|
||||
name="file"
|
||||
headers={{
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
}}
|
||||
action={action}
|
||||
multiple={count > 1}
|
||||
fileList={fileList}
|
||||
accept={accept}
|
||||
listType={listType}
|
||||
showUploadList={true}
|
||||
disabled={disabled || loading}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleChange}
|
||||
onRemove={handleRemove}
|
||||
maxCount={count}
|
||||
></Upload>
|
||||
<div className={`${style.uploadContainer} ${className || ""}`}>
|
||||
<ImageUploader
|
||||
value={fileList}
|
||||
onChange={handleChange}
|
||||
upload={upload}
|
||||
beforeUpload={beforeUpload}
|
||||
onDelete={handleDelete}
|
||||
onCountExceed={handleCountExceed}
|
||||
multiple={count > 1}
|
||||
maxCount={count}
|
||||
showUpload={fileList.length < count && !disabled}
|
||||
accept={accept}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Button, Toast, SpinLoading, Card } from "antd-mobile";
|
||||
import { Input, TimePicker, Select } from "antd";
|
||||
import { Input, Select } from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
SaveOutlined,
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
createContentItem,
|
||||
updateContentItem,
|
||||
} from "./api";
|
||||
import { ContentItem } from "./data";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
const { Option } = Select;
|
||||
@@ -43,7 +42,6 @@ const MaterialForm: React.FC = () => {
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [material, setMaterial] = useState<ContentItem | null>(null);
|
||||
|
||||
// 表单状态
|
||||
const [contentType, setContentType] = useState<number>(4);
|
||||
@@ -52,9 +50,6 @@ const MaterialForm: React.FC = () => {
|
||||
const [comment, setComment] = useState("");
|
||||
const [sendTime, setSendTime] = useState("");
|
||||
const [resUrls, setResUrls] = useState<string[]>([]);
|
||||
const [urls, setUrls] = useState<
|
||||
{ desc: string; image: string; url: string }[]
|
||||
>([]);
|
||||
|
||||
// 链接相关状态
|
||||
const [linkDesc, setLinkDesc] = useState("");
|
||||
@@ -68,13 +63,7 @@ const MaterialForm: React.FC = () => {
|
||||
const isEdit = !!materialId;
|
||||
|
||||
// 获取素材详情
|
||||
useEffect(() => {
|
||||
if (isEdit && materialId) {
|
||||
fetchMaterialDetail();
|
||||
}
|
||||
}, [isEdit, materialId]);
|
||||
|
||||
const fetchMaterialDetail = async () => {
|
||||
const fetchMaterialDetail = useCallback(async () => {
|
||||
if (!materialId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -95,7 +84,6 @@ const MaterialForm: React.FC = () => {
|
||||
}
|
||||
|
||||
setResUrls(response.resUrls || []);
|
||||
setUrls(response.urls || []);
|
||||
|
||||
// 设置链接相关数据
|
||||
if (response.urls && response.urls.length > 0) {
|
||||
@@ -111,7 +99,13 @@ const MaterialForm: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [materialId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && materialId) {
|
||||
fetchMaterialDetail();
|
||||
}
|
||||
}, [isEdit, materialId, fetchMaterialDetail]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!libraryId) return;
|
||||
@@ -147,16 +141,16 @@ const MaterialForm: React.FC = () => {
|
||||
sendTime: sendTime || "",
|
||||
resUrls,
|
||||
urls: finalUrls,
|
||||
type: contentType,
|
||||
};
|
||||
|
||||
let response;
|
||||
if (isEdit) {
|
||||
response = await updateContentItem({
|
||||
await updateContentItem({
|
||||
id: materialId!,
|
||||
...params,
|
||||
});
|
||||
} else {
|
||||
response = await createContentItem(params);
|
||||
await createContentItem(params);
|
||||
}
|
||||
|
||||
// 直接使用返回数据,无需判断code
|
||||
@@ -191,7 +185,30 @@ const MaterialForm: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title={isEdit ? "编辑素材" : "新建素材"} />}>
|
||||
<Layout
|
||||
header={<NavCommon title={isEdit ? "编辑素材" : "新建素材"} />}
|
||||
footer={
|
||||
<div className={style["form-actions"]}>
|
||||
<Button
|
||||
fill="outline"
|
||||
onClick={handleBack}
|
||||
className={style["back-btn"]}
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={saving}
|
||||
className={style["submit-btn"]}
|
||||
>
|
||||
<SaveOutlined />
|
||||
{isEdit ? " 保存修改" : " 保存素材"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style["form-page"]}>
|
||||
<div className={style["form"]}>
|
||||
{/* 基础信息 */}
|
||||
@@ -269,7 +286,6 @@ const MaterialForm: React.FC = () => {
|
||||
value={linkImage ? [linkImage] : []}
|
||||
onChange={urls => setLinkImage(urls[0] || "")}
|
||||
count={1}
|
||||
listType="picture-card"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -302,17 +318,29 @@ const MaterialForm: React.FC = () => {
|
||||
{/* 素材上传(仅图片类型和小程序类型) */}
|
||||
{[1, 5].includes(contentType) && (
|
||||
<Card className={style["form-card"]}>
|
||||
<div className={style["card-title"]}>素材上传</div>
|
||||
<div className={style["card-title"]}>
|
||||
素材上传 (当前类型: {contentType})
|
||||
</div>
|
||||
|
||||
{contentType === 1 && (
|
||||
<div className={style["form-item"]}>
|
||||
<label className={style["form-label"]}>图片上传</label>
|
||||
<UploadComponent
|
||||
value={resUrls}
|
||||
onChange={setResUrls}
|
||||
count={9}
|
||||
listType="picture-card"
|
||||
/>
|
||||
<div>
|
||||
<UploadComponent
|
||||
value={resUrls}
|
||||
onChange={setResUrls}
|
||||
count={9}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "#666",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
当前内容类型: {contentType}, 图片数量: {resUrls.length}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -344,7 +372,6 @@ const MaterialForm: React.FC = () => {
|
||||
value={resUrls}
|
||||
onChange={setResUrls}
|
||||
count={9}
|
||||
listType="picture-card"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -367,27 +394,6 @@ const MaterialForm: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className={style["form-actions"]}>
|
||||
<Button
|
||||
fill="outline"
|
||||
onClick={handleBack}
|
||||
className={style["back-btn"]}
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={saving}
|
||||
className={style["submit-btn"]}
|
||||
>
|
||||
<SaveOutlined />
|
||||
{isEdit ? "保存修改" : "保存素材"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function updateContentItem(params: UpdateContentItemParams) {
|
||||
|
||||
// 删除素材
|
||||
export function deleteContentItem(id: string) {
|
||||
return request("/v1/content/item/delete", { id }, "DELETE");
|
||||
return request("/v1/content/library/delete-item", { id }, "DELETE");
|
||||
}
|
||||
|
||||
// 获取内容库详情
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
.materials-page {
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
@@ -338,3 +336,280 @@
|
||||
background: white;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
// 内容类型标签样式
|
||||
.content-type-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
|
||||
// 图片类型预览样式
|
||||
.material-image-preview {
|
||||
margin: 12px 0;
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
// 1张图片:宽度拉伸,高度自适应
|
||||
&.single {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 2张图片:左右并列
|
||||
&.double {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 3张图片:三张并列
|
||||
&.triple {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 4张图片:2x2网格布局
|
||||
&.quad {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 5张及以上:网格布局
|
||||
&.grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 链接类型预览样式
|
||||
.material-link-preview {
|
||||
margin: 12px 0;
|
||||
|
||||
.link-card {
|
||||
display: flex;
|
||||
background: #e9f8ff;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid #cde6ff;
|
||||
&:hover {
|
||||
background: #cde6ff;
|
||||
}
|
||||
|
||||
.link-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.link-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.link-url {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 视频类型预览样式
|
||||
.material-video-preview {
|
||||
margin: 12px 0;
|
||||
|
||||
.video-thumbnail {
|
||||
video {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-video {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 120px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本类型预览样式
|
||||
.material-text-preview {
|
||||
margin: 12px 0;
|
||||
|
||||
.text-content {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 小程序类型预览样式
|
||||
.material-miniprogram-preview {
|
||||
margin: 12px 0;
|
||||
|
||||
.miniprogram-card {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.miniprogram-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.miniprogram-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图文类型预览样式
|
||||
.material-article-preview {
|
||||
margin: 12px 0;
|
||||
|
||||
.article-image {
|
||||
margin-bottom: 12px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.article-content {
|
||||
.article-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.article-text {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认预览样式
|
||||
.material-default-preview {
|
||||
margin: 12px 0;
|
||||
|
||||
.default-content {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
DeleteOutlined,
|
||||
UserOutlined,
|
||||
BarChartOutlined,
|
||||
PictureOutlined,
|
||||
LinkOutlined,
|
||||
VideoCameraOutlined,
|
||||
FileTextOutlined,
|
||||
AppstoreOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
@@ -17,6 +22,16 @@ import { getContentItemList, deleteContentItem } from "./api";
|
||||
import { ContentItem } from "./data";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
// 内容类型配置
|
||||
const contentTypeConfig = {
|
||||
1: { label: "图片", icon: PictureOutlined, color: "#52c41a" },
|
||||
2: { label: "链接", icon: LinkOutlined, color: "#1890ff" },
|
||||
3: { label: "视频", icon: VideoCameraOutlined, color: "#722ed1" },
|
||||
4: { label: "文本", icon: FileTextOutlined, color: "#fa8c16" },
|
||||
5: { label: "小程序", icon: AppstoreOutlined, color: "#eb2f96" },
|
||||
6: { label: "图文", icon: PictureOutlined, color: "#13c2c2" },
|
||||
};
|
||||
|
||||
const MaterialsList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -73,19 +88,12 @@ const MaterialsList: React.FC = () => {
|
||||
|
||||
if (result) {
|
||||
try {
|
||||
const response = await deleteContentItem(materialId);
|
||||
if (response.code === 200) {
|
||||
Toast.show({
|
||||
content: "删除成功",
|
||||
position: "top",
|
||||
});
|
||||
fetchMaterials();
|
||||
} else {
|
||||
Toast.show({
|
||||
content: response.msg || "删除失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
await deleteContentItem(materialId.toString());
|
||||
Toast.show({
|
||||
content: "删除成功",
|
||||
position: "top",
|
||||
});
|
||||
fetchMaterials();
|
||||
} catch (error: unknown) {
|
||||
console.error("删除素材失败:", error);
|
||||
Toast.show({
|
||||
@@ -114,6 +122,161 @@ const MaterialsList: React.FC = () => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// 渲染内容类型标签
|
||||
const renderContentTypeTag = (contentType: number) => {
|
||||
const config =
|
||||
contentTypeConfig[contentType as keyof typeof contentTypeConfig];
|
||||
if (!config) return null;
|
||||
|
||||
const IconComponent = config.icon;
|
||||
return (
|
||||
<div
|
||||
className={style["content-type-tag"]}
|
||||
style={{ backgroundColor: config.color + "20", color: config.color }}
|
||||
>
|
||||
<IconComponent style={{ fontSize: 12, marginRight: 4 }} />
|
||||
{config.label}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染素材内容预览
|
||||
const renderContentPreview = (material: ContentItem) => {
|
||||
const { contentType, content, resUrls, urls, coverImage } = material;
|
||||
|
||||
switch (contentType) {
|
||||
case 1: // 图片
|
||||
return (
|
||||
<div className={style["material-image-preview"]}>
|
||||
{resUrls && resUrls.length > 0 ? (
|
||||
<div
|
||||
className={`${style["image-grid"]} ${
|
||||
resUrls.length === 1
|
||||
? style.single
|
||||
: resUrls.length === 2
|
||||
? style.double
|
||||
: resUrls.length === 3
|
||||
? style.triple
|
||||
: resUrls.length === 4
|
||||
? style.quad
|
||||
: style.grid
|
||||
}`}
|
||||
>
|
||||
{resUrls.slice(0, 9).map((url, index) => (
|
||||
<img key={index} src={url} alt={`图片${index + 1}`} />
|
||||
))}
|
||||
{resUrls.length > 9 && (
|
||||
<div className={style["image-more"]}>
|
||||
+{resUrls.length - 9}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : coverImage ? (
|
||||
<div className={`${style["image-grid"]} ${style.single}`}>
|
||||
<img src={coverImage} alt="封面图" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={style["no-image"]}>暂无图片</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 2: // 链接
|
||||
return (
|
||||
<div className={style["material-link-preview"]}>
|
||||
{urls && urls.length > 0 && (
|
||||
<div
|
||||
className={style["link-card"]}
|
||||
onClick={() => {
|
||||
window.open(urls[0].url, "_blank");
|
||||
}}
|
||||
>
|
||||
{urls[0].image && (
|
||||
<div className={style["link-image"]}>
|
||||
<img src={urls[0].image} alt="链接预览" />
|
||||
</div>
|
||||
)}
|
||||
<div className={style["link-content"]}>
|
||||
<div className={style["link-title"]}>
|
||||
{urls[0].desc || "链接"}
|
||||
</div>
|
||||
<div className={style["link-url"]}>{urls[0].url}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 3: // 视频
|
||||
return (
|
||||
<div className={style["material-video-preview"]}>
|
||||
{resUrls && resUrls.length > 0 ? (
|
||||
<div className={style["video-thumbnail"]}>
|
||||
<video src={resUrls[0]} controls />
|
||||
</div>
|
||||
) : (
|
||||
<div className={style["no-video"]}>暂无视频</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 4: // 文本
|
||||
return (
|
||||
<div className={style["material-text-preview"]}>
|
||||
<div className={style["text-content"]}>
|
||||
{content.length > 100
|
||||
? `${content.substring(0, 100)}...`
|
||||
: content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 5: // 小程序
|
||||
return (
|
||||
<div className={style["material-miniprogram-preview"]}>
|
||||
{resUrls && resUrls.length > 0 && (
|
||||
<div className={style["miniprogram-card"]}>
|
||||
<img src={resUrls[0]} alt="小程序封面" />
|
||||
<div className={style["miniprogram-info"]}>
|
||||
<div className={style["miniprogram-title"]}>
|
||||
{material.title || "小程序"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 6: // 图文
|
||||
return (
|
||||
<div className={style["material-article-preview"]}>
|
||||
{coverImage && (
|
||||
<div className={style["article-image"]}>
|
||||
<img src={coverImage} alt="文章封面" />
|
||||
</div>
|
||||
)}
|
||||
<div className={style["article-content"]}>
|
||||
<div className={style["article-title"]}>
|
||||
{material.title || "图文内容"}
|
||||
</div>
|
||||
<div className={style["article-text"]}>
|
||||
{content.length > 80
|
||||
? `${content.substring(0, 80)}...`
|
||||
: content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className={style["material-default-preview"]}>
|
||||
<div className={style["default-content"]}>{content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
@@ -130,7 +293,7 @@ const MaterialsList: React.FC = () => {
|
||||
<div className="search-bar">
|
||||
<div className="search-input-wrapper">
|
||||
<Input
|
||||
placeholder="搜索计划名称"
|
||||
placeholder="搜索素材内容"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
@@ -185,7 +348,7 @@ const MaterialsList: React.FC = () => {
|
||||
<>
|
||||
{materials.map(material => (
|
||||
<Card key={material.id} className={style["material-card"]}>
|
||||
{/* 顶部头像+系统创建+ID */}
|
||||
{/* 顶部信息 */}
|
||||
<div className={style["card-header"]}>
|
||||
<div className={style["avatar-section"]}>
|
||||
<div className={style["avatar"]}>
|
||||
@@ -193,41 +356,23 @@ const MaterialsList: React.FC = () => {
|
||||
</div>
|
||||
<div className={style["header-info"]}>
|
||||
<span className={style["creator-name"]}>
|
||||
{material.senderNickname}
|
||||
{material.senderNickname || "系统创建"}
|
||||
</span>
|
||||
<span className={style["material-id"]}>
|
||||
ID: {material.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{renderContentTypeTag(material.contentType)}
|
||||
</div>
|
||||
|
||||
{/* 主标题 */}
|
||||
<div className={style["material-title"]}>
|
||||
{material.content}
|
||||
</div>
|
||||
|
||||
{/* 链接预览 */}
|
||||
{material.urls && material.urls.length > 0 && (
|
||||
<div
|
||||
className={style["link-preview"]}
|
||||
onClick={() => {
|
||||
window.open(material.urls[0].url, "_blank");
|
||||
}}
|
||||
>
|
||||
<div className={style["link-icon"]}>
|
||||
<img src={material.urls[0].image} />
|
||||
</div>
|
||||
<div className={style["link-content"]}>
|
||||
<div className={style["link-title"]}>
|
||||
{material.urls[0].desc}
|
||||
</div>
|
||||
<div className={style["link-url"]}>
|
||||
{material.urls[0].url}
|
||||
</div>
|
||||
</div>
|
||||
{/* 标题 */}
|
||||
{material.contentType != 4 && (
|
||||
<div className={style["card-title"]}>
|
||||
{material.content}
|
||||
</div>
|
||||
)}
|
||||
{/* 内容预览 */}
|
||||
{renderContentPreview(material)}
|
||||
|
||||
{/* 操作按钮区 */}
|
||||
<div className={style["action-buttons"]}>
|
||||
|
||||
@@ -13,15 +13,29 @@ export default defineConfig({
|
||||
open: true,
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500, // 提高警告阈值,减少无关警告
|
||||
chunkSizeWarningLimit: 2000,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 减少文件数量,合并更多依赖
|
||||
manualChunks: {
|
||||
"react-vendor": ["react", "react-dom"],
|
||||
"antd-vendor": ["antd", "@ant-design/icons", "antd-mobile"],
|
||||
"echarts-vendor": ["echarts", "echarts-for-react"],
|
||||
// 核心框架
|
||||
vendor: ["react", "react-dom", "react-router-dom"],
|
||||
// UI组件库
|
||||
ui: ["antd", "@ant-design/icons", "antd-mobile"],
|
||||
// 工具库
|
||||
utils: ["axios", "dayjs", "zustand"],
|
||||
// 图表库
|
||||
charts: ["echarts", "echarts-for-react"],
|
||||
},
|
||||
// 文件名格式
|
||||
chunkFileNames: "assets/[name]-[hash].js",
|
||||
entryFileNames: "assets/[name]-[hash].js",
|
||||
assetFileNames: "assets/[name]-[hash].[ext]",
|
||||
},
|
||||
},
|
||||
// 启用压缩
|
||||
minify: "esbuild",
|
||||
// 启用源码映射(可选,生产环境可以关闭)
|
||||
sourcemap: false,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user