diff --git a/nkebao/.env.development b/nkebao/.env.development index 3b39062c..9ac98215 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -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 - diff --git a/nkebao/.eslintrc.js b/nkebao/.eslintrc.js index 92249b4b..0ffa2239 100644 --- a/nkebao/.eslintrc.js +++ b/nkebao/.eslintrc.js @@ -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: { diff --git a/nkebao/.prettierrc b/nkebao/.prettierrc index e3a5954f..588b7055 100644 --- a/nkebao/.prettierrc +++ b/nkebao/.prettierrc @@ -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" -} +} \ No newline at end of file diff --git a/nkebao/package.json b/nkebao/package.json index 809c3594..a8f4b087 100644 --- a/nkebao/package.json +++ b/nkebao/package.json @@ -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}\"", diff --git a/nkebao/src/components/Upload/README.md b/nkebao/src/components/Upload/README.md index 9cfee0ec..05c1a327 100644 --- a/nkebao/src/components/Upload/README.md +++ b/nkebao/src/components/Upload/README.md @@ -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"; -// 基础用法 - +const MyComponent = () => { + const [images, setImages] = useState([]); -// 单文件上传 - setSingleImage(urls[0])} - count={1} - listType="picture-card" -/> + return ( + + ); +}; ``` -#### 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([ + "https://example.com/image1.jpg", + "https://example.com/image2.jpg", + ]); - + return ( + + ); +}; ``` -#### 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 + ``` -或者: +## 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 默认样式 + } + } } ``` + +## 错误处理 + +- 文件类型不匹配时会显示错误提示 +- 文件大小超限时会显示错误提示 +- 上传失败时会显示错误提示 +- 网络错误时会显示错误提示 diff --git a/nkebao/src/components/Upload/index.module.scss b/nkebao/src/components/Upload/index.module.scss index 54a4c006..beab0535 100644 --- a/nkebao/src/components/Upload/index.module.scss +++ b/nkebao/src/components/Upload/index.module.scss @@ -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; + } + } + } } } diff --git a/nkebao/src/components/Upload/index.tsx b/nkebao/src/components/Upload/index.tsx index 23a233b9..73ec84a9 100644 --- a/nkebao/src/components/Upload/index.tsx +++ b/nkebao/src/components/Upload/index.tsx @@ -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 = ({ onChange, count = 9, accept = "image/*", - listType = "picture-card", disabled = false, className, }) => { - const [loading, setLoading] = useState(false); - const [fileList, setFileList] = useState([]); + const [fileList, setFileList] = useState([]); // 将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 = ({ // 文件验证 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 ( - 1} - fileList={fileList} - accept={accept} - listType={listType} - showUploadList={true} - disabled={disabled || loading} - beforeUpload={beforeUpload} - onChange={handleChange} - onRemove={handleRemove} - maxCount={count} - > +
+ 1} + maxCount={count} + showUpload={fileList.length < count && !disabled} + accept={accept} + /> +
); }; diff --git a/nkebao/src/pages/mobile/content/materials/form/index.tsx b/nkebao/src/pages/mobile/content/materials/form/index.tsx index 6987d8d0..708caa52 100644 --- a/nkebao/src/pages/mobile/content/materials/form/index.tsx +++ b/nkebao/src/pages/mobile/content/materials/form/index.tsx @@ -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(null); // 表单状态 const [contentType, setContentType] = useState(4); @@ -52,9 +50,6 @@ const MaterialForm: React.FC = () => { const [comment, setComment] = useState(""); const [sendTime, setSendTime] = useState(""); const [resUrls, setResUrls] = useState([]); - 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 ( - }> + } + footer={ +
+ + +
+ } + >
{/* 基础信息 */} @@ -269,7 +286,6 @@ const MaterialForm: React.FC = () => { value={linkImage ? [linkImage] : []} onChange={urls => setLinkImage(urls[0] || "")} count={1} - listType="picture-card" />
@@ -302,17 +318,29 @@ const MaterialForm: React.FC = () => { {/* 素材上传(仅图片类型和小程序类型) */} {[1, 5].includes(contentType) && ( -
素材上传
+
+ 素材上传 (当前类型: {contentType}) +
{contentType === 1 && (
- +
+ +
+
+ 当前内容类型: {contentType}, 图片数量: {resUrls.length} +
)} @@ -344,7 +372,6 @@ const MaterialForm: React.FC = () => { value={resUrls} onChange={setResUrls} count={9} - listType="picture-card" />
@@ -367,27 +394,6 @@ const MaterialForm: React.FC = () => { /> - - {/* 操作按钮 */} -
- - -
diff --git a/nkebao/src/pages/mobile/content/materials/list/api.ts b/nkebao/src/pages/mobile/content/materials/list/api.ts index d102c693..6037391f 100644 --- a/nkebao/src/pages/mobile/content/materials/list/api.ts +++ b/nkebao/src/pages/mobile/content/materials/list/api.ts @@ -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"); } // 获取内容库详情 diff --git a/nkebao/src/pages/mobile/content/materials/list/index.module.scss b/nkebao/src/pages/mobile/content/materials/list/index.module.scss index d1d17fa2..666b75ad 100644 --- a/nkebao/src/pages/mobile/content/materials/list/index.module.scss +++ b/nkebao/src/pages/mobile/content/materials/list/index.module.scss @@ -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; + } +} diff --git a/nkebao/src/pages/mobile/content/materials/list/index.tsx b/nkebao/src/pages/mobile/content/materials/list/index.tsx index eeefccb6..11c2ed0f 100644 --- a/nkebao/src/pages/mobile/content/materials/list/index.tsx +++ b/nkebao/src/pages/mobile/content/materials/list/index.tsx @@ -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 ( +
+ + {config.label} +
+ ); + }; + + // 渲染素材内容预览 + const renderContentPreview = (material: ContentItem) => { + const { contentType, content, resUrls, urls, coverImage } = material; + + switch (contentType) { + case 1: // 图片 + return ( +
+ {resUrls && resUrls.length > 0 ? ( +
+ {resUrls.slice(0, 9).map((url, index) => ( + {`图片${index + ))} + {resUrls.length > 9 && ( +
+ +{resUrls.length - 9} +
+ )} +
+ ) : coverImage ? ( +
+ 封面图 +
+ ) : ( +
暂无图片
+ )} +
+ ); + + case 2: // 链接 + return ( +
+ {urls && urls.length > 0 && ( +
{ + window.open(urls[0].url, "_blank"); + }} + > + {urls[0].image && ( +
+ 链接预览 +
+ )} +
+
+ {urls[0].desc || "链接"} +
+
{urls[0].url}
+
+
+ )} +
+ ); + + case 3: // 视频 + return ( +
+ {resUrls && resUrls.length > 0 ? ( +
+
+ ) : ( +
暂无视频
+ )} +
+ ); + + case 4: // 文本 + return ( +
+
+ {content.length > 100 + ? `${content.substring(0, 100)}...` + : content} +
+
+ ); + + case 5: // 小程序 + return ( +
+ {resUrls && resUrls.length > 0 && ( +
+ 小程序封面 +
+
+ {material.title || "小程序"} +
+
+
+ )} +
+ ); + + case 6: // 图文 + return ( +
+ {coverImage && ( +
+ 文章封面 +
+ )} +
+
+ {material.title || "图文内容"} +
+
+ {content.length > 80 + ? `${content.substring(0, 80)}...` + : content} +
+
+
+ ); + + default: + return ( +
+
{content}
+
+ ); + } + }; + return ( {
setSearchQuery(e.target.value)} prefix={} @@ -185,7 +348,7 @@ const MaterialsList: React.FC = () => { <> {materials.map(material => ( - {/* 顶部头像+系统创建+ID */} + {/* 顶部信息 */}
@@ -193,41 +356,23 @@ const MaterialsList: React.FC = () => {
- {material.senderNickname} + {material.senderNickname || "系统创建"} ID: {material.id}
+ {renderContentTypeTag(material.contentType)}
- - {/* 主标题 */} -
- {material.content} -
- - {/* 链接预览 */} - {material.urls && material.urls.length > 0 && ( -
{ - window.open(material.urls[0].url, "_blank"); - }} - > -
- -
-
-
- {material.urls[0].desc} -
-
- {material.urls[0].url} -
-
+ {/* 标题 */} + {material.contentType != 4 && ( +
+ {material.content}
)} + {/* 内容预览 */} + {renderContentPreview(material)} {/* 操作按钮区 */}
diff --git a/nkebao/vite.config.ts b/nkebao/vite.config.ts index f1a085ec..921473b6 100644 --- a/nkebao/vite.config.ts +++ b/nkebao/vite.config.ts @@ -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, }, });