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/src/pages/mobile/mine/traffic-pool/detail/api.ts b/nkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts index a31a1c34..dfad5195 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts +++ b/nkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts @@ -1,5 +1,31 @@ import request from "@/api/request"; +import type { + TrafficPoolUserDetail, + UserJourneyResponse, + UserTagsResponse, +} from "./data"; -export function getTrafficPoolDetail(id: string): Promise { - return request("/v1/workbench/detail", { id }, "GET"); +export function getTrafficPoolDetail( + wechatId: string +): Promise { + return request("/v1/wechats/getWechatInfo", { wechatId }, "GET"); +} + +// 获取用户旅程记录 +export function getUserJourney(params: { + page: number; + pageSize: number; + userId: string; +}): Promise { + return request("/v1/traffic/pool/getUserJourney", params, "GET"); +} + +// 获取用户标签 +export function getUserTags(userId: string): Promise { + return request("/v1/traffic/pool/getUserTags", { userId }, "GET"); +} + +// 添加用户标签 +export function addUserTag(userId: string, tagData: any): Promise { + return request("/v1/user/tags", { userId, ...tagData }, "POST"); } diff --git a/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss b/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss index e69de29b..60165acb 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss +++ b/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss @@ -0,0 +1,420 @@ +.container { + padding: 0; + background: #f5f5f5; + min-height: 100vh; +} + +// 头部样式 +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + + .title { + font-size: 18px; + font-weight: 600; + color: #333; + } + + .closeBtn { + padding: 8px; + border: none; + background: transparent; + color: #999; + font-size: 16px; + } +} + +// 用户卡片 +.userCard { + margin: 16px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .userInfo { + display: flex; + align-items: flex-start; + gap: 16px; + } + + .avatar { + width: 60px; + height: 60px; + border-radius: 50%; + flex-shrink: 0; + } + + .userDetails { + flex: 1; + min-width: 0; + } + + .nickname { + font-size: 18px; + font-weight: 600; + color: #333; + margin-bottom: 4px; + } + + .wechatId { + font-size: 14px; + color: #666; + margin-bottom: 8px; + } + + .tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .userTag { + font-size: 12px; + padding: 4px 8px; + border-radius: 12px; + } +} + +// 标签导航 +.tabNav { + display: flex; + background: #fff; + margin: 0 16px; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + .tabItem { + flex: 1; + padding: 12px 16px; + text-align: center; + font-size: 14px; + color: #666; + cursor: pointer; + transition: all 0.3s ease; + border-bottom: 2px solid transparent; + + &.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); + background: rgba(24, 142, 238, 0.05); + } + + &:hover { + background: rgba(24, 142, 238, 0.05); + } + } +} + +// 内容区域 +.content { + padding: 16px; +} + +.tabContent { + display: flex; + flex-direction: column; + gap: 16px; +} + +// 信息卡片 +.infoCard { + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + + :global(.adm-card-header) { + padding: 16px; + border-bottom: 1px solid #f0f0f0; + font-weight: 600; + color: #333; + } + + :global(.adm-card-body) { + padding: 0; + } +} + +// RFM评分网格 +.rfmGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + padding: 16px; +} + +.rfmItem { + text-align: center; + padding: 12px; + background: #f8f9fa; + border-radius: 8px; +} + +.rfmLabel { + font-size: 12px; + color: #666; + margin-bottom: 4px; +} + +.rfmValue { + font-size: 18px; + font-weight: 600; +} + +// 流量池区域 +.poolSection { + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.currentPool, +.availablePools { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.poolLabel { + font-size: 14px; + color: #666; + white-space: nowrap; +} + +// 统计数据网格 +.statsGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + padding: 16px; +} + +.statItem { + text-align: center; + padding: 12px; + background: #f8f9fa; + border-radius: 8px; +} + +.statValue { + font-size: 18px; + font-weight: 600; + margin-bottom: 4px; +} + +.statLabel { + font-size: 12px; + color: #666; +} + +// 用户旅程 +.journeyItem { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #666; + margin-top: 4px; +} + +.timestamp { + color: #999; +} + +// 加载状态 +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 16px; + text-align: center; +} + +.loadingText { + font-size: 14px; + color: #999; + margin-top: 8px; +} + +.loadingMore { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 16px; + color: #666; + font-size: 14px; +} + +.loadMoreBtn { + display: flex; + justify-content: center; + padding: 16px; +} + +// 标签区域 +.tagsSection { + padding: 16px; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.valueTagsSection { + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.tagItem { + font-size: 12px; + padding: 6px 12px; + border-radius: 16px; +} + +.valueTagContainer { + display: flex; + flex-direction: column; + gap: 8px; +} + +.valueTagRow { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.rfmScoreText { + font-size: 12px; + color: #666; + white-space: nowrap; +} + +.valueLevelLabel { + font-size: 12px; + color: #666; + white-space: nowrap; +} + +.valueTagItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } +} + +.valueInfo { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: #666; +} + +// 添加标签按钮 +.addTagBtn { + margin-top: 16px; + border-radius: 8px; + height: 48px; + font-size: 16px; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +// 空状态 +.emptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 16px; + text-align: center; +} + +.emptyIcon { + margin-bottom: 16px; + opacity: 0.6; +} + +.emptyText { + font-size: 16px; + color: #666; + margin-bottom: 8px; + font-weight: 500; +} + +.emptyDesc { + font-size: 14px; + color: #999; + line-height: 1.4; +} + +// 限制记录样式 +.restrictionTitle { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-size: 14px; + font-weight: 500; + color: #333; + line-height: 1.4; +} + +.restrictionLevel { + font-size: 10px; + padding: 2px 6px; + border-radius: 8px; + flex-shrink: 0; +} + +.restrictionContent { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; + color: #666; + line-height: 1.4; + margin-top: 4px; +} + +// 响应式设计 +@media (max-width: 375px) { + .rfmGrid, + .statsGrid { + grid-template-columns: 1fr; + } + + .userInfo { + flex-direction: column; + text-align: center; + } + + .avatar { + align-self: center; + } + + .restrictionTitle { + font-size: 13px; + } + + .restrictionContent { + font-size: 11px; + } +} diff --git a/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx index 1b89ca17..f478fdfc 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx +++ b/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx @@ -1,47 +1,285 @@ import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; +import { + Card, + Button, + Avatar, + Tag, + Tabs, + List, + Badge, + SpinLoading, +} from "antd-mobile"; +import { + UserOutlined, + CrownOutlined, + PlusOutlined, + CloseOutlined, + EyeOutlined, + DollarOutlined, + MobileOutlined, + TagOutlined, + FileTextOutlined, + UserAddOutlined, +} from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; -import { getTrafficPoolDetail } from "./api"; -import type { TrafficPoolUserDetail } from "./data"; -import { Card, Button, Avatar, Tag, Spin } from "antd"; - -const tabList = [ - { key: "base", label: "基本信息" }, - { key: "journey", label: "用户旅程" }, - { key: "tags", label: "用户标签" }, -]; +import NavCommon from "@/components/NavCommon"; +import { getTrafficPoolDetail, getUserJourney, getUserTags } from "./api"; +import type { + TrafficPoolUserDetail, + ExtendedUserDetail, + InteractionRecord, + UserJourneyRecord, + UserTagsResponse, + UserTagItem, +} from "./data"; +import styles from "./index.module.scss"; const TrafficPoolDetail: React.FC = () => { - const { id } = useParams(); + const { wxid, userId } = useParams(); const navigate = useNavigate(); const [loading, setLoading] = useState(true); - const [user, setUser] = useState(null); - const [activeTab, setActiveTab] = useState<"base" | "journey" | "tags">( - "base" - ); + const [user, setUser] = useState(null); + const [activeTab, setActiveTab] = useState("basic"); + + // 用户旅程相关状态 + const [journeyLoading, setJourneyLoading] = useState(false); + const [journeyList, setJourneyList] = useState([]); + const [journeyPage, setJourneyPage] = useState(1); + const [journeyTotal, setJourneyTotal] = useState(0); + const pageSize = 10; + + // 用户标签相关状态 + const [tagsLoading, setTagsLoading] = useState(false); + const [userTagsList, setUserTagsList] = useState([]); useEffect(() => { - if (!id) return; + if (!wxid) return; setLoading(true); - getTrafficPoolDetail(id as string) - .then(res => setUser(res)) + getTrafficPoolDetail(wxid as string) + .then(res => { + // 将API数据转换为扩展的用户详情数据 + const extendedUser: ExtendedUserDetail = { + ...res, + // 模拟RFM评分数据 + rfmScore: { + recency: 5, + frequency: 5, + monetary: 5, + totalScore: 15, + }, + // 模拟流量池数据 + trafficPools: { + currentPool: "新用户池", + availablePools: ["高价值客户池", "活跃用户池"], + }, + // 模拟用户标签数据 + userTags: [ + { id: "1", name: "近期活跃", color: "success", type: "user" }, + { id: "2", name: "高频互动", color: "primary", type: "user" }, + { id: "3", name: "高消费", color: "warning", type: "user" }, + { id: "4", name: "老客户", color: "danger", type: "user" }, + ], + // 模拟价值标签数据 + valueTags: [ + { + id: "1", + name: "重要保持客户", + color: "primary", + icon: "crown", + rfmScore: 14, + valueLevel: "高价值", + }, + ], + }; + setUser(extendedUser); + }) .finally(() => setLoading(false)); - }, [id]); + }, [wxid]); + + // 获取用户旅程数据 + const fetchUserJourney = async (page: number = 1) => { + if (!userId) return; + + setJourneyLoading(true); + try { + const response = await getUserJourney({ + page, + pageSize, + userId: userId, + }); + + if (page === 1) { + setJourneyList(response.list); + } else { + setJourneyList(prev => [...prev, ...response.list]); + } + setJourneyTotal(response.total); + setJourneyPage(page); + } catch (error) { + console.error("获取用户旅程失败:", error); + } finally { + setJourneyLoading(false); + } + }; + + // 获取用户标签数据 + const fetchUserTags = async () => { + if (!userId) return; + + setTagsLoading(true); + try { + const response: UserTagsResponse = await getUserTags(userId); + setUserTagsList(response.siteLabels || []); + } catch (error) { + console.error("获取用户标签失败:", error); + } finally { + setTagsLoading(false); + } + }; + + // 标签切换处理 + const handleTabChange = (tab: string) => { + setActiveTab(tab); + if (tab === "journey" && journeyList.length === 0) { + fetchUserJourney(1); + } + if (tab === "tags" && userTagsList.length === 0) { + fetchUserTags(); + } + }; + + const handleClose = () => { + navigate(-1); + }; + + const getJourneyTypeIcon = (type: number) => { + switch (type) { + case 0: // 浏览 + return ; + case 2: // 提交订单 + return ; + case 3: // 注册 + return ; + default: + return ; + } + }; + + const getJourneyTypeText = (type: number) => { + switch (type) { + case 0: + return "浏览行为"; + case 2: + return "提交订单"; + case 3: + return "注册行为"; + default: + return "其他行为"; + } + }; + + const formatDateTime = (dateTime: string) => { + try { + const date = new Date(dateTime); + return date.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + } catch (error) { + return dateTime; + } + }; + + const getActionIcon = (type: string) => { + switch (type) { + case "click": + return ; + case "view": + return ; + case "purchase": + return ; + default: + return ; + } + }; + + const formatCurrency = (amount: number) => { + return `¥${amount.toLocaleString()}`; + }; + + const getGenderText = (gender: number) => { + switch (gender) { + case 1: + return "男"; + case 2: + return "女"; + default: + return "未知"; + } + }; + + const getGenderColor = (gender: number) => { + switch (gender) { + case 1: + return "#1677ff"; + case 2: + return "#eb2f96"; + default: + return "#999"; + } + }; + + const getRestrictionLevelText = (level: number) => { + switch (level) { + case 1: + return "轻微"; + case 2: + return "中等"; + case 3: + return "严重"; + default: + return "未知"; + } + }; + + const getRestrictionLevelColor = (level: number) => { + switch (level) { + case 1: + return "warning"; + case 2: + return "danger"; + case 3: + return "danger"; + default: + return "default"; + } + }; + + const formatDate = (timestamp: number | null) => { + if (!timestamp) return "--"; + try { + const date = new Date(timestamp * 1000); + return date.toLocaleDateString("zh-CN"); + } catch (error) { + return "--"; + } + }; + + // 获取标签颜色 + const getTagColor = (index: number): string => { + const colors = ["primary", "success", "warning", "danger", "default"]; + return colors[index % colors.length]; + }; - if (loading) { - return ( - -
- -
-
- ); - } if (!user) { return ( - -
- 未找到该用户 + } loading={loading}> +
+
未找到该用户
); @@ -49,249 +287,418 @@ const TrafficPoolDetail: React.FC = () => { return ( - -
用户详情
-
+ <> + + {/* 用户基本信息 */} + +
+ } + /> +
+
{user.userInfo.nickname}
+
{user.userInfo.wechatId}
+
+ + + 重要价值客户 + + + 优先添加 + +
+
+
+
+ {/* 导航标签 */} +
+
handleTabChange("basic")} + > + 基本信息 +
+
handleTabChange("journey")} + > + 用户旅程 +
+
handleTabChange("tags")} + > + 用户标签 +
+
+ } > -
- {/* 顶部信息 */} -
- -
-
{user.nickname}
-
- {user.wechatId} -
- {user.packages && - user.packages.length > 0 && - user.packages.map(pkg => ( - - {pkg} - - ))} -
-
- {/* Tab栏 */} -
- {tabList.map(tab => ( -
setActiveTab(tab.key as any)} - > - {tab.label} -
- ))} -
- {/* Tab内容 */} - {activeTab === "base" && ( - <> - -
-
设备:{user.deviceName || "--"}
-
微信号:{user.wechatAccountName || "--"}
-
客服:{user.customerServiceName || "--"}
-
添加时间:{user.addTime || "--"}
-
最近互动:{user.lastInteraction || "--"}
-
-
- -
-
-
- {user.rfmScore?.recency ?? "-"} -
-
最近性(R)
-
-
-
- {user.rfmScore?.frequency ?? "-"} -
-
频率(F)
-
-
-
- {user.rfmScore?.monetary ?? "-"} -
-
金额(M)
-
-
-
- -
-
-
- ¥{user.totalSpent ?? "-"} -
-
总消费
-
-
-
- {user.interactionCount ?? "-"} -
-
互动次数
-
-
-
- {user.conversionRate ?? "-"} -
-
转化率
-
-
-
- {user.status === "failed" - ? "添加失败" - : user.status === "added" - ? "添加成功" - : "未添加"} -
-
添加状态
-
-
-
- - )} - {activeTab === "journey" && ( - - {user.interactions && user.interactions.length > 0 ? ( - user.interactions.slice(0, 4).map(it => ( -
-
- {it.type === "click" && "📱"} - {it.type === "message" && "💬"} - {it.type === "purchase" && "💲"} - {it.type === "view" && "👁️"} -
-
-
- {it.type === "click" && "点击行为"} - {it.type === "message" && "消息互动"} - {it.type === "purchase" && "购买行为"} - {it.type === "view" && "页面浏览"} +
+ {/* 内容区域 */} +
+ {activeTab === "basic" && ( +
+ {/* 关联信息 */} + + + 设备 + 微信号 + 客服 + 添加时间 + 最近互动 + + + + {/* RFM评分 */} + {user.rfmScore && ( + +
+
+
最近性(R)
+
+ {user.rfmScore.recency} +
-
- {it.content} - {it.type === "purchase" && it.value && ( - - ¥{it.value} - - )} +
+
频率(F)
+
+ {user.rfmScore.frequency} +
+
+
+
金额(M)
+
+ {user.rfmScore.monetary} +
+
+
+
总分
+
+ {user.rfmScore.totalScore}/15 +
-
- {it.timestamp} -
-
- )) - ) : ( -
- 暂无互动记录 -
- )} -
- )} - {activeTab === "tags" && ( - -
- {user.tags && user.tags.length > 0 ? ( - user.tags.map(tag => ( - - {tag} - - )) - ) : ( - 暂无标签 + )} + + {/* 流量池 */} + {user.trafficPools && ( + +
+
+ 当前池: + + {user.trafficPools.currentPool} + +
+
+ 可选池: + {user.trafficPools.availablePools.map((pool, index) => ( + + {pool} + + ))} +
+
+
+ )} + + {/* 统计数据 */} + +
+
+
+ ¥9561 +
+
总消费
+
+
+
+ 6 +
+
互动次数
+
+
+
+ 3% +
+
转化率
+
+
+
+ 未添加 +
+
添加状态
+
+
+
+ + {/* 好友统计 */} + +
+
+
+ {user.userInfo.friendShip.totalFriend} +
+
总好友
+
+
+
+ {user.userInfo.friendShip.maleFriend} +
+
男性好友
+
+
+
+ {user.userInfo.friendShip.femaleFriend} +
+
女性好友
+
+
+
+ {user.userInfo.friendShip.unknowFriend} +
+
未知性别
+
+
+
+ + {/* 限制记录 */} + + {user.restrictions && user.restrictions.length > 0 ? ( + + {user.restrictions.map(restriction => ( + + {restriction.reason || "未知原因"} + + {getRestrictionLevelText(restriction.level)} + +
+ } + description={ +
+ 限制ID: {restriction.id} + {restriction.date && ( + + 限制时间: {formatDate(restriction.date)} + + )} +
+ } + /> + ))} + + ) : ( +
+
+ +
+
暂无限制记录
+
+ 该用户没有任何限制记录 +
+
+ )} +
- - - )} + )} + + {activeTab === "journey" && ( +
+ + {journeyLoading && journeyList.length === 0 ? ( +
+ +
加载中...
+
+ ) : journeyList.length === 0 ? ( +
+
+ +
+
暂无互动记录
+
+ 该用户还没有任何互动行为 +
+
+ ) : ( + + {journeyList.map(record => ( + + {record.remark} + + {formatDateTime(record.createTime)} + +
+ } + /> + ))} + {journeyLoading && journeyList.length > 0 && ( +
+ + 加载更多... +
+ )} + {!journeyLoading && journeyList.length < journeyTotal && ( +
+ +
+ )} + + )} + +
+ )} + + {activeTab === "tags" && ( +
+ {/* 用户标签 */} + + {tagsLoading && userTagsList.length === 0 ? ( +
+ +
加载中...
+
+ ) : userTagsList.length === 0 ? ( +
+
+ +
+
暂无用户标签
+
该用户还没有任何标签
+
+ ) : ( +
+ {userTagsList.map((tag, index) => ( + + {tag.name} + + ))} +
+ )} +
+ + {/* 价值标签 */} + + {user.valueTags && user.valueTags.length > 0 ? ( +
+ {user.valueTags.map(tag => ( +
+
+ + {tag.icon === "crown" && } + {tag.name} + + + RFM总分: {tag.rfmScore}/15 + +
+
+ + 价值等级: + + + {tag.valueLevel} + +
+
+ ))} +
+ ) : ( +
+
+ +
+
暂无价值标签
+
+ 该用户还没有任何价值标签 +
+
+ )} +
+ + {/* 添加新标签按钮 */} + +
+ )} +
); diff --git a/nkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx index a505ea41..7e000524 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/nkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -191,7 +191,9 @@ const TrafficPoolList: React.FC = () => {
navigate(`/traffic-pool/detail/${item.id}`)} + onClick={() => + navigate(`/traffic-pool/detail/${item.sourceId}/${item.id}`) + } >
{ path: "/workspace/traffic-distribution", bgColor: "#e6f7ff", }, - { - id: "ai-assistant", - name: "AI对话助手", - description: "智能回复,提高互动质量", - icon: ( - - ), - path: "/workspace/ai-assistant", - bgColor: "#e6f7ff", - isNew: true, - }, ]; // AI智能助手 @@ -176,7 +165,7 @@ const Workspace: React.FC = () => {
{/* AI智能助手 */} -
+ {/*

AI 智能助手

{aiFeatures.map(feature => ( @@ -205,7 +194,7 @@ const Workspace: React.FC = () => { ))}
-
+
*/}
); diff --git a/nkebao/src/router/module/mine.tsx b/nkebao/src/router/module/mine.tsx index 7a190068..46e852e8 100644 --- a/nkebao/src/router/module/mine.tsx +++ b/nkebao/src/router/module/mine.tsx @@ -29,7 +29,7 @@ const routes = [ auth: true, }, { - path: "/traffic-pool/detail/:id", + path: "/traffic-pool/detail/:wxid/:userId", element: , auth: true, }, 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, }, });