diff --git a/Cunkebao/README.md b/Cunkebao/README.md index be60268f..7167bc10 100644 --- a/Cunkebao/README.md +++ b/Cunkebao/README.md @@ -2,23 +2,26 @@ ## 📋 项目简介 -内客宝是一个专业的微信获客和流量管理平台,基于 React 技术栈构建。平台提供智能化的客户获取、管理和运营解决方案,集成了多种自动化工具,帮助企业高效管理微信营销活动。 +内客宝是一个专业的微信获客和流量管理平台,基于 React 技术栈构建。平台提供智能化的客户获取、管理和运营解决方案,集成了多种自动化工具,帮助企业高效管理存客宝活动。 ## 🚀 技术栈详解 ### 核心框架 + - **React 18.2.0** - 现代化的用户界面库 - **TypeScript 4.9.5** - 类型安全的 JavaScript 超集 - **Create React App (CRA) 5.0.1** - React 应用脚手架 - **React Router DOM 6.20.0** - 客户端路由管理 ### 构建工具 + - **CRACO 7.1.0** - Create React App Configuration Override - 支持自定义 webpack 配置 - 路径别名配置 - 构建优化 ### UI 组件库 + - **Radix UI** - 无样式的可访问组件库 - 完整的组件生态系统(30+ 组件) - 优秀的无障碍访问支持 @@ -29,32 +32,38 @@ - 原子化 CSS 类 ### 图标和样式 + - **Lucide React 0.454.0** - 精美的图标库 - **Tailwind CSS Animate** - CSS 动画库 - **Class Variance Authority** - 组件变体管理 - **Tailwind Merge** - Tailwind 类名合并工具 ### 状态管理和表单 + - **React Hook Form 7.54.1** - 高性能表单库 - **Zod 3.24.1** - TypeScript 优先的模式验证 - **@hookform/resolvers 3.9.1** - 表单验证解析器 ### 数据可视化 + - **Recharts** - 基于 React 的图表库 - **Chart.js 4.5.0** - 灵活的图表库 - **@ant-design/plots** - Ant Design 图表组件 ### HTTP 请求和数据处理 + - **Axios 1.6.0** - HTTP 客户端 - **Crypto-js 4.2.0** - 加密库 - **Date-fns** - 日期处理库 - **XLSX 0.18.5** - Excel 文件处理 ### 通知和反馈 + - **React Hot Toast 2.5.2** - 轻量级通知库 - **Sonner 1.7.4** - 现代化 Toast 组件 ### 高级组件 + - **@tanstack/react-table** - 功能强大的表格组件 - **Embla Carousel React 8.5.1** - 轮播组件 - **React Resizable Panels 2.1.7** - 可调整大小的面板 @@ -63,6 +72,7 @@ - **React Day Picker** - 日期选择器 ### 开发工具 + - **PostCSS 8** - CSS 后处理器 - **Autoprefixer 10.4.20** - CSS 前缀自动添加 - **ESLint** - 代码质量检查 @@ -107,6 +117,7 @@ nkebao/ ## 🎯 核心功能模块 ### 工作台 (Workspace) + - **自动点赞** - 智能点赞管理和配置 - **自动建群** - 群组自动化创建和管理 - **群消息推送** - 群组消息批量发送 @@ -115,21 +126,25 @@ nkebao/ - **流量分发** - 流量分配和策略管理 ### 设备管理 (Devices) + - 设备状态监控和配置 - 设备性能分析 - 设备权限管理 ### 场景管理 (Scenarios) + - 营销场景配置 - 自动化流程设计 - 场景效果分析 ### 内容管理 (Content) + - 内容创建与编辑 - 内容模板管理 - 内容发布调度 ### 其他模块 + - 用户管理 (Users) - 订单管理 (Orders) - 流量池管理 (Traffic Pool) @@ -138,10 +153,12 @@ nkebao/ ## 🛠️ 开发指南 ### 环境要求 -- **Node.js** 16+ + +- **Node.js** 16+ - **npm** 或 **yarn** ### 安装依赖 + ```bash # 使用 npm npm install @@ -151,6 +168,7 @@ yarn install ``` ### 开发环境启动 + ```bash # 使用 npm npm start @@ -160,6 +178,7 @@ yarn start ``` ### 构建生产版本 + ```bash # 使用 npm npm run build @@ -169,6 +188,7 @@ yarn build ``` ### 运行测试 + ```bash # 使用 npm npm test @@ -180,7 +200,9 @@ yarn test ## 🔧 配置说明 ### 路径别名配置 + 项目使用 CRACO 配置了路径别名: + ```javascript '@': path.resolve(__dirname, 'src'), '@/components': path.resolve(__dirname, 'src/components'), @@ -193,11 +215,13 @@ yarn test ``` ### Tailwind CSS 配置 + - 自定义字体大小和间距 - 响应式断点配置 - 主题颜色系统 ### TypeScript 配置 + - 严格模式启用 - 路径映射配置 - JSX 支持 @@ -205,6 +229,7 @@ yarn test ## 📱 响应式设计 项目采用移动优先的响应式设计: + - 支持桌面端、平板端、移动端 - 自适应布局组件 - 触摸友好的交互设计 @@ -212,11 +237,13 @@ yarn test ## 🎨 UI 设计系统 ### 设计原则 + - 简洁现代的设计风格 - 一致的用户体验 - 无障碍访问支持 ### 组件库特点 + - 基于 Radix UI 的高质量组件 - 完整的表单组件系统 - 数据展示组件 @@ -263,4 +290,4 @@ yarn test **项目名称**: 内客宝 (nkebao2) **版本**: 0.1.0 **技术栈**: React + TypeScript + CRA + Tailwind CSS -**最后更新**: 2024年12月 \ No newline at end of file +**最后更新**: 2024 年 12 月 diff --git a/ckApp/.gitignore b/ckApp/.gitignore new file mode 100644 index 00000000..efe77355 --- /dev/null +++ b/ckApp/.gitignore @@ -0,0 +1 @@ +unpackage \ No newline at end of file diff --git a/ckApp/App.vue b/ckApp/App.vue new file mode 100644 index 00000000..f3393a5b --- /dev/null +++ b/ckApp/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/ckApp/index.html b/ckApp/index.html new file mode 100644 index 00000000..b5d330d1 --- /dev/null +++ b/ckApp/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/ckApp/main.js b/ckApp/main.js new file mode 100644 index 00000000..c1caf360 --- /dev/null +++ b/ckApp/main.js @@ -0,0 +1,22 @@ +import App from './App' + +// #ifndef VUE3 +import Vue from 'vue' +import './uni.promisify.adaptor' +Vue.config.productionTip = false +App.mpType = 'app' +const app = new Vue({ + ...App +}) +app.$mount() +// #endif + +// #ifdef VUE3 +import { createSSRApp } from 'vue' +export function createApp() { + const app = createSSRApp(App) + return { + app + } +} +// #endif \ No newline at end of file diff --git a/ckApp/manifest.json b/ckApp/manifest.json new file mode 100644 index 00000000..bb255e1e --- /dev/null +++ b/ckApp/manifest.json @@ -0,0 +1,74 @@ +{ + "name" : "ckApp", + "appid" : "__UNI__2B34F1A", + "description" : "", + "versionName" : "1.0.0", + "versionCode" : "100", + "transformPx" : false, + /* 5+App特有相关 */ + "app-plus" : { + "usingComponents" : true, + "nvueStyleCompiler" : "uni-app", + "compilerVersion" : 3, + "splashscreen" : { + "alwaysShowBeforeRender" : true, + "waiting" : true, + "autoclose" : true, + "delay" : 0 + }, + /* 模块配置 */ + "modules" : {}, + /* 应用发布信息 */ + "distribute" : { + /* android打包配置 */ + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + /* ios打包配置 */ + "ios" : { + "dSYMs" : false + }, + /* SDK配置 */ + "sdkConfigs" : {} + } + }, + /* 快应用特有相关 */ + "quickapp" : {}, + /* 小程序特有相关 */ + "mp-weixin" : { + "appid" : "", + "setting" : { + "urlCheck" : false + }, + "usingComponents" : true + }, + "mp-alipay" : { + "usingComponents" : true + }, + "mp-baidu" : { + "usingComponents" : true + }, + "mp-toutiao" : { + "usingComponents" : true + }, + "uniStatistics" : { + "enable" : false + }, + "vueVersion" : "3" +} diff --git a/ckApp/pages.json b/ckApp/pages.json new file mode 100644 index 00000000..570407ef --- /dev/null +++ b/ckApp/pages.json @@ -0,0 +1,24 @@ +{ + "pages": [ + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "", + "navigationStyle": "custom" + } + }, + { + "path": "pages/index/test", + "style": { + "navigationBarTitleText": "", + "navigationStyle": "custom" + } + } + ], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8" + }, + "uniIdRouter": {} +} diff --git a/ckApp/pages/index/index.vue b/ckApp/pages/index/index.vue new file mode 100644 index 00000000..7d0341d2 --- /dev/null +++ b/ckApp/pages/index/index.vue @@ -0,0 +1,100 @@ + + + \ No newline at end of file diff --git a/ckApp/pages/index/test.vue b/ckApp/pages/index/test.vue new file mode 100644 index 00000000..538d3a3c --- /dev/null +++ b/ckApp/pages/index/test.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/ckApp/static/logo.png b/ckApp/static/logo.png new file mode 100644 index 00000000..b5771e20 Binary files /dev/null and b/ckApp/static/logo.png differ diff --git a/ckApp/uni.promisify.adaptor.js b/ckApp/uni.promisify.adaptor.js new file mode 100644 index 00000000..5fec4f33 --- /dev/null +++ b/ckApp/uni.promisify.adaptor.js @@ -0,0 +1,13 @@ +uni.addInterceptor({ + returnValue (res) { + if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { + return res; + } + return new Promise((resolve, reject) => { + res.then((res) => { + if (!res) return resolve(res) + return res[0] ? reject(res[0]) : resolve(res[1]) + }); + }); + }, +}); \ No newline at end of file diff --git a/ckApp/uni.scss b/ckApp/uni.scss new file mode 100644 index 00000000..b9249e9d --- /dev/null +++ b/ckApp/uni.scss @@ -0,0 +1,76 @@ +/** + * 这里是uni-app内置的常用样式变量 + * + * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 + * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App + * + */ + +/** + * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 + * + * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 + */ + +/* 颜色变量 */ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color:#333;//基本色 +$uni-text-color-inverse:#fff;//反色 +$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable:#c0c0c0; + +/* 背景颜色 */ +$uni-bg-color:#ffffff; +$uni-bg-color-grey:#f8f8f8; +$uni-bg-color-hover:#f1f1f1;//点击状态颜色 +$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 + +/* 边框颜色 */ +$uni-border-color:#c8c7cc; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm:12px; +$uni-font-size-base:14px; +$uni-font-size-lg:16px; + +/* 图片尺寸 */ +$uni-img-size-sm:20px; +$uni-img-size-base:26px; +$uni-img-size-lg:40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2C405A; // 文章标题颜色 +$uni-font-size-title:20px; +$uni-color-subtitle: #555555; // 二级标题颜色 +$uni-font-size-subtitle:26px; +$uni-color-paragraph: #3F536E; // 文章段落颜色 +$uni-font-size-paragraph:15px; diff --git a/ckApp/utils/common.js b/ckApp/utils/common.js new file mode 100644 index 00000000..f50cc5c5 --- /dev/null +++ b/ckApp/utils/common.js @@ -0,0 +1,36 @@ +/** + * 异步获取设备顶部安全区域高度 + * @returns {Promise} 顶部安全区域高度 + */ +export function getTopSafeAreaHeightAsync() { + return new Promise((resolve, reject) => { + uni.getSystemInfo({ + success: (res) => { + try { + const safeAreaInsets = res.safeAreaInsets; + + if (safeAreaInsets && safeAreaInsets.top !== undefined) { + resolve(safeAreaInsets.top); + return; + } + + if (res.safeArea) { + const safeArea = res.safeArea; + const statusBarHeight = res.statusBarHeight || 0; + const topSafeHeight = safeArea.top - statusBarHeight; + resolve(Math.max(0, topSafeHeight)); + return; + } + + resolve(`${res.statusBarHeight*2 || 0}px`); + + } catch (error) { + reject(error); + } + }, + fail: (error) => { + reject(error); + } + }); + }); + } \ No newline at end of file diff --git a/ckApp/vite.config.js b/ckApp/vite.config.js new file mode 100644 index 00000000..3910715c --- /dev/null +++ b/ckApp/vite.config.js @@ -0,0 +1,29 @@ +import { defineConfig } from 'vite' +import uni from '@dcloudio/vite-plugin-uni' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [uni()], + resolve: { + alias: { + // 根目录别名 + '@': resolve(__dirname, '/'), + '@root': resolve(__dirname, '/'), + + // 页面和组件别名 + '@pages': resolve(__dirname, 'pages'), + '@components': resolve(__dirname, 'components'), + + // 工具和配置别名 + '@utils': resolve(__dirname, 'utils'), + } + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@import "@/uni.scss";` + } + } + } +}) \ No newline at end of file diff --git a/nkebao/AI_ICON_GUIDE.md b/nkebao/AI_ICON_GUIDE.md deleted file mode 100644 index bc536673..00000000 --- a/nkebao/AI_ICON_GUIDE.md +++ /dev/null @@ -1,274 +0,0 @@ -# AI图标使用指南 - -## 概述 - -本文档为AI助手提供标准化的图标选择和使用流程,确保在代码生成时使用正确的图标包和图标名称。 - -## AI使用流程 - -### 1. 项目类型判断 -首先判断项目类型: -- **PC端项目**: 使用 `@ant-design/icons` -- **移动端项目**: 使用 `antd-mobile-icons` - -### 2. 图标查找流程 -1. 根据功能需求确定图标类型 -2. 在对应图标包中查找合适的图标 -3. 如果图标不存在,查找替代方案 -4. 使用正确的导入语法 - -### 3. 代码生成模板 - -#### PC端项目模板 -```typescript -import { - // 导航类 - HomeOutlined, - UserOutlined, - SettingOutlined, - - // 操作类 - PlusOutlined, - EditOutlined, - DeleteOutlined, - CopyOutlined, - SearchOutlined, - ReloadOutlined, - - // 状态类 - CheckOutlined, - CloseOutlined, - CheckCircleOutlined, - CloseCircleOutlined, - ExclamationCircleOutlined, - InfoCircleOutlined, - LoadingOutlined, - - // 方向类 - UpOutlined, - DownOutlined, - LeftOutlined, - RightOutlined, - ArrowLeftOutlined, - - // 其他 - MessageOutlined, - CalendarOutlined, - ClockCircleOutlined, - PictureOutlined, - FileOutlined, - CameraOutlined, - QrcodeOutlined, -} from '@ant-design/icons'; -``` - -#### 移动端项目模板 -```typescript -import { - // 导航类 - HomeOutline, - UserOutline, - SettingOutline, - - // 操作类 - AddOutline, - EditSOutline, - DeleteOutline, - CopyOutline, - SearchOutline, - RefreshOutline, - - // 状态类 - CheckOutline, - CloseOutline, - CheckCircleOutline, - CloseCircleOutline, - ExclamationCircleOutline, - InfoCircleOutline, - LoadingOutline, - - // 方向类 - UpOutline, - DownOutline, - LeftOutline, - RightOutline, - - // 其他 - MessageOutline, - CalendarOutline, - ClockCircleOutline, - PictureOutline, - FileOutline, - CameraOutline, - QrCodeOutline, -} from 'antd-mobile-icons'; -``` - -## 功能到图标映射 - -### 基础功能映射 -| 功能需求 | PC端图标 | 移动端图标 | 说明 | -|---------|---------|-----------|------| -| 添加/新建 | PlusOutlined | AddOutline | 通用添加功能 | -| 编辑/修改 | EditOutlined | EditSOutline | 编辑功能 | -| 删除/移除 | DeleteOutlined | DeleteOutline | 删除功能 | -| 复制/克隆 | CopyOutlined | CopyOutline | 复制功能 | -| 搜索/查找 | SearchOutlined | SearchOutline | 搜索功能 | -| 刷新/重新加载 | ReloadOutlined | RefreshOutline | 刷新功能 | -| 设置/配置 | SettingOutlined | SettingOutline | 设置功能 | -| 用户/个人 | UserOutlined | UserOutline | 用户相关 | -| 首页/主页 | HomeOutlined | HomeOutline | 首页导航 | -| 返回/后退 | ArrowLeftOutlined | LeftOutline | 返回功能 | -| 关闭/取消 | CloseOutlined | CloseOutline | 关闭功能 | -| 确认/确定 | CheckOutlined | CheckOutline | 确认功能 | - -### 状态指示映射 -| 状态需求 | PC端图标 | 移动端图标 | 说明 | -|---------|---------|-----------|------| -| 成功/完成 | CheckCircleOutlined | CheckCircleOutline | 成功状态 | -| 错误/失败 | CloseCircleOutlined | CloseCircleOutline | 错误状态 | -| 警告/注意 | ExclamationCircleOutlined | ExclamationCircleOutline | 警告状态 | -| 信息/提示 | InfoCircleOutlined | InfoCircleOutline | 信息提示 | -| 加载/等待 | LoadingOutlined | LoadingOutline | 加载状态 | -| 时间/等待 | ClockCircleOutlined | ClockCircleOutline | 时间相关 | - -### 方向导航映射 -| 方向需求 | PC端图标 | 移动端图标 | 说明 | -|---------|---------|-----------|------| -| 向上/上升 | UpOutlined | UpOutline | 向上方向 | -| 向下/下降 | DownOutlined | DownOutline | 向下方向 | -| 向左/后退 | LeftOutlined | LeftOutline | 向左方向 | -| 向右/前进 | RightOutlined | RightOutline | 向右方向 | - -### 业务功能映射 -| 业务需求 | PC端图标 | 移动端图标 | 说明 | -|---------|---------|-----------|------| -| 消息/通知 | MessageOutlined | MessageOutline | 消息功能 | -| 日历/日期 | CalendarOutlined | CalendarOutline | 日历功能 | -| 图片/照片 | PictureOutlined | PictureOutline | 图片功能 | -| 文件/文档 | FileOutlined | FileOutline | 文件功能 | -| 相机/拍照 | CameraOutlined | CameraOutline | 相机功能 | -| 二维码 | QrcodeOutlined | QrCodeOutline | 二维码功能 | -| 微信/社交 | WechatOutlined | WechatOutline | 微信功能 | -| 设备/手机 | MobileOutlined | MobileOutline | 设备功能 | -| 团队/群组 | TeamOutlined | TeamOutline | 团队功能 | -| 订单/购物 | ShoppingOutlined | ShoppingOutline | 订单功能 | -| 支付/钱包 | PayCircleOutlined | PayCircleOutline | 支付功能 | - -## 特殊替换规则 - -### 移动端不存在的图标替换 -| 原需求 | 替换方案 | 说明 | -|--------|----------|------| -| RiseOutlined | UpOutline | 上升趋势 | -| ThumbsUpOutlined | LikeOutline | 点赞功能 | -| ShareAltOutlined | LinkOutline | 分享功能 | -| BarChartOutlined | PieOutline | 图表功能 | -| LineChartOutlined | PieOutline | 图表功能 | -| UserAddOutlined | UserOutline | 用户添加 | -| Progress | 自定义div | 进度条组件 | - -### PC端不存在的图标替换 -| 原需求 | 替换方案 | 说明 | -|--------|----------|------| -| AntOutline | HomeOutlined | 蚂蚁图标 | -| AppOutline | AppstoreOutlined | 应用图标 | - -## AI代码生成示例 - -### 场景1: 移动端列表页面 -```typescript -// AI应该生成的代码 -import { - AddOutline, // 添加按钮 - EditSOutline, // 编辑按钮 - DeleteOutline, // 删除按钮 - CopyOutline, // 复制按钮 - SearchOutline, // 搜索框 - RefreshOutline, // 刷新按钮 - UserOutline, // 用户信息 - CalendarOutline, // 时间信息 - UpOutline, // 上升趋势(替换RiseOutlined) -} from 'antd-mobile-icons'; - -// 使用示例 - - - -``` - -### 场景2: PC端管理页面 -```typescript -// AI应该生成的代码 -import { - PlusOutlined, // 添加按钮 - EditOutlined, // 编辑按钮 - DeleteOutlined, // 删除按钮 - CopyOutlined, // 复制按钮 - SearchOutlined, // 搜索框 - ReloadOutlined, // 刷新按钮 - UserOutlined, // 用户信息 - CalendarOutlined, // 时间信息 - RiseOutlined, // 上升趋势(PC端存在) -} from '@ant-design/icons'; - -// 使用示例 - - - -``` - -## 错误检测和修正 - -### 常见错误模式 -1. **混用图标包**: 同时导入PC端和移动端图标 -2. **使用不存在的图标**: 在移动端使用PC端特有的图标 -3. **命名错误**: 图标名称大小写错误 -4. **导入路径错误**: 使用错误的包名 - -### 修正策略 -1. **统一图标包**: 根据项目类型选择单一图标包 -2. **查找替代**: 使用对照表查找替代图标 -3. **验证存在**: 确保图标在目标包中存在 -4. **测试验证**: 在代码中测试图标是否正常显示 - -## AI使用检查清单 - -### 代码生成前 -- [ ] 确认项目类型(PC端/移动端) -- [ ] 选择对应的图标包 -- [ ] 根据功能需求选择合适图标 -- [ ] 检查图标是否存在 - -### 代码生成中 -- [ ] 使用正确的导入语法 -- [ ] 图标名称大小写正确 -- [ ] 避免混用不同包的图标 -- [ ] 为不存在的图标提供替代方案 - -### 代码生成后 -- [ ] 验证图标导入正确 -- [ ] 检查图标使用语法 -- [ ] 确保样式设置合理 -- [ ] 提供使用示例 - -## 更新和维护 - -- 定期更新图标对照表 -- 记录新发现的图标差异 -- 更新替换规则 -- 优化AI使用流程 - -## 注意事项 - -1. **优先使用语义化图标**: 选择最能表达功能的图标 -2. **保持一致性**: 在同一项目中保持图标风格一致 -3. **考虑可访问性**: 为图标添加适当的aria-label -4. **性能优化**: 按需导入图标,避免全量导入 -5. **版本兼容**: 注意图标包版本与UI框架版本的兼容性 \ No newline at end of file diff --git a/nkebao/ICON_DETAILED_MAPPING.md b/nkebao/ICON_DETAILED_MAPPING.md deleted file mode 100644 index b2c73166..00000000 --- a/nkebao/ICON_DETAILED_MAPPING.md +++ /dev/null @@ -1,205 +0,0 @@ -# 详细图标对照表 - -## 基础图标对照 - -### 导航类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 首页 | HomeOutlined | HomeOutline | ✅ | -| 返回 | ArrowLeftOutlined | LeftOutline | ✅ | -| 菜单 | MenuOutlined | MenuOutline | ✅ | -| 设置 | SettingOutlined | SettingOutline | ✅ | -| 用户 | UserOutlined | UserOutline | ✅ | -| 个人中心 | UserOutlined | UserOutline | ✅ | - -### 操作类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 添加 | PlusOutlined | AddOutline | ✅ | -| 编辑 | EditOutlined | EditSOutline | ✅ | -| 删除 | DeleteOutlined | DeleteOutline | ✅ | -| 复制 | CopyOutlined | CopyOutline | ✅ | -| 保存 | SaveOutlined | SaveOutline | ✅ | -| 刷新 | ReloadOutlined | RefreshOutline | ✅ | -| 搜索 | SearchOutlined | SearchOutline | ✅ | -| 关闭 | CloseOutlined | CloseOutline | ✅ | -| 确认 | CheckOutlined | CheckOutline | ✅ | -| 取消 | CloseOutlined | CloseOutline | ✅ | - -### 状态类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 成功 | CheckCircleOutlined | CheckCircleOutline | ✅ | -| 错误 | CloseCircleOutlined | CloseCircleOutline | ✅ | -| 警告 | ExclamationCircleOutlined | ExclamationCircleOutline | ✅ | -| 信息 | InfoCircleOutlined | InfoCircleOutline | ✅ | -| 加载 | LoadingOutlined | LoadingOutline | ✅ | -| 等待 | ClockCircleOutlined | ClockCircleOutline | ✅ | - -### 方向类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 向上 | UpOutlined | UpOutline | ✅ | -| 向下 | DownOutlined | DownOutline | ✅ | -| 向左 | LeftOutlined | LeftOutline | ✅ | -| 向右 | RightOutlined | RightOutline | ✅ | -| 向上圆形 | UpCircleOutlined | UpCircleOutline | ✅ | -| 向下圆形 | DownCircleOutlined | DownCircleOutline | ✅ | - -### 通信类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 消息 | MessageOutlined | MessageOutline | ✅ | -| 邮件 | MailOutlined | MailOutline | ✅ | -| 电话 | PhoneOutlined | PhoneOutline | ✅ | -| 视频 | VideoCameraOutlined | VideoCameraOutline | ✅ | -| 语音 | AudioOutlined | AudioOutline | ✅ | - -### 媒体类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 图片 | PictureOutlined | PictureOutline | ✅ | -| 文件 | FileOutlined | FileOutline | ✅ | -| 文件夹 | FolderOutlined | FolderOutline | ✅ | -| 相机 | CameraOutlined | CameraOutline | ✅ | -| 二维码 | QrcodeOutlined | QrCodeOutline | ✅ | - -### 时间类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 日历 | CalendarOutlined | CalendarOutline | ✅ | -| 时钟 | ClockCircleOutlined | ClockCircleOutline | ✅ | -| 历史 | HistoryOutlined | HistoryOutline | ✅ | - -### 数据类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 统计 | BarChartOutlined | BarChartOutline | ✅ | -| 饼图 | PieChartOutlined | PieChartOutline | ✅ | -| 折线图 | LineChartOutlined | LineChartOutline | ✅ | -| 表格 | TableOutlined | TableOutline | ✅ | -| 列表 | UnorderedListOutlined | UnorderedListOutline | ✅ | - -## 特殊图标对照 - -### 业务相关 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 设备 | MobileOutlined | MobileOutline | ✅ | -| 微信 | WechatOutlined | WechatOutline | ✅ | -| 群组 | TeamOutlined | TeamOutline | ✅ | -| 客户 | UserAddOutlined | UserAddOutline | ❌ | -| 订单 | ShoppingOutlined | ShoppingOutline | ✅ | -| 支付 | PayCircleOutlined | PayCircleOutline | ✅ | - -### 工具类 -| 功能 | PC端 | 移动端 | 状态 | -|------|------|--------|------| -| 工具 | ToolOutlined | ToolOutline | ✅ | -| 配置 | SettingOutlined | SettingOutline | ✅ | -| 帮助 | QuestionCircleOutlined | QuestionCircleOutline | ✅ | -| 反馈 | MessageOutlined | MessageOutline | ✅ | -| 分享 | ShareAltOutlined | ShareOutline | ❌ | - -## 不存在的图标替换方案 - -### PC端存在但移动端不存在的图标 -| PC端图标 | 推荐替换 | 说明 | -|----------|----------|------| -| UserAddOutlined | UserOutline | 用户添加功能 | -| ShareAltOutlined | LinkOutline | 分享功能 | -| RiseOutlined | UpOutline | 上升趋势 | -| ThumbsUpOutlined | LikeOutline | 点赞功能 | -| BarChartOutlined | PieOutline | 图表功能 | -| LineChartOutlined | PieOutline | 图表功能 | - -### 移动端存在但PC端不存在的图标 -| 移动端图标 | 推荐替换 | 说明 | -|------------|----------|------| -| AntOutline | HomeOutlined | 蚂蚁图标 | -| AppOutline | AppstoreOutlined | 应用图标 | - -## 使用规范 - -### 1. 导入规范 -```typescript -// PC端项目 -import { - HomeOutlined, - UserOutlined, - SettingOutlined, -} from '@ant-design/icons'; - -// 移动端项目 -import { - HomeOutline, - UserOutline, - SettingOutline, -} from 'antd-mobile-icons'; -``` - -### 2. 命名规范 -- PC端:使用 `Outlined` 后缀 -- 移动端:使用 `Outline` 后缀 -- 保持语义化命名 - -### 3. 使用建议 -- 优先使用语义明确的图标 -- 保持图标风格一致性 -- 考虑图标在不同尺寸下的清晰度 -- 为图标添加适当的aria-label - -### 4. 错误处理 -当图标不存在时: -1. 查找语义相近的图标 -2. 使用通用图标(如QuestionOutlined) -3. 考虑使用文字替代 -4. 创建自定义图标组件 - -## 项目中的实际应用 - -### 场景获客模块使用的图标 -```typescript -// 移动端项目中的图标使用 -import { - AddOutline, // 添加 - UpOutline, // 上升趋势(替换RiseOutline) - UserOutline, // 用户 - CalendarOutline, // 日历 - CopyOutline, // 复制 - DeleteOutline, // 删除 - EditSOutline, // 编辑 - SettingOutline, // 设置 - SearchOutline, // 搜索 - RefreshOutline, // 刷新 - QrCodeOutline, // 二维码 -} from 'antd-mobile-icons'; -``` - -### 工作台模块使用的图标 -```typescript -// 移动端项目中的图标使用 -import { - LikeOutline, // 点赞(替换ThumbsUpOutline) - LinkOutline, // 链接(替换ShareOutline) - PieOutline, // 饼图(替换BarChartOutline/LineChartOutline) - UserOutline, // 用户 - TeamOutline, // 团队 - MessageOutline, // 消息 -} from 'antd-mobile-icons'; -``` - -## 更新和维护 - -1. **定期检查**: 定期检查新版本中新增的图标 -2. **文档更新**: 及时更新图标对照表 -3. **团队协作**: 团队成员共享图标使用规范 -4. **代码审查**: 在代码审查中检查图标使用是否正确 - -## 注意事项 - -1. **包版本**: 确保图标包版本与UI框架版本兼容 -2. **按需导入**: 避免全量导入图标,影响打包体积 -3. **样式覆盖**: 可以通过CSS自定义图标样式 -4. **无障碍**: 为图标添加适当的无障碍属性 -5. **性能**: 大量使用图标时注意性能优化 \ No newline at end of file diff --git a/nkebao/ICON_MAPPING_GUIDE.md b/nkebao/ICON_MAPPING_GUIDE.md deleted file mode 100644 index 2b237842..00000000 --- a/nkebao/ICON_MAPPING_GUIDE.md +++ /dev/null @@ -1,230 +0,0 @@ -# PC端与移动端图标对照文档 - -## 概述 - -本文档记录了PC端(@ant-design/icons)和移动端(antd-mobile-icons)的图标名称对照,以及正确的导入方式。 - -## 导入方式 - -### PC端图标 (@ant-design/icons) -```typescript -import { - HomeOutlined, - UserOutlined, - SettingOutlined, - // ... 其他图标 -} from '@ant-design/icons'; -``` - -### 移动端图标 (antd-mobile-icons) -```typescript -import { - AntOutline, - ArrowDownCircleOutline, - UserOutline, - // ... 其他图标 -} from 'antd-mobile-icons'; -``` - -## 图标对照表 - -### 常用图标对照 - -| 功能描述 | PC端图标 | 移动端图标 | 备注 | -|---------|---------|-----------|------| -| 首页 | HomeOutlined | HomeOutline | 完全对应 | -| 用户 | UserOutlined | UserOutline | 完全对应 | -| 设置 | SettingOutlined | SettingOutline | 完全对应 | -| 搜索 | SearchOutlined | SearchOutline | 完全对应 | -| 添加 | PlusOutlined | AddOutline | 完全对应 | -| 编辑 | EditOutlined | EditSOutline | 移动端略有不同 | -| 删除 | DeleteOutlined | DeleteOutline | 完全对应 | -| 复制 | CopyOutlined | CopyOutline | 完全对应 | -| 刷新 | ReloadOutlined | RefreshOutline | 完全对应 | -| 二维码 | QrcodeOutlined | QrCodeOutline | 完全对应 | -| 日历 | CalendarOutlined | CalendarOutline | 完全对应 | -| 时钟 | ClockCircleOutlined | ClockCircleOutline | 完全对应 | -| 箭头向上 | UpOutlined | UpOutline | 完全对应 | -| 箭头向下 | DownOutlined | DownOutline | 完全对应 | -| 箭头向左 | LeftOutlined | LeftOutline | 完全对应 | -| 箭头向右 | RightOutlined | RightOutline | 完全对应 | -| 返回 | ArrowLeftOutlined | LeftOutline | 移动端使用LeftOutline | -| 关闭 | CloseOutlined | CloseOutline | 完全对应 | -| 检查 | CheckOutlined | CheckOutline | 完全对应 | -| 警告 | ExclamationCircleOutlined | ExclamationCircleOutline | 完全对应 | -| 信息 | InfoCircleOutlined | InfoCircleOutline | 完全对应 | -| 成功 | CheckCircleOutlined | CheckCircleOutline | 完全对应 | -| 错误 | CloseCircleOutlined | CloseCircleOutline | 完全对应 | - -### 方向性图标 - -| 功能描述 | PC端图标 | 移动端图标 | -|---------|---------|-----------| -| 向上 | UpOutlined | UpOutline | -| 向下 | DownOutlined | DownOutline | -| 向左 | LeftOutlined | LeftOutline | -| 向右 | RightOutlined | RightOutline | -| 向上圆形 | UpCircleOutlined | UpCircleOutline | -| 向下圆形 | DownCircleOutlined | DownCircleOutline | -| 向左圆形 | LeftCircleOutlined | LeftCircleOutline | -| 向右圆形 | RightCircleOutlined | RightCircleOutline | - -### 编辑类图标 - -| 功能描述 | PC端图标 | 移动端图标 | -|---------|---------|-----------| -| 编辑 | EditOutlined | EditSOutline | -| 删除 | DeleteOutlined | DeleteOutline | -| 复制 | CopyOutlined | CopyOutline | -| 剪切 | ScissorOutlined | ScissorOutline | -| 撤销 | UndoOutlined | UndoOutline | -| 重做 | RedoOutlined | RedoOutline | -| 保存 | SaveOutlined | SaveOutline | - -### 通信类图标 - -| 功能描述 | PC端图标 | 移动端图标 | -|---------|---------|-----------| -| 消息 | MessageOutlined | MessageOutline | -| 邮件 | MailOutlined | MailOutline | -| 电话 | PhoneOutlined | PhoneOutline | -| 视频通话 | VideoCameraOutlined | VideoCameraOutline | -| 语音 | AudioOutlined | AudioOutline | - -### 媒体类图标 - -| 功能描述 | PC端图标 | 移动端图标 | -|---------|---------|-----------| -| 图片 | PictureOutlined | PictureOutline | -| 视频 | VideoCameraOutlined | VideoCameraOutline | -| 音频 | AudioOutlined | AudioOutline | -| 文件 | FileOutlined | FileOutline | -| 文件夹 | FolderOutlined | FolderOutline | -| 相机 | CameraOutlined | CameraOutline | - -### 导航类图标 - -| 功能描述 | PC端图标 | 移动端图标 | -|---------|---------|-----------| -| 菜单 | MenuOutlined | MenuOutline | -| 汉堡菜单 | MenuFoldOutlined | MenuOutline | -| 展开菜单 | MenuUnfoldOutlined | MenuOutline | -| 面包屑 | BreadcrumbOutlined | BreadcrumbOutline | -| 分页 | PaginationOutlined | PaginationOutline | - -### 数据展示类图标 - -| 功能描述 | PC端图标 | 移动端图标 | -|---------|---------|-----------| -| 表格 | TableOutlined | TableOutline | -| 列表 | UnorderedListOutlined | UnorderedListOutline | -| 卡片 | CreditCardOutlined | CreditCardOutline | -| 统计 | BarChartOutlined | BarChartOutline | -| 饼图 | PieChartOutlined | PieChartOutline | -| 折线图 | LineChartOutlined | LineChartOutline | -| 仪表盘 | DashboardOutlined | DashboardOutline | - -### 反馈类图标 - -| 功能描述 | PC端图标 | 移动端图标 | -|---------|---------|-----------| -| 成功 | CheckCircleOutlined | CheckCircleOutline | -| 错误 | CloseCircleOutlined | CloseCircleOutline | -| 警告 | ExclamationCircleOutlined | ExclamationCircleOutline | -| 信息 | InfoCircleOutlined | InfoCircleOutline | -| 加载 | LoadingOutlined | LoadingOutline | -| 等待 | ClockCircleOutlined | ClockCircleOutline | - -## 使用建议 - -### 1. 项目类型判断 -- **PC端项目**: 使用 `@ant-design/icons` -- **移动端项目**: 使用 `antd-mobile-icons` - -### 2. 图标选择原则 -- 优先选择语义化图标 -- 保持图标风格一致性 -- 考虑图标在不同尺寸下的清晰度 - -### 3. 常见错误避免 -- 不要混用PC端和移动端图标 -- 注意图标名称的大小写 -- 确保图标在对应包中存在 - -### 4. 图标替换策略 -当某个图标在目标包中不存在时: -1. 查找语义相近的图标 -2. 使用通用图标(如QuestionOutlined) -3. 考虑使用文字替代 -4. 创建自定义图标组件 - -## 实际项目中的使用示例 - -### PC端项目示例 -```typescript -import { - HomeOutlined, - UserOutlined, - SettingOutlined, - SearchOutlined, - PlusOutlined, - EditOutlined, - DeleteOutlined, - CopyOutlined, - ReloadOutlined, - QrcodeOutlined, - CalendarOutlined, - ClockCircleOutlined, - UpOutlined, - DownOutlined, - LeftOutlined, - RightOutlined, - CloseOutlined, - CheckOutlined, - ExclamationCircleOutlined, - InfoCircleOutlined, - CheckCircleOutlined, - CloseCircleOutlined, -} from '@ant-design/icons'; -``` - -### 移动端项目示例 -```typescript -import { - HomeOutline, - UserOutline, - SettingOutline, - SearchOutline, - AddOutline, - EditSOutline, - DeleteOutline, - CopyOutline, - RefreshOutline, - QrCodeOutline, - CalendarOutline, - ClockCircleOutline, - UpOutline, - DownOutline, - LeftOutline, - RightOutline, - CloseOutline, - CheckOutline, - ExclamationCircleOutline, - InfoCircleOutline, - CheckCircleOutline, - CloseCircleOutline, -} from 'antd-mobile-icons'; -``` - -## 注意事项 - -1. **包依赖**: 确保项目中已安装对应的图标包 -2. **版本兼容**: 注意图标包版本与UI框架版本的兼容性 -3. **性能考虑**: 按需导入图标,避免全量导入 -4. **样式覆盖**: 可以通过CSS自定义图标颜色和大小 -5. **无障碍**: 为图标添加适当的aria-label属性 - -## 更新记录 - -- 2024-01-XX: 初始版本,包含常用图标对照 -- 后续根据实际使用情况持续更新 \ No newline at end of file diff --git a/nkebao/ICON_QUICK_REFERENCE.md b/nkebao/ICON_QUICK_REFERENCE.md deleted file mode 100644 index 1e64fe9a..00000000 --- a/nkebao/ICON_QUICK_REFERENCE.md +++ /dev/null @@ -1,271 +0,0 @@ -# 图标快速参考表 - -## 快速查找 - -### 🔍 按功能查找 - -| 功能 | PC端 | 移动端 | 导入方式 | -|------|------|--------|----------| -| **添加** | PlusOutlined | AddOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **编辑** | EditOutlined | EditSOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **删除** | DeleteOutlined | DeleteOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **复制** | CopyOutlined | CopyOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **搜索** | SearchOutlined | SearchOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **刷新** | ReloadOutlined | RefreshOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **设置** | SettingOutlined | SettingOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **用户** | UserOutlined | UserOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **首页** | HomeOutlined | HomeOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **返回** | ArrowLeftOutlined | LeftOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **关闭** | CloseOutlined | CloseOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **确认** | CheckOutlined | CheckOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **成功** | CheckCircleOutlined | CheckCircleOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **错误** | CloseCircleOutlined | CloseCircleOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **警告** | ExclamationCircleOutlined | ExclamationCircleOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **信息** | InfoCircleOutlined | InfoCircleOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **加载** | LoadingOutlined | LoadingOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **向上** | UpOutlined | UpOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **向下** | DownOutlined | DownOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **向左** | LeftOutlined | LeftOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **向右** | RightOutlined | RightOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **消息** | MessageOutlined | MessageOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **电话** | PhoneOutlined | PhoneOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **日历** | CalendarOutlined | CalendarOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **时钟** | ClockCircleOutlined | ClockCircleOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **图片** | PictureOutlined | PictureOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **文件** | FileOutlined | FileOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **相机** | CameraOutlined | CameraOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **二维码** | QrcodeOutlined | QrCodeOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **微信** | WechatOutlined | WechatOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **设备** | MobileOutlined | MobileOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **团队** | TeamOutlined | TeamOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **订单** | ShoppingOutlined | ShoppingOutline | `@ant-design/icons` / `antd-mobile-icons` | -| **支付** | PayCircleOutlined | PayCircleOutline | `@ant-design/icons` / `antd-mobile-icons` | - -### 🔄 常见替换 - -| 原图标 | 替换为 | 说明 | -|--------|--------|------| -| RiseOutlined | UpOutline | 上升趋势 | -| ThumbsUpOutlined | LikeOutline | 点赞功能 | -| ShareAltOutlined | LinkOutline | 分享功能 | -| BarChartOutlined | PieOutline | 图表功能 | -| LineChartOutlined | PieOutline | 图表功能 | -| UserAddOutlined | UserOutline | 用户添加 | -| SettingOutline | SettingOutline | 设置(移动端) | - -## 导入模板 - -### PC端项目模板 -```typescript -import { - HomeOutlined, - UserOutlined, - SettingOutlined, - SearchOutlined, - PlusOutlined, - EditOutlined, - DeleteOutlined, - CopyOutlined, - ReloadOutlined, - CloseOutlined, - CheckOutlined, - UpOutlined, - DownOutlined, - LeftOutlined, - RightOutlined, - MessageOutlined, - CalendarOutlined, - ClockCircleOutlined, - PictureOutlined, - FileOutlined, - CameraOutlined, - QrcodeOutlined, - WechatOutlined, - MobileOutlined, - TeamOutlined, - ShoppingOutlined, - PayCircleOutlined, - CheckCircleOutlined, - CloseCircleOutlined, - ExclamationCircleOutlined, - InfoCircleOutlined, - LoadingOutlined, -} from '@ant-design/icons'; -``` - -### 移动端项目模板 -```typescript -import { - HomeOutline, - UserOutline, - SettingOutline, - SearchOutline, - AddOutline, - EditSOutline, - DeleteOutline, - CopyOutline, - RefreshOutline, - CloseOutline, - CheckOutline, - UpOutline, - DownOutline, - LeftOutline, - RightOutline, - MessageOutline, - CalendarOutline, - ClockCircleOutline, - PictureOutline, - FileOutline, - CameraOutline, - QrCodeOutline, - WechatOutline, - MobileOutline, - TeamOutline, - ShoppingOutline, - PayCircleOutline, - CheckCircleOutline, - CloseCircleOutline, - ExclamationCircleOutline, - InfoCircleOutline, - LoadingOutline, -} from 'antd-mobile-icons'; -``` - -## 使用示例 - -### 基础使用 -```typescript -// PC端 -import { HomeOutlined, UserOutlined } from '@ant-design/icons'; - - - - -// 移动端 -import { HomeOutline, UserOutline } from 'antd-mobile-icons'; - - - -``` - -### 按钮中使用 -```typescript -// PC端 -import { PlusOutlined, EditOutlined } from '@ant-design/icons'; - - - - -// 移动端 -import { AddOutline, EditSOutline } from 'antd-mobile-icons'; - - - -``` - -### 列表中使用 -```typescript -// PC端 -import { DeleteOutlined, CopyOutlined } from '@ant-design/icons'; - - - - -// 移动端 -import { DeleteOutline, CopyOutline } from 'antd-mobile-icons'; - - - -``` - -## 常见错误 - -### ❌ 错误示例 -```typescript -// 错误:混用PC端和移动端图标 -import { HomeOutlined } from '@ant-design/icons'; // PC端 -import { UserOutline } from 'antd-mobile-icons'; // 移动端 - -// 错误:使用不存在的图标 -import { RiseOutlined } from 'antd-mobile-icons'; // 不存在 -import { UserAddOutline } from 'antd-mobile-icons'; // 不存在 -``` - -### ✅ 正确示例 -```typescript -// 正确:统一使用移动端图标 -import { - HomeOutline, - UserOutline, - UpOutline, // 替换RiseOutlined - UserOutline // 替换UserAddOutline -} from 'antd-mobile-icons'; - -// 正确:统一使用PC端图标 -import { - HomeOutlined, - UserOutlined, - RiseOutlined, // PC端存在 - UserAddOutlined // PC端存在 -} from '@ant-design/icons'; -``` - -## 快速检查清单 - -### 开发前检查 -- [ ] 确认项目类型(PC端/移动端) -- [ ] 选择对应的图标包 -- [ ] 检查图标是否存在 -- [ ] 准备替换方案 - -### 开发中检查 -- [ ] 使用正确的导入方式 -- [ ] 图标名称大小写正确 -- [ ] 避免混用不同包的图标 -- [ ] 为图标添加适当的样式 - -### 开发后检查 -- [ ] 图标显示正常 -- [ ] 样式符合设计要求 -- [ ] 无障碍属性完整 -- [ ] 性能影响最小 - -## 紧急替换方案 - -当遇到图标不存在时,使用以下通用图标: - -```typescript -// 移动端通用图标 -import { - QuestionCircleOutline, // 通用问号 - AppOutline, // 通用应用 - ToolOutline, // 通用工具 - SettingOutline, // 通用设置 - UserOutline, // 通用用户 -} from 'antd-mobile-icons'; - -// PC端通用图标 -import { - QuestionCircleOutlined, // 通用问号 - AppstoreOutlined, // 通用应用 - ToolOutlined, // 通用工具 - SettingOutlined, // 通用设置 - UserOutlined, // 通用用户 -} from '@ant-design/icons'; -``` - -## 更新日志 - -- 2024-01-XX: 初始版本 -- 添加常用图标对照 -- 添加错误示例和正确示例 -- 添加快速检查清单 -- 添加紧急替换方案 \ No newline at end of file diff --git a/nkebao/index.html b/nkebao/index.html index 784f0cf8..22f819b6 100644 --- a/nkebao/index.html +++ b/nkebao/index.html @@ -1,13 +1,19 @@ - + Nkebao Base - + + +
- \ No newline at end of file + diff --git a/nkebao/package-lock.json b/nkebao/package-lock.json index 5932cee5..3a81b88f 100644 --- a/nkebao/package-lock.json +++ b/nkebao/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@ant-design/icons": "^5.6.1", + "@capacitor/android": "^7.4.2", "antd": "^5.13.1", "antd-mobile": "^5.39.1", "axios": "^1.6.7", @@ -444,6 +445,25 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor/android": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-7.4.2.tgz", + "integrity": "sha512-FZ7M9NwFkljR7EP5eXiE32mAIfZNcYw2CzRMCG3rQu0u0ZaIoeOeq5/oK4YcDnGpNmu8jpngKJqZ+9OiSQSwDg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^7.4.0" + } + }, + "node_modules/@capacitor/core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.2.tgz", + "integrity": "sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", diff --git a/nkebao/public/logo.png b/nkebao/public/logo.png new file mode 100644 index 00000000..40daafb0 Binary files /dev/null and b/nkebao/public/logo.png differ diff --git a/nkebao/public/manifest.json b/nkebao/public/manifest.json new file mode 100644 index 00000000..fc1e9f5a --- /dev/null +++ b/nkebao/public/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "Cunkebao", + "short_name": "Cunkebao", + "description": "Cunkebao Mobile App", + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone", + "orientation": "portrait", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "logo.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} \ No newline at end of file diff --git a/nkebao/public/websdk.js b/nkebao/public/websdk.js new file mode 100644 index 00000000..99870fa4 --- /dev/null +++ b/nkebao/public/websdk.js @@ -0,0 +1,308 @@ +!(function (e, n) { + "object" == typeof exports && "undefined" != typeof module + ? (module.exports = n()) + : "function" == typeof define && define.amd + ? define(n) + : ((e = e || self).uni = n()); +})(this, function () { + "use strict"; + try { + var e = {}; + (Object.defineProperty(e, "passive", { + get: function () { + !0; + }, + }), + window.addEventListener("test-passive", null, e)); + } catch (e) {} + var n = Object.prototype.hasOwnProperty; + function i(e, i) { + return n.call(e, i); + } + var t = []; + function o() { + return window.__dcloud_weex_postMessage || window.__dcloud_weex_; + } + function a() { + return window.__uniapp_x_postMessage || window.__uniapp_x_; + } + var r = function (e, n) { + var i = { options: { timestamp: +new Date() }, name: e, arg: n }; + if (a()) { + if ("postMessage" === e) { + var r = { data: n }; + return window.__uniapp_x_postMessage + ? window.__uniapp_x_postMessage(r) + : window.__uniapp_x_.postMessage(JSON.stringify(r)); + } + var d = { + type: "WEB_INVOKE_APPSERVICE", + args: { data: i, webviewIds: t }, + }; + window.__uniapp_x_postMessage + ? window.__uniapp_x_postMessageToService(d) + : window.__uniapp_x_.postMessageToService(JSON.stringify(d)); + } else if (o()) { + if ("postMessage" === e) { + var s = { data: [n] }; + return window.__dcloud_weex_postMessage + ? window.__dcloud_weex_postMessage(s) + : window.__dcloud_weex_.postMessage(JSON.stringify(s)); + } + var w = { + type: "WEB_INVOKE_APPSERVICE", + args: { data: i, webviewIds: t }, + }; + window.__dcloud_weex_postMessage + ? window.__dcloud_weex_postMessageToService(w) + : window.__dcloud_weex_.postMessageToService(JSON.stringify(w)); + } else { + if (!window.plus) + return window.parent.postMessage( + { type: "WEB_INVOKE_APPSERVICE", data: i, pageId: "" }, + "*", + ); + if (0 === t.length) { + var u = plus.webview.currentWebview(); + if (!u) throw new Error("plus.webview.currentWebview() is undefined"); + var g = u.parent(), + v = ""; + ((v = g ? g.id : u.id), t.push(v)); + } + if (plus.webview.getWebviewById("__uniapp__service")) + plus.webview.postMessageToUniNView( + { type: "WEB_INVOKE_APPSERVICE", args: { data: i, webviewIds: t } }, + "__uniapp__service", + ); + else { + var c = JSON.stringify(i); + plus.webview + .getLaunchWebview() + .evalJS( + 'UniPlusBridge.subscribeHandler("' + .concat("WEB_INVOKE_APPSERVICE", '",') + .concat(c, ",") + .concat(JSON.stringify(t), ");"), + ); + } + } + }, + d = { + navigateTo: function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + n = e.url; + r("navigateTo", { url: encodeURI(n) }); + }, + navigateBack: function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + n = e.delta; + r("navigateBack", { delta: parseInt(n) || 1 }); + }, + switchTab: function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + n = e.url; + r("switchTab", { url: encodeURI(n) }); + }, + reLaunch: function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + n = e.url; + r("reLaunch", { url: encodeURI(n) }); + }, + redirectTo: function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + n = e.url; + r("redirectTo", { url: encodeURI(n) }); + }, + getEnv: function (e) { + a() + ? e({ uvue: !0 }) + : o() + ? e({ nvue: !0 }) + : window.plus + ? e({ plus: !0 }) + : e({ h5: !0 }); + }, + postMessage: function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; + r("postMessage", e.data || {}); + }, + }, + s = /uni-app/i.test(navigator.userAgent), + w = /Html5Plus/i.test(navigator.userAgent), + u = /complete|loaded|interactive/; + var g = + window.my && + navigator.userAgent.indexOf( + ["t", "n", "e", "i", "l", "C", "y", "a", "p", "i", "l", "A"] + .reverse() + .join(""), + ) > -1; + var v = + window.swan && window.swan.webView && /swan/i.test(navigator.userAgent); + var c = + window.qq && + window.qq.miniProgram && + /QQ/i.test(navigator.userAgent) && + /miniProgram/i.test(navigator.userAgent); + var p = + window.tt && + window.tt.miniProgram && + /toutiaomicroapp/i.test(navigator.userAgent); + var _ = + window.wx && + window.wx.miniProgram && + /micromessenger/i.test(navigator.userAgent) && + /miniProgram/i.test(navigator.userAgent); + var m = window.qa && /quickapp/i.test(navigator.userAgent); + var f = + window.ks && + window.ks.miniProgram && + /micromessenger/i.test(navigator.userAgent) && + /miniProgram/i.test(navigator.userAgent); + var l = + window.tt && + window.tt.miniProgram && + /Lark|Feishu/i.test(navigator.userAgent); + var E = + window.jd && window.jd.miniProgram && /jdmp/i.test(navigator.userAgent); + var x = + window.xhs && + window.xhs.miniProgram && + /xhsminiapp/i.test(navigator.userAgent); + for ( + var S, + h = function () { + ((window.UniAppJSBridge = !0), + document.dispatchEvent( + new CustomEvent("UniAppJSBridgeReady", { + bubbles: !0, + cancelable: !0, + }), + )); + }, + y = [ + function (e) { + if (s || w) + return ( + window.__uniapp_x_postMessage || + window.__uniapp_x_ || + window.__dcloud_weex_postMessage || + window.__dcloud_weex_ + ? document.addEventListener("DOMContentLoaded", e) + : window.plus && u.test(document.readyState) + ? setTimeout(e, 0) + : document.addEventListener("plusready", e), + d + ); + }, + function (e) { + if (_) + return ( + window.WeixinJSBridge && window.WeixinJSBridge.invoke + ? setTimeout(e, 0) + : document.addEventListener("WeixinJSBridgeReady", e), + window.wx.miniProgram + ); + }, + function (e) { + if (c) + return ( + window.QQJSBridge && window.QQJSBridge.invoke + ? setTimeout(e, 0) + : document.addEventListener("QQJSBridgeReady", e), + window.qq.miniProgram + ); + }, + function (e) { + if (g) { + document.addEventListener("DOMContentLoaded", e); + var n = window.my; + return { + navigateTo: n.navigateTo, + navigateBack: n.navigateBack, + switchTab: n.switchTab, + reLaunch: n.reLaunch, + redirectTo: n.redirectTo, + postMessage: n.postMessage, + getEnv: n.getEnv, + }; + } + }, + function (e) { + if (v) + return ( + document.addEventListener("DOMContentLoaded", e), + window.swan.webView + ); + }, + function (e) { + if (p) + return ( + document.addEventListener("DOMContentLoaded", e), + window.tt.miniProgram + ); + }, + function (e) { + if (m) { + window.QaJSBridge && window.QaJSBridge.invoke + ? setTimeout(e, 0) + : document.addEventListener("QaJSBridgeReady", e); + var n = window.qa; + return { + navigateTo: n.navigateTo, + navigateBack: n.navigateBack, + switchTab: n.switchTab, + reLaunch: n.reLaunch, + redirectTo: n.redirectTo, + postMessage: n.postMessage, + getEnv: n.getEnv, + }; + } + }, + function (e) { + if (f) + return ( + window.WeixinJSBridge && window.WeixinJSBridge.invoke + ? setTimeout(e, 0) + : document.addEventListener("WeixinJSBridgeReady", e), + window.ks.miniProgram + ); + }, + function (e) { + if (l) + return ( + document.addEventListener("DOMContentLoaded", e), + window.tt.miniProgram + ); + }, + function (e) { + if (E) + return ( + window.JDJSBridgeReady && window.JDJSBridgeReady.invoke + ? setTimeout(e, 0) + : document.addEventListener("JDJSBridgeReady", e), + window.jd.miniProgram + ); + }, + function (e) { + if (x) return window.xhs.miniProgram; + }, + function (e) { + return (document.addEventListener("DOMContentLoaded", e), d); + }, + ], + M = 0; + M < y.length && !(S = y[M](h)); + M++ + ); + S || (S = {}); + var P = "undefined" != typeof uni ? uni : {}; + if (!P.navigateTo) for (var b in S) i(S, b) && (P[b] = S[b]); + return ((P.webView = S), P); +}); diff --git a/nkebao/src/api/common.ts b/nkebao/src/api/common.ts index ebe966eb..77a0e7de 100644 --- a/nkebao/src/api/common.ts +++ b/nkebao/src/api/common.ts @@ -7,7 +7,7 @@ import request from "./request"; */ export async function uploadFile( file: File, - uploadUrl: string = "/v1/attachment/upload" + uploadUrl: string = "/v1/attachment/upload", ): Promise { try { // 创建 FormData 对象用于文件上传 diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index 18173f8d..2235a552 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -47,7 +47,7 @@ instance.interceptors.response.use( err => { Toast.show({ content: err.message || "网络异常", position: "top" }); return Promise.reject(err); - } + }, ); export function request( @@ -55,7 +55,7 @@ export function request( data?: any, method: Method = "GET", config?: AxiosRequestConfig, - debounceGap?: number + debounceGap?: number, ): Promise { const gap = typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP; diff --git a/nkebao/src/components/AccountSelection/index.tsx b/nkebao/src/components/AccountSelection/index.tsx index be8023b8..3f8aace6 100644 --- a/nkebao/src/components/AccountSelection/index.tsx +++ b/nkebao/src/components/AccountSelection/index.tsx @@ -117,7 +117,7 @@ export default function AccountSelection({ acc => acc.userName.includes(searchQuery) || acc.realName.includes(searchQuery) || - acc.departmentName.includes(searchQuery) + acc.departmentName.includes(searchQuery), ); // 处理账号选择 diff --git a/nkebao/src/components/ContentLibrarySelection/index.tsx b/nkebao/src/components/ContentLibrarySelection/index.tsx index a7e8507c..b053f1d9 100644 --- a/nkebao/src/components/ContentLibrarySelection/index.tsx +++ b/nkebao/src/components/ContentLibrarySelection/index.tsx @@ -58,7 +58,7 @@ interface ContentLibrarySelectionProps { readonly?: boolean; onConfirm?: ( selectedIds: string[], - selectedItems: ContentLibraryItem[] + selectedItems: ContentLibraryItem[], ) => void; } @@ -86,7 +86,7 @@ export default function ContentLibrarySelection({ // 获取已选内容库详细信息 const selectedLibraryObjs = libraries.filter(item => - selectedLibraries.includes(item.id) + selectedLibraries.includes(item.id), ); // 删除已选内容库 @@ -161,7 +161,7 @@ export default function ContentLibrarySelection({ onSelect(newSelected); if (onSelectDetail) { const selectedObjs = libraries.filter(item => - newSelected.includes(item.id) + newSelected.includes(item.id), ); onSelectDetail(selectedObjs); } diff --git a/nkebao/src/components/DeviceSelection/selectionPopup.tsx b/nkebao/src/components/DeviceSelection/selectionPopup.tsx index d1e002f0..fc3ea3c2 100644 --- a/nkebao/src/components/DeviceSelection/selectionPopup.tsx +++ b/nkebao/src/components/DeviceSelection/selectionPopup.tsx @@ -61,7 +61,7 @@ const SelectionPopup: React.FC = ({ wxid: d.wechatId || "", nickname: d.nickname || "", usedInPlans: d.usedInPlans || 0, - })) + })), ); setTotal(res.total || 0); } @@ -71,7 +71,7 @@ const SelectionPopup: React.FC = ({ setLoading(false); } }, - [] + [], ); // 打开弹窗时获取第一页 diff --git a/nkebao/src/components/FriendSelection/index.tsx b/nkebao/src/components/FriendSelection/index.tsx index e2f37c36..c416d011 100644 --- a/nkebao/src/components/FriendSelection/index.tsx +++ b/nkebao/src/components/FriendSelection/index.tsx @@ -137,7 +137,7 @@ export default function FriendSelection({ // 如果有 onSelectDetail 回调,传递完整的好友对象 if (onSelectDetail) { const selectedFriendObjs = friends.filter(friend => - newSelectedFriends.includes(friend.id) + newSelectedFriends.includes(friend.id), ); onSelectDetail(selectedFriendObjs); } diff --git a/nkebao/src/components/GroupSelection/index.tsx b/nkebao/src/components/GroupSelection/index.tsx index fe027edf..423cf142 100644 --- a/nkebao/src/components/GroupSelection/index.tsx +++ b/nkebao/src/components/GroupSelection/index.tsx @@ -59,7 +59,7 @@ export default function GroupSelection({ // 获取已选群聊详细信息 const selectedGroupObjs = groups.filter(group => - selectedGroups.includes(group.id) + selectedGroups.includes(group.id), ); // 删除已选群聊 @@ -141,7 +141,7 @@ export default function GroupSelection({ // 如果有 onSelectDetail 回调,传递完整的群聊对象 if (onSelectDetail) { const selectedGroupObjs = groups.filter(group => - newSelectedGroups.includes(group.id) + newSelectedGroups.includes(group.id), ); onSelectDetail(selectedGroupObjs); } diff --git a/nkebao/src/components/Layout/Layout.tsx b/nkebao/src/components/Layout/Layout.tsx index 43c51f1f..160d993e 100644 --- a/nkebao/src/components/Layout/Layout.tsx +++ b/nkebao/src/components/Layout/Layout.tsx @@ -20,7 +20,7 @@ const Layout: React.FC = ({ const setRealHeight = () => { document.documentElement.style.setProperty( "--real-vh", - `${window.innerHeight * 0.01}px` + `${window.innerHeight * 0.01}px`, ); }; setRealHeight(); diff --git a/nkebao/src/components/NavCommon/index.tsx b/nkebao/src/components/NavCommon/index.tsx index 8002fc25..0ee5ec1b 100644 --- a/nkebao/src/components/NavCommon/index.tsx +++ b/nkebao/src/components/NavCommon/index.tsx @@ -1,40 +1,61 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { NavBar } from "antd-mobile"; import { ArrowLeftOutlined } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; - +import { getSafeAreaHeight } from "@/utils/common"; interface NavCommonProps { title: string; backFn?: () => void; right?: React.ReactNode; + left?: React.ReactNode; } -const NavCommon: React.FC = ({ title, backFn, right }) => { +const NavCommon: React.FC = ({ + title, + backFn, + right, + left, +}) => { const navigate = useNavigate(); + const [paddingTop, setPaddingTop] = useState("0px"); + useEffect(() => { + setPaddingTop(getSafeAreaHeight() + "px"); + }, []); + return ( - - { - if (backFn) { - backFn(); - } else { - navigate(-1); - } - }} - /> - - } - right={right} +
- - {title} - - + + { + if (backFn) { + backFn(); + } else { + navigate(-1); + } + }} + /> +
+ ) + } + right={right} + > + + {title} + +
+ ); }; diff --git a/nkebao/src/components/Upload/AvatarUpload.tsx b/nkebao/src/components/Upload/AvatarUpload.tsx new file mode 100644 index 00000000..42409314 --- /dev/null +++ b/nkebao/src/components/Upload/AvatarUpload.tsx @@ -0,0 +1,181 @@ +import React, { useState, useEffect } from "react"; +import { Toast, Dialog } from "antd-mobile"; +import { UserOutlined, CameraOutlined } from "@ant-design/icons"; +import style from "./index.module.scss"; + +interface AvatarUploadProps { + value?: string; + onChange?: (url: string) => void; + disabled?: boolean; + className?: string; + size?: number; // 头像尺寸 +} + +const AvatarUpload: React.FC = ({ + value = "", + onChange, + disabled = false, + className, + size = 100, +}) => { + const [uploading, setUploading] = useState(false); + const [avatarUrl, setAvatarUrl] = useState(value); + + useEffect(() => { + setAvatarUrl(value); + }, [value]); + + // 文件验证 + const beforeUpload = (file: File) => { + // 检查文件类型 + const isValidType = file.type.startsWith("image/"); + if (!isValidType) { + Toast.show("只能上传图片文件!"); + return null; + } + + // 检查文件大小 (5MB) + const isLt5M = file.size / 1024 / 1024 < 5; + if (!isLt5M) { + Toast.show("图片大小不能超过5MB!"); + return null; + } + + 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 handleAvatarChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file || disabled || uploading) return; + + const validatedFile = beforeUpload(file); + if (!validatedFile) return; + + setUploading(true); + try { + const result = await upload(validatedFile); + setAvatarUrl(result.url); + onChange?.(result.url); + } catch (error) { + console.error("头像上传失败:", error); + } finally { + setUploading(false); + } + }; + + // 删除头像 + const handleDelete = () => { + return Dialog.confirm({ + content: "确定要删除头像吗?", + onConfirm: () => { + setAvatarUrl(""); + onChange?.(""); + Toast.show("头像已删除"); + }, + }); + }; + + return ( +
+
+ {avatarUrl ? ( + 头像 + ) : ( +
+ +
+ )} + + {/* 上传覆盖层 */} +
+ !disabled && !uploading && fileInputRef.current?.click() + } + > + {uploading ? ( +
上传中...
+ ) : ( + + )} +
+ + {/* 删除按钮 */} + {avatarUrl && !disabled && ( +
+ × +
+ )} +
+ + {/* 隐藏的文件输入 */} + + + {/* 提示文字 */} +
+ {uploading + ? "正在上传头像..." + : "点击头像可更换,支持JPG、PNG格式,大小不超过5MB"} +
+
+ ); +}; + +// 创建 ref +const fileInputRef = React.createRef(); + +export default AvatarUpload; diff --git a/nkebao/src/components/Upload/README.md b/nkebao/src/components/Upload/README.md index 05c1a327..3005bf29 100644 --- a/nkebao/src/components/Upload/README.md +++ b/nkebao/src/components/Upload/README.md @@ -12,6 +12,21 @@ - ✅ 数量限制 - ✅ 编辑和新增状态支持 - ✅ 响应式设计 +- ✅ 头像上传专用组件 + +## 组件列表 + +### 1. UploadComponent (图片上传) + +通用的图片上传组件,支持多张图片上传。 + +### 2. AvatarUpload (头像上传) + +专门的头像上传组件,支持圆形头像显示、删除功能。 + +### 3. VideoUpload (视频上传) + +视频上传组件,支持视频文件上传和预览。 ## 使用方法 @@ -35,6 +50,26 @@ const MyComponent = () => { }; ``` +### 头像上传 + +```tsx +import React, { useState } from "react"; +import AvatarUpload from "@/components/Upload/AvatarUpload"; + +const AvatarComponent = () => { + const [avatar, setAvatar] = useState(""); + + return ( + + ); +}; +``` + ### 编辑模式 ```tsx @@ -63,7 +98,7 @@ const EditComponent = () => { ## API -### Props +### UploadComponent Props | 参数 | 说明 | 类型 | 默认值 | | --------- | -------------- | -------------------------- | ----------- | @@ -74,11 +109,30 @@ const EditComponent = () => { | disabled | 是否禁用 | `boolean` | `false` | | className | 自定义类名 | `string` | - | +### AvatarUpload Props + +| 参数 | 说明 | 类型 | 默认值 | +| --------- | ------------ | ----------------------- | ------- | +| value | 头像URL | `string` | `""` | +| onChange | 头像变化回调 | `(url: string) => void` | - | +| disabled | 是否禁用 | `boolean` | `false` | +| className | 自定义类名 | `string` | - | +| size | 头像尺寸 | `number` | `100` | + +### VideoUpload Props + +| 参数 | 说明 | 类型 | 默认值 | +| --------- | ------------ | ----------------------- | ------- | +| value | 视频URL | `string` | `""` | +| onChange | 视频变化回调 | `(url: string) => void` | - | +| disabled | 是否禁用 | `boolean` | `false` | +| className | 自定义类名 | `string` | - | + ### 事件 | 事件名 | 说明 | 回调参数 | | -------- | ------------------ | -------------------------- | -| onChange | 图片列表变化时触发 | `(urls: string[]) => void` | +| onChange | 文件列表变化时触发 | `(urls: string[]) => void` | ## 注意事项 @@ -88,6 +142,7 @@ const EditComponent = () => { 4. **认证**: 自动携带 token 进行认证 5. **预览**: 点击图片可预览 6. **删除**: 删除图片会有确认提示 +7. **头像组件**: 支持圆形显示、删除按钮、上传覆盖层 ## 样式定制 @@ -102,6 +157,13 @@ const EditComponent = () => { } } } + +.avatarUploadContainer { + // 头像上传组件样式 + .avatarWrapper { + // 头像容器样式 + } +} ``` ## 错误处理 @@ -110,3 +172,12 @@ const EditComponent = () => { - 文件大小超限时会显示错误提示 - 上传失败时会显示错误提示 - 网络错误时会显示错误提示 + +## 头像上传特性 + +- **圆形显示**: 头像以圆形方式显示 +- **占位符**: 无头像时显示用户图标 +- **上传覆盖**: 鼠标悬停显示上传图标 +- **删除功能**: 右上角删除按钮 +- **加载状态**: 上传时显示加载提示 +- **尺寸可调**: 支持自定义头像尺寸 diff --git a/nkebao/src/components/Upload/index.module.scss b/nkebao/src/components/Upload/index.module.scss index beab0535..286e3e7e 100644 --- a/nkebao/src/components/Upload/index.module.scss +++ b/nkebao/src/components/Upload/index.module.scss @@ -106,3 +106,105 @@ } } } + +// 头像上传组件样式 +.avatarUploadContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + .avatarWrapper { + position: relative; + border-radius: 50%; + overflow: hidden; + background: #f0f0f0; + border: 2px solid #e0e0e0; + cursor: pointer; + transition: all 0.3s ease; + + &:hover { + border-color: var(--primary-color); + box-shadow: 0 4px 12px rgba(24, 142, 238, 0.3); + } + + .avatarImage { + width: 100%; + height: 100%; + object-fit: cover; + } + + .avatarPlaceholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-size: 40px; + } + + .avatarUploadOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 24px; + opacity: 0; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1; + } + + .uploadLoading { + font-size: 12px; + text-align: center; + line-height: 1.4; + } + } + + .avatarDeleteBtn { + position: absolute; + top: -8px; + right: -8px; + width: 24px; + height: 24px; + background: #ff4d4f; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + z-index: 10; + + &:hover { + background: #ff7875; + transform: scale(1.1); + } + } + + &:hover .avatarUploadOverlay { + opacity: 1; + } + } + + .avatarTip { + font-size: 12px; + color: #999; + text-align: center; + line-height: 1.4; + max-width: 200px; + } +} diff --git a/nkebao/src/components/Upload/index.tsx b/nkebao/src/components/Upload/index.tsx index 73ec84a9..da1a2a21 100644 --- a/nkebao/src/components/Upload/index.tsx +++ b/nkebao/src/components/Upload/index.tsx @@ -68,7 +68,7 @@ const UploadComponent: React.FC = ({ Authorization: `Bearer ${localStorage.getItem("token")}`, }, body: formData, - } + }, ); if (!response.ok) { diff --git a/nkebao/src/pages/guide/api.ts b/nkebao/src/pages/guide/api.ts new file mode 100644 index 00000000..d86d80ce --- /dev/null +++ b/nkebao/src/pages/guide/api.ts @@ -0,0 +1,13 @@ +import request from "@/api/request"; + +// 获取设备二维码 +export const fetchDeviceQRCode = (accountId: string) => + request("/v1/api/device/add", { accountId }, "POST"); + +// 通过IMEI添加设备 +export const addDeviceByImei = (imei: string, name: string) => + request("/v1/api/device/add-by-imei", { imei, name }, "POST"); + +// 获取设备列表 +export const fetchDeviceList = (params: { accountId?: string }) => + request("/v1/devices/add-results", params, "GET"); diff --git a/nkebao/src/pages/guide/index.module.scss b/nkebao/src/pages/guide/index.module.scss new file mode 100644 index 00000000..3acb01ac --- /dev/null +++ b/nkebao/src/pages/guide/index.module.scss @@ -0,0 +1,341 @@ +.guideContainer { + height: 100vh; + background: var(--primary-color); + padding: 16px; + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; + pointer-events: none; + } +} + +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: var(--primary-color); +} + +.loadingText { + color: white; + font-size: 16px; + margin-top: 16px; + font-weight: 500; +} + +.header { + text-align: center; + margin-bottom: 20px; + position: relative; + z-index: 1; +} + +.iconContainer { + width: 60px; + height: 60px; + background: #fff; + border-radius: 50%; + margin: 0 auto 12px; + border: 1px solid rgba(255, 255, 255, 0.3); + overflow: hidden; +} + +.logo { + width: 100%; + height: 100%; + object-fit: contain; +} + +.title { + color: white; + font-size: 22px; + font-weight: 700; + margin-bottom: 8px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.subtitle { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + line-height: 1.4; + max-width: 280px; + margin: 0 auto; +} + +.content { + flex: 1; + position: relative; + z-index: 1; + overflow-y: auto; + padding-right: 4px; +} + +.deviceStatus { + margin-bottom: 16px; +} + +.statusCard { + background: rgba(255, 255, 255, 0.95); + border-radius: 12px; + padding: 12px; + display: flex; + align-items: center; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.statusIcon { + width: 40px; + height: 40px; + background: var(--primary-color); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + color: white; + font-size: 20px; +} + +.statusInfo { + flex: 1; +} + +.statusTitle { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 2px; +} + +.statusValue { + font-size: 12px; + color: #666; +} + +.deviceCount { + color: var(--primary-color); + font-weight: 700; + font-size: 16px; +} + +.guideSteps { + margin-bottom: 16px; +} + +.stepsTitle { + color: white; + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.stepList { + display: flex; + flex-direction: column; + gap: 8px; +} + +.stepItem { + background: rgba(255, 255, 255, 0.95); + border-radius: 8px; + padding: 10px; + display: flex; + align-items: flex-start; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + transition: transform 0.2s ease; + + &:hover { + transform: translateY(-1px); + } +} + +.stepNumber { + width: 24px; + height: 24px; + background: var(--primary-color); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 700; + font-size: 12px; + margin-right: 10px; + flex-shrink: 0; +} + +.stepContent { + flex: 1; +} + +.stepTitle { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 2px; +} + +.stepDesc { + font-size: 12px; + color: #666; + line-height: 1.3; +} + +.tips { + background: rgba(255, 255, 255, 0.95); + border-radius: 8px; + padding: 12px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.tipsTitle { + display: flex; + align-items: center; + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.tipsIcon { + color: #ff6b6b; + margin-right: 6px; + font-size: 16px; +} + +.tipsContent { + p { + font-size: 12px; + color: #666; + line-height: 1.4; + margin-bottom: 4px; + + &:last-child { + margin-bottom: 0; + } + } +} + +.footer { + margin-top: 16px; + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: 8px; +} + +.primaryButton { + background: white; + border: none; + border-radius: 8px; + height: 44px; + font-size: 15px; + font-weight: 600; + color: var(--primary-color); + box-shadow: 0 2px 12px rgba(255, 255, 255, 0.4); + transition: all 0.3s ease; + + &:active { + transform: translateY(1px); + box-shadow: 0 1px 6px rgba(255, 255, 255, 0.4); + } +} + +.buttonIcon { + margin-left: 6px; + font-size: 12px; +} + +.secondaryButton { + border: 2px solid rgba(255, 255, 255, 0.8); + border-radius: 8px; + height: 44px; + font-size: 15px; + font-weight: 600; + color: white; + background: transparent; + backdrop-filter: blur(10px); + + &:active { + background: rgba(255, 255, 255, 0.1); + } +} + +// 响应式设计 +@media (max-width: 480px) { + .guideContainer { + padding: 12px; + } + + .title { + font-size: 20px; + } + + .subtitle { + font-size: 13px; + } + + .statusCard { + padding: 10px; + } + + .stepItem { + padding: 8px; + } + + .tips { + padding: 10px; + } +} + +// 动画效果 +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.header, +.deviceStatus, +.guideSteps, +.tips { + animation: fadeInUp 0.5s ease-out; +} + +.guideSteps { + animation-delay: 0.1s; +} + +.tips { + animation-delay: 0.2s; +} + +.footer { + animation: fadeInUp 0.5s ease-out 0.3s both; +} diff --git a/nkebao/src/pages/guide/index.tsx b/nkebao/src/pages/guide/index.tsx new file mode 100644 index 00000000..da42ed05 --- /dev/null +++ b/nkebao/src/pages/guide/index.tsx @@ -0,0 +1,349 @@ +import React, { useEffect, useState, useCallback, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button, Toast, Popup, Tabs, Input } from "antd-mobile"; +import { + MobileOutlined, + ExclamationCircleOutlined, + ArrowRightOutlined, + QrcodeOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import { fetchDeviceQRCode, addDeviceByImei, fetchDeviceList } from "./api"; +import { useUserStore } from "@/store/module/user"; +import styles from "./index.module.scss"; +const Guide: React.FC = () => { + const navigate = useNavigate(); + const { user } = useUserStore(); + const [loading, setLoading] = useState(false); + const [deviceCount, setDeviceCount] = useState(user?.deviceTotal || 0); + + // 添加设备弹窗状态 + const [addVisible, setAddVisible] = useState(false); + const [addTab, setAddTab] = useState("scan"); + const [qrLoading, setQrLoading] = useState(false); + const [qrCode, setQrCode] = useState(null); + const [imei, setImei] = useState(""); + const [name, setName] = useState(""); + const [addLoading, setAddLoading] = useState(false); + + // 轮询监听相关 + const [isPolling, setIsPolling] = useState(false); + const pollingRef = useRef(null); + const initialDeviceCountRef = useRef(deviceCount); + + // 检查设备绑定状态 + const checkDeviceStatus = useCallback(async () => { + try { + setLoading(true); + // 使用store中的设备数量 + const deviceNum = user?.deviceTotal || 0; + setDeviceCount(deviceNum); + + // 如果已有设备,直接跳转到首页 + if (deviceNum > 0) { + navigate("/"); + return; + } + } catch (error) { + console.error("检查设备状态失败:", error); + Toast.show({ + content: "检查设备状态失败,请重试", + position: "top", + }); + } finally { + setLoading(false); + } + }, [user?.deviceTotal, navigate]); + + useEffect(() => { + checkDeviceStatus(); + }, [checkDeviceStatus]); + + // 开始轮询监听设备状态 + const startPolling = useCallback(() => { + if (isPolling) return; + + setIsPolling(true); + initialDeviceCountRef.current = deviceCount; + + const pollDeviceStatus = async () => { + try { + // 这里可以调用一个简单的设备数量接口来检查是否有新设备 + // 或者使用其他方式检测设备状态变化 + // 暂时使用store中的数量,实际项目中可能需要调用专门的接口 + let currentDeviceCount = user?.deviceTotal || 0; + const res = await fetchDeviceList({ accountId: user?.s2_accountId }); + if (res.added) { + currentDeviceCount = 1; + Toast.show({ content: "设备添加成功!", position: "top" }); + setAddVisible(false); + setDeviceCount(currentDeviceCount); + setIsPolling(false); + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + // 可以选择跳转到首页或继续留在当前页面 + navigate("/"); + return; + } + } catch (error) { + console.error("轮询检查设备状态失败:", error); + } + }; + + // 每3秒检查一次设备状态 + pollingRef.current = setInterval(pollDeviceStatus, 3000); + }, [isPolling, user?.s2_accountId]); + + // 停止轮询 + const stopPolling = useCallback(() => { + setIsPolling(false); + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + }, []); + + // 组件卸载时清理轮询 + useEffect(() => { + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + }; + }, []); + + // 获取二维码 + const handleGetQr = async () => { + setQrLoading(true); + setQrCode(null); + try { + const accountId = user?.s2_accountId; + if (!accountId) throw new Error("未获取到用户信息"); + const res = await fetchDeviceQRCode(accountId); + setQrCode(res.qrCode); + // 获取二维码后开始轮询监听 + startPolling(); + } catch (e: any) { + Toast.show({ content: e.message || "获取二维码失败", position: "top" }); + } finally { + setQrLoading(false); + } + }; + + // 跳转到设备管理页面 + const handleGoToDevices = () => { + handleGetQr(); + setAddVisible(true); + }; + + // 手动添加设备 + const handleAddDevice = async () => { + if (!imei.trim() || !name.trim()) { + Toast.show({ content: "请填写完整信息", position: "top" }); + return; + } + setAddLoading(true); + try { + await addDeviceByImei(imei, name); + Toast.show({ content: "添加成功", position: "top" }); + setAddVisible(false); + setImei(""); + setName(""); + // 重新检查设备状态 + await checkDeviceStatus(); + } catch (e: any) { + Toast.show({ content: e.message || "添加失败", position: "top" }); + } finally { + setAddLoading(false); + } + }; + + // 关闭弹窗时停止轮询 + const handleClosePopup = () => { + setAddVisible(false); + stopPolling(); + setQrCode(null); + }; + + if (loading) { + return ( + +
+
检查设备状态中...
+
+
+ ); + } + + return ( + +
+ {/* 头部区域 */} +
+
+ 存客宝 +
+

欢迎使用存客宝

+

请先绑定设备以获得完整功能体验

+
+ + {/* 内容区域 */} +
+
+
+
+ +
+
+
设备绑定状态
+
+ 已绑定: + {deviceCount} 台 +
+
+
+
+ +
+

绑定步骤

+
+
+
1
+
+
准备设备
+
+ 确保手机已安装存客宝应用 +
+
+
+
+
2
+
+
扫描二维码
+
在设备管理页面扫描绑定
+
+
+
+
3
+
+
开始使用
+
+ 绑定成功后即可使用所有功能 +
+
+
+
+
+ +
+
+ + 温馨提示 +
+
+

• 绑定设备后可享受完整功能体验

+

• 每个账号最多可绑定10台设备

+

• 如需帮助请联系客服

+
+
+
+ + {/* 底部按钮区域 */} +
+ +
+
+ + {/* 添加设备弹窗 */} + +
+ + + + + {addTab === "scan" && ( +
+ + {qrCode && ( +
+ 二维码 +
+ 请用手机扫码添加设备 +
+ {isPolling && ( +
+ 正在监听设备添加状态... +
+ )} +
+ )} +
+ )} + {addTab === "manual" && ( +
+ setName(val)} + clearable + /> + setImei(val)} + clearable + /> + +
+ )} +
+
+
+ ); +}; + +export default Guide; diff --git a/nkebao/src/pages/iframe/index.module.scss b/nkebao/src/pages/iframe/index.module.scss new file mode 100644 index 00000000..748327a6 --- /dev/null +++ b/nkebao/src/pages/iframe/index.module.scss @@ -0,0 +1,323 @@ +.iframe-debug-page { + min-height: 100vh; + background: var(--primary-gradient); + padding: 20px; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +.header { + text-align: center; + color: white; + margin-bottom: 30px; + + h1 { + font-size: 2.5rem; + margin: 0 0 10px 0; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + } + + p { + font-size: 1.1rem; + margin: 0; + opacity: 0.9; + } +} + +.content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; + margin-bottom: 30px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 20px; + } +} + +.control-panel, +.message-panel { + background: white; + border-radius: 15px; + padding: 25px; + box-shadow: 0 10px 30px var(--primary-shadow); + + h3 { + margin: 0 0 20px 0; + color: #333; + font-size: 1.3rem; + border-bottom: 2px solid var(--primary-color); + padding-bottom: 10px; + } +} + +.input-group { + display: flex; + gap: 10px; + margin-bottom: 20px; + + @media (max-width: 480px) { + flex-direction: column; + } +} + +.button-group { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.btn { + padding: 12px 20px; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); + } + + &:active { + transform: translateY(0); + } + + &.btn-primary { + background: var(--primary-gradient); + color: white; + + &:hover { + background: linear-gradient( + 135deg, + var(--primary-color-dark) 0%, + var(--primary-color) 100% + ); + } + } + + &.btn-secondary { + background: linear-gradient( + 135deg, + var(--primary-color-light) 0%, + var(--primary-color) 100% + ); + color: white; + + &:hover { + background: linear-gradient( + 135deg, + var(--primary-color) 0%, + var(--primary-color-dark) 100% + ); + } + } + + &.btn-danger { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); + color: white; + + &:hover { + background: linear-gradient(135deg, #ee5a52 0%, #d63031 100%); + } + } +} + +.message-list { + max-height: 400px; + overflow-y: auto; + border: 1px solid #e1e5e9; + border-radius: 8px; + padding: 10px; + background: #f8f9fa; + font-size: 12px; +} + +.no-messages { + text-align: center; + color: #6c757d; + font-style: italic; + padding: 20px; +} + +.message-item { + background: white; + padding: 12px 15px; + margin-bottom: 8px; + border-radius: 6px; + border-left: 4px solid var(--primary-color); + box-shadow: 0 2px 4px var(--primary-shadow-light); + font-size: 12px; + &:last-child { + margin-bottom: 0; + } +} + +.message-text { + font-family: "Courier New", monospace; + font-size: 12px; + color: #333; + word-break: break-all; + line-height: 1.4; +} + +.info-panel { + background: white; + border-radius: 15px; + padding: 25px; + box-shadow: 0 10px 30px var(--primary-shadow); + + h3 { + margin: 0 0 20px 0; + color: #333; + font-size: 1.3rem; + border-bottom: 2px solid var(--primary-color); + padding-bottom: 10px; + } +} + +.info-item { + margin-bottom: 12px; + padding: 10px; + background: #f8f9fa; + border-radius: 6px; + border-left: 3px solid var(--primary-color); + + strong { + color: #495057; + margin-right: 8px; + } + + &:last-child { + margin-bottom: 0; + } +} + +// 滚动条样式 +.message-list::-webkit-scrollbar { + width: 8px; +} + +.message-list::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.message-list::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; + + &:hover { + background: #a8a8a8; + } +} + +// 响应式设计 +@media (max-width: 768px) { + .iframe-debug-page { + padding: 15px; + } + + .header h1 { + font-size: 2rem; + } + + .control-panel, + .message-panel, + .info-panel { + padding: 20px; + } + + .btn { + padding: 10px 16px; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + .header h1 { + font-size: 1.8rem; + } + + .content { + gap: 15px; + } + + .control-panel, + .message-panel, + .info-panel { + padding: 15px; + } + + .button-group { + flex-direction: column; + } + + .btn { + width: 100%; + } +} + +// URL 参数区域样式 +.url-params-section { + margin-top: 20px; + padding-top: 20px; + border-top: 2px solid #e1e5e9; + + h4 { + margin: 0 0 15px 0; + color: #333; + font-size: 1.1rem; + font-weight: 600; + } +} + +.no-params { + color: #666; + font-style: italic; + text-align: center; + padding: 20px; + background: #f8f9fa; + border-radius: 8px; + border: 2px dashed #dee2e6; +} + +.params-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.param-item { + display: flex; + align-items: center; + padding: 10px 15px; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid var(--primary-color); + transition: all 0.3s ease; + + &:hover { + background: #e9ecef; + transform: translateX(5px); + } +} + +.param-key { + font-weight: 600; + color: #333; + min-width: 80px; + margin-right: 10px; +} + +.param-value { + color: #666; + font-family: "Courier New", monospace; + word-break: break-all; + flex: 1; +} diff --git a/nkebao/src/pages/iframe/index.tsx b/nkebao/src/pages/iframe/index.tsx new file mode 100644 index 00000000..a935c6ce --- /dev/null +++ b/nkebao/src/pages/iframe/index.tsx @@ -0,0 +1,242 @@ +import React, { useState, useEffect } from "react"; +import style from "./index.module.scss"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import { LoadingOutlined, CheckCircleOutlined } from "@ant-design/icons"; +import { Input } from "antd"; +// 声明全局的 uni 对象 +declare global { + interface Window { + uni: any; + } +} + +interface Message { + type: number; // 数据类型:0数据交互 1App功能调用 + data: any; +} + +const TYPE_EMUE = { + CONNECT: 0, + DATA: 1, + FUNCTION: 2, + CONFIG: 3, +}; +const IframeDebugPage: React.FC = () => { + const [receivedMessages, setReceivedMessages] = useState([]); + const [messageId, setMessageId] = useState(0); + const [inputMessage, setInputMessage] = useState(""); + const [connectStatus, setConnectStatus] = useState(false); + + // 解析 URL 参数中的消息 + const parseUrlMessage = () => { + const search = window.location.search.substring(1); + let messageParam = null; + + if (search) { + const pairs = search.split("&"); + for (const pair of pairs) { + const [key, value] = pair.split("="); + if (key === "message" && value) { + messageParam = decodeURIComponent(value); + break; + } + } + } + + if (messageParam) { + try { + const message = JSON.parse(decodeURIComponent(messageParam)); + console.log("[存客宝]ReceiveMessage=>\n" + JSON.stringify(message)); + handleReceivedMessage(message); + // 清除URL中的message参数 + const newUrl = + window.location.pathname + + window.location.search + .replace(/[?&]message=[^&]*/, "") + .replace(/^&/, "?"); + window.history.replaceState({}, "", newUrl); + } catch (e) { + console.error("解析URL消息失败:", e); + } + } + }; + + useEffect(() => { + parseUrlMessage(); + // 监听 SDK 初始化完成事件 + }, []); + + // 处理接收到的消息 + const handleReceivedMessage = (message: Message) => { + const messageText = `[${new Date().toLocaleTimeString()}] 收到: ${JSON.stringify(message)}`; + setReceivedMessages(prev => [...prev, messageText]); + console.log("message.type", message.type); + if ([TYPE_EMUE.CONNECT].includes(message.type)) { + setConnectStatus(true); + } + }; + + // 向 App 发送消息 + const sendMessageToParent = (message: Message) => { + if (window.uni && window.uni.postMessage) { + try { + window.uni.postMessage({ + data: message, + }); + console.log("[存客宝]SendMessage=>\n" + JSON.stringify(message)); + } catch (e) { + console.error( + "[存客宝]SendMessage=>\n" + JSON.stringify(message) + "发送失败:", + e, + ); + } + } else { + console.error( + "[存客宝]SendMessage=>\n" + JSON.stringify(message) + "无法发送消息", + ); + } + }; + + // 发送自定义消息到 App + const sendCustomMessage = () => { + if (!inputMessage.trim()) return; + + const newMessageId = messageId + 1; + setMessageId(newMessageId); + + const message: Message = { + type: TYPE_EMUE.DATA, // 数据交互 + data: { + id: newMessageId, + content: inputMessage, + source: "存客宝消息源", + timestamp: Date.now(), + }, + }; + + sendMessageToParent(message); + setInputMessage(""); + }; + + // 发送测试消息到 App + const sendTestMessage = () => { + const newMessageId = messageId + 1; + setMessageId(newMessageId); + + const message: Message = { + type: TYPE_EMUE.DATA, // 数据交互 + data: { + id: newMessageId, + action: "ping", + content: `存客宝测试消息 ${newMessageId}`, + random: Math.random(), + }, + }; + + sendMessageToParent(message); + }; + + // 发送App功能调用消息 + const sendAppFunctionCall = () => { + const message: Message = { + type: 1, // App功能调用 + data: { + action: "showToast", + params: { + title: "来自H5的功能调用", + icon: "success", + }, + }, + }; + + sendMessageToParent(message); + }; + + // 清空消息列表 + const clearMessages = () => { + setInputMessage(""); + setReceivedMessages([]); + }; + + return ( + + ) : ( + + 连接中... + + + ) + } + /> + } + > +
+
+
+

接收到的消息

+
+ {receivedMessages.length === 0 ? ( +
暂无消息
+ ) : ( + receivedMessages.map((msg, index) => ( +
+ {msg} +
+ )) + )} +
+
+ +
+

控制面板

+ +
+ setInputMessage(e.target.value)} + placeholder="输入要发送的消息" + /> + +
+ +
+ + + +
+
+
+
+
+ ); +}; + +export default IframeDebugPage; diff --git a/nkebao/src/pages/iframe/init.tsx b/nkebao/src/pages/iframe/init.tsx new file mode 100644 index 00000000..b49e01db --- /dev/null +++ b/nkebao/src/pages/iframe/init.tsx @@ -0,0 +1,227 @@ +import React, { useState, useEffect } from "react"; +import style from "./index.module.scss"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import { Input } from "antd"; +import { useNavigate } from "react-router-dom"; + +// 声明全局的 uni 对象 +declare global { + interface Window { + uni: any; + } +} + +interface Message { + type: number; // 数据类型:0数据交互 1App功能调用 + data: any; +} + +const TYPE_EMUE = { + CONNECT: 0, + DATA: 1, + FUNCTION: 2, + CONFIG: 3, +}; +const IframeDebugPage: React.FC = () => { + const [receivedMessages, setReceivedMessages] = useState([]); + const [messageId, setMessageId] = useState(0); + const [inputMessage, setInputMessage] = useState(""); + const navigate = useNavigate(); + // 解析 URL 参数中的消息 + const parseUrlMessage = () => { + const search = window.location.search.substring(1); + let messageParam = null; + + if (search) { + const pairs = search.split("&"); + for (const pair of pairs) { + const [key, value] = pair.split("="); + if (key === "message" && value) { + messageParam = decodeURIComponent(value); + break; + } + } + } + + if (messageParam) { + try { + const message = JSON.parse(decodeURIComponent(messageParam)); + console.log("[存客宝]ReceiveMessage=>\n" + JSON.stringify(message)); + handleReceivedMessage(message); + // 清除URL中的message参数 + const newUrl = + window.location.pathname + + window.location.search + .replace(/[?&]message=[^&]*/, "") + .replace(/^&/, "?"); + window.history.replaceState({}, "", newUrl); + } catch (e) { + console.error("解析URL消息失败:", e); + } + } + }; + + useEffect(() => { + parseUrlMessage(); + // 监听 SDK 初始化完成事件 + }, []); + + // 处理接收到的消息 + const handleReceivedMessage = (message: Message) => { + const messageText = `[${new Date().toLocaleTimeString()}] 收到: ${JSON.stringify(message)}`; + setReceivedMessages(prev => [...prev, messageText]); + console.log("message.type", message.type); + if ([TYPE_EMUE.CONFIG].includes(message.type)) { + localStorage.setItem("paddingTop", message.data.paddingTop); + navigate("/"); + } + }; + + // 向 App 发送消息 + const sendMessageToParent = (message: Message) => { + if (window.uni && window.uni.postMessage) { + try { + window.uni.postMessage({ + data: message, + }); + console.log("[存客宝]SendMessage=>\n" + JSON.stringify(message)); + } catch (e) { + console.error( + "[存客宝]SendMessage=>\n" + JSON.stringify(message) + "发送失败:", + e, + ); + } + } else { + console.error( + "[存客宝]SendMessage=>\n" + JSON.stringify(message) + "无法发送消息", + ); + } + }; + + // 发送自定义消息到 App + const sendCustomMessage = () => { + if (!inputMessage.trim()) return; + + const newMessageId = messageId + 1; + setMessageId(newMessageId); + + const message: Message = { + type: TYPE_EMUE.DATA, // 数据交互 + data: { + id: newMessageId, + content: inputMessage, + source: "存客宝消息源", + timestamp: Date.now(), + }, + }; + + sendMessageToParent(message); + setInputMessage(""); + }; + + // 发送测试消息到 App + const sendTestMessage = () => { + const newMessageId = messageId + 1; + setMessageId(newMessageId); + + const message: Message = { + type: TYPE_EMUE.DATA, // 数据交互 + data: { + id: newMessageId, + action: "ping", + content: `存客宝测试消息 ${newMessageId}`, + random: Math.random(), + }, + }; + + sendMessageToParent(message); + }; + + // 发送App功能调用消息 + const sendAppFunctionCall = () => { + const message: Message = { + type: 1, // App功能调用 + data: { + action: "showToast", + params: { + title: "来自H5的功能调用", + icon: "success", + }, + }, + }; + + sendMessageToParent(message); + }; + + // 清空消息列表 + const clearMessages = () => { + setInputMessage(""); + setReceivedMessages([]); + }; + + return ( + }> +
+
+
+

接收到的消息

+
+ {receivedMessages.length === 0 ? ( +
暂无消息
+ ) : ( + receivedMessages.map((msg, index) => ( +
+ {msg} +
+ )) + )} +
+
+ +
+

控制面板

+ +
+ setInputMessage(e.target.value)} + placeholder="输入要发送的消息" + /> + +
+ +
+ + + +
+
+
+
+
+ ); +}; + +export default IframeDebugPage; diff --git a/nkebao/src/pages/login/api.ts b/nkebao/src/pages/login/api.ts index 617d0bed..9c1f6e6c 100644 --- a/nkebao/src/pages/login/api.ts +++ b/nkebao/src/pages/login/api.ts @@ -11,6 +11,7 @@ export interface LoginResponse { data: { token: string; token_expired: string; + deviceTotal: number; // 设备总数 member: { id: string; name: string; diff --git a/nkebao/src/pages/login/login.module.scss b/nkebao/src/pages/login/login.module.scss index 28a3a722..7574341f 100644 --- a/nkebao/src/pages/login/login.module.scss +++ b/nkebao/src/pages/login/login.module.scss @@ -116,10 +116,6 @@ margin: 0; } -.form-container { - margin-bottom: 20px; -} - // 标签页样式 .tab-container { display: flex; diff --git a/nkebao/src/pages/login/login.tsx b/nkebao/src/pages/login/login.tsx index f53ccc6d..1bc13773 100644 --- a/nkebao/src/pages/login/login.tsx +++ b/nkebao/src/pages/login/login.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; -import { Form, Input, Button, Toast, Tabs, Checkbox } from "antd-mobile"; +import { Form, Input, Button, Toast, Checkbox } from "antd-mobile"; import { EyeInvisibleOutline, EyeOutline, @@ -9,6 +9,8 @@ import { import { useUserStore } from "@/store/module/user"; import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api"; import style from "./login.module.scss"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; const Login: React.FC = () => { const [form] = Form.useForm(); @@ -95,22 +97,32 @@ const Login: React.FC = () => { } console.log(response, "response"); + // 获取设备总数 + const deviceTotal = response.deviceTotal || 0; + console.log(deviceTotal, "deviceTotal"); + // 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中) - login(response.token, response.member); + login(response.token, response.member, deviceTotal); Toast.show({ content: "登录成功", position: "top" }); - // 跳转到首页或重定向URL - const returnUrl = searchParams.get("returnUrl"); - if (returnUrl) { - const decodedUrl = decodeURIComponent(returnUrl); - if (isLoginPage(decodedUrl)) { - navigate("/"); + // 根据设备数量判断跳转 + if (deviceTotal > 0) { + // 有设备,跳转到首页或重定向URL + const returnUrl = searchParams.get("returnUrl"); + if (returnUrl) { + const decodedUrl = decodeURIComponent(returnUrl); + if (isLoginPage(decodedUrl)) { + navigate("/"); + } else { + window.location.href = decodedUrl; + } } else { - window.location.href = decodedUrl; + navigate("/"); } } else { - navigate("/"); + // 没有设备,跳转到引导页面 + navigate("/guide"); } } catch (error: any) { // 错误已在request中处理,这里不需要额外处理 @@ -127,9 +139,11 @@ const Login: React.FC = () => { const handleAppleLogin = () => { Toast.show({ content: "Apple登录功能开发中", position: "top" }); }; - + const paddingTop = localStorage.getItem("paddingTop") || "44px"; return (
+
+
{/* 背景装饰 */}
diff --git a/nkebao/src/pages/mobile/content/form/api.ts b/nkebao/src/pages/mobile/content/form/api.ts index 4cce9f26..bc136d88 100644 --- a/nkebao/src/pages/mobile/content/form/api.ts +++ b/nkebao/src/pages/mobile/content/form/api.ts @@ -12,14 +12,14 @@ export function getContentLibraryDetail(id: string): Promise { // 创建内容库 export function createContentLibrary( - params: CreateContentLibraryParams + params: CreateContentLibraryParams, ): Promise { return request("/v1/content/library/create", params, "POST"); } // 更新内容库 export function updateContentLibrary( - params: UpdateContentLibraryParams + params: UpdateContentLibraryParams, ): Promise { const { id, ...data } = params; return request(`/v1/content/library/update`, { id, ...data }, "POST"); diff --git a/nkebao/src/pages/mobile/content/list/api.ts b/nkebao/src/pages/mobile/content/list/api.ts index 7bb985ee..51821b6f 100644 --- a/nkebao/src/pages/mobile/content/list/api.ts +++ b/nkebao/src/pages/mobile/content/list/api.ts @@ -22,14 +22,14 @@ export function getContentLibraryDetail(id: string): Promise { // 创建内容库 export function createContentLibrary( - params: CreateContentLibraryParams + params: CreateContentLibraryParams, ): Promise { return request("/v1/content/library/create", params, "POST"); } // 更新内容库 export function updateContentLibrary( - params: UpdateContentLibraryParams + params: UpdateContentLibraryParams, ): Promise { const { id, ...data } = params; return request(`/v1/content/library/update`, { id, ...data }, "POST"); @@ -43,7 +43,7 @@ export function deleteContentLibrary(id: string): Promise { // 切换内容库状态 export function toggleContentLibraryStatus( id: string, - status: number + status: number, ): Promise { return request("/v1/content/library/update-status", { id, status }, "POST"); } diff --git a/nkebao/src/pages/mobile/content/list/index.tsx b/nkebao/src/pages/mobile/content/list/index.tsx index f3b0eb13..a601710c 100644 --- a/nkebao/src/pages/mobile/content/list/index.tsx +++ b/nkebao/src/pages/mobile/content/list/index.tsx @@ -182,7 +182,7 @@ const ContentLibraryList: React.FC = () => { const filteredLibraries = libraries.filter( library => library.name.toLowerCase().includes(searchQuery.toLowerCase()) || - library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase()) + library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase()), ); return ( diff --git a/nkebao/src/pages/mobile/home/index.tsx b/nkebao/src/pages/mobile/home/index.tsx index 5ff2672a..3293455c 100644 --- a/nkebao/src/pages/mobile/home/index.tsx +++ b/nkebao/src/pages/mobile/home/index.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { NavBar } from "antd-mobile"; +import NavCommon from "@/components/NavCommon"; import { - BellOutlined, MobileOutlined, - UserOutlined, MessageOutlined, TeamOutlined, RiseOutlined, @@ -27,13 +25,6 @@ interface DashboardData { aliveWechatNum?: number; } -interface TodayStatsData { - momentsNum?: number; - groupPushNum?: number; - passRate?: string; - sysActive?: string; -} - interface SevenDayStatsData { date?: string[]; allNum?: number[]; @@ -140,15 +131,13 @@ const Home: React.FC = () => { navigate("/wechat-accounts"); }; + const handleAliveWechatClick = () => { + navigate("/wechat-accounts?wechatStatus=1"); + }; + return ( -
- 存客宝 -
- - } + header={} title="存客宝" />} footer={} loading={isLoading} > @@ -170,7 +159,10 @@ const Home: React.FC = () => {
-
+
在线微信号
{dashboard.aliveWechatNum || 35} @@ -206,8 +198,8 @@ const Home: React.FC = () => { onClick={() => navigate( `/scenarios/list/${scenario.id}/${encodeURIComponent( - scenario.name - )}` + scenario.name, + )}`, ) } > diff --git a/nkebao/src/pages/mobile/mine/consumption-records/api.ts b/nkebao/src/pages/mobile/mine/consumption-records/api.ts new file mode 100644 index 00000000..52f95337 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/consumption-records/api.ts @@ -0,0 +1,17 @@ +import request from "@/api/request"; +import { ConsumptionRecordsResponse, ConsumptionRecordDetail } from "./data"; + +// 获取消费记录列表 +export function getConsumptionRecords(params: { + page: number; + limit: number; +}): Promise { + return request("/v1/consumption-records", params, "GET"); +} + +// 获取消费记录详情 +export function getConsumptionRecordDetail( + id: string, +): Promise { + return request(`/v1/consumption-records/${id}`, {}, "GET"); +} diff --git a/nkebao/src/pages/mobile/mine/consumption-records/data.ts b/nkebao/src/pages/mobile/mine/consumption-records/data.ts new file mode 100644 index 00000000..391b40dd --- /dev/null +++ b/nkebao/src/pages/mobile/mine/consumption-records/data.ts @@ -0,0 +1,26 @@ +// 消费记录类型定义 +export interface ConsumptionRecord { + id: string; + type: "recharge" | "ai_service" | "version_upgrade"; + amount: number; + description: string; + createTime: string; + status: "success" | "pending" | "failed"; + balance?: number; +} + +// API响应类型 +export interface ConsumptionRecordsResponse { + list: ConsumptionRecord[]; + total: number; + page: number; + limit: number; +} + +// 消费记录详情 +export interface ConsumptionRecordDetail extends ConsumptionRecord { + orderNo?: string; + paymentMethod?: string; + remark?: string; + operator?: string; +} diff --git a/nkebao/src/pages/mobile/mine/consumption-records/index.module.scss b/nkebao/src/pages/mobile/mine/consumption-records/index.module.scss new file mode 100644 index 00000000..f244cccc --- /dev/null +++ b/nkebao/src/pages/mobile/mine/consumption-records/index.module.scss @@ -0,0 +1,141 @@ +.records-page { + padding: 16px; + background: #f7f8fa; + min-height: 100vh; +} + +.records-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.record-card { + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + background: #fff; +} + +.record-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; +} + +.record-info { + display: flex; + align-items: flex-start; + gap: 12px; + flex: 1; +} + +.type-icon-wrapper { + width: 40px; + height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.type-icon { + font-size: 20px; + color: #666; +} + +.record-details { + flex: 1; + min-width: 0; +} + +.record-description { + font-size: 16px; + font-weight: 500; + color: #222; + margin-bottom: 4px; + line-height: 1.4; +} + +.record-time { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: #999; +} + +.time-icon { + font-size: 12px; +} + +.record-amount { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + flex-shrink: 0; +} + +.amount-text { + font-size: 16px; + font-weight: 600; +} + +.status-tag { + font-size: 11px; + padding: 2px 6px; + border-radius: 8px; +} + +.balance-info { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid #f0f0f0; + font-size: 12px; + color: #666; +} + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 20px; +} + +.loading-text { + font-size: 14px; + color: #666; +} + +.load-more { + text-align: center; + padding: 16px; + color: var(--primary-color); + font-size: 14px; + font-weight: 500; + cursor: pointer; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: background-color 0.2s ease; + + &:hover { + background-color: #f8f9fa; + } + + &:active { + background-color: #e9ecef; + } +} + +.empty-state { + margin-top: 60px; +} + +.empty-icon { + font-size: 48px; + color: #ccc; +} diff --git a/nkebao/src/pages/mobile/mine/consumption-records/index.tsx b/nkebao/src/pages/mobile/mine/consumption-records/index.tsx new file mode 100644 index 00000000..64e20ac5 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/consumption-records/index.tsx @@ -0,0 +1,212 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Card, List, Tag, SpinLoading, Empty } from "antd-mobile"; +import { useUserStore } from "@/store/module/user"; +import style from "./index.module.scss"; +import { + WalletOutlined, + RobotOutlined, + CrownOutlined, + ClockCircleOutlined, +} from "@ant-design/icons"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import { getConsumptionRecords } from "./api"; +import { ConsumptionRecord } from "./data"; + +const ConsumptionRecords: React.FC = () => { + const navigate = useNavigate(); + const { user } = useUserStore(); + const [records, setRecords] = useState([]); + const [loading, setLoading] = useState(true); + const [hasMore, setHasMore] = useState(true); + const [page, setPage] = useState(1); + + useEffect(() => { + loadRecords(); + }, []); + + const loadRecords = async (reset = false) => { + if (loading) return; + setLoading(true); + try { + const currentPage = reset ? 1 : page; + const response = await getConsumptionRecords({ + page: currentPage, + limit: 20, + }); + + const newRecords = response.list || []; + setRecords(prev => (reset ? newRecords : [...prev, ...newRecords])); + setHasMore(newRecords.length === 20); + if (reset) setPage(1); + else setPage(currentPage + 1); + } catch (error) { + console.error("加载消费记录失败:", error); + } finally { + setLoading(false); + } + }; + + const getTypeIcon = (type: string) => { + switch (type) { + case "recharge": + return ; + case "ai_service": + return ; + case "version_upgrade": + return ; + default: + return ; + } + }; + + const getTypeColor = (type: string) => { + switch (type) { + case "recharge": + return "#52c41a"; + case "ai_service": + return "#1890ff"; + case "version_upgrade": + return "#722ed1"; + default: + return "#666"; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "success": + return "成功"; + case "pending": + return "处理中"; + case "failed": + return "失败"; + default: + return "未知"; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "success": + return "#52c41a"; + case "pending": + return "#faad14"; + case "failed": + return "#ff4d4f"; + default: + return "#666"; + } + }; + + const formatAmount = (amount: number, type: string) => { + if (type === "recharge") { + return `+¥${amount.toFixed(2)}`; + } else { + return `-¥${amount.toFixed(2)}`; + } + }; + + const formatTime = (timeStr: string) => { + const date = new Date(timeStr); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) { + return date.toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + }); + } else if (days === 1) { + return ( + "昨天 " + + date.toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + }) + ); + } else if (days < 7) { + return `${days}天前`; + } else { + return date.toLocaleDateString("zh-CN"); + } + }; + + const renderRecordItem = (record: ConsumptionRecord) => ( + +
+
+
+ {getTypeIcon(record.type)} +
+
+
+ {record.description} +
+
+ + {formatTime(record.createTime)} +
+
+
+
+
+ {formatAmount(record.amount, record.type)} +
+ + {getStatusText(record.status)} + +
+
+ {record.balance !== undefined && ( +
+ 余额: ¥{record.balance.toFixed(2)} +
+ )} +
+ ); + + return ( + }> +
+ {records.length === 0 && !loading ? ( + } + /> + ) : ( +
+ {records.map(renderRecordItem)} + {loading && ( +
+ +
加载中...
+
+ )} + {!loading && hasMore && ( +
loadRecords()}> + 加载更多 +
+ )} +
+ )} +
+
+ ); +}; + +export default ConsumptionRecords; diff --git a/nkebao/src/pages/mobile/mine/devices/DeviceDetail.tsx b/nkebao/src/pages/mobile/mine/devices/DeviceDetail.tsx index 0c8add5d..71398a96 100644 --- a/nkebao/src/pages/mobile/mine/devices/DeviceDetail.tsx +++ b/nkebao/src/pages/mobile/mine/devices/DeviceDetail.tsx @@ -1,17 +1,24 @@ import React, { useEffect, useState, useCallback, useRef } from "react"; import { useParams, useNavigate } from "react-router-dom"; -import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile"; -import { SettingOutlined, RedoOutlined } from "@ant-design/icons"; +import { + NavBar, + Tabs, + Switch, + Toast, + SpinLoading, + Button, + Avatar, +} from "antd-mobile"; +import { SettingOutlined, RedoOutlined, UserOutlined } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; import { fetchDeviceDetail, fetchDeviceRelatedAccounts, fetchDeviceHandleLogs, updateDeviceTaskConfig, -} from "@/api/devices"; +} from "./api"; import type { Device, WechatAccount, HandleLog } from "@/types/device"; - +import NavCommon from "@/components/NavCommon"; const DeviceDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -23,7 +30,7 @@ const DeviceDetail: React.FC = () => { const [logs, setLogs] = useState([]); const [logsLoading, setLogsLoading] = useState(false); const [featureSaving, setFeatureSaving] = useState<{ [k: string]: boolean }>( - {} + {}, ); // 获取设备详情 @@ -82,7 +89,7 @@ const DeviceDetail: React.FC = () => { // 功能开关 const handleFeatureChange = async ( feature: keyof Device["features"], - checked: boolean + checked: boolean, ) => { if (!id) return; setFeatureSaving(prev => ({ ...prev, [feature]: true })); @@ -94,7 +101,7 @@ const DeviceDetail: React.FC = () => { ...prev, features: { ...prev.features, [feature]: checked }, } - : prev + : prev, ); Toast.show({ content: `${getFeatureName(feature)}已${checked ? "开启" : "关闭"}`, @@ -119,19 +126,50 @@ const DeviceDetail: React.FC = () => { return ( navigate(-1)} - style={{ background: "#fff" }} - right={ - - } - > - - 设备详情 - - + <> + + + {/* 基本信息卡片 */} + {device && ( +
+
+ {device.memo || "未命名设备"} +
+
+ IMEI: {device.imei} +
+
+ 微信号: {device.wechatId || "未绑定"} +
+
+ 好友数: {device.totalFriend ?? "-"} +
+
+ {device.status === "online" || device.alive === 1 + ? "在线" + : "离线"} +
+
+ )} + } loading={loading} > @@ -142,43 +180,6 @@ const DeviceDetail: React.FC = () => {
) : (
- {/* 基本信息卡片 */} -
-
- {device.memo || "未命名设备"} -
-
- IMEI: {device.imei} -
-
- 微信号: {device.wechatId || "未绑定"} -
-
- 好友数: {device.totalFriend ?? "-"} -
-
- {device.status === "online" || device.alive === 1 - ? "在线" - : "离线"} -
-
{/* 标签页 */} @@ -199,9 +200,9 @@ const DeviceDetail: React.FC = () => { }} > {["autoAddFriend", "autoReply", "momentsSync", "aiChat"].map( - f => ( + (f, index) => (
{ onChange={checked => handleFeatureChange( f as keyof Device["features"], - checked + checked, ) } />
- ) + ), )}
)} @@ -254,9 +255,9 @@ const DeviceDetail: React.FC = () => {
- {accounts.map(acc => ( + {accounts.map((acc, index) => (
{ borderRadius: 8, padding: 10, }} + onClick={() => { + navigate(`/wechat-accounts/detail/${acc.wechatId}`); + }} > - {acc.nickname} + +
+ } />
{acc.nickname}
@@ -334,9 +355,9 @@ const DeviceDetail: React.FC = () => {
- {logs.map(log => ( + {logs.map((log, index) => (
export const fetchDeviceHandleLogs = ( id: string | number, page = 1, - limit = 10 + limit = 10, ) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET"); // 更新设备任务配置 diff --git a/nkebao/src/pages/mobile/mine/devices/index.tsx b/nkebao/src/pages/mobile/mine/devices/index.tsx index 84464d2f..444cfea3 100644 --- a/nkebao/src/pages/mobile/mine/devices/index.tsx +++ b/nkebao/src/pages/mobile/mine/devices/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState, useCallback } from "react"; -import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; +import { Popup, Tabs, Toast, SpinLoading } from "antd-mobile"; import { Button, Input, Pagination, Checkbox } from "antd"; import { useNavigate } from "react-router-dom"; import { AddOutline, DeleteOutline } from "antd-mobile-icons"; @@ -7,18 +7,18 @@ import { ReloadOutlined, SearchOutlined, QrcodeOutlined, - ArrowLeftOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; import { fetchDeviceList, fetchDeviceQRCode, addDeviceByImei, deleteDevice, -} from "@/api/devices"; +} from "./api"; import type { Device } from "@/types/device"; import { comfirm } from "@/utils/common"; +import { useUserStore } from "@/store/module/user"; +import NavCommon from "@/components/NavCommon"; const Devices: React.FC = () => { // 设备列表相关 @@ -47,6 +47,7 @@ const Devices: React.FC = () => { const [delLoading, setDelLoading] = useState(false); const navigate = useNavigate(); + const { user } = useUserStore(); // 加载设备列表 const loadDevices = useCallback( async (reset = false) => { @@ -68,7 +69,7 @@ const Devices: React.FC = () => { setLoading(false); } }, - [loading, search, page] + [loading, search, page], ); // 首次加载和搜索 @@ -86,7 +87,7 @@ const Devices: React.FC = () => { setPage(p => p + 1); } }, - { threshold: 0.5 } + { threshold: 0.5 }, ); if (observerRef.current) observer.observe(observerRef.current); return () => observer.disconnect(); @@ -112,7 +113,7 @@ const Devices: React.FC = () => { setQrLoading(true); setQrCode(null); try { - const accountId = localStorage.getItem("s2_accountId") || ""; + const accountId = user.s2_accountId; if (!accountId) throw new Error("未获取到用户信息"); const res = await fetchDeviceQRCode(accountId); setQrCode(res.qrCode); @@ -123,6 +124,11 @@ const Devices: React.FC = () => { } }; + const addDevice = async () => { + await handleGetQr(); + setAddVisible(true); + }; + // 手动添加设备 const handleAddDevice = async () => { if (!imei.trim() || !name.trim()) { @@ -166,7 +172,7 @@ const Devices: React.FC = () => { try { await comfirm( `将删除${selected.length}个设备,删除后本设备配置的计划任务操作也将失效。确认删除?`, - { title: "确认删除", confirmText: "确认删除", cancelText: "取消" } + { title: "确认删除", confirmText: "确认删除", cancelText: "取消" }, ); handleDelete(); } catch { @@ -189,33 +195,15 @@ const Devices: React.FC = () => { - - navigate(-1)} - /> -
- } - style={{ background: "#fff" }} + setAddVisible(true)} - > + } - > - - 设备管理 - - - + />
{/* 搜索栏 */}
@@ -301,7 +289,7 @@ const Devices: React.FC = () => { setSelected(prev => e.target.checked ? [...prev, device.id!] - : prev.filter(id => id !== device.id) + : prev.filter(id => id !== device.id), ); }} onClick={e => e.stopPropagation()} @@ -373,7 +361,7 @@ const Devices: React.FC = () => { {addTab === "scan" && (
- - {/* 退出登录确认对话框 */} - setShowLogoutDialog(false)} - /> ); }; diff --git a/nkebao/src/pages/mobile/mine/recharge/index.module.scss b/nkebao/src/pages/mobile/mine/recharge/index.module.scss index b92276d9..c7e3615a 100644 --- a/nkebao/src/pages/mobile/mine/recharge/index.module.scss +++ b/nkebao/src/pages/mobile/mine/recharge/index.module.scss @@ -1,11 +1,52 @@ .recharge-page { - padding: 16px 0 60px 0; - background: #f7f8fa; - min-height: 100vh; +} + +.record-btn { + color: var(--primary-color); + font-size: 14px; + font-weight: 500; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba(24, 142, 238, 0.1); + } + + &:active { + background-color: rgba(24, 142, 238, 0.2); + } +} + +.recharge-tabs { + :global(.adm-tabs-header) { + background: #fff; + border-bottom: 1px solid #f0f0f0; + position: sticky; + top: 0; + z-index: 10; + } + + :global(.adm-tabs-tab) { + font-size: 16px; + font-weight: 500; + } + + :global(.adm-tabs-tab-active) { + color: var(--primary-color); + } + + :global(.adm-tabs-tab-line) { + background: var(--primary-color); + } +} + +.tab-content { } .balance-card { - margin: 16px; + margin-bottom: 16px; background: #f6ffed; border: 1px solid #b7eb8f; border-radius: 12px; @@ -43,24 +84,24 @@ } .quick-card { - margin: 16px; + margin-bottom: 16px; .quick-list { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 6px; justify-content: flex-start; margin-bottom: 8px; } } .desc-card { - margin: 16px; + margin: 16px 0px; background: #fffbe6; border: 1px solid #ffe58f; } .warn-card { - margin: 16px; + margin: 16px 0; background: #fff2e8; border: 1px solid #ffbb96; } @@ -125,3 +166,275 @@ color: #faad14; font-size: 14px; } + +// AI服务样式 +.ai-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.ai-title { + display: flex; + align-items: center; + font-size: 20px; + font-weight: 700; + color: #222; +} + +.ai-icon { + font-size: 24px; + color: var(--primary-color); + margin-right: 8px; +} + +.ai-tag { + background: #ff6b35; + color: #fff; + font-size: 12px; + padding: 4px 8px; + border-radius: 12px; + font-weight: 500; +} + +.ai-description { + color: #666; + font-size: 14px; + margin-bottom: 20px; +} + +.ai-services { + display: flex; + flex-direction: column; + gap: 16px; +} + +.ai-service-card { + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.service-header { + margin-bottom: 12px; +} + +.service-info { + display: flex; + align-items: center; + gap: 12px; +} + +.service-icon { + font-size: 24px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: #f0f0f0; + border-radius: 8px; +} + +.service-details { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; +} + +.service-name { + font-size: 16px; + font-weight: 600; + color: #222; +} + +.service-price { + font-size: 16px; + font-weight: 700; + color: #ff4d4f; +} + +.service-description { + color: #666; + font-size: 14px; + margin-bottom: 12px; + line-height: 1.5; +} + +.service-features { + margin-bottom: 16px; +} + +.feature-item { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; + font-size: 14px; + color: #333; +} + +.feature-check { + color: #52c41a; + font-weight: bold; + font-size: 16px; +} + +.usage-progress { + margin-top: 12px; +} + +.usage-label { + font-size: 14px; + color: #666; + margin-bottom: 8px; +} + +.progress-bar { + width: 100%; + height: 6px; + background: #f0f0f0; + border-radius: 3px; + overflow: hidden; + margin-bottom: 6px; +} + +.progress-fill { + height: 100%; + background: var(--primary-color); + border-radius: 3px; + transition: width 0.3s ease; +} + +.usage-text { + font-size: 12px; + color: #999; + text-align: right; +} + +// 版本套餐样式 +.version-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + font-size: 20px; + font-weight: 700; + color: #222; +} + +.version-icon { + font-size: 24px; + color: #722ed1; +} + +.version-description { + color: #666; + font-size: 14px; + margin-bottom: 20px; +} + +.version-packages { + display: flex; + flex-direction: column; + gap: 16px; +} + +.version-card { + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + position: relative; +} + +.package-header { + margin-bottom: 12px; +} + +.package-info { + display: flex; + align-items: center; + gap: 12px; +} + +.package-icon { + font-size: 24px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: #f0f0f0; + border-radius: 8px; +} + +.package-details { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; +} + +.package-name { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: #222; +} + +.package-tag { + font-size: 12px; + padding: 2px 8px; + border-radius: 10px; + font-weight: 500; +} + +.tag-blue { + background: #e6f7ff; + color: #1890ff; +} + +.tag-green { + background: #f6ffed; + color: #52c41a; +} + +.package-price { + font-size: 18px; + font-weight: 700; + color: var(--primary-color); +} + +.package-description { + color: #666; + font-size: 14px; + margin-bottom: 12px; + line-height: 1.5; +} + +.package-features { + margin-bottom: 16px; +} + +.features-title { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.package-status { + text-align: center; + color: #52c41a; + font-size: 14px; + font-weight: 500; + margin-bottom: 12px; +} + +.upgrade-btn { + border-radius: 8px; + font-size: 16px; + font-weight: 600; +} diff --git a/nkebao/src/pages/mobile/mine/recharge/index.tsx b/nkebao/src/pages/mobile/mine/recharge/index.tsx index 52ea5325..e322aca2 100644 --- a/nkebao/src/pages/mobile/mine/recharge/index.tsx +++ b/nkebao/src/pages/mobile/mine/recharge/index.tsx @@ -1,14 +1,123 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Card, Button, Toast, NavBar } from "antd-mobile"; +import { Card, Button, Toast, NavBar, Tabs } from "antd-mobile"; import { useUserStore } from "@/store/module/user"; import style from "./index.module.scss"; -import { WalletOutlined, WarningOutlined } from "@ant-design/icons"; +import { + WalletOutlined, + WarningOutlined, + ClockCircleOutlined, + RobotOutlined, + CrownOutlined, +} from "@ant-design/icons"; import NavCommon from "@/components/NavCommon"; import Layout from "@/components/Layout/Layout"; const quickAmounts = [50, 100, 200, 500, 1000]; +// AI服务套餐数据 +const aiServicePackages = [ + { + id: 1, + name: "入门套餐", + tag: "推荐", + tagColor: "blue", + description: "适合个人用户体验AI服务", + usage: "可使用AI服务约110次", + price: 100, + originalPrice: 110, + gift: 10, + actualAmount: 110, + }, + { + id: 2, + name: "标准套餐", + tag: "热门", + tagColor: "green", + description: "适合小团队日常使用", + usage: "可使用AI服务约580次", + price: 500, + originalPrice: 580, + gift: 80, + actualAmount: 580, + }, +]; + +// AI服务列表数据 +const aiServices = [ + { + id: 1, + name: "添加好友及打招呼", + icon: "💬", + price: 1, + description: "AI智能添加好友并发送个性化打招呼消息", + features: ["智能筛选目标用户", "发送个性化打招呼消息", "自动记录添加结果"], + usage: { current: 15, total: 450 }, + }, + { + id: 2, + name: "小室AI内容生产", + icon: "⚡", + price: 1, + description: "AI智能创建朋友圈内容,智能配文与朋友圈内容", + features: ["智能生成朋友圈文案", "AI配文智能文案", "内容智能排版优化"], + usage: { current: 28, total: 680 }, + }, + { + id: 3, + name: "智能分发服务", + icon: "📤", + price: 1, + description: "AI智能分发内容到多个平台", + features: ["多平台智能分发", "内容智能优化", "分发效果分析"], + usage: { current: 12, total: 300 }, + }, +]; + +// 版本套餐数据 +const versionPackages = [ + { + id: 1, + name: "普通版本", + icon: "📦", + price: "免费", + description: "充值即可使用,包含基础AI功能", + features: ["基础AI服务", "标准客服支持", "基础数据统计"], + status: "当前使用中", + buttonText: null, + tagColor: undefined, + }, + { + id: 2, + name: "标准版本", + icon: "👑", + price: "¥98/月", + tag: "推荐", + tagColor: "blue", + description: "适合中小企业,AI功能更丰富", + features: ["高级AI服务", "优先客服支持", "详细数据分析", "API接口访问"], + status: null, + buttonText: "立即升级", + }, + { + id: 3, + name: "企业版本", + icon: "🏢", + price: "¥1980/月", + description: "适合大型企业,提供专属服务", + features: [ + "专属AI服务", + "24小时专属客服", + "高级数据分析", + "API接口访问", + "专属技术支持", + ], + status: null, + buttonText: "立即升级", + tagColor: undefined, + }, +]; + const Recharge: React.FC = () => { const navigate = useNavigate(); const { user } = useUserStore(); @@ -16,6 +125,7 @@ const Recharge: React.FC = () => { const [balance, setBalance] = useState(0); const [selected, setSelected] = useState(null); const [loading, setLoading] = useState(false); + const [activeTab, setActiveTab] = useState("account"); // 充值操作 const handleRecharge = async () => { @@ -31,68 +141,228 @@ const Recharge: React.FC = () => { }, 1200); }; - return ( - }> -
- -
- -
-
当前余额
-
- ¥{balance.toFixed(2)} + // 渲染账户充值tab内容 + const renderAccountRecharge = () => ( +
+ +
+ +
+
当前余额
+
+ ¥{balance.toFixed(2)} +
+
+
+
+ +
快捷充值
+
+ {quickAmounts.map(amt => ( + + ))} +
+ +
+ +
服务消耗
+
+ 使用以下服务将从余额中扣除相应费用。 +
+
+ {balance < 10 && ( + +
+ +
+
余额不足提醒
+
+ 当前余额较低,建议及时充值以免影响服务使用
- -
快捷充值
-
- {quickAmounts.map(amt => ( - - ))} -
- -
- -
服务消耗
-
- 使用以下服务将从余额中扣除相应费用。 -
-
- {balance < 10 && ( - -
- -
-
余额不足提醒
-
- 当前余额较低,建议及时充值以免影响服务使用 + )} +
+ ); + + // 渲染AI服务tab内容 + const renderAiServices = () => ( +
+
+
+ + AI智能服务收费 +
+
统一按次收费
+
+
+ 三项核心AI服务,按使用次数收费,每次1元 +
+ +
+ {aiServices.map(service => ( + +
+
+
{service.icon}
+
+
{service.name}
+
+ ¥{service.price}/次 +
+
+ {service.description} +
+
+ {service.features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+
+
今日使用进度
+
+
+
+
+ {service.usage.current} / {service.usage.total} +
+
- )} + ))} +
+
+ ); + + // 渲染版本套餐tab内容 + const renderVersionPackages = () => ( +
+
+ + 存客宝版本套餐 +
+
+ 选择适合的版本,享受不同级别的AI服务 +
+ +
+ {versionPackages.map(pkg => ( + +
+
+
{pkg.icon}
+
+
+ {pkg.name} + {pkg.tag && ( + + {pkg.tag} + + )} +
+
{pkg.price}
+
+
+
+
+ {pkg.description} +
+
+
包含功能:
+ {pkg.features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+ {pkg.status && ( +
{pkg.status}
+ )} + {pkg.buttonText && ( + + )} +
+ ))} +
+
+ ); + + return ( + navigate("/mine/consumption-records")} + > + +  记录 +
+ } + /> + } + > +
+ + + {renderAccountRecharge()} + + + {renderAiServices()} + + + {renderVersionPackages()} + +
); diff --git a/nkebao/src/pages/mobile/mine/setting/About.tsx b/nkebao/src/pages/mobile/mine/setting/About.tsx new file mode 100644 index 00000000..e1f22402 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/setting/About.tsx @@ -0,0 +1,152 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { NavBar, Card } from "antd-mobile"; +import { + InfoCircleOutlined, + MailOutlined, + PhoneOutlined, + GlobalOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import style from "./index.module.scss"; +import NavCommon from "@/components/NavCommon"; +const About: React.FC = () => { + const navigate = useNavigate(); + + // 应用信息 + const appInfo = { + name: "存客宝管理系统", + version: "1.0.0", + buildNumber: "20241201", + description: "专业的存客宝管理平台,提供设备管理、自动营销、数据分析等功能", + }; + + // 功能特性 + const features = [ + { + title: "设备管理", + description: "统一管理微信设备和账号,实时监控设备状态", + }, + { + title: "自动营销", + description: "智能点赞、群发推送、朋友圈同步等自动化营销功能", + }, + { + title: "流量池管理", + description: "高效管理用户流量池,精准分组和标签管理", + }, + { + title: "内容库", + description: "丰富的营销内容库,支持多种媒体格式", + }, + { + title: "数据分析", + description: "详细的数据统计和分析,助力营销决策", + }, + ]; + + // 联系信息 + const contactInfo = [ + { + id: "email", + title: "邮箱支持", + value: "support@example.com", + icon: , + action: () => { + // 复制邮箱到剪贴板 + navigator.clipboard.writeText("support@example.com"); + }, + }, + { + id: "phone", + title: "客服热线", + value: "400-123-4567", + icon: , + action: () => { + // 拨打电话 + window.location.href = "tel:400-123-4567"; + }, + }, + { + id: "website", + title: "官方网站", + value: "www.example.com", + icon: , + action: () => { + // 打开网站 + window.open("https://www.example.com", "_blank"); + }, + }, + ]; + + return ( + }> +
+ {/* 应用信息卡片 */} + +
+
+
+ logo +
+
+
+
{appInfo.name}
+
版本 {appInfo.version}
+
+ Build {appInfo.buildNumber} +
+
+
+
{appInfo.description}
+
+ + {/* 功能特性 */} + +
功能特性
+
+ {features.map((feature, index) => ( +
+
+
{index + 1}
+
+
+
{feature.title}
+
+ {feature.description} +
+
+
+ ))} +
+
+ + {/* 联系信息 */} + {/* +
联系我们
+ + {contactInfo.map(item => ( + } + onClick={item.action} + arrow + /> + ))} + +
*/} + + {/* 版权信息 */} +
+
© 2024 存客宝管理系统
+
保留所有权利
+
+
+
+ ); +}; + +export default About; diff --git a/nkebao/src/pages/mobile/mine/setting/Privacy.tsx b/nkebao/src/pages/mobile/mine/setting/Privacy.tsx new file mode 100644 index 00000000..ab3f0737 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/setting/Privacy.tsx @@ -0,0 +1,135 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { NavBar, Card } from "antd-mobile"; +import Layout from "@/components/Layout/Layout"; +import style from "./index.module.scss"; + +const Privacy: React.FC = () => { + const navigate = useNavigate(); + + return ( + navigate(-1)} style={{ background: "#fff" }}> + + 用户隐私协议 + + + } + > +
+ +
+

用户隐私协议

+

更新时间:2024年12月1日

+ +
+

1. 信息收集

+

我们收集的信息包括:

+
    +
  • 账户信息:用户名、手机号、邮箱等注册信息
  • +
  • 设备信息:设备型号、操作系统版本、设备标识符
  • +
  • 使用数据:应用使用情况、功能访问记录
  • +
  • 微信相关:微信账号信息、好友数据(经您授权)
  • +
+
+ +
+

2. 信息使用

+

我们使用收集的信息用于:

+
    +
  • 提供和改进服务功能
  • +
  • 个性化用户体验
  • +
  • 安全防护和风险控制
  • +
  • 客户支持和问题解决
  • +
  • 合规性要求和法律义务
  • +
+
+ +
+

3. 信息共享

+

我们不会向第三方出售、交易或转让您的个人信息,除非:

+
    +
  • 获得您的明确同意
  • +
  • 法律法规要求
  • +
  • 保护用户和公众的安全
  • +
  • 与授权合作伙伴共享必要信息
  • +
+
+ +
+

4. 数据安全

+

我们采取多种安全措施保护您的信息:

+
    +
  • 数据加密传输和存储
  • +
  • 访问控制和身份验证
  • +
  • 定期安全审计和更新
  • +
  • 员工保密培训
  • +
+
+ +
+

5. 您的权利

+

您享有以下权利:

+
    +
  • 访问和查看您的个人信息
  • +
  • 更正或更新不准确的信息
  • +
  • 删除您的账户和相关数据
  • +
  • 撤回同意和限制处理
  • +
  • 数据可携带性
  • +
+
+ +
+

6. 数据保留

+

我们仅在必要期间保留您的信息:

+
    +
  • 账户活跃期间持续保留
  • +
  • 法律法规要求的保留期
  • +
  • 业务运营必要的保留期
  • +
  • 您主动删除后及时清除
  • +
+
+ +
+

7. 儿童隐私

+

+ 我们的服务不面向13岁以下儿童。如果发现收集了儿童信息,我们将立即删除。 +

+
+ +
+

8. 国际传输

+

+ 您的信息可能在中国境内或境外处理。我们将确保适当的保护措施。 +

+
+ +
+

9. 协议更新

+

+ 我们可能会更新本隐私协议。重大变更将通过应用内通知或邮件告知您。 +

+
+ +
+

10. 联系我们

+

如果您对本隐私协议有任何疑问,请联系我们:

+
    +
  • 邮箱:privacy@example.com
  • +
  • 电话:400-123-4567
  • +
  • 地址:北京市朝阳区xxx大厦
  • +
+
+ +
+

感谢您使用存客宝管理系统!

+
+
+
+
+
+ ); +}; + +export default Privacy; diff --git a/nkebao/src/pages/mobile/mine/setting/README.md b/nkebao/src/pages/mobile/mine/setting/README.md new file mode 100644 index 00000000..c8a454b6 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/setting/README.md @@ -0,0 +1,188 @@ +# 设置功能说明 + +## 概述 + +设置功能为存客宝管理系统提供了完整的用户配置管理,包括账户设置、通知设置、应用设置等多个模块。 + +## 功能模块 + +### 1. 主设置页面 (`index.tsx`) + +**功能特性:** + +- 用户信息展示 +- 分组设置项管理 +- 设置状态持久化 + +**主要组件:** + +- 用户信息卡片:显示头像、昵称、账号、角色 +- 设置分组:账户设置、通知设置、应用设置、其他 +- 版本信息:显示应用版本和版权信息 + +### 2. 安全设置页面 (`SecuritySetting.tsx`) + +**功能特性:** + +- 密码修改 +- 手机号绑定 +- 登录设备管理 +- 安全建议 + +**主要功能:** + +- 修改密码:支持旧密码验证和新密码确认 +- 绑定手机号:提高账号安全性 +- 设备管理:查看和管理已登录设备 +- 安全提醒:提供账号安全建议 + +### 3. 关于页面 (`About.tsx`) + +**功能特性:** + +- 应用信息展示 +- 功能特性介绍 +- 联系方式 +- 法律信息 + +**主要内容:** + +- 应用版本信息 +- 功能介绍:设备管理、自动营销、流量池管理等 +- 联系方式:邮箱、电话、官网 +- 法律文档:隐私政策、用户协议、开源许可 + +## 设置管理 + +### 设置Store (`settings.ts`) + +**功能特性:** + +- 全局设置状态管理 +- 设置持久化存储 +- 设置工具函数 + +**支持的设置项:** + +#### 通知设置 + +- `pushNotification`: 推送通知开关 +- `emailNotification`: 邮件通知开关 +- `soundNotification`: 声音提醒开关 + +#### 应用设置 + +- `autoLogin`: 自动登录开关 +- `language`: 语言设置 +- `timezone`: 时区设置 + +#### 隐私设置 + +- `analyticsEnabled`: 数据分析开关 +- `crashReportEnabled`: 崩溃报告开关 + +#### 功能设置 + +- `autoSave`: 自动保存开关 +- `showTutorial`: 教程显示开关 + +### 工具函数 + +```typescript +// 获取设置值 +const value = getSetting("pushNotification"); + +// 设置值 +setSetting("autoLogin", true); +``` + +## 样式设计 + +### 设计原则 + +- 移动端优先设计 +- 统一的视觉风格 +- 良好的用户体验 + +### 样式特性 + +- 响应式布局 +- 卡片式设计 +- 圆角边框 +- 阴影效果 +- 渐变背景 + +## 路由配置 + +```typescript +// 设置相关路由 +{ + path: "/settings", + element: , + auth: true, +}, +{ + path: "/security", + element: , + auth: true, +}, +{ + path: "/about", + element: , + auth: true, +} +``` + +## 使用示例 + +### 基本使用 + +```typescript +import { useSettingsStore } from '@/store/module/settings'; + +const MyComponent = () => { + const { settings, updateSetting } = useSettingsStore(); + + const handleToggleNotification = () => { + updateSetting('pushNotification', !settings.pushNotification); + }; + + return ( + + ); +}; +``` + +## 扩展功能 + +### 添加新设置项 + +1. 在 `AppSettings` 接口中添加新字段 +2. 在 `defaultSettings` 中设置默认值 +3. 在设置页面中添加对应的UI组件 +4. 在样式文件中添加相应的样式 + +### 添加新设置页面 + +1. 创建新的页面组件 +2. 在路由配置中添加路由 +3. 在主设置页面中添加导航链接 +4. 添加相应的样式 + +## 注意事项 + +1. **数据持久化**:所有设置都会自动保存到本地存储 +2. **权限控制**:某些设置可能需要管理员权限 +3. **兼容性**:确保在不同设备和浏览器上的兼容性 +4. **性能优化**:避免频繁的设置更新影响性能 + +## 未来规划 + +- [ ] 多语言支持 +- [ ] 设置导入导出 +- [ ] 云端同步设置 +- [ ] 设置备份恢复 +- [ ] 高级设置选项 diff --git a/nkebao/src/pages/mobile/mine/setting/SecuritySetting.tsx b/nkebao/src/pages/mobile/mine/setting/SecuritySetting.tsx new file mode 100644 index 00000000..9ae89537 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/setting/SecuritySetting.tsx @@ -0,0 +1,224 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { NavBar, List, Dialog, Toast, Card, Input } from "antd-mobile"; +import { + LockOutlined, + MobileOutlined, + SafetyOutlined, + RightOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import { useUserStore } from "@/store/module/user"; +import style from "./index.module.scss"; +import NavCommon from "@/components/NavCommon"; +const SecuritySetting: React.FC = () => { + const navigate = useNavigate(); + const { user } = useUserStore(); + const [showPasswordDialog, setShowPasswordDialog] = useState(false); + const [passwordForm, setPasswordForm] = useState({ + oldPassword: "", + newPassword: "", + confirmPassword: "", + }); + + // 修改密码 + const handleChangePassword = async () => { + const { oldPassword, newPassword, confirmPassword } = passwordForm; + + if (!oldPassword || !newPassword || !confirmPassword) { + Toast.show({ content: "请填写完整信息", position: "top" }); + return; + } + + if (newPassword !== confirmPassword) { + Toast.show({ content: "两次输入的新密码不一致", position: "top" }); + return; + } + + if (newPassword.length < 6) { + Toast.show({ content: "新密码长度不能少于6位", position: "top" }); + return; + } + + try { + // TODO: 调用修改密码API + Toast.show({ content: "密码修改成功", position: "top" }); + setShowPasswordDialog(false); + setPasswordForm({ + oldPassword: "", + newPassword: "", + confirmPassword: "", + }); + } catch (error: any) { + Toast.show({ content: error.message || "密码修改失败", position: "top" }); + } + }; + + // 绑定手机号 + const handleBindPhone = () => { + Toast.show({ content: "功能开发中", position: "top" }); + }; + + // 登录设备管理 + const handleDeviceManagement = () => { + Toast.show({ content: "功能开发中", position: "top" }); + }; + + // 安全设置项 + const securityItems = [ + { + id: "password", + title: "修改密码", + description: "定期更换密码,保护账号安全", + icon: , + onClick: () => setShowPasswordDialog(true), + }, + { + id: "phone", + title: "绑定手机号", + description: user?.phone + ? `已绑定:${user.phone}` + : "绑定手机号,提高账号安全性", + icon: , + onClick: handleBindPhone, + }, + { + id: "devices", + title: "登录设备管理", + description: "查看和管理已登录的设备", + icon: , + onClick: handleDeviceManagement, + }, + ]; + + return ( + }> +
+ {/* 安全提示卡片 */} + +
+ +
+
账号安全提醒
+
+ 建议定期更换密码,开启双重验证,保护您的账号安全 +
+
+
+
+ + {/* 安全设置列表 */} + +
安全设置
+ + {securityItems.map(item => ( + + ))} + +
+ + {/* 安全建议 */} + +
安全建议
+
+
+ + 使用强密码,包含字母、数字和特殊字符 +
+
+ + 定期更换密码,建议每3个月更换一次 +
+
+ + 不要在公共场所登录账号 +
+
+ + 及时清理不常用的登录设备 +
+
+
+
+ + {/* 修改密码对话框 */} + +
+ + setPasswordForm(prev => ({ ...prev, oldPassword: value })) + } + /> +
+
+ + setPasswordForm(prev => ({ ...prev, newPassword: value })) + } + /> +
+
+ + setPasswordForm(prev => ({ ...prev, confirmPassword: value })) + } + /> +
+
+ } + closeOnAction + actions={[ + [ + { + key: "cancel", + text: "取消", + onClick: () => { + setShowPasswordDialog(false); + setPasswordForm({ + oldPassword: "", + newPassword: "", + confirmPassword: "", + }); + }, + }, + { + key: "confirm", + text: "确认修改", + bold: true, + onClick: handleChangePassword, + }, + ], + ]} + onClose={() => { + setShowPasswordDialog(false); + setPasswordForm({ + oldPassword: "", + newPassword: "", + confirmPassword: "", + }); + }} + /> + + ); +}; + +export default SecuritySetting; diff --git a/nkebao/src/pages/mobile/mine/setting/UserSetting.tsx b/nkebao/src/pages/mobile/mine/setting/UserSetting.tsx new file mode 100644 index 00000000..bb36d8e0 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/setting/UserSetting.tsx @@ -0,0 +1,180 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { NavBar, Input, Button, Toast, Card } from "antd-mobile"; +import Layout from "@/components/Layout/Layout"; +import AvatarUpload from "@/components/Upload/AvatarUpload"; +import { useUserStore } from "@/store/module/user"; +import style from "./index.module.scss"; +import NavCommon from "@/components/NavCommon"; +// 更新用户信息接口 +const updateUserInfo = async (data: { username: string; avatar?: string }) => { + const response = await fetch("/api/user/update", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error("更新用户信息失败"); + } + + return await response.json(); +}; + +const UserSetting: React.FC = () => { + const navigate = useNavigate(); + const { user, setUser } = useUserStore(); + const [nickname, setNickname] = useState(user?.username || ""); + const [avatar, setAvatar] = useState(user?.avatar || ""); + const [saving, setSaving] = useState(false); + + // 保存个人信息 + const handleSave = async () => { + if (!nickname.trim()) { + Toast.show({ content: "昵称不能为空", position: "top" }); + return; + } + + if (nickname.length > 20) { + Toast.show({ content: "昵称长度不能超过20个字符", position: "top" }); + return; + } + + if (!user) { + Toast.show({ content: "用户信息不存在", position: "top" }); + return; + } + + setSaving(true); + try { + // 调用API更新用户信息 + const updateData: { username: string; avatar?: string } = { + username: nickname, + }; + + // 如果头像有变化,也一起更新 + if (avatar !== user.avatar) { + updateData.avatar = avatar; + } + + await updateUserInfo(updateData); + + // 更新本地用户信息 + setUser({ + ...user, + username: nickname, + avatar: avatar, + }); + + Toast.show({ content: "保存成功", position: "top" }); + navigate(-1); + } catch (error: any) { + console.error("保存失败:", error); + Toast.show({ content: error.message || "保存失败", position: "top" }); + } finally { + setSaving(false); + } + }; + + // 重置信息 + const handleReset = () => { + setNickname(user?.username || ""); + setAvatar(user?.avatar || ""); + Toast.show({ content: "已重置", position: "top" }); + }; + + return ( + } + footer={ +
+ +
+ } + > +
+ {/* 头像设置 */} + +
+
头像
+
+ +
+
+
+ + {/* 基本信息 */} + +
+
基本信息
+ +
+ + +
{nickname.length}/20
+
+ +
+ +
+ {user?.account || "未知账号"} +
+
+ +
+ +
+ {user?.phone || "未绑定"} +
+
+ +
+ +
+ {user?.isAdmin === 1 ? "管理员" : "普通用户"} +
+
+
+
+ + {/* 提示信息 */} + +
+
温馨提示
+
+
+ • 昵称修改后将在下次登录时生效 +
+
+ • 头像支持JPG、PNG格式,建议尺寸200x200像素 +
+
+ • 请确保上传的头像符合相关法律法规 +
+
+
+
+
+
+ ); +}; + +export default UserSetting; diff --git a/nkebao/src/pages/mobile/mine/setting/index.module.scss b/nkebao/src/pages/mobile/mine/setting/index.module.scss new file mode 100644 index 00000000..ef57738d --- /dev/null +++ b/nkebao/src/pages/mobile/mine/setting/index.module.scss @@ -0,0 +1,852 @@ +.save-buttons { + padding: 12px; +} +.setting-page { + padding: 12px; + + .user-card { + margin-bottom: 16px; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + background: linear-gradient( + 135deg, + var(--primary-color) 0%, + var(--primary-color-dark) 100% + ); + position: relative; + + .user-info { + display: flex; + align-items: center; + padding: 20px; + gap: 16px; + position: relative; + z-index: 1; + + .avatar { + width: 70px; + height: 70px; + border-radius: 35px; + overflow: hidden; + flex-shrink: 0; + background: rgba(255, 255, 255, 0.2); + border: 3px solid rgba(255, 255, 255, 0.3); + position: relative; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .avatar-placeholder { + width: 100%; + height: 100%; + background: linear-gradient( + 135deg, + var(--primary-color) 0%, + var(--primary-color-dark) 100% + ); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 28px; + font-weight: bold; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + } + } + + .user-details { + flex: 1; + min-width: 0; + + .username { + font-size: 20px; + font-weight: 700; + color: white; + margin-bottom: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + } + + .account { + font-size: 14px; + color: rgba(255, 255, 255, 0.8); + margin-bottom: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .role { + font-size: 12px; + color: rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.2); + padding: 4px 12px; + border-radius: 12px; + display: inline-block; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); + } + } + + .user-actions { + flex-shrink: 0; + } + } + } + + .setting-group { + margin-bottom: 16px; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + background: white; + border: 1px solid rgba(0, 0, 0, 0.05); + + .group-title { + padding: 16px 20px 12px; + font-size: 15px; + font-weight: 700; + color: #333; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-bottom: 1px solid #f0f0f0; + display: flex; + align-items: center; + gap: 8px; + + .group-icon { + font-size: 16px; + } + } + + .setting-list { + :global(.adm-list) { + --border-inner: solid 1px #f0f0f0; + --border-top: none; + --border-bottom: none; + --adm-font-size-main: 16px; + --adm-color-text: #333; + --adm-color-text-secondary: #666; + + .adm-list-item { + padding: 16px 20px; + min-height: 64px; + transition: all 0.2s ease; + position: relative; + + &:hover { + background: #f8f9fa; + transform: translateX(4px); + } + + &:active { + background: #e9ecef; + transform: scale(0.98); + } + + .adm-list-item-content { + padding: 0; + } + + .adm-list-item-content-prefix { + margin-right: 16px; + } + + .adm-list-item-content-main { + flex: 1; + min-width: 0; + + .adm-list-item-content-main-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 4px; + } + + .adm-list-item-content-main-description { + font-size: 14px; + color: #666; + line-height: 1.4; + } + } + + .adm-list-item-content-extra { + margin-left: 8px; + } + + &:last-child { + border-bottom: none; + } + } + } + } + } + + .setting-icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + background: rgba(24, 144, 255, 0.1); + color: var(--primary-color); + transition: all 0.2s ease; + + &:hover { + transform: scale(1.1); + background: rgba(24, 144, 255, 0.2); + } + } + + .setting-title { + display: flex; + align-items: center; + gap: 8px; + } + + .setting-badge { + background: #ff4d4f; + color: white; + font-size: 10px; + padding: 2px 6px; + border-radius: 8px; + font-weight: 500; + } + + .setting-item { + transition: all 0.2s ease; + } + + .version-info { + .version-card { + background: white; + border-radius: 16px; + padding: 24px; + margin-bottom: 16px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + display: flex; + align-items: center; + gap: 16px; + + .app-logo { + flex-shrink: 0; + width: 60px; + height: 60px; + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .version-details { + flex: 1; + text-align: left; + + .app-name { + font-size: 18px; + font-weight: 700; + color: #333; + margin-bottom: 4px; + } + + .version-text { + font-size: 14px; + color: #666; + margin-bottom: 2px; + } + + .build-info { + font-size: 12px; + color: #999; + } + } + } + + .copyright { + color: #999; + font-size: 12px; + line-height: 1.6; + text-align: center; + span { + display: block; + margin-bottom: 4px; + + &:last-child { + margin-bottom: 0; + color: #ccc; + } + } + } + } + + // 安全设置页面样式 + .security-tip-card { + margin-bottom: 12px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%); + + .tip-content { + display: flex; + align-items: flex-start; + padding: 16px; + gap: 12px; + + .tip-icon { + font-size: 24px; + color: #fa8c16; + flex-shrink: 0; + margin-top: 2px; + } + + .tip-text { + flex: 1; + + .tip-title { + font-size: 16px; + font-weight: 600; + color: #d46b08; + margin-bottom: 4px; + } + + .tip-description { + font-size: 14px; + color: #873800; + line-height: 1.4; + } + } + } + } + + .security-advice-card { + margin-bottom: 12px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .advice-title { + padding: 16px 16px 12px; + font-size: 16px; + font-weight: 600; + color: #333; + border-bottom: 1px solid #f0f0f0; + } + + .advice-list { + padding: 16px; + + .advice-item { + display: flex; + align-items: flex-start; + margin-bottom: 12px; + font-size: 14px; + color: #666; + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } + + .advice-dot { + color: var(--primary-color); + margin-right: 8px; + font-weight: bold; + flex-shrink: 0; + margin-top: 1px; + } + } + } + } + + .password-form { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px 0; + .line { + margin-bottom: 12px; + } + :global(.adm-input) { + border: 1px solid #d9d9d9; + border-radius: 8px; + padding: 12px; + font-size: 16px; + + &:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(24, 142, 238, 0.2); + } + } + } + + // 关于页面样式 + .app-info-card { + margin-bottom: 12px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .app-info { + display: flex; + align-items: center; + padding: 20px 16px; + gap: 16px; + + .app-logo { + width: 80px; + height: 80px; + border-radius: 20px; + overflow: hidden; + flex-shrink: 0; + + .logo-placeholder { + width: 100%; + height: 100%; + background: linear-gradient( + 135deg, + var(--primary-color) 0%, + var(--primary-color-dark) 100% + ); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 32px; + } + } + + .app-details { + flex: 1; + min-width: 0; + + .app-name { + font-size: 20px; + font-weight: 600; + color: #333; + margin-bottom: 4px; + } + + .app-version { + font-size: 14px; + color: #666; + margin-bottom: 2px; + } + + .app-build { + font-size: 12px; + color: #999; + } + } + } + + .app-description { + padding: 0 16px 20px; + font-size: 14px; + color: #666; + line-height: 1.5; + } + } + + .features-list { + padding: 16px; + + .feature-item { + margin-bottom: 16px; + padding: 16px; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid var(--primary-color); + + &:last-child { + margin-bottom: 0; + } + + .feature-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 8px; + } + + .feature-description { + font-size: 14px; + color: #666; + line-height: 1.5; + } + } + } + + // 新的功能特性网格布局 + .features-grid { + padding: 12px; + display: grid; + grid-template-columns: 1fr; + gap: 8px; + + .feature-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: #ffffff; + border-radius: 8px; + border: 1px solid #f0f0f0; + transition: all 0.2s ease; + position: relative; + + &:hover { + background: #f8f9fa; + border-color: var(--primary-color); + transform: translateX(4px); + } + + .feature-icon { + flex-shrink: 0; + + .icon-placeholder { + width: 32px; + height: 32px; + border-radius: 6px; + background: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 14px; + font-weight: 600; + } + } + + .feature-content { + flex: 1; + min-width: 0; + + .feature-title { + font-size: 15px; + font-weight: 500; + color: #333; + margin-bottom: 2px; + line-height: 1.3; + } + + .feature-description { + font-size: 12px; + color: #999; + line-height: 1.4; + } + } + + // 为不同卡片添加不同的图标颜色 + &:nth-child(1) .icon-placeholder { + background: #1890ff; + } + + &:nth-child(2) .icon-placeholder { + background: #52c41a; + } + + &:nth-child(3) .icon-placeholder { + background: #722ed1; + } + + &:nth-child(4) .icon-placeholder { + background: #fa8c16; + } + + &:nth-child(5) .icon-placeholder { + background: #eb2f96; + } + } + } + + .copyright-info { + text-align: center; + padding: 32px 16px; + color: #999; + font-size: 12px; + line-height: 1.6; + + .copyright-text { + font-weight: 500; + margin-bottom: 4px; + } + + .copyright-subtext { + color: #ccc; + } + } + + // 隐私协议页面样式 + .privacy-card { + margin-bottom: 12px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .privacy-content { + padding: 20px 16px; + + h2 { + font-size: 20px; + font-weight: 600; + color: #333; + margin-bottom: 8px; + text-align: center; + } + + .update-time { + font-size: 12px; + color: #999; + text-align: center; + margin-bottom: 24px; + } + + section { + margin-bottom: 24px; + + h3 { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + } + + p { + font-size: 14px; + color: #666; + line-height: 1.6; + margin-bottom: 8px; + } + + ul { + margin: 8px 0; + padding-left: 20px; + + li { + font-size: 14px; + color: #666; + line-height: 1.6; + margin-bottom: 4px; + } + } + } + + .privacy-footer { + text-align: center; + margin-top: 32px; + padding-top: 20px; + border-top: 1px solid #f0f0f0; + + p { + font-size: 14px; + color: #999; + font-style: italic; + } + } + } + } + + // 个人信息页面样式 + .save-buttons { + padding: 16px; + background: #fff; + border-top: 1px solid #f0f0f0; + } + + .avatar-card { + margin-bottom: 12px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .avatar-section { + padding: 20px 16px; + + .avatar-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 16px; + } + + .avatar-container { + display: flex; + justify-content: center; + margin-bottom: 12px; + + .avatar-wrapper { + position: relative; + width: 100px; + height: 100px; + border-radius: 50px; + overflow: hidden; + background: #f0f0f0; + border: 2px solid #e0e0e0; + cursor: pointer; + + .avatar-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + .avatar-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-size: 40px; + } + + .avatar-upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 24px; + opacity: 0; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1; + } + + .upload-loading { + font-size: 12px; + text-align: center; + line-height: 1.4; + } + } + + &:hover .avatar-upload-overlay { + opacity: 1; + } + } + } + + .avatar-tip { + font-size: 12px; + color: #999; + text-align: center; + line-height: 1.4; + } + } + } + + .info-card { + margin-bottom: 12px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .info-section { + padding: 20px 16px; + + .info-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 20px; + } + + .input-group { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + + .input-label { + display: block; + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 8px; + } + + .input-field { + width: 100%; + border: 1px solid #d9d9d9; + border-radius: 8px; + padding: 12px; + font-size: 16px; + background: #fff; + + &:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(24, 142, 238, 0.2); + } + } + + .readonly-field { + padding: 12px; + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + font-size: 16px; + color: #666; + min-height: 48px; + display: flex; + align-items: center; + } + + .input-tip { + font-size: 12px; + color: #999; + margin-top: 4px; + text-align: right; + } + } + } + } + + .tip-card { + margin-bottom: 12px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .tip-content { + padding: 16px; + + .tip-title { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + } + + .tip-list { + .tip-item { + font-size: 12px; + color: #666; + line-height: 1.6; + margin-bottom: 6px; + + &:last-child { + margin-bottom: 0; + } + } + } + } + } +} diff --git a/nkebao/src/pages/mobile/mine/setting/index.tsx b/nkebao/src/pages/mobile/mine/setting/index.tsx new file mode 100644 index 00000000..8090bdc2 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/setting/index.tsx @@ -0,0 +1,302 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { NavBar, List, Switch, Button, Dialog, Toast, Card } from "antd-mobile"; +import { + UserOutlined, + SafetyOutlined, + InfoCircleOutlined, + LogoutOutlined, + SettingOutlined, + LockOutlined, + HeartOutlined, + StarOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import { useUserStore } from "@/store/module/user"; +import { useSettingsStore } from "@/store/module/settings"; +import style from "./index.module.scss"; +import NavCommon from "@/components/NavCommon"; +interface SettingItem { + id: string; + title: string; + description?: string; + icon: React.ReactNode; + type: "navigate" | "switch" | "button"; + value?: boolean; + path?: string; + onClick?: () => void; + badge?: string; + color?: string; +} + +const Setting: React.FC = () => { + const navigate = useNavigate(); + const { user, logout } = useUserStore(); + const { settings, updateSetting } = useSettingsStore(); + const [showLogoutDialog, setShowLogoutDialog] = useState(false); + const [avatarError, setAvatarError] = useState(false); + + // 处理头像加载错误 + const handleAvatarError = () => { + setAvatarError(true); + }; + + // 退出登录 + const handleLogout = () => { + logout(); + setShowLogoutDialog(false); + navigate("/login"); + Toast.show({ + content: "退出成功", + position: "top", + }); + }; + + // 清除缓存 + const handleClearCache = () => { + Dialog.confirm({ + content: "确定要清除缓存吗?这将清除所有本地数据。", + onConfirm: () => { + localStorage.clear(); + sessionStorage.clear(); + Toast.show({ + content: "缓存已清除", + position: "top", + }); + }, + }); + }; + + // 设置项配置 + const settingGroups: { title: string; items: SettingItem[] }[] = [ + { + title: "账户设置", + items: [ + { + id: "profile", + title: "个人信息", + description: "修改头像、昵称等基本信息", + icon: , + type: "navigate", + path: "/userSet", + color: "var(--primary-color)", + }, + { + id: "security", + title: "安全设置", + description: "密码修改、登录设备管理", + icon: , + type: "navigate", + path: "/security", + color: "var(--primary-color)", + }, + ], + }, + { + title: "应用设置", + items: [ + { + id: "privacy", + title: "隐私保护", + description: "数据隐私、权限管理", + icon: , + type: "navigate", + path: "/privacy", + color: "var(--primary-color)", + }, + { + id: "clearCache", + title: "清除缓存", + description: "清除本地缓存数据", + icon: , + type: "button", + onClick: handleClearCache, + color: "var(--primary-color)", + badge: "2.3MB", + }, + ], + }, + { + title: "其他", + items: [ + { + id: "about", + title: "关于我们", + description: "版本信息、联系方式", + icon: , + type: "navigate", + path: "/about", + color: "var(--primary-color)", + }, + { + id: "logout", + title: "退出登录", + description: "安全退出当前账号", + icon: , + type: "button", + onClick: () => setShowLogoutDialog(true), + color: "#ff4d4f", + }, + ], + }, + ]; + + // 渲染设置项 + const renderSettingItem = (item: SettingItem) => { + const handleClick = () => { + if (item.type === "navigate" && item.path) { + navigate(item.path); + } else if (item.type === "switch" && item.onClick) { + item.onClick(); + } else if (item.type === "button" && item.onClick) { + item.onClick(); + } + }; + + return ( + + {item.icon} +
+ } + title={ +
+ {item.title} + {item.badge && ( + {item.badge} + )} +
+ } + description={item.description} + extra={ + item.type === "switch" ? ( + item.onClick?.()} + style={ + { + "--checked-color": item.color || "var(--primary-color)", + } as React.CSSProperties + } + /> + ) : null + } + onClick={handleClick} + arrow={item.type === "navigate"} + className={style["setting-item"]} + /> + ); + }; + + return ( + }> +
+ {/* 用户信息卡片 */} + +
+
+ {user?.avatar && !avatarError ? ( + 头像 + ) : ( +
+ {user?.username?.charAt(0) || "用"} +
+ )} +
+
+
+ {user?.username || "未设置昵称"} +
+
+ {user?.account || "未知账号"} +
+
+ {user?.isAdmin === 1 ? "管理员" : "普通用户"} +
+
+
+ +
+
+
+ + {/* 设置列表 */} + {settingGroups.map((group, groupIndex) => ( + +
+ ⚙️ + {group.title} +
+ + {group.items.map(renderSettingItem)} + +
+ ))} + + {/* 版本信息 */} +
+
+
+ +
+
+
存客宝
+
版本 3.0.0
+
Build 2025-7-30
+
+
+
+ © 2024 存客宝管理系统 + 让客户管理更简单 +
+
+
+ + {/* 退出登录确认对话框 */} + setShowLogoutDialog(false)} + /> +
+ ); +}; + +export default Setting; 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 dfad5195..6d4c4b83 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts +++ b/nkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts @@ -1,13 +1,7 @@ import request from "@/api/request"; -import type { - TrafficPoolUserDetail, - UserJourneyResponse, - UserTagsResponse, -} from "./data"; +import type { UserTagsResponse } from "./data"; -export function getTrafficPoolDetail( - wechatId: string -): Promise { +export function getTrafficPoolDetail(wechatId: string) { return request("/v1/wechats/getWechatInfo", { wechatId }, "GET"); } @@ -16,7 +10,7 @@ export function getUserJourney(params: { page: number; pageSize: number; userId: string; -}): Promise { +}) { return request("/v1/traffic/pool/getUserJourney", params, "GET"); } diff --git a/nkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts b/nkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts index fd55131e..43615992 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts +++ b/nkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts @@ -30,3 +30,79 @@ export interface TrafficPoolUserDetail { value?: number; }>; } + +// 扩展的用户详情类型 +export interface ExtendedUserDetail extends TrafficPoolUserDetail { + userInfo: { + nickname: string; + avatar: string; + wechatId: string; + friendShip: { + totalFriend: number; + maleFriend: number; + femaleFriend: number; + unknowFriend: number; + }; + }; + rfmScore: { + recency: number; + frequency: number; + monetary: number; + totalScore: number; + }; + trafficPools: { + currentPool: string; + availablePools: string[]; + }; + userTags: Array<{ + id: string; + name: string; + color: string; + type: string; + }>; + valueTags: Array<{ + id: string; + name: string; + color: string; + icon: string; + rfmScore: number; + valueLevel: string; + }>; + restrictions?: Array<{ + id: string; + reason: string; + level: number; + date: number | null; + }>; +} + +// 互动记录类型 +export interface InteractionRecord { + id: string; + type: string; + content: string; + timestamp: string; + value?: number; +} + +// 用户旅程记录类型 +export interface UserJourneyRecord { + id: string; + type: number; + remark: string; + createTime: string; +} + +// 用户标签响应类型 +export interface UserTagsResponse { + wechat: string[]; + siteLabels: UserTagItem[]; +} + +// 用户标签项类型 +export interface UserTagItem { + id: string; + name: string; + color?: string; + type?: string; +} 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 60165acb..6b896216 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 @@ -47,6 +47,18 @@ flex-shrink: 0; } + .avatarFallback { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-size: 24px; + border-radius: 50%; + } + .userDetails { flex: 1; min-width: 0; @@ -343,7 +355,7 @@ flex-direction: column; align-items: center; justify-content: center; - padding: 60px 16px; + padding: 20px 16px; text-align: center; } @@ -353,14 +365,14 @@ } .emptyText { - font-size: 16px; + font-size: 14px; color: #666; - margin-bottom: 8px; + margin-bottom: 4px; font-weight: 500; } .emptyDesc { - font-size: 14px; + font-size: 12px; color: #999; line-height: 1.4; } 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 f478fdfc..4c7ce51b 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx +++ b/nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx @@ -1,20 +1,9 @@ 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 { Card, Button, Avatar, Tag, List, SpinLoading } from "antd-mobile"; import { UserOutlined, CrownOutlined, - PlusOutlined, - CloseOutlined, EyeOutlined, DollarOutlined, MobileOutlined, @@ -26,9 +15,7 @@ import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; import { getTrafficPoolDetail, getUserJourney, getUserTags } from "./api"; import type { - TrafficPoolUserDetail, ExtendedUserDetail, - InteractionRecord, UserJourneyRecord, UserTagsResponse, UserTagItem, @@ -52,6 +39,7 @@ const TrafficPoolDetail: React.FC = () => { // 用户标签相关状态 const [tagsLoading, setTagsLoading] = useState(false); const [userTagsList, setUserTagsList] = useState([]); + const [wechatTagsList, setWechatTagsList] = useState([]); useEffect(() => { if (!wxid) return; @@ -61,6 +49,8 @@ const TrafficPoolDetail: React.FC = () => { // 将API数据转换为扩展的用户详情数据 const extendedUser: ExtendedUserDetail = { ...res, + // 添加userInfo属性 + userInfo: res.userInfo, // 模拟RFM评分数据 rfmScore: { recency: 5, @@ -92,6 +82,8 @@ const TrafficPoolDetail: React.FC = () => { }, ], }; + console.log(extendedUser); + setUser(extendedUser); }) .finally(() => setLoading(false)); @@ -131,6 +123,7 @@ const TrafficPoolDetail: React.FC = () => { try { const response: UserTagsResponse = await getUserTags(userId); setUserTagsList(response.siteLabels || []); + setWechatTagsList(response.wechat || []); } catch (error) { console.error("获取用户标签失败:", error); } finally { @@ -149,10 +142,6 @@ const TrafficPoolDetail: React.FC = () => { } }; - const handleClose = () => { - navigate(-1); - }; - const getJourneyTypeIcon = (type: number) => { switch (type) { case 0: // 浏览 @@ -207,32 +196,6 @@ const TrafficPoolDetail: React.FC = () => { } }; - 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: @@ -297,7 +260,11 @@ const TrafficPoolDetail: React.FC = () => { } + fallback={ +
+ +
+ } />
{user.userInfo.nickname}
@@ -520,7 +487,7 @@ const TrafficPoolDetail: React.FC = () => { {restriction.reason || "未知原因"} { {activeTab === "tags" && (
- {/* 用户标签 */} - + {/* 站内标签 */} + {tagsLoading && userTagsList.length === 0 ? (
- +
加载中...
) : userTagsList.length === 0 ? (
- + +
+
暂无站内标签
+
+ 该用户还没有任何站内标签
-
暂无用户标签
-
该用户还没有任何标签
) : (
@@ -648,6 +617,39 @@ const TrafficPoolDetail: React.FC = () => { )} + {/* 微信标签 */} + + {tagsLoading && wechatTagsList.length === 0 ? ( +
+ +
加载中...
+
+ ) : wechatTagsList.length === 0 ? ( +
+
+ +
+
暂无微信标签
+
+ 该用户还没有任何微信标签 +
+
+ ) : ( +
+ {wechatTagsList.map((tag, index) => ( + + {tag} + + ))} +
+ )} +
+ {/* 价值标签 */} {user.valueTags && user.valueTags.length > 0 ? ( diff --git a/nkebao/src/pages/mobile/mine/traffic-pool/list/api.ts b/nkebao/src/pages/mobile/mine/traffic-pool/list/api.ts index b5b9bc36..0a6162a8 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/list/api.ts +++ b/nkebao/src/pages/mobile/mine/traffic-pool/list/api.ts @@ -1,6 +1,4 @@ import request from "@/api/request"; -import type { TrafficPoolListResponse, DeviceOption } from "./data"; -import { fetchDeviceList } from "@/api/devices"; // 获取流量池列表 export function fetchTrafficPoolList(params: { @@ -11,16 +9,6 @@ export function fetchTrafficPoolList(params: { return request("/v1/traffic/pool", params, "GET"); } -// 获取设备列表(真实接口) -export async function fetchDeviceOptions(): Promise { - const res = await fetchDeviceList({ page: 1, limit: 100 }); - // 假设返回 { list: [{ id, name, ... }], ... } - return (res.list || []).map((item: any) => ({ - id: String(item.id), - name: item.name, - })); -} - // 获取分组列表(如无真实接口可用mock) export async function fetchPackageOptions(): Promise { // TODO: 替换为真实接口 diff --git a/nkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx b/nkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx index 2a5597a6..7a84040a 100644 --- a/nkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx +++ b/nkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx @@ -1,9 +1,5 @@ import { useState, useEffect, useMemo } from "react"; -import { - fetchTrafficPoolList, - fetchDeviceOptions, - fetchPackageOptions, -} from "./api"; +import { fetchTrafficPoolList, fetchPackageOptions } from "./api"; import type { TrafficPoolUser, DeviceOption, @@ -69,7 +65,6 @@ export function useTrafficPoolListLogic() { // 获取筛选项 useEffect(() => { - fetchDeviceOptions().then(setDeviceOptions); fetchPackageOptions().then(setPackageOptions); }, []); @@ -90,7 +85,7 @@ export function useTrafficPoolListLogic() { // 单选 const handleSelect = (id: number, checked: boolean) => { setSelectedIds(prev => - checked ? [...prev, id] : prev.filter(i => i !== id) + checked ? [...prev, id] : prev.filter(i => i !== id), ); }; diff --git a/nkebao/src/pages/mobile/mine/userSet/index.module.scss b/nkebao/src/pages/mobile/mine/userSet/index.module.scss deleted file mode 100644 index 448cf14c..00000000 --- a/nkebao/src/pages/mobile/mine/userSet/index.module.scss +++ /dev/null @@ -1,115 +0,0 @@ -.user-set-page { - background: #f7f8fa; -} -.user-card { - margin: 18px 16px 0 16px; - border-radius: 14px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); -} -.user-info { - display: flex; - align-items: flex-start; - padding: 24px 20px 20px 20px; -} -.avatar { - width: 64px; - height: 64px; - border-radius: 50%; - background: #f5f5f5; - display: flex; - align-items: center; - justify-content: center; - font-size: 30px; - font-weight: 700; - color: #1890ff; - margin-right: 22px; - overflow: hidden; -} -.avatar-placeholder { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - font-size: 30px; - font-weight: 700; - color: #1890ff; - background: #e6f7ff; - border-radius: 50%; -} -.info-list { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 14px; -} -.info-item { - display: flex; - align-items: center; - font-size: 16px; -} -.label { - color: #888; - min-width: 70px; - font-size: 15px; -} -.value { - color: #222; - font-weight: 500; - font-size: 16px; - margin-left: 8px; - word-break: break-all; -} -.avatar-upload { - position: relative; - cursor: pointer; - width: 64px; - height: 64px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - overflow: hidden; - background: #f5f5f5; - transition: box-shadow 0.2s; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); -} -.avatar-upload img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 50%; -} -.avatar-edit { - position: absolute; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.45); - color: #fff; - font-size: 13px; - text-align: center; - padding: 3px 0 2px 0; - border-radius: 0 0 32px 32px; - opacity: 0; - transition: opacity 0.2s; - pointer-events: none; -} -.avatar-upload:hover .avatar-edit { - opacity: 1; - pointer-events: auto; -} -.edit-input { - flex: 1; - min-width: 0; - font-size: 16px; - border-radius: 8px; - border: 1px solid #e5e6eb; - padding: 4px 10px; - background: #fafbfc; -} -.save-btn { - padding: 12px; - background: #fff; -} diff --git a/nkebao/src/pages/mobile/mine/userSet/index.tsx b/nkebao/src/pages/mobile/mine/userSet/index.tsx deleted file mode 100644 index 5624e6a7..00000000 --- a/nkebao/src/pages/mobile/mine/userSet/index.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useRef, useState } from "react"; -import { useUserStore } from "@/store/module/user"; -import { Card, Button, Input, Toast } from "antd-mobile"; -import style from "./index.module.scss"; -import { useNavigate } from "react-router-dom"; -import Layout from "@/components/Layout/Layout"; -import NavCommon from "@/components/NavCommon"; - -const UserSetting: React.FC = () => { - const { user, setUser } = useUserStore(); - const navigate = useNavigate(); - const [nickname, setNickname] = useState(user?.username || ""); - const [avatar, setAvatar] = useState(user?.avatar || ""); - const [uploading, setUploading] = useState(false); - const fileInputRef = useRef(null); - - // 头像上传 - const handleAvatarChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = ev => { - setAvatar(ev.target?.result as string); - }; - reader.readAsDataURL(file); - }; - - // 保存 - const handleSave = async () => { - if (!nickname.trim()) { - Toast.show({ content: "昵称不能为空", position: "top" }); - return; - } - if (!user) return; - setUser({ ...user, id: user.id, username: nickname, avatar }); - Toast.show({ content: "保存成功", position: "top" }); - navigate(-1); - }; - - return ( - } - footer={ -
- -
- } - > -
- -
-
-
fileInputRef.current?.click()} - > - {avatar ? ( - 头像 - ) : ( -
- )} -
更换头像
- -
-
-
-
- 昵称 - -
-
- 手机号 - {user?.phone || "-"} -
-
- 账号 - {user?.account || "-"} -
-
- 角色 - - {user?.isAdmin === 1 ? "管理员" : "普通用户"} - -
-
-
-
-
-
- ); -}; - -export default UserSetting; diff --git a/nkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts b/nkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts index 0cb79281..d98ec6d2 100644 --- a/nkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts +++ b/nkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts @@ -19,7 +19,7 @@ export function getWechatFriends(params: { limit: params.limit, keyword: params.keyword, }, - "GET" + "GET", ); } diff --git a/nkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/nkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index 83666dbc..25271b23 100644 --- a/nkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/nkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -133,7 +133,7 @@ const WechatAccountDetail: React.FC = () => { setIsFetchingFriends(false); } }, - [id] + [id], ); // 搜索好友 @@ -153,7 +153,7 @@ const WechatAccountDetail: React.FC = () => { setFriendsPage(page); fetchFriendsList(page, searchQuery); }, - [searchQuery, fetchFriendsList] + [searchQuery, fetchFriendsList], ); // 初始化数据 diff --git a/nkebao/src/pages/mobile/mine/wechat-accounts/list/api.ts b/nkebao/src/pages/mobile/mine/wechat-accounts/list/api.ts index 005b3544..db6f3a2c 100644 --- a/nkebao/src/pages/mobile/mine/wechat-accounts/list/api.ts +++ b/nkebao/src/pages/mobile/mine/wechat-accounts/list/api.ts @@ -5,6 +5,7 @@ export function getWechatAccounts(params: { page: number; page_size: number; keyword?: string; + wechatStatus?: string; }) { return request("v1/wechats", params, "GET"); } diff --git a/nkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx b/nkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx index df93801c..4b761b60 100644 --- a/nkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx +++ b/nkebao/src/pages/mobile/mine/wechat-accounts/list/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import { Button, SpinLoading, Toast } from "antd-mobile"; import { Pagination, Input, Tooltip } from "antd"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; import { getWechatAccounts } from "./api"; @@ -26,6 +26,7 @@ const PAGE_SIZE = 10; const WechatAccounts: React.FC = () => { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [accounts, setAccounts] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [currentPage, setCurrentPage] = useState(1); @@ -33,14 +34,24 @@ const WechatAccounts: React.FC = () => { const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); + // 获取路由参数 wechatStatus + const wechatStatus = searchParams.get("wechatStatus"); + const fetchAccounts = async (page = 1, keyword = "") => { setIsLoading(true); try { - const res = await getWechatAccounts({ + const params: any = { page, page_size: PAGE_SIZE, keyword, - }); + }; + + // 如果有 wechatStatus 参数,添加到请求参数中 + if (wechatStatus) { + params.wechatStatus = wechatStatus; + } + + const res = await getWechatAccounts(params); if (res && res.list) { setAccounts(res.list); setTotalAccounts(res.total || 0); @@ -87,7 +98,9 @@ const WechatAccounts: React.FC = () => { - +
= { - douyin: "通过抖音平台进行精准获客", - xiaohongshu: "利用小红书平台进行内容营销获客", - gongzhonghao: "通过微信公众号进行获客", - haibao: "通过海报分享进行获客", - phone: "通过电话营销进行获客", - weixinqun: "通过微信群进行获客", - payment: "通过付款码进行获客", - api: "通过API接口进行获客", -}; - const Scene: React.FC = () => { const navigate = useNavigate(); const [scenarios, setScenarios] = useState([]); @@ -46,9 +36,7 @@ const Scene: React.FC = () => { image: item.image || "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png", - description: - scenarioDescriptions[item.name?.toLowerCase()] || - "通过该平台进行获客", + description: "", count: item.count, growth: item.growth, status: item.status, @@ -69,7 +57,7 @@ const Scene: React.FC = () => { const handleScenarioClick = (scenarioId: string, scenarioName: string) => { navigate( - `/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}` + `/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}`, ); }; @@ -81,13 +69,17 @@ const Scene: React.FC = () => { return ( -
场景获客
- - + } + title="场景获客" + right={ + + } + /> } + footer={} >
{error}
@@ -103,10 +95,9 @@ const Scene: React.FC = () => { 场景获客
} + title={""} right={ } - > + /> } footer={} > @@ -144,11 +135,7 @@ const Scene: React.FC = () => {
{scenario.name}
- {scenario.description && ( -
- {scenario.description} -
- )} +
今日: {scenario.count} diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/index.module.scss b/nkebao/src/pages/mobile/scenarios/plan/list/index.module.scss index ee58c81c..6b4ca580 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/list/index.module.scss +++ b/nkebao/src/pages/mobile/scenarios/plan/list/index.module.scss @@ -39,6 +39,47 @@ gap: 12px; } +.pagination-container { + display: flex; + justify-content: center; + padding: 14px 0; + background: white; + border-radius: 12px; + margin-top: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + :global(.ant-pagination) { + .ant-pagination-item { + border-radius: 6px; + border: 1px solid #d9d9d9; + + &:hover { + border-color: var(--primary-color); + } + + &.ant-pagination-item-active { + background: var(--primary-color); + border-color: var(--primary-color); + + a { + color: white; + } + } + } + + .ant-pagination-prev, + .ant-pagination-next { + border-radius: 6px; + border: 1px solid #d9d9d9; + + &:hover { + border-color: var(--primary-color); + color: var(--primary-color); + } + } + } +} + .plan-item { background: white; border-radius: 12px; diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx index 68db5ee5..cac548d8 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { - NavBar, Button, Toast, SpinLoading, @@ -10,7 +9,7 @@ import { Card, Tag, } from "antd-mobile"; -import { Input } from "antd"; +import { Input, Pagination } from "antd"; import { PlusOutlined, CopyOutlined, @@ -22,7 +21,6 @@ import { EditOutlined, MoreOutlined, ClockCircleOutlined, - DownOutlined, } from "@ant-design/icons"; import NavCommon from "@/components/NavCommon"; import Layout from "@/components/Layout/Layout"; @@ -157,6 +155,12 @@ const ScenarioList: React.FC = () => { await fetchPlanList(currentPage + 1, true); }; + // 分页改变处理 + const handlePageChange = async (page: number) => { + setCurrentPage(page); + await fetchPlanList(page, false); + }; + const handleCopyPlan = async (taskId: string) => { const taskToCopy = tasks.find(task => task.id === taskId); if (!taskToCopy) return; @@ -211,7 +215,7 @@ const ScenarioList: React.FC = () => { if (response) { // 处理webhook URL,使用工具函数构建完整地址 const webhookUrl = buildApiUrl( - response.textUrl?.fullUrl || `webhook/${taskId}` + response.textUrl?.fullUrl || `webhook/${taskId}`, ); setCurrentApiSettings({ @@ -252,6 +256,21 @@ const ScenarioList: React.FC = () => { } }; + // 卡片点击处理 - 执行二维码动作 + const handleCardClick = (taskId: string, event: React.MouseEvent) => { + // 检查点击是否在更多按钮区域内 + const target = event.target as HTMLElement; + const moreButton = target.closest(`.${style["more-btn"]}`); + + // 如果点击的是更多按钮或其子元素,不执行卡片点击动作 + if (moreButton) { + return; + } + + // 执行二维码动作 + handleShowQrCode(taskId); + }; + const getStatusColor = (status: number) => { switch (status) { case 1: @@ -286,7 +305,7 @@ const ScenarioList: React.FC = () => { }; const filteredTasks = tasks.filter(task => - task.name.toLowerCase().includes(searchTerm.toLowerCase()) + task.name.toLowerCase().includes(searchTerm.toLowerCase()), ); // 生成操作菜单 @@ -389,6 +408,18 @@ const ScenarioList: React.FC = () => { } loading={loading} + footer={ +
+ +
+ } >
{/* 计划列表 */} @@ -409,7 +440,11 @@ const ScenarioList: React.FC = () => { ) : ( <> {filteredTasks.map(task => ( - + handleCardClick(task.id, e)} + > {/* 头部:标题、状态和操作菜单 */}
{task.name}
@@ -421,7 +456,10 @@ const ScenarioList: React.FC = () => { size="mini" fill="none" className={style["more-btn"]} - onClick={() => setShowActionMenu(task.id)} + onClick={e => { + e.stopPropagation(); // 阻止事件冒泡 + setShowActionMenu(task.id); + }} > @@ -465,39 +503,6 @@ const ScenarioList: React.FC = () => {
))} - - {/* 加载更多按钮 */} - {hasMore && ( -
- -
- )} - - {/* 没有更多数据提示 */} - {!hasMore && filteredTasks.length > 0 && ( -
- 没有更多数据了 -
- )} )}
@@ -537,7 +542,7 @@ const ScenarioList: React.FC = () => { {item.icon} {item.text}
- ) + ), )}
diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx b/nkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx index 386cf58a..ee67b59c 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx @@ -317,7 +317,7 @@ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.o onClick={() => handleCopy( codeExamples[activeLanguage as keyof typeof codeExamples], - "代码" + "代码", ) } className={style["copy-code-btn"]} diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/index.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/index.tsx index 013b0218..f75c6cc7 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/index.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/new/index.tsx @@ -146,7 +146,7 @@ export default function NewPlan() { ? error : isEdit ? "更新计划失败,请重试" - : "创建计划失败,请重试" + : "创建计划失败,请重试", ); } }; diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index 40431a5e..eaec26ce 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -105,10 +105,10 @@ const BasicSettings: React.FC = ({ const [accounts] = useState(generateRandomAccounts(50)); const [materials] = useState(generatePosterMaterials()); const [selectedAccounts, setSelectedAccounts] = useState( - formData.accounts?.length > 0 ? formData.accounts : [] + formData.accounts?.length > 0 ? formData.accounts : [], ); const [selectedMaterials, setSelectedMaterials] = useState( - formData.materials?.length > 0 ? formData.materials : [] + formData.materials?.length > 0 ? formData.materials : [], ); // showAllScenarios 默认为 true const [showAllScenarios, setShowAllScenarios] = useState(true); @@ -128,7 +128,7 @@ const BasicSettings: React.FC = ({ const [customTags, setCustomTags] = useState(formData.customTags || []); const [tips, setTips] = useState(formData.tips || ""); const [selectedScenarioTags, setSelectedScenarioTags] = useState( - formData.scenarioTags || [] + formData.scenarioTags || [], ); // 电话获客相关状态 @@ -140,10 +140,10 @@ const BasicSettings: React.FC = ({ // 群设置相关状态 const [weixinqunName, setWeixinqunName] = useState( - formData.weixinqunName || "" + formData.weixinqunName || "", ); const [weixinqunNotice, setWeixinqunNotice] = useState( - formData.weixinqunNotice || "" + formData.weixinqunNotice || "", ); // 新增:自定义海报相关状态 @@ -232,7 +232,7 @@ const BasicSettings: React.FC = ({ onChange({ ...formData, customTags: updatedCustomTags }); // 同时从选中标签中移除 const updatedSelectedTags = selectedScenarioTags.filter( - (t: string) => t !== tagId + (t: string) => t !== tagId, ); setSelectedScenarioTags(updatedSelectedTags); onChange({ @@ -292,12 +292,12 @@ const BasicSettings: React.FC = ({ // 账号多选切换 const handleAccountToggle = (account: Account) => { const isSelected = selectedAccounts.some( - (a: Account) => a.id === account.id + (a: Account) => a.id === account.id, ); let newSelected; if (isSelected) { newSelected = selectedAccounts.filter( - (a: Account) => a.id !== account.id + (a: Account) => a.id !== account.id, ); } else { newSelected = [...selectedAccounts, account]; @@ -362,7 +362,7 @@ const BasicSettings: React.FC = ({ const [orderUploaded, setOrderUploaded] = useState(false); const handleOrderFileUpload = async ( - event: React.ChangeEvent + event: React.ChangeEvent, ) => { const file = event.target.files?.[0]; if (file) { @@ -518,7 +518,7 @@ const BasicSettings: React.FC = ({
{[...materials, ...customPosters].map(material => { const isSelected = selectedMaterials.some( - m => m.id === material.id + m => m.id === material.id, ); const isCustom = material.id.startsWith("custom-"); return ( diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx index 42b89d4f..01775fcc 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/FriendRequestSettings.tsx @@ -38,7 +38,7 @@ const FriendRequestSettings: React.FC = ({ const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); const [hasWarnings, setHasWarnings] = useState(false); const [selectedDevices, setSelectedDevices] = useState( - formData.device || [] + formData.device || [], ); const [showRemarkTip, setShowRemarkTip] = useState(false); diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx index 29d7d120..6ce86565 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/MessageSettings.tsx @@ -125,7 +125,7 @@ const MessageSettings: React.FC = ({ const handleUpdateMessage = ( dayIndex: number, messageIndex: number, - updates: Partial + updates: Partial, ) => { const updatedPlans = [...dayPlans]; updatedPlans[dayIndex].messages[messageIndex] = { @@ -181,7 +181,7 @@ const MessageSettings: React.FC = ({ setSelectedGroupId(groupId); setIsGroupSelectOpen(false); message.success( - `已选择群组:${mockGroups.find(g => g.id === groupId)?.name}` + `已选择群组:${mockGroups.find(g => g.id === groupId)?.name}`, ); }; @@ -189,7 +189,7 @@ const MessageSettings: React.FC = ({ const triggerUpload = ( dayIdx: number, msgIdx: number, - type: "miniprogram" | "link" + type: "miniprogram" | "link", ) => { setUploadingDay(dayIdx); setUploadingMsgIdx(msgIdx); @@ -539,7 +539,7 @@ const MessageSettings: React.FC = ({ handleFileUpload( dayIndex, messageIndex, - message.type as any + message.type as any, ) } > diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts b/nkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts index 1f5a5150..8c68b53f 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts @@ -33,7 +33,7 @@ export function deleteScenario(id: string) { export function getPlanList( scenarioId: string, page: number = 1, - limit: number = 20 + limit: number = 20, ) { return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, "GET"); } @@ -214,7 +214,7 @@ export function deleteAutoLikeTask(taskId: string) { return request( `/api/workspace/auto-like/tasks/${taskId}`, undefined, - "DELETE" + "DELETE", ); } @@ -240,7 +240,7 @@ export function deleteGroupPushTask(taskId: string) { return request( `/api/workspace/group-push/tasks/${taskId}`, undefined, - "DELETE" + "DELETE", ); } @@ -266,7 +266,7 @@ export function deleteAutoGroupTask(taskId: string) { return request( `/api/workspace/auto-group/tasks/${taskId}`, undefined, - "DELETE" + "DELETE", ); } @@ -287,7 +287,7 @@ export function getAIAnalysisReport() { return request( "/api/workspace/ai-assistant/analysis-report", undefined, - "GET" + "GET", ); } @@ -379,6 +379,6 @@ export function markNotificationAsRead(notificationId: string) { return request( `/api/system/notifications/${notificationId}/read`, undefined, - "PUT" + "PUT", ); } diff --git a/nkebao/src/pages/mobile/test/index.tsx b/nkebao/src/pages/mobile/test/index.tsx new file mode 100644 index 00000000..5ddee21e --- /dev/null +++ b/nkebao/src/pages/mobile/test/index.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { Card, Button, Space, Typography, Tag } from "antd"; +import { MessageOutlined, SelectOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; +import { isDevelopment } from "@/utils/env"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; + +const { Title, Text } = Typography; + +const TestIndex: React.FC = () => { + const navigate = useNavigate(); + + return ( + }> +
+ + 测试页面 + {isDevelopment && ( + <Tag color="orange" style={{ marginLeft: 8, fontSize: "12px" }}> + 开发环境 + </Tag> + )} + + + + + + + + + + + + + 这里提供各种功能的测试页面,方便开发和调试。 + + +
+
+ ); +}; + +export default TestIndex; diff --git a/nkebao/src/pages/mobile/component-test/index.tsx b/nkebao/src/pages/mobile/test/select.tsx similarity index 93% rename from nkebao/src/pages/mobile/component-test/index.tsx rename to nkebao/src/pages/mobile/test/select.tsx index 1334cda6..c8127880 100644 --- a/nkebao/src/pages/mobile/component-test/index.tsx +++ b/nkebao/src/pages/mobile/test/select.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { NavBar, Tabs } from "antd-mobile"; +import { NavBar, Tabs, Tag } from "antd-mobile"; import { ArrowLeftOutlined } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; import Layout from "@/components/Layout/Layout"; @@ -9,6 +9,7 @@ import FriendSelection from "@/components/FriendSelection"; import GroupSelection from "@/components/GroupSelection"; import ContentLibrarySelection from "@/components/ContentLibrarySelection"; import AccountSelection from "@/components/AccountSelection"; +import { isDevelopment } from "@/utils/env"; const ComponentTest: React.FC = () => { const navigate = useNavigate(); @@ -26,11 +27,19 @@ const ComponentTest: React.FC = () => { // 内容库选择状态 const [selectedLibraries, setSelectedLibraries] = useState([]); - const [selectedAccounts, setSelectedAccounts] = useState([]); + const [selectedAccounts, setSelectedAccounts] = useState([]); return ( }>
+ {isDevelopment && ( +
+ + 开发环境 - 组件测试 + +
+ )} +
diff --git a/nkebao/src/pages/mobile/workspace/auto-group/detail/index.tsx b/nkebao/src/pages/mobile/workspace/auto-group/detail/index.tsx index 73426064..a0d88e7d 100644 --- a/nkebao/src/pages/mobile/workspace/auto-group/detail/index.tsx +++ b/nkebao/src/pages/mobile/workspace/auto-group/detail/index.tsx @@ -59,7 +59,7 @@ const mockTaskDetail: GroupTaskDetail = { nickname: `用户${mIndex + 1}`, wechatId: `wx_${mIndex}`, tags: [`标签${(mIndex % 3) + 1}`], - }) + }), ), })), createTime: "2024-11-20 19:04:14", @@ -169,10 +169,10 @@ const GroupCreationProgress: React.FC<{ }> = ({ taskDetail, onComplete }) => { const [groups, setGroups] = useState(taskDetail.groups); const [currentGroupIndex, setCurrentGroupIndex] = useState( - taskDetail.currentGroupIndex + taskDetail.currentGroupIndex, ); const [status, setStatus] = useState( - taskDetail.status + taskDetail.status, ); useEffect(() => { @@ -207,7 +207,7 @@ const GroupCreationProgress: React.FC<{ }; } return group; - }) + }), ); }; diff --git a/nkebao/src/pages/mobile/workspace/auto-group/list/index.tsx b/nkebao/src/pages/mobile/workspace/auto-group/list/index.tsx index 0ef82861..1b0a9094 100644 --- a/nkebao/src/pages/mobile/workspace/auto-group/list/index.tsx +++ b/nkebao/src/pages/mobile/workspace/auto-group/list/index.tsx @@ -149,8 +149,8 @@ const AutoGroupList: React.FC = () => { ...task, status: task.status === "running" ? "paused" : "running", } - : task - ) + : task, + ), ); Toast.show({ content: "状态已切换" }); }; @@ -160,7 +160,7 @@ const AutoGroupList: React.FC = () => { }; const filteredTasks = tasks.filter(task => - task.name.toLowerCase().includes(searchTerm.toLowerCase()) + task.name.toLowerCase().includes(searchTerm.toLowerCase()), ); return ( diff --git a/nkebao/src/pages/mobile/workspace/auto-like/list/api.ts b/nkebao/src/pages/mobile/workspace/auto-like/list/api.ts index 23b89dc1..7f828d56 100644 --- a/nkebao/src/pages/mobile/workspace/auto-like/list/api.ts +++ b/nkebao/src/pages/mobile/workspace/auto-like/list/api.ts @@ -9,7 +9,7 @@ import { // 获取自动点赞任务列表 export function fetchAutoLikeTasks( - params = { type: 1, page: 1, limit: 100 } + params = { type: 1, page: 1, limit: 100 }, ): Promise { return request("/v1/workbench/list", params, "GET"); } @@ -49,7 +49,7 @@ export function fetchLikeRecords( workbenchId: string, page: number = 1, limit: number = 20, - keyword?: string + keyword?: string, ): Promise> { const params: any = { workbenchId, diff --git a/nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx b/nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx index 33cbb242..a4910269 100644 --- a/nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx +++ b/nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx @@ -224,7 +224,7 @@ const AutoLike: React.FC = () => { // 过滤任务 const filteredTasks = tasks.filter(task => - task.name.toLowerCase().includes(searchTerm.toLowerCase()) + task.name.toLowerCase().includes(searchTerm.toLowerCase()), ); return ( diff --git a/nkebao/src/pages/mobile/workspace/auto-like/new/index.tsx b/nkebao/src/pages/mobile/workspace/auto-like/new/index.tsx index 4443b09b..56cd6a5a 100644 --- a/nkebao/src/pages/mobile/workspace/auto-like/new/index.tsx +++ b/nkebao/src/pages/mobile/workspace/auto-like/new/index.tsx @@ -82,7 +82,7 @@ const NewAutoLike: React.FC = () => { }); setAutoEnabled( (taskDetail as any).status === 1 || - (taskDetail as any).status === "running" + (taskDetail as any).status === "running", ); } } catch (error) { diff --git a/nkebao/src/pages/mobile/workspace/auto-like/record/api.ts b/nkebao/src/pages/mobile/workspace/auto-like/record/api.ts index 81a7c26b..6142fde8 100644 --- a/nkebao/src/pages/mobile/workspace/auto-like/record/api.ts +++ b/nkebao/src/pages/mobile/workspace/auto-like/record/api.ts @@ -9,7 +9,7 @@ import { // 获取自动点赞任务列表 export function fetchAutoLikeTasks( - params = { type: 1, page: 1, limit: 100 } + params = { type: 1, page: 1, limit: 100 }, ): Promise { return request("/v1/workbench/list", params, "GET"); } @@ -49,7 +49,7 @@ export function fetchLikeRecords( workbenchId: string, page: number = 1, limit: number = 20, - keyword?: string + keyword?: string, ): Promise> { const params: any = { workbenchId, diff --git a/nkebao/src/pages/mobile/workspace/group-push/detail/groupPush.ts b/nkebao/src/pages/mobile/workspace/group-push/detail/groupPush.ts index 8692db6e..0295e2f9 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/detail/groupPush.ts +++ b/nkebao/src/pages/mobile/workspace/group-push/detail/groupPush.ts @@ -40,12 +40,12 @@ export async function deleteGroupPushTask(id: string): Promise { export async function toggleGroupPushTask( id: string, - status: string + status: string, ): Promise { return request( `/v1/workspace/group-push/tasks/${id}/toggle`, { status }, - "POST" + "POST", ); } @@ -54,20 +54,20 @@ export async function copyGroupPushTask(id: string): Promise { } export async function createGroupPushTask( - taskData: Partial + taskData: Partial, ): Promise { return request("/v1/workspace/group-push/tasks", taskData, "POST"); } export async function updateGroupPushTask( id: string, - taskData: Partial + taskData: Partial, ): Promise { return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT"); } export async function getGroupPushTaskDetail( - id: string + id: string, ): Promise { return request(`/v1/workspace/group-push/tasks/${id}`); } diff --git a/nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx b/nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx index 7ab43049..ab2a9a0f 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx @@ -226,7 +226,7 @@ const Detail: React.FC = () => {
diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx index c2d98bd8..34ba3f9e 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx @@ -105,7 +105,7 @@ const BasicSettings: React.FC = ({ onChange={e => handleChange( "dailyPushCount", - Number.parseInt(e.target.value) || 1 + Number.parseInt(e.target.value) || 1, ) } style={{ width: 80, textAlign: "center" }} diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx index 2d35916e..ebcb1b52 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx @@ -81,7 +81,7 @@ const ContentSelector: React.FC = ({ const [libraries] = useState(mockLibraries); const filteredLibraries = libraries.filter(library => - library.name.toLowerCase().includes(searchTerm.toLowerCase()) + library.name.toLowerCase().includes(searchTerm.toLowerCase()), ); const handleLibraryToggle = (library: ContentLibrary, checked: boolean) => { diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx index 016e76f6..c7513845 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx @@ -101,7 +101,9 @@ const GroupSelector: React.FC = ({ const filteredGroups = groups.filter( group => group.name.toLowerCase().includes(searchTerm.toLowerCase()) || - group.serviceAccount.name.toLowerCase().includes(searchTerm.toLowerCase()) + group.serviceAccount.name + .toLowerCase() + .includes(searchTerm.toLowerCase()), ); const handleGroupToggle = (group: WechatGroup, checked: boolean) => { diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/index.api.ts b/nkebao/src/pages/mobile/workspace/group-push/form/index.api.ts index 7b4b9ecc..15384c11 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/index.api.ts +++ b/nkebao/src/pages/mobile/workspace/group-push/form/index.api.ts @@ -1,6 +1,6 @@ import request from "@/api/request"; export async function createGroupPushTask( - taskData: Partial + taskData: Partial, ): Promise { return request("/v1/workspace/group-push/tasks", taskData, "POST"); } diff --git a/nkebao/src/pages/mobile/workspace/group-push/list/index.api.ts b/nkebao/src/pages/mobile/workspace/group-push/list/index.api.ts index 8692db6e..0295e2f9 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/list/index.api.ts +++ b/nkebao/src/pages/mobile/workspace/group-push/list/index.api.ts @@ -40,12 +40,12 @@ export async function deleteGroupPushTask(id: string): Promise { export async function toggleGroupPushTask( id: string, - status: string + status: string, ): Promise { return request( `/v1/workspace/group-push/tasks/${id}/toggle`, { status }, - "POST" + "POST", ); } @@ -54,20 +54,20 @@ export async function copyGroupPushTask(id: string): Promise { } export async function createGroupPushTask( - taskData: Partial + taskData: Partial, ): Promise { return request("/v1/workspace/group-push/tasks", taskData, "POST"); } export async function updateGroupPushTask( id: string, - taskData: Partial + taskData: Partial, ): Promise { return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT"); } export async function getGroupPushTaskDetail( - id: string + id: string, ): Promise { return request(`/v1/workspace/group-push/tasks/${id}`); } diff --git a/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx b/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx index daf048be..7634c769 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx @@ -97,7 +97,7 @@ const GroupPush: React.FC = () => { }; const filteredTasks = tasks.filter(task => - task.name.toLowerCase().includes(searchTerm.toLowerCase()) + task.name.toLowerCase().includes(searchTerm.toLowerCase()), ); const getStatusColor = (status: number) => { @@ -361,7 +361,7 @@ const GroupPush: React.FC = () => {
diff --git a/nkebao/src/pages/mobile/workspace/main/index.module.scss b/nkebao/src/pages/mobile/workspace/main/index.module.scss index 4867a014..e8537ed6 100644 --- a/nkebao/src/pages/mobile/workspace/main/index.module.scss +++ b/nkebao/src/pages/mobile/workspace/main/index.module.scss @@ -1,7 +1,5 @@ .workspace { - padding: 16px; - background-color: #f5f5f5; - min-height: 100vh; + padding: 12px; } .section { diff --git a/nkebao/src/pages/mobile/workspace/main/index.tsx b/nkebao/src/pages/mobile/workspace/main/index.tsx index 2f443115..734a941e 100644 --- a/nkebao/src/pages/mobile/workspace/main/index.tsx +++ b/nkebao/src/pages/mobile/workspace/main/index.tsx @@ -3,19 +3,15 @@ import { Link } from "react-router-dom"; import { Card, NavBar, Badge } from "antd-mobile"; import { LikeOutlined, - MessageOutlined, SendOutlined, TeamOutlined, LinkOutlined, - AppstoreOutlined, - PieChartOutlined, - BarChartOutlined, ClockCircleOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; import styles from "./index.module.scss"; - +import NavCommon from "@/components/NavCommon"; const Workspace: React.FC = () => { // 常用功能 const commonFeatures = [ @@ -75,60 +71,9 @@ const Workspace: React.FC = () => { }, ]; - // AI智能助手 - const aiFeatures = [ - { - id: "ai-analyzer", - name: "AI数据分析", - description: "智能分析客户行为特征", - icon: ( - - ), - path: "/workspace/ai-analyzer", - bgColor: "#f0f0ff", - isNew: true, - }, - { - id: "ai-strategy", - name: "AI策略优化", - description: "智能优化获客策略", - icon: ( - - ), - path: "/workspace/ai-strategy", - bgColor: "#e6fffb", - isNew: true, - }, - { - id: "ai-forecast", - name: "AI销售预测", - description: "智能预测销售趋势", - icon: ( - - ), - path: "/workspace/ai-forecast", - bgColor: "#fffbe6", - }, - ]; - return ( -
- 工作台 -
- - } + header={} title="工作台" />} footer={} >
diff --git a/nkebao/src/pages/mobile/workspace/moments-sync/Detail.tsx b/nkebao/src/pages/mobile/workspace/moments-sync/Detail.tsx index 5f3b6bff..3b1f0ffd 100644 --- a/nkebao/src/pages/mobile/workspace/moments-sync/Detail.tsx +++ b/nkebao/src/pages/mobile/workspace/moments-sync/Detail.tsx @@ -73,7 +73,7 @@ const MomentsSyncDetail: React.FC = () => { await request( "/v1/workbench/update-status", { id, status: newStatus }, - "POST" + "POST", ); setTask({ ...task, status: newStatus }); message.success(newStatus === 1 ? "任务已开启" : "任务已暂停"); diff --git a/nkebao/src/pages/mobile/workspace/moments-sync/MomentsSync.tsx b/nkebao/src/pages/mobile/workspace/moments-sync/MomentsSync.tsx index 0815f954..15c5bcfc 100644 --- a/nkebao/src/pages/mobile/workspace/moments-sync/MomentsSync.tsx +++ b/nkebao/src/pages/mobile/workspace/moments-sync/MomentsSync.tsx @@ -55,7 +55,7 @@ const MomentsSync: React.FC = () => { const res = await request( "/v1/workbench/list", { type: 2, page: 1, limit: 100 }, - "GET" + "GET", ); setTasks(res.list || []); } catch (e) { @@ -96,10 +96,10 @@ const MomentsSync: React.FC = () => { await request( "/v1/workbench/update-status", { id, status: newStatus }, - "POST" + "POST", ); setTasks(prev => - prev.map(t => (t.id === id ? { ...t, status: newStatus } : t)) + prev.map(t => (t.id === id ? { ...t, status: newStatus } : t)), ); message.success("操作成功"); } catch { @@ -108,7 +108,7 @@ const MomentsSync: React.FC = () => { }; const filteredTasks = tasks.filter(task => - task.name.toLowerCase().includes(searchTerm.toLowerCase()) + task.name.toLowerCase().includes(searchTerm.toLowerCase()), ); // 菜单 diff --git a/nkebao/src/pages/mobile/workspace/traffic-distribution/form/api.ts b/nkebao/src/pages/mobile/workspace/traffic-distribution/form/api.ts index 4c8b968b..db35a752 100644 --- a/nkebao/src/pages/mobile/workspace/traffic-distribution/form/api.ts +++ b/nkebao/src/pages/mobile/workspace/traffic-distribution/form/api.ts @@ -8,14 +8,14 @@ export const getTrafficDistributionDetail = (id: string) => { // 更新流量分发 export const updateTrafficDistribution = ( - data: TrafficDistributionFormData + data: TrafficDistributionFormData, ) => { return request("/v1/workbench/update", data, "POST"); }; // 创建流量分发 export const createTrafficDistribution = ( - data: TrafficDistributionFormData + data: TrafficDistributionFormData, ) => { return request("/v1/workbench/create", data, "POST"); }; diff --git a/nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx b/nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx index 7ebd417f..5155245c 100644 --- a/nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx +++ b/nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx @@ -393,7 +393,7 @@ const TrafficDistributionForm: React.FC = () => { setSelectedPools(val => e.target.checked ? [...val, pool.id] - : val.filter(v => v !== pool.id) + : val.filter(v => v !== pool.id), ); }} /> diff --git a/nkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts b/nkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts index 0a071cfd..ceacca1c 100644 --- a/nkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts +++ b/nkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts @@ -17,7 +17,7 @@ export function updateDistributionRule(data: any): Promise { // 暂停/启用计划 export function toggleDistributionRuleStatus( id: number, - status: 0 | 1 + status: 0 | 1, ): Promise { return request("/v1/workbench/update-status", { id, status }, "POST"); } diff --git a/nkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx b/nkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx index d38018ab..5e9fd5a8 100644 --- a/nkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx +++ b/nkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx @@ -115,7 +115,7 @@ const TrafficDistributionList: React.FC = () => { // 新增:Switch点击切换计划状态 const handleSwitchChange = async ( checked: boolean, - item: DistributionRule + item: DistributionRule, ) => { setMenuLoadingId(item.id); try { @@ -124,8 +124,8 @@ const TrafficDistributionList: React.FC = () => { // 本地只更新当前item的status,不刷新全列表 setList(prevList => prevList.map(rule => - rule.id === item.id ? { ...rule, status: checked ? 1 : 0 } : rule - ) + rule.id === item.id ? { ...rule, status: checked ? 1 : 0 } : rule, + ), ); } catch (e) { message.error("操作失败"); diff --git a/nkebao/src/router/config.ts b/nkebao/src/router/config.ts index 136e3cb5..1b560e87 100644 --- a/nkebao/src/router/config.ts +++ b/nkebao/src/router/config.ts @@ -14,7 +14,7 @@ export const routeGroups = { // 基础路由 basic: { name: "基础功能", - routes: ["/", "/login", "/scene", "/work", "/mine"], + routes: ["/", "/login", "/guide", "/scene", "/work", "/mine"], }, // 设备管理 @@ -109,6 +109,7 @@ export const routePermissions = { user: [ "/", "/login", + "/guide", "/scene", "/work", "/mine", @@ -136,6 +137,7 @@ export const routePermissions = { export const routeTitles: Record = { "/": "首页", "/login": "登录", + "/guide": "设备绑定引导", "/scene": "场景获客", "/work": "工作台", "/mine": "我的", @@ -159,7 +161,7 @@ export const getRouteTitle = (path: string): string => { // 检查路由权限 export const checkRoutePermission = ( path: string, - userRole: string = "user" + userRole: string = "user", ): boolean => { const allowedRoutes = routePermissions[userRole as keyof typeof routePermissions] || []; diff --git a/nkebao/src/router/index.tsx b/nkebao/src/router/index.tsx index 4a02716e..117681a7 100644 --- a/nkebao/src/router/index.tsx +++ b/nkebao/src/router/index.tsx @@ -16,7 +16,7 @@ Object.values(modules).forEach((mod: any) => { // 权限包装 function wrapWithPermission( - route: RouteObject & { auth?: boolean; requiredRole?: string } + route: RouteObject & { auth?: boolean; requiredRole?: string }, ) { if (route.auth) { return { diff --git a/nkebao/src/router/module/auth.tsx b/nkebao/src/router/module/auth.tsx index b89c7d5d..8d74fd49 100644 --- a/nkebao/src/router/module/auth.tsx +++ b/nkebao/src/router/module/auth.tsx @@ -1,4 +1,5 @@ import Login from "@/pages/login/login"; +import Guide from "@/pages/guide"; const authRoutes = [ { @@ -6,6 +7,11 @@ const authRoutes = [ element: , auth: false, // 不需要权限 }, + { + path: "/guide", + element: , + auth: true, // 需要登录权限 + }, ]; export default authRoutes; diff --git a/nkebao/src/router/module/component-test.tsx b/nkebao/src/router/module/component-test.tsx deleted file mode 100644 index c2ccd384..00000000 --- a/nkebao/src/router/module/component-test.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import ComponentTest from "@/pages/mobile/component-test"; - -const componentTestRoutes = [ - { - path: "/component-test", - element: , - auth: true, - }, -]; - -export default componentTestRoutes; diff --git a/nkebao/src/router/module/iframe.tsx b/nkebao/src/router/module/iframe.tsx new file mode 100644 index 00000000..32af69e2 --- /dev/null +++ b/nkebao/src/router/module/iframe.tsx @@ -0,0 +1,12 @@ +import IframeDebugPage from "@/pages/iframe"; + +// iframe 调试路由 +const iframeRoutes = [ + { + path: "/iframe", + element: , + auth: false, // 不需要认证,方便调试 + }, +]; + +export default iframeRoutes; diff --git a/nkebao/src/router/module/index.tsx b/nkebao/src/router/module/index.tsx index 9a2489ee..4adcd781 100644 --- a/nkebao/src/router/module/index.tsx +++ b/nkebao/src/router/module/index.tsx @@ -1,4 +1,5 @@ import Home from "@/pages/mobile/home/index"; +import Init from "@/pages/iframe/init"; const routes = [ // 基础路由 @@ -7,6 +8,11 @@ const routes = [ element: , auth: true, // 需要登录 }, + { + path: "/init", + element: , + auth: false, // 需要登录 + }, ]; export default routes; diff --git a/nkebao/src/router/module/mine.tsx b/nkebao/src/router/module/mine.tsx index 46e852e8..431c3da2 100644 --- a/nkebao/src/router/module/mine.tsx +++ b/nkebao/src/router/module/mine.tsx @@ -6,7 +6,12 @@ import TrafficPoolDetail from "@/pages/mobile/mine/traffic-pool/detail/index"; import WechatAccounts from "@/pages/mobile/mine/wechat-accounts/list/index"; import WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index"; import Recharge from "@/pages/mobile/mine/recharge/index"; -import UserSetting from "@/pages/mobile/mine/userSet/index"; +import Setting from "@/pages/mobile/mine/setting/index"; +import SecuritySetting from "@/pages/mobile/mine/setting/SecuritySetting"; +import About from "@/pages/mobile/mine/setting/About"; +import Privacy from "@/pages/mobile/mine/setting/Privacy"; +import UserSetting from "@/pages/mobile/mine/setting/UserSetting"; + const routes = [ { path: "/mine", @@ -51,6 +56,26 @@ const routes = [ }, { path: "/settings", + element: , + auth: true, + }, + { + path: "/security", + element: , + auth: true, + }, + { + path: "/about", + element: , + auth: true, + }, + { + path: "/privacy", + element: , + auth: true, + }, + { + path: "/userSet", element: , auth: true, }, diff --git a/nkebao/src/router/module/test.tsx b/nkebao/src/router/module/test.tsx new file mode 100644 index 00000000..becad3a3 --- /dev/null +++ b/nkebao/src/router/module/test.tsx @@ -0,0 +1,22 @@ +import SelectTest from "@/pages/mobile/test/select"; +import PostMessageTest from "@/pages/mobile/test/postMessage"; +import TestIndex from "@/pages/mobile/test/index"; +import { DEV_FEATURES } from "@/utils/env"; + +// 只在开发环境启用测试路由 +const componentTestRoutes = DEV_FEATURES.SHOW_TEST_PAGES + ? [ + { + path: "/test", + element: , + auth: true, + }, + { + path: "/test/select", + element: , + auth: true, + }, + ] + : []; + +export default componentTestRoutes; diff --git a/nkebao/src/store/createPersistStore.ts b/nkebao/src/store/createPersistStore.ts index cf2c80c7..79d7b634 100644 --- a/nkebao/src/store/createPersistStore.ts +++ b/nkebao/src/store/createPersistStore.ts @@ -5,12 +5,12 @@ import { persist, PersistOptions } from "zustand/middleware"; export function createPersistStore( createState: (set: any, get: any) => T, name: string, - partialize?: (state: T) => Partial + partialize?: (state: T) => Partial, ) { return create()( persist(createState, { name, partialize, - } as PersistOptions) + } as PersistOptions), ); } diff --git a/nkebao/src/store/module/app.ts b/nkebao/src/store/module/app.ts new file mode 100644 index 00000000..9b6e73a4 --- /dev/null +++ b/nkebao/src/store/module/app.ts @@ -0,0 +1,130 @@ +import { createPersistStore } from "@/store/createPersistStore"; + +export interface AppState { + // 应用状态 + isLoading: boolean; + isOnline: boolean; + lastActiveTime: number; + + // 主题设置 + theme: "light" | "dark" | "auto"; + + // 缓存设置 + cacheEnabled: boolean; + cacheExpiry: number; // 缓存过期时间(毫秒) + + // 调试设置 + debugMode: boolean; + logLevel: "error" | "warn" | "info" | "debug"; +} + +interface AppStoreState { + app: AppState; + setAppState: (app: Partial) => void; + setLoading: (loading: boolean) => void; + setOnline: (online: boolean) => void; + setTheme: (theme: AppState["theme"]) => void; + setDebugMode: (debug: boolean) => void; + updateLastActiveTime: () => void; + resetAppState: () => void; +} + +// 默认应用状态 +const defaultAppState: AppState = { + isLoading: false, + isOnline: navigator.onLine, + lastActiveTime: Date.now(), + theme: "auto", + cacheEnabled: true, + cacheExpiry: 24 * 60 * 60 * 1000, // 24小时 + debugMode: false, + logLevel: "info", +}; + +export const useAppStore = createPersistStore( + (set, get) => ({ + app: defaultAppState, + + setAppState: newAppState => + set(state => ({ + app: { ...state.app, ...newAppState }, + })), + + setLoading: loading => + set(state => ({ + app: { ...state.app, isLoading: loading }, + })), + + setOnline: online => + set(state => ({ + app: { ...state.app, isOnline: online }, + })), + + setTheme: theme => + set(state => ({ + app: { ...state.app, theme }, + })), + + setDebugMode: debug => + set(state => ({ + app: { ...state.app, debugMode: debug }, + })), + + updateLastActiveTime: () => + set(state => ({ + app: { ...state.app, lastActiveTime: Date.now() }, + })), + + resetAppState: () => set({ app: defaultAppState }), + }), + "app-store", + state => ({ + app: state.app, + }), +); + +// 应用状态工具函数 +export const getAppState = (): AppState => { + const { app } = useAppStore.getState(); + return app; +}; + +export const setAppLoading = (loading: boolean): void => { + const { setLoading } = useAppStore.getState(); + setLoading(loading); +}; + +export const setAppTheme = (theme: AppState["theme"]): void => { + const { setTheme } = useAppStore.getState(); + setTheme(theme); +}; + +export const toggleDebugMode = (): void => { + const { app, setDebugMode } = useAppStore.getState(); + setDebugMode(!app.debugMode); +}; + +// 监听网络状态变化 +if (typeof window !== "undefined") { + window.addEventListener("online", () => { + const { setOnline } = useAppStore.getState(); + setOnline(true); + }); + + window.addEventListener("offline", () => { + const { setOnline } = useAppStore.getState(); + setOnline(false); + }); + + // 监听用户活动 + const updateLastActive = () => { + const { updateLastActiveTime } = useAppStore.getState(); + updateLastActiveTime(); + }; + + ["mousedown", "mousemove", "keypress", "scroll", "touchstart"].forEach( + event => { + document.addEventListener(event, updateLastActive, true); + }, + ); +} diff --git a/nkebao/src/store/module/settings.ts b/nkebao/src/store/module/settings.ts new file mode 100644 index 00000000..af884e36 --- /dev/null +++ b/nkebao/src/store/module/settings.ts @@ -0,0 +1,73 @@ +import { createPersistStore } from "@/store/createPersistStore"; + +export interface AppSettings { + // 应用设置 + language: string; + timezone: string; + + // 隐私设置 + analyticsEnabled: boolean; + crashReportEnabled: boolean; + + // 功能设置 + autoSave: boolean; + showTutorial: boolean; +} + +interface SettingsState { + settings: AppSettings; + setSettings: (settings: Partial) => void; + resetSettings: () => void; + updateSetting: ( + key: K, + value: AppSettings[K], + ) => void; +} + +// 默认设置 +const defaultSettings: AppSettings = { + language: "zh-CN", + timezone: "Asia/Shanghai", + analyticsEnabled: true, + crashReportEnabled: true, + autoSave: true, + showTutorial: true, +}; + +export const useSettingsStore = createPersistStore( + set => ({ + settings: defaultSettings, + + setSettings: newSettings => + set(state => ({ + settings: { ...state.settings, ...newSettings }, + })), + + resetSettings: () => set({ settings: defaultSettings }), + + updateSetting: (key, value) => + set(state => ({ + settings: { ...state.settings, [key]: value }, + })), + }), + "settings-store", + state => ({ + settings: state.settings, + }), +); + +// 设置工具函数 +export const getSetting = ( + key: K, +): AppSettings[K] => { + const { settings } = useSettingsStore.getState(); + return settings[key]; +}; + +export const setSetting = ( + key: K, + value: AppSettings[K], +) => { + const { updateSetting } = useSettingsStore.getState(); + updateSetting(key, value); +}; diff --git a/nkebao/src/store/module/user.ts b/nkebao/src/store/module/user.ts index 6ecd32f7..bee4f4c6 100644 --- a/nkebao/src/store/module/user.ts +++ b/nkebao/src/store/module/user.ts @@ -15,6 +15,7 @@ export interface User { updateTime: string | null; lastLoginIp: string; lastLoginTime: number; + deviceTotal: number; // 设备总数 } interface UserState { @@ -24,7 +25,7 @@ interface UserState { setUser: (user: User) => void; setToken: (token: string) => void; clearUser: () => void; - login: (token: string, userInfo: User) => void; + login: (token: string, userInfo: User, deviceTotal: number) => void; logout: () => void; } @@ -36,7 +37,7 @@ export const useUserStore = createPersistStore( setUser: user => set({ user, isLoggedIn: true }), setToken: token => set({ token }), clearUser: () => set({ user: null, token: null, isLoggedIn: false }), - login: (token, userInfo) => { + login: (token, userInfo, deviceTotal) => { // 只将token存储到localStorage localStorage.setItem("token", token); @@ -56,6 +57,7 @@ export const useUserStore = createPersistStore( updateTime: userInfo.updateTime, lastLoginIp: userInfo.lastLoginIp, lastLoginTime: userInfo.lastLoginTime, + deviceTotal: deviceTotal, }; set({ user, token, isLoggedIn: true }); }, @@ -70,5 +72,5 @@ export const useUserStore = createPersistStore( user: state.user, token: state.token, isLoggedIn: state.isLoggedIn, - }) + }), ); diff --git a/nkebao/src/styles/global.scss b/nkebao/src/styles/global.scss index 261aaf87..ca4e01d0 100644 --- a/nkebao/src/styles/global.scss +++ b/nkebao/src/styles/global.scss @@ -160,6 +160,17 @@ textarea { } /* 6. 移动端/PC端兼容基础样式 */ + +// 安全区域CSS变量定义 +:root { + --safe-area-top: 0px; + --safe-area-bottom: 0px; + --safe-area-left: 0px; + --safe-area-right: 0px; + --status-bar-height: 0px; + --nav-bar-height: 44px; +} + html, body { height: 100%; diff --git a/nkebao/src/utils/common.ts b/nkebao/src/utils/common.ts index 3c54d78e..a71c3f87 100644 --- a/nkebao/src/utils/common.ts +++ b/nkebao/src/utils/common.ts @@ -12,7 +12,7 @@ export const comfirm = ( title?: string; cancelText?: string; confirmText?: string; - } + }, ): Promise => { return new Promise((resolve, reject) => { Modal.show({ @@ -35,3 +35,36 @@ export const comfirm = ( }); }); }; + +export function getSafeAreaHeight() { + // 1. 优先使用 CSS 环境变量 + if (CSS.supports("padding-top", "env(safe-area-inset-top)")) { + const safeAreaTop = getComputedStyle( + document.documentElement, + ).getPropertyValue("env(safe-area-inset-top)"); + const height = parseInt(safeAreaTop) || 0; + if (height > 0) return height; + } + + // 2. 设备检测 + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); + const isAndroid = /Android/.test(navigator.userAgent); + + if (isIOS) { + // iOS 设备 + const isIPhoneX = window.screen.height >= 812; + return isIPhoneX ? 44 : 20; + } else if (isAndroid) { + // Android 设备 + return 24; + } + + // 3. 默认值 + return 0; +} +// 设置全局 CSS 变量 +export function initSafeArea() { + const root = document.documentElement; + const height = getSafeAreaHeight(); + root.style.setProperty("--safe-area-top", `${height}px`); +} diff --git a/nkebao/src/utils/env.ts b/nkebao/src/utils/env.ts new file mode 100644 index 00000000..d80a41f2 --- /dev/null +++ b/nkebao/src/utils/env.ts @@ -0,0 +1,46 @@ +// 环境配置 +export const isDevelopment = import.meta.env.DEV; +export const isProduction = import.meta.env.PROD; +export const isTest = import.meta.env.MODE === "test"; + +// 开发环境特性开关 +export const DEV_FEATURES = { + // 是否显示测试页面 + SHOW_TEST_PAGES: isDevelopment, + + // 是否启用调试日志 + ENABLE_DEBUG_LOGS: isDevelopment, + + // 是否显示开发工具 + SHOW_DEV_TOOLS: isDevelopment, + + // 是否启用Mock数据 + ENABLE_MOCK_DATA: isDevelopment, +}; + +// 获取环境变量 +export const getEnvVar = (key: string, defaultValue?: string): string => { + return import.meta.env[key] || defaultValue || ""; +}; + +// 环境信息 +export const ENV_INFO = { + MODE: import.meta.env.MODE, + DEV: import.meta.env.DEV, + PROD: import.meta.env.PROD, + VITE_APP_TITLE: getEnvVar("VITE_APP_TITLE", "存客宝"), + VITE_API_BASE_URL: getEnvVar("VITE_API_BASE_URL", ""), + VITE_APP_VERSION: getEnvVar("VITE_APP_VERSION", "1.0.0"), +}; + +// 开发环境检查 +export const checkDevEnvironment = () => { + if (isDevelopment) { + // console.log("🚀 开发环境已启用"); + // console.log("📋 环境信息:", ENV_INFO); + // console.log("⚙️ 开发特性:", DEV_FEATURES); + } +}; + +// 初始化环境检查 +checkDevEnvironment(); diff --git a/nkebao/vite-pwa.config.ts b/nkebao/vite-pwa.config.ts new file mode 100644 index 00000000..4c544f8a --- /dev/null +++ b/nkebao/vite-pwa.config.ts @@ -0,0 +1,58 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { VitePWA } from "vite-plugin-pwa"; + +export default defineConfig({ + plugins: [ + react(), + VitePWA({ + registerType: "autoUpdate", + workbox: { + globPatterns: ["**/*.{js,css,html,ico,png,svg}"], + runtimeCaching: [ + { + urlPattern: /^https:\/\/api\./, + handler: "NetworkFirst", + options: { + cacheName: "api-cache", + expiration: { + maxEntries: 100, + maxAgeSeconds: 60 * 60 * 24 * 7, // 7 days + }, + }, + }, + ], + }, + manifest: { + name: "Cunkebao", + short_name: "Cunkebao", + description: "Cunkebao Mobile App", + theme_color: "#ffffff", + background_color: "#ffffff", + display: "standalone", + orientation: "portrait", + scope: "/", + start_url: "/", + icons: [ + { + src: "favicon.ico", + sizes: "64x64 32x32 24x24 16x16", + type: "image/x-icon", + }, + { + src: "logo.png", + sizes: "192x192", + type: "image/png", + purpose: "any maskable", + }, + { + src: "logo.png", + sizes: "512x512", + type: "image/png", + purpose: "any maskable", + }, + ], + }, + }), + ], +}); diff --git a/nkebao/vite.config.ts b/nkebao/vite.config.ts index 921473b6..eb0ce0a3 100644 --- a/nkebao/vite.config.ts +++ b/nkebao/vite.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ }, server: { open: true, + port: 3000, }, build: { chunkSizeWarningLimit: 2000,