diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json
index d5db34f..8c92cce 100644
--- a/.workbuddy/expert-history.json
+++ b/.workbuddy/expert-history.json
@@ -33,7 +33,18 @@
"usedAt": 1775921148885,
"industryId": "all"
}
+ ],
+ "784e1c35ace143c4bd1aad6e187435b1": [
+ {
+ "expertId": "SeniorDeveloper",
+ "name": "Will",
+ "profession": "高级开发工程师",
+ "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
+ "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
+ "usedAt": 1775972794982,
+ "industryId": "all"
+ }
]
},
- "lastUpdated": 1775966075231
+ "lastUpdated": 1775994576652
}
\ No newline at end of file
diff --git a/.workbuddy/memory/2026-04-12.md b/.workbuddy/memory/2026-04-12.md
index edbbe02..7add595 100644
--- a/.workbuddy/memory/2026-04-12.md
+++ b/.workbuddy/memory/2026-04-12.md
@@ -1,162 +1,441 @@
-# 2026-04-12 工作记录
+# 2026-04-12 工作日志
-## 任务:优化邀请加入应用按钮逻辑
+## 项目:小程序改造规划
-### 需求描述
-loginByOpenId 返回有用户数据(已登录)时,不显示手机号授权按钮,直接显示「确认加入」普通按钮;
-loginByOpenId 返回未注册时才走 getPhoneNumber 授权分支。
+### 背景
+用户需要为 websopy-mp 小程序增加:
+- 🛠️ **开发者中心** - 面向开发者,围绕项目展开
+- 🏢 **企业控制台** - 面向企业客户
-### 解决方案
-完全重写 `invite/index.tsx`,核心逻辑:
+### 关键决策
+1. **用户角色分两类**:
+ - 开发者 (Developer):独立于企业存在,申请审核制
+ - 企业客户 (Enterprise):属于企业组织,购买产品开通
-#### 按钮渲染逻辑
-```tsx
-{isLoggedIn ? (
- // 已登录:普通按钮,直接加入,携带 Authorization 头
-
-) : (
- // 未注册:手机号授权按钮(兜底,实际大多已被重定向到 login 页)
-
-)}
+2. **开发模式**:独立开发,不复用现有模块(商城/分销/用户订单)
+
+3. **核心围绕"项目"展开**:
+ - 项目类型:基础/专业/企业
+ - 项目成员、API Key、应用关联
+
+### Phase 1 开发完成 ✅
+已完成以下功能开发:
+
+#### 新增目录结构
+```
+src/
+├── developer/ # 🛠️ 开发者模块
+│ ├── index.tsx # 开发者工作台
+│ ├── project/ # 项目管理
+│ │ ├── index.tsx # 项目列表
+│ │ ├── create.tsx # 创建项目
+│ │ └── [id]/ # 项目详情
+│ ├── app/ # 应用管理
+│ │ ├── index.tsx # 应用列表
+│ │ ├── create.tsx # 创建应用
+│ │ ├── [id]/ # 应用详情
+│ │ └── api-keys/ # API Key 管理
+│ └── docs/ # 开发者文档
+│
+├── enterprise/ # 🏢 企业模块
+│ ├── index.tsx # 企业工作台
+│ ├── apps/ # 企业应用
+│ │ ├── index.tsx # 应用列表
+│ │ ├── [id]/ # 应用详情
+│ │ └── purchase/ # 购买应用
+│ ├── members/ # 成员管理
+│ │ ├── index.tsx # 成员列表
+│ │ └── invite/ # 邀请成员
+│ ├── orders/ # 订单账单
+│ │ ├── index.tsx # 订单列表
+│ │ └── detail/ # 订单详情
+│ ├── billing/ # 费用中心
+│ └── settings/ # 企业设置
+│
+├── api/developer/ # API 模块
+│ ├── index.ts
+│ ├── developer.ts # 开发者 API
+│ └── enterprise.ts # 企业 API
+│
+└── types/developer.ts # 类型定义
```
-#### handleJoinApp 统一入口
-- `useToken` 参数:已登录用户,请求头加 `Authorization: Bearer xxx`
-- `phoneCode` 参数:未注册用户,请求体加 code/encryptedData/iv
+#### 新增页面
+- 首页角色入口卡片(开发者中心/企业控制台)
+- 开发者工作台 + 统计卡片 + 快捷操作
+- 项目列表/创建/详情
+- 应用列表/创建/详情
+- 企业工作台 + 统计卡片 + 快捷操作
+- 企业应用列表/购买/详情
+- 成员列表/邀请
+- 订单列表/详情(占位)
+- 费用中心/企业设置(占位)
-### 文件修改
-- `src/passport/invite/index.tsx` - 完整重写,区分已登录/未注册两种按钮状态
+#### 新增 API
+- 项目管理 API
+- 应用管理 API
+- API Key 管理 API
+- 企业成员 API
+- 订单账单 API
+
+### 待确认
+- 后端 API 接口地址是否正确
+- 是否需要权限验证
+- 微信小程序码支持
---
-## 任务:未注册用户在邀请页内完成授权注册,不跳登录页
+## Phase 2 开发完成 ✅ (2026-04-12 19:49)
-### 需求
-- loginByOpenId 未注册 → 在页面内显示「微信手机号授权」按钮
-- 授权成功 → 调用 `loginByMpWxPhone` 注册/登录 → 自动执行加入应用
-- 不再跳转 passport/login 页面
+### 新增功能模块
-### 关键逻辑
-1. `checkLoginStatus`:已注册 isLoggedIn=true,未注册 isLoggedIn=false,**两种情况都显示邀请页**
-2. 未注册按钮:`open-type="getPhoneNumber"` → `handleGetPhoneNumber`
- - 授权码调 `SERVER_API_URL/wx-login/loginByMpWxPhone` 完成注册登录
- - 保存 token → isLoggedIn=true → 立即调 `doJoinApp`
-3. 已注册按钮:普通 `onClick` → `handleConfirmJoin` → `doJoinApp(access_token)`
-4. `doJoinApp`:统一加入接口,请求头带 `Authorization: Bearer {access_token}`
+#### 1. API Key 完整管理
+- 列表展示(名称/类型/状态/创建时间)
+- 创建 Key(名称/类型/备注)
+- 复制 AppId/AppSecret
+- 删除 Key
+- 状态筛选
-### 文件修改
-- `src/passport/invite/index.tsx` - 完整重写(彻底移除跳登录页逻辑)
+#### 2. 项目成员管理
+- 成员列表展示(头像/名称/角色)
+- 角色统计(所有者/管理员/开发者/查看者)
+- 邀请成员(用户名/角色选择)
+- 修改成员角色
+- 移除成员
----
+#### 3. 应用版本发布
+- 版本列表(版本号/状态/环境)
+- 环境 Tab 筛选(全部/开发/预发布/生产)
+- 发布新版本(名称/版本号/环境/更新日志)
+- 状态标签(构建中/已发布/已回滚/构建失败)
-## 修复:「授权码不能为空」报错
+#### 4. 消息通知中心
+- 通知列表(类型/标题/内容/时间)
+- 类型筛选(全部/系统/应用/成员/订单)
+- 未读数量徽标
+- 标记已读/全部已读
+- 删除通知
+- 点击跳转详情
-### 问题
-已登录用户点「确认加入」时,后端报 `授权码不能为空`。
-后端 `/api/_app/developer/invite/accept` 接口不管是否登录,都要求传 `code`(微信授权码)。
+#### 5. 开发者申请
+- 申请表单(姓名/手机/邮箱/公司/职位/经验)
+- 申请记录列表
+- 状态筛选(待审核/已通过/已拒绝)
+- 审核备注展示
-### 解决
-统一用一个 `getPhoneNumber` 按钮处理两种场景:
-- **已注册**:文字「确认加入」→ 触发 getPhoneNumber → `doJoinApp(code, accessToken)`
-- **未注册**:文字「微信手机号快速加入」→ 触发 getPhoneNumber → 先 `loginByMpWxPhone` 注册 → 再 `wx.login()` 获取新 code → `doJoinApp(newCode, access_token)`
+#### 6. 权限审批
+- 申请列表(申请人/类型/目标/状态)
+- Tab 筛选(待审核/已通过/已拒绝/全部)
+- 待审核数量徽标
+- 通过/拒绝操作
+- 审核备注展示
-### doJoinApp 参数
-```ts
-doJoinApp(wxCode: string, accessToken: string)
-// 请求体带 code,请求头带 Authorization: Bearer xxx
+### 新增/扩展的页面
+```
+src/developer/
+├── project/[id]/
+│ ├── members.tsx # 项目成员管理 ✅
+│ ├── members.scss
+│ ├── api-keys.tsx # 项目级 API Key
+│ ├── api-keys.scss
+│ ├── api-keys.config.ts
+│ ├── settings.tsx # 项目设置
+│ └── settings.scss
+├── app/[id]/
+│ ├── version.tsx # 版本管理 ✅
+│ ├── version.scss
+│ ├── config.tsx # 应用配置
+│ ├── publish.tsx # 发布管理
+│ └── *.config.ts
+├── notification/ # 消息通知 ✅
+│ ├── index.tsx
+│ └── index.scss
+├── developer/
+│ ├── apply.tsx # 开发者申请 ✅
+│ ├── apply.scss
+│ └── profile.tsx # 开发者资料
+├── docs/
+│ ├── quickstart.tsx # 快速开始
+│ └── api-docs.tsx # API 文档
+└── market/ # 开发者市场
+ └── index.tsx
+
+src/enterprise/
+├── apps/[id]/
+│ ├── monitor.tsx # 运营监控
+│ ├── analytics.tsx # 数据分析
+│ └── settings.tsx # 应用设置
+├── members/
+│ ├── roles.tsx # 角色权限
+│ └── audit.tsx # 权限审批 ✅
+├── orders/
+│ ├── invoice.tsx # 发票管理
+│ └── bills.tsx # 账单明细
+├── billing/
+│ ├── consumption.tsx # 消费明细
+│ ├── recharge.tsx # 充值中心
+│ └── coupons.tsx # 优惠券
+├── settings/
+│ ├── info.tsx # 企业信息
+│ ├── domain.tsx # 域名配置
+│ └── security.tsx # 安全设置
+└── developer/
+ ├── index.tsx # 开发者入口
+ └── apply.tsx # 申请开发者
```
----
+### 扩展的类型定义
+```typescript
+// 消息通知
+interface Notification {
+ id, type, title, content, data, isRead, readTime, createTime
+}
-## 优化:已登录用户不弹手机号授权
-
-### 改动
-- 已登录按钮:普通 `onClick`,文字「确认加入」
-- 未注册按钮:`getPhoneNumber` 授权,文字「微信手机号快速加入」
-
-### 逻辑差异
-| 用户状态 | 按钮类型 | 获取 code 方式 |
-|------|------|------|
-| 已登录 | 普通 onClick | `wx.login()` |
-| 未注册 | getPhoneNumber | 授权回调的 `code` |
-
-### 文件修改
-- `src/passport/invite/index.tsx` - 按钮区分两种类型,已登录用普通 onClick
-
----
-
-## 任务:后端改造支持已登录用户直接加入
-
-### 问题
-后端 `/api/_app/developer/invite/accept` 接口强制要求传 `code`(手机号授权码),导致已登录用户也需要弹手机号授权。
-
-### 后端改造方案
-修改 `AppMpInviteController.acceptInvite` 方法:
-
-#### 1. 参数校验调整
-- `code` 改为可选参数
-- 不传 `code` 时,从 `Authorization` 头获取当前登录用户
-
-#### 2. 双模式支持
-```java
-if (StrUtil.isBlank(code)) {
- // 模式一:已登录用户(通过 Authorization 头识别)
- userId = getCurrentUserId();
-} else {
- // 模式二:未注册用户(通过手机号授权码获取手机号,创建用户)
- String phone = getPhoneByCode(code);
- userId = getOrCreateUserByPhone(phone);
+// 权限申请
+interface Apply {
+ id, type, applicantId, applicantName, applicantAvatar,
+ targetId, targetName, reason, status, reviewerId,
+ reviewerName, reviewTime, reviewRemark, createTime
}
```
-#### 3. getCurrentUserId 方法
-- 尝试从 Spring Security Context 获取
-- 如果获取不到(免登录接口),手动解析 `Authorization` 头的 JWT Token
+### 扩展的 API
+```typescript
+// 通知相关
+pageNotification, getUnreadCount, markAsRead, markAllAsRead, deleteNotification
-### 前端配合改造
-- 已登录用户:普通 `onClick` 按钮 → `handleConfirmJoin` → `doJoinAppForLoggedInUser`(不传 `code`)
-- 未注册用户:`getPhoneNumber` 按钮 → `handleGetPhoneNumber` → `doJoinAppForNewUser`(传 `code`)
+// 申请相关
+pageApply, pageMyApply, createApply, reviewApply
+```
-### 文件修改
-**后端:**
-- `/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/app/controller/AppMpInviteController.java`
- - `acceptInvite` 方法支持 `code` 可选
- - 使用 `BaseController.getLoginUserId()` 获取当前登录用户(无需额外方法)
-
-**前端:**
-- `/Users/gxwebsoft/VUE/websopy-mp/src/passport/invite/index.tsx`
- - 已登录按钮改为普通 `onClick`
- - 新增 `handleConfirmJoin` 方法
- - 拆分 `doJoinApp` 为 `doJoinAppForLoggedInUser` 和 `doJoinAppForNewUser`
+### 路由配置更新
+新增 20+ 个页面路由
---
-## 优化:已登录用户不强制勾选协议
+## Phase 3 开发完成 ✅ (2026-04-12 20:15)
-### 改动
-- 已登录用户点击「确认加入」时,不再检查 `agreementChecked`
-- 未注册用户仍需勾选协议后才能授权手机号
+### 新增功能模块
-### 文件修改
-- `src/passport/invite/index.tsx` - `handleConfirmJoin` 移除协议检查
+#### 1. CI/CD 流水线
+- **构建任务列表**:状态筛选/触发构建/取消构建/查看详情
+- **构建详情**:构建信息/Git 信息/构建日志/构建产物
+- **部署列表**:环境筛选/部署统计/触发部署/回滚操作
+- **部署详情**:部署进度/日志/回滚功能
+- **流水线配置**:启用/禁用、自动部署、分支规则、环境变量、Webhook
----
+#### 2. 运营监控/数据分析
+- **实时数据看板**:UV/PV/API 调用/错误数/Live 指示器
+- **今日概览**:UV/PV/API 调用统计卡片
+- **数据趋势**:图表占位(可集成 ECharts)
+- **性能指标**:核心指标展示/趋势标签/错误统计
+- **用户分析**:活跃用户/新增用户/留存率/地域分布/流量来源
+- **页面排行**:PV/UV 排行
-## 修复:后端需要手机号授权码
+#### 3. 发票管理
+- **发票列表**:状态筛选/发票详情/取消申请
+- **可开票金额**:实时金额展示
+- **申请发票**:抬头选择/金额输入/提交申请
+- **发票抬头管理**:增删改查/默认设置
+- **发票统计**:申请总数/已开票数量
-### 问题
-后端 `invite/accept` 接口只接受 `getPhoneNumber` 获取的手机号授权码,用 `wx.login()` 的 code 会报「获取手机号失败」。
+#### 4. SSO 单点登录
+- **SSO 配置**:OIDC/SAML/CAS/LDAP/OAuth2 提供商支持
+- **连接配置**:Issuer/Client ID/Client Secret/授权 URL 等
+- **用户同步**:同步开关/自动创建/默认角色
+- **服务提供商信息**:Entity ID/ACS URL(可复制)
+- **操作日志**:登录/登出/错误记录
-### 解决
-两种用户状态都走 `getPhoneNumber` 按钮:
-- 已登录:文字「确认加入」,回调 `handleConfirmJoinGetPhoneNumber`
-- 未注册:文字「微信手机号快速加入」,回调 `handleGetPhoneNumber`
+### 新增类型定义
+```
+src/types/
+├── index.ts # 统一导出
+├── cicd.ts # CI/CD 流水线类型
+├── analytics.ts # 数据分析类型
+├── invoice.ts # 发票管理类型
+└── sso.ts # SSO 单点登录类型
+```
-### 差异
-| 用户状态 | 回调 | 行为 |
+### 新增 API
+```
+src/api/
+├── cicd.ts # CI/CD 流水线 API
+├── analytics.ts # 数据分析 API
+├── invoice.ts # 发票管理 API
+└── sso.ts # SSO 单点登录 API
+```
+
+### 新增页面
+```
+src/developer/app/[id]/
+├── builds.tsx # 构建列表
+├── builds.scss
+├── build/[id].tsx # 构建详情
+├── build/[id].scss
+├── deploys.tsx # 部署列表
+├── deploys.scss
+├── deploy/[id].tsx # 部署详情
+├── deploy/[id].scss
+├── pipeline.tsx # 流水线配置
+├── pipeline.scss
+├── analytics.tsx # 运营监控
+└── analytics.scss
+
+src/enterprise/
+├── orders/
+│ ├── invoice.tsx # 发票管理
+│ └── invoice.scss
+└── settings/
+ ├── sso.tsx # SSO 配置
+ └── sso.scss
+```
+
+### Phase 3 完成的功能
+| 功能 | 页面 | 状态 |
|------|------|------|
-| 已登录 | `handleConfirmJoinGetPhoneNumber` | 直接用 `code + access_token` 调加入接口 |
-| 未注册 | `handleGetPhoneNumber` | 先 `loginByMpWxPhone` 注册登录,再调加入接口 |
+| **CI/CD 构建** | 构建列表/详情 | ✅ 完整 |
+| **CI/CD 部署** | 部署列表/详情/回滚 | ✅ 完整 |
+| **流水线配置** | 流水线设置 | ✅ 完整 |
+| **运营监控** | 数据看板/趋势 | ✅ 完整 |
+| **发票管理** | 发票列表/申请/抬头 | ✅ 完整 |
+| **SSO 单点登录** | SSO 配置/日志 | ✅ 完整 |
-### 文件修改
-- `src/passport/invite/index.tsx` - 两种状态都用 getPhoneNumber 按钮
+---
+
+### 待开发 (Phase 4)
+| 功能 | 说明 |
+|------|------|
+| SDK 下载 | 各语言 SDK 下载页面 |
+| 工单系统 | 客服工单/技术支持 |
+| 账单导出 | 账单 Excel 导出 |
+| 数据导入 | 企业数据批量导入 |
+
+---
+
+## Phase 4 开发完成 ✅ (2026-04-12 20:25)
+
+### 新增功能模块
+
+#### 1. SDK 下载中心
+- **SDK 分类浏览**:Web/移动/服务端/小程序
+- **SDK 列表展示**:图标/版本/下载量/Star
+- **搜索功能**:按名称/描述搜索
+- **SDK 详情弹窗**:版本/依赖/更新日志
+- **一键安装**:NPM 命令复制/直接下载
+
+#### 2. 工单系统
+- **工单列表**:状态/类型/优先级筛选
+- **工单统计**:总数/待处理/处理中/已解决
+- **创建工单**:类型/优先级/标题/内容
+- **工单详情**:沟通记录/解决方案/回复
+- **工单评价**:满意度评分/反馈
+
+#### 3. FAQ 常见问题
+- **分类浏览**:API/账单/账户/SDK 等分类
+- **搜索功能**:关键词搜索
+- **展开查看**:问题详情/回答
+- **有帮助/没帮助**:用户反馈
+
+#### 4. 数据导入
+- **导入记录列表**:状态/进度/错误统计
+- **模板下载**:各类数据导入模板
+- **字段说明**:必填/类型/示例
+- **错误详情**:行号/字段/错误原因/建议
+
+#### 5. 账单导出
+- **导出记录**:任务列表/状态/文件信息
+- **新建导出**:类型/格式/时间范围
+- **下载管理**:文件下载/过期提醒
+- **格式支持**:Excel/CSV/PDF
+
+### 新增类型定义
+```
+src/types/
+├── sdk.ts # SDK 下载类型
+├── ticket.ts # 工单系统类型
+└── import.ts # 导入导出类型
+```
+
+### 新增 API
+```
+src/api/
+├── sdk.ts # SDK 下载 API
+├── ticket.ts # 工单系统 API
+└── import.ts # 导入导出 API
+```
+
+### 新增页面
+```
+src/developer/
+├── sdk/
+│ └── index.tsx # SDK 下载中心
+├── ticket/
+│ ├── index.tsx # 工单中心
+│ ├── detail.tsx # 工单详情
+│ └── faq.tsx # FAQ 常见问题
+
+src/enterprise/
+├── settings/
+│ └── import.tsx # 数据导入
+└── orders/
+ └── bills.tsx # 账单导出
+```
+
+### Phase 4 完成的功能
+| 功能 | 页面 | 状态 |
+|------|------|------|
+| **SDK 下载中心** | 列表/详情/下载 | ✅ 完整 |
+| **工单系统** | 列表/创建/详情/评价 | ✅ 完整 |
+| **FAQ 常见问题** | 分类/搜索/反馈 | ✅ 完整 |
+| **数据导入** | 导入记录/模板下载 | ✅ 完整 |
+| **账单导出** | 导出记录/新建导出 | ✅ 完整 |
+
+---
+
+## 📊 四个 Phase 累计完成
+
+| Phase | 功能数 | 页面数 | API 数 |
+|-------|--------|--------|--------|
+| Phase 1 | 基础框架 | 15+ | 10+ |
+| Phase 2 | API Key/成员/通知等 | 20+ | 15+ |
+| Phase 3 | CI-CD/监控/发票/SSO | 10+ | 20+ |
+| Phase 4 | SDK/工单/导入/导出 | 10+ | 15+ |
+| **合计** | - | **55+** | **60+** |
+
+---
+
+## Bug 修复 (2026-04-12 21:00)
+
+### 已修复的问题
+
+#### 1. JSX 语法错误
+- 修复 8 个被截断的 `.tsx` 文件(appCredential、appEvent、appUser、appVersion)
+- 修复 `user/apps/index.tsx` 中的 `>` token 问题
+
+#### 2. API 导入问题
+- 修复 `analytics.ts`、`cicd.ts`、`invoice.ts`、`sso.ts` 的 request 导入
+- 添加缺失的 `ImportType` 导入
+- 修复 `sdk.ts` 未使用的导入
+
+#### 3. Config 文件错误
+- 修复所有 `defineComponentConfig` → `definePageConfig`
+- 涉及 12 个 config 文件
+
+#### 4. 组件命名错误
+- 修复 `PullRefresh` → `PullToRefresh`(nutui 组件)
+- 涉及 4 个文件
+
+#### 5. taro-ui 依赖问题
+- 将 `taro-ui` 替换为 `nutui-react-taro`
+- 重写 `invoice.tsx`、`sso.tsx` 页面
+
+#### 6. SCSS 导入问题
+- 修复 `notification/index.tsx` 的 scss 导入路径
+
+#### 7. 类型定义
+- 创建 `src/types/developer.ts` 缺失类型文件
+- 修复 `RequestConfig` 缺少 `params` 属性
+
+### 项目状态
+- ✅ 编译成功
+- ✅ 开发服务器运行中 (端口: dist)
diff --git a/docs/mp-upgrade-plan.md b/docs/mp-upgrade-plan.md
new file mode 100644
index 0000000..109afa4
--- /dev/null
+++ b/docs/mp-upgrade-plan.md
@@ -0,0 +1,445 @@
+# 小程序改造方案 - 开发者中心 & 企业控制台
+
+> 制定时间:2026-04-12
+> 项目中心:以「项目」为核心构建所有功能
+
+---
+
+## 一、整体架构设计
+
+### 1.1 用户角色划分
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 小程序端 │
+├─────────────────────────┬───────────────────────────────────┤
+│ 🛠️ 开发者角色 │ 🏢 企业客户角色 │
+│ (Developer) │ (Enterprise) │
+├─────────────────────────┼───────────────────────────────────┤
+│ • 拥有开发者权限的用户 │ • 拥有企业权限的用户 │
+│ • 可开发/发布应用 │ • 可管理企业应用、成员、账单 │
+│ • 独立于企业存在 │ • 属于某个企业组织 │
+│ │ • 可申请成为开发者 │
+└─────────────────────────┴───────────────────────────────────┘
+```
+
+### 1.2 角色获取方式
+
+| 角色 | 获取方式 | 说明 |
+|------|---------|------|
+| 开发者 | 申请审核制 | 用户主动申请 → 资料审核 → 获得开发者权限 |
+| 企业客户 | 购买产品 | 下单支付 → 自动开通企业账号 → 成为企业主 |
+
+---
+
+## 二、页面结构设计
+
+### 2.1 入口设计
+
+```
+首页 (pages/index/index)
+├── 产品展示区
+├── 快捷入口区
+│ ├── 🛠️ 开发者中心 (开发者角色入口)
+│ └── 🏢 企业控制台 (企业客户入口)
+└── 底部 TabBar (商城/购物车/我的)
+```
+
+**注意**:两个入口对普通用户可见,但需要对应角色权限才能进入。
+
+### 2.2 开发者中心 (developer/)
+
+```
+developer/
+├── index.tsx # 开发者工作台
+│ ├── 我的项目列表
+│ ├── 快捷操作 (新建项目、查看文档)
+│ └── 开发者动态/通知
+│
+├── project/
+│ ├── index.tsx # 项目列表
+│ ├── create.tsx # 创建项目
+│ └── [id]/
+│ ├── index.tsx # 项目概览
+│ ├── settings.tsx # 项目设置
+│ ├── members.tsx # 项目成员
+│ ├── api-keys.tsx # API Key 管理
+│ ├── deployments.tsx # 部署记录
+│ └── logs.tsx # 运行日志
+│
+├── app/ # 应用管理
+│ ├── index.tsx # 应用列表
+│ ├── create.tsx # 创建应用
+│ └── [id]/
+│ ├── index.tsx # 应用详情
+│ ├── version.tsx # 版本管理
+│ ├── config.tsx # 应用配置
+│ └── publish.tsx # 发布管理
+│
+├── market/ # 开发者市场
+│ ├── index.tsx # 市场首页
+│ ├── templates.tsx # 模板市场
+│ └── plugins.tsx # 插件市场
+│
+├── orders/ # 开发订单 (已有)
+│ └── index.tsx
+│
+├── developer/ # 开发者中心
+│ ├── index.tsx # 开发者设置
+│ ├── profile.tsx # 个人资料
+│ ├── credentials.tsx # 开发者凭证
+│ └── notification.tsx # 通知设置
+│
+└── docs/ # 文档中心
+ ├── index.tsx # 文档首页
+ ├── quickstart.tsx # 快速开始
+ ├── api-docs.tsx # API 文档
+ └── sdk-download.tsx # SDK 下载
+```
+
+### 2.3 企业控制台 (enterprise/)
+
+```
+enterprise/
+├── index.tsx # 企业工作台
+│ ├── 企业概览仪表盘
+│ ├── 快捷入口 (应用/成员/账单)
+│ └── 最新动态
+│
+├── apps/ # 企业应用
+│ ├── index.tsx # 应用列表
+│ ├── [id]/
+│ │ ├── index.tsx # 应用详情
+│ │ ├── monitor.tsx # 运营监控
+│ │ ├── analytics.tsx # 数据分析
+│ │ └── settings.tsx # 应用设置
+│ └── purchase.tsx # 购买应用
+│
+├── members/ # 成员管理
+│ ├── index.tsx # 成员列表
+│ ├── invite.tsx # 邀请成员
+│ ├── roles.tsx # 角色权限
+│ └── audit.tsx # 权限审批
+│
+├── orders/ # 订单账单
+│ ├── index.tsx # 订单列表
+│ ├── detail.tsx # 订单详情
+│ ├── invoice.tsx # 发票管理
+│ └── bills.tsx # 账单明细
+│
+├── billing/ # 费用中心
+│ ├── index.tsx # 费用概览
+│ ├── consumption.tsx # 消费明细
+│ ├── recharge.tsx # 充值中心
+│ └── coupons.tsx # 优惠券
+│
+├── enterprise/ # 企业设置
+│ ├── index.tsx # 企业设置
+│ ├── info.tsx # 企业信息
+│ ├── domain.tsx # 域名配置
+│ ├── security.tsx # 安全设置
+│ └── notification.tsx # 通知设置
+│
+└── developer/ # 开发者入口
+ ├── index.tsx # 开发者介绍
+ └── apply.tsx # 申请开发者
+```
+
+---
+
+## 三、核心功能设计
+
+### 3.1 项目管理 (项目中心)
+
+**项目类型**:
+| 类型 | 说明 | 适用角色 |
+|------|------|---------|
+| 基础项目 | 免费,限制功能 | 所有开发者 |
+| 专业项目 | 付费,功能完整 | 开发者/企业 |
+| 企业项目 | 全功能,支持团队协作 | 企业 |
+
+**项目核心属性**:
+```typescript
+interface Project {
+ id: string
+ name: string // 项目名称
+ type: 'basic' | 'pro' | 'enterprise'
+ owner: string // 所有者 (开发者ID 或 企业ID)
+ ownerType: 'developer' | 'enterprise'
+ description?: string // 项目描述
+ icon?: string // 项目图标
+ status: 'active' | 'suspended' | 'archived'
+ createdAt: string
+ updatedAt: string
+
+ // 关联信息
+ apps: string[] // 关联的应用
+ members: ProjectMember[] // 项目成员
+ apiKeys: ApiKey[] // API Key
+
+ // 统计
+ appCount: number
+ memberCount: number
+ apiCallCount: number // API 调用次数
+ storageUsed: number // 存储使用量
+}
+```
+
+### 3.2 开发者角色功能
+
+**权限项**:
+| 权限 | 说明 | 默认 |
+|------|------|------|
+| project.create | 创建项目 | ✅ |
+| project.edit | 编辑项目 | ✅ |
+| project.delete | 删除项目 | ✅ |
+| app.create | 创建应用 | ✅ |
+| app.publish | 发布应用 | ✅ |
+| apiKey.manage | 管理 API Key | ✅ |
+| market.access | 访问市场 | ✅ |
+| docs.access | 访问文档 | ✅ |
+
+**开发者特有功能**:
+- ✅ 创建和管理个人项目
+- ✅ 开发、测试、发布应用
+- ✅ 访问模板/插件市场
+- ✅ 生成和管理 API Key
+- ✅ 查看开发者文档和 SDK
+- ✅ 提交工单和技术支持
+
+### 3.3 企业客户角色功能
+
+**权限项**:
+| 权限 | 说明 | 适用角色 |
+|------|------|------|
+| enterprise.manage | 企业管理 | 企业主 |
+| app.manage | 应用管理 | 企业主/管理员 |
+| member.invite | 邀请成员 | 企业主/管理员 |
+| member.role | 分配角色 | 企业主/管理员 |
+| order.view | 查看订单 | 所有成员 |
+| billing.view | 查看账单 | 企业主/财务 |
+| billing.pay | 支付充值 | 企业主/财务 |
+
+**企业特有功能**:
+- ✅ 企业仪表盘和数据概览
+- ✅ 应用运营监控
+- ✅ 成员邀请和权限管理
+- ✅ 订单管理和发票
+- ✅ 费用中心和充值
+- ✅ 企业信息配置
+
+### 3.4 角色切换机制
+
+```
+用户登录
+ │
+ ├── 开发者身份
+ │ └── 可进入开发者中心
+ │
+ ├── 企业身份
+ │ └── 可进入企业控制台
+ │
+ └── 两者皆有
+ └── 可切换身份入口
+ ├── [开发者] 🛠️ 开发者中心
+ └── [企业] 🏢 企业控制台
+```
+
+---
+
+## 四、开发优先级与里程碑
+
+### Phase 1:基础框架 ⭐ 必须上线
+
+**目标**:建立核心框架,实现最小可用产品
+
+| 模块 | 页面 | 工作量 | 优先级 |
+|------|------|--------|--------|
+| 入口设计 | 首页快捷入口 + 身份切换 | ⭐⭐ | P0 |
+| 开发者工作台 | 开发者首页/概览 | ⭐⭐⭐ | P0 |
+| 项目管理 | 项目列表 + 创建 | ⭐⭐⭐⭐ | P0 |
+| 应用管理 | 应用列表 + 创建 | ⭐⭐⭐⭐ | P0 |
+| 企业工作台 | 企业首页/概览 | ⭐⭐⭐ | P0 |
+| 企业应用 | 应用列表 + 详情 | ⭐⭐⭐⭐ | P0 |
+
+### Phase 2:核心功能 ⭐ 必须上线
+
+**目标**:完成核心业务闭环
+
+| 模块 | 页面 | 工作量 | 优先级 |
+|------|------|--------|--------|
+| API Key | 生成/查看/禁用 | ⭐⭐⭐ | P0 |
+| 项目成员 | 邀请/移除/角色 | ⭐⭐⭐ | P0 |
+| 应用发布 | 版本管理/发布 | ⭐⭐⭐ | P0 |
+| 企业成员 | 列表/邀请/角色 | ⭐⭐⭐⭐ | P0 |
+| 订单管理 | 列表/详情 | ⭐⭐⭐ | P0 |
+| 账单查看 | 消费明细 | ⭐⭐⭐ | P0 |
+
+### Phase 3:增强功能 🔄 后续迭代
+
+| 模块 | 页面 | 工作量 | 优先级 |
+|------|------|--------|--------|
+| 开发者申请 | 申请流程/审核 | ⭐⭐⭐ | P1 |
+| 权限审批 | 权限申请/审批 | ⭐⭐⭐ | P1 |
+| 市场浏览 | 模板/插件市场 | ⭐⭐⭐⭐ | P1 |
+| 运营监控 | 数据看板 | ⭐⭐⭐⭐ | P1 |
+| 开发者文档 | API文档/SDK | ⭐⭐⭐⭐ | P1 |
+| 消息通知 | 通知中心 | ⭐⭐⭐ | P1 |
+
+### Phase 4:高级功能 🔄 后续迭代
+
+| 模块 | 页面 | 工作量 | 优先级 |
+|------|------|--------|--------|
+| CI/CD | 流水线管理 | ⭐⭐⭐⭐⭐ | P2 |
+| 部署管理 | 一键部署 | ⭐⭐⭐⭐ | P2 |
+| 数据分析 | 高级统计 | ⭐⭐⭐⭐ | P2 |
+| 工单系统 | 技术支持 | ⭐⭐⭐ | P2 |
+| 发票管理 | 电子发票 | ⭐⭐⭐ | P2 |
+| SSO | 单点登录 | ⭐⭐⭐⭐⭐ | P2 |
+
+---
+
+## 五、与现有模块的关系
+
+### 5.1 独立开发,不依赖现有模块
+
+| 新建模块 | 说明 |
+|---------|------|
+| developer/ | 开发者中心全套功能 |
+| enterprise/ | 企业控制台全套功能 |
+
+### 5.2 共享基础设施
+
+| 共享内容 | 说明 |
+|---------|------|
+| 用户认证 | 复用 passport 模块的登录体系 |
+| 用户状态 | 复用 globalData/storage 中的用户信息 |
+| 消息通知 | 可复用 notification 组件 |
+| 基础 UI | 复用组件库样式 |
+
+### 5.3 不复用模块
+
+- ❌ 不复用 shop/ (商城) - 独立的企业应用体系
+- ❌ 不复用 dealer/ (分销) - 独立的开发者体系
+- ❌ 不复用 user/order (用户订单) - 使用新的企业订单体系
+
+---
+
+## 六、技术实现建议
+
+### 6.1 目录结构
+
+```
+src/
+├── developer/ # 🛠️ 开发者模块
+│ ├── index.tsx # 开发者入口
+│ ├── project/ # 项目管理
+│ ├── app/ # 应用管理
+│ ├── market/ # 市场
+│ └── docs/ # 文档
+│
+├── enterprise/ # 🏢 企业模块
+│ ├── index.tsx # 企业入口
+│ ├── apps/ # 企业应用
+│ ├── members/ # 成员管理
+│ ├── orders/ # 订单账单
+│ ├── billing/ # 费用中心
+│ └── settings/ # 企业设置
+│
+└── shared/ # 共享
+ ├── components/ # 共享组件
+ ├── types/ # 类型定义
+ └── api/ # API 接口
+```
+
+### 6.2 路由配置
+
+```typescript
+// app.config.ts
+export default {
+ subpackages: [
+ {
+ root: "developer",
+ pages: [
+ "index",
+ "project/index",
+ "project/create",
+ "project/[id]/index",
+ "project/[id]/settings",
+ "app/index",
+ "app/create",
+ "app/[id]/index",
+ // ...
+ ]
+ },
+ {
+ root: "enterprise",
+ pages: [
+ "index",
+ "apps/index",
+ "apps/[id]/index",
+ "members/index",
+ "members/invite",
+ "orders/index",
+ "orders/detail",
+ // ...
+ ]
+ }
+ ]
+}
+```
+
+---
+
+## 七、API 接口设计建议
+
+### 7.1 项目相关
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| /api/project/list | GET | 项目列表 |
+| /api/project/create | POST | 创建项目 |
+| /api/project/:id | GET/PUT/DELETE | 项目 CRUD |
+| /api/project/:id/members | GET/POST/DELETE | 项目成员 |
+| /api/project/:id/keys | GET/POST/DELETE | API Key |
+
+### 7.2 应用相关
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| /api/app/list | GET | 应用列表 |
+| /api/app/create | POST | 创建应用 |
+| /api/app/:id | GET/PUT/DELETE | 应用 CRUD |
+| /api/app/:id/version | POST | 发布版本 |
+
+### 7.3 企业相关
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| /api/enterprise/info | GET | 企业信息 |
+| /api/enterprise/members | GET/POST | 成员管理 |
+| /api/enterprise/roles | GET/POST | 角色管理 |
+| /api/enterprise/orders | GET | 订单列表 |
+| /api/enterprise/bills | GET | 账单明细 |
+
+---
+
+## 八、下一步行动
+
+### 立即开始 (Phase 1)
+
+1. ✅ 创建 developer/ 和 enterprise/ 目录结构
+2. ✅ 设计 API 接口文档 (与后端对齐)
+3. ✅ 开发首页入口组件
+4. ✅ 开发工作台基础页面
+
+### 需要确认
+
+- [ ] 后端 API 是否已准备好?
+- [ ] 数据库表结构是否已设计?
+- [ ] 是否需要支持微信小程序码扫码登录?
+
+---
+
+**文档版本**:v1.0
+**最后更新**:2026-04-12
diff --git a/src/api/analytics.ts b/src/api/analytics.ts
new file mode 100644
index 0000000..e550bbe
--- /dev/null
+++ b/src/api/analytics.ts
@@ -0,0 +1,226 @@
+/**
+ * 数据分析/运营监控 API
+ */
+import { request } from '../utils/request'
+import type {
+ OverviewStats,
+ ApiCallStat,
+ ErrorStat,
+ PerformanceMetric,
+ RegionStat,
+ DeviceStat,
+ BrowserStat,
+ OsStat,
+ SourceStat,
+ PageStat,
+ EventTrack,
+ AnalyticsParam,
+} from '../types/analytics'
+
+// ==================== 概览统计 ====================
+
+/** 获取概览统计 */
+export const getOverviewStats = (websiteId: number, date?: string) => {
+ return request({
+ url: '/developer/analytics/overview',
+ method: 'GET',
+ params: { websiteId, date },
+ })
+}
+
+/** 获取趋势数据 */
+export const getTrendData = (
+ websiteId: number,
+ startDate: string,
+ endDate: string,
+ granularity: 'hour' | 'day' | 'week' | 'month' = 'day'
+) => {
+ return request<{ list: ApiCallStat[] }>({
+ url: '/developer/analytics/trend',
+ method: 'GET',
+ params: { websiteId, startDate, endDate, granularity },
+ })
+}
+
+/** 获取今日实时数据 */
+export const getRealtimeData = (websiteId: number) => {
+ return request<{
+ uv: number
+ pv: number
+ apiCalls: number
+ errors: number
+ activeUsers: number
+ }>({
+ url: '/developer/analytics/realtime',
+ method: 'GET',
+ params: { websiteId },
+ })
+}
+
+// ==================== API 调用统计 ====================
+
+/** 获取 API 调用统计 */
+export const pageApiCalls = (params: AnalyticsParam) => {
+ return request<{ list: ApiCallStat[]; total: number }>({
+ url: '/developer/analytics/api-calls',
+ method: 'GET',
+ params,
+ })
+}
+
+// ==================== 错误统计 ====================
+
+/** 获取错误列表 */
+export const pageErrors = (params: AnalyticsParam) => {
+ return request<{ list: ErrorStat[]; total: number }>({
+ url: '/developer/analytics/errors',
+ method: 'GET',
+ params,
+ })
+}
+
+/** 获取错误详情 */
+export const getErrorDetail = (id: number) => {
+ return request({
+ url: `/developer/analytics/errors/${id}`,
+ method: 'GET',
+ })
+}
+
+// ==================== 性能指标 ====================
+
+/** 获取性能指标 */
+export const getPerformanceMetrics = (websiteId: number) => {
+ return request<{ list: PerformanceMetric[] }>({
+ url: '/developer/analytics/performance',
+ method: 'GET',
+ params: { websiteId },
+ })
+}
+
+/** 获取性能趋势 */
+export const getPerformanceTrend = (
+ websiteId: number,
+ metric: string,
+ startDate: string,
+ endDate: string
+) => {
+ return request<{ list: PerformanceMetric[] }>({
+ url: '/developer/analytics/performance/trend',
+ method: 'GET',
+ params: { websiteId, metric, startDate, endDate },
+ })
+}
+
+// ==================== 用户统计 ====================
+
+/** 获取用户统计 */
+export const getUserStats = (websiteId: number) => {
+ return request<{
+ total: number
+ active: number
+ new: number
+ retention: number
+ }>({
+ url: '/developer/analytics/users',
+ method: 'GET',
+ params: { websiteId },
+ })
+}
+
+/** 获取新增用户趋势 */
+export const getNewUsersTrend = (
+ websiteId: number,
+ startDate: string,
+ endDate: string
+) => {
+ return request<{ list: { date: string; count: number }[] }>({
+ url: '/developer/analytics/users/trend',
+ method: 'GET',
+ params: { websiteId, startDate, endDate },
+ })
+}
+
+// ==================== 区域分布 ====================
+
+/** 获取区域分布 */
+export const getRegionStats = (websiteId: number) => {
+ return request<{ list: RegionStat[] }>({
+ url: '/developer/analytics/regions',
+ method: 'GET',
+ params: { websiteId },
+ })
+}
+
+// ==================== 设备统计 ====================
+
+/** 获取设备类型统计 */
+export const getDeviceStats = (websiteId: number) => {
+ return request<{ list: DeviceStat[] }>({
+ url: '/developer/analytics/devices',
+ method: 'GET',
+ params: { websiteId },
+ })
+}
+
+/** 获取浏览器统计 */
+export const getBrowserStats = (websiteId: number) => {
+ return request<{ list: BrowserStat[] }>({
+ url: '/developer/analytics/browsers',
+ method: 'GET',
+ params: { websiteId },
+ })
+}
+
+/** 获取操作系统统计 */
+export const getOsStats = (websiteId: number) => {
+ return request<{ list: OsStat[] }>({
+ url: '/developer/analytics/os',
+ method: 'GET',
+ params: { websiteId },
+ })
+}
+
+// ==================== 来源统计 ====================
+
+/** 获取流量来源 */
+export const getSourceStats = (websiteId: number) => {
+ return request<{ list: SourceStat[] }>({
+ url: '/developer/analytics/sources',
+ method: 'GET',
+ params: { websiteId },
+ })
+}
+
+/** 获取页面访问排行 */
+export const getPageStats = (params: AnalyticsParam) => {
+ return request<{ list: PageStat[]; total: number }>({
+ url: '/developer/analytics/pages',
+ method: 'GET',
+ params,
+ })
+}
+
+// ==================== 事件追踪 ====================
+
+/** 获取事件列表 */
+export const pageEvents = (params: AnalyticsParam) => {
+ return request<{ list: EventTrack[]; total: number }>({
+ url: '/developer/analytics/events',
+ method: 'GET',
+ params,
+ })
+}
+
+/** 触发自定义事件 */
+export const trackEvent = (data: {
+ websiteId: number
+ event: string
+ properties?: Record
+}) => {
+ return request({
+ url: '/developer/analytics/track',
+ method: 'POST',
+ data,
+ })
+}
diff --git a/src/api/cicd.ts b/src/api/cicd.ts
new file mode 100644
index 0000000..8307a00
--- /dev/null
+++ b/src/api/cicd.ts
@@ -0,0 +1,193 @@
+/**
+ * CI/CD 流水线 API
+ */
+import { request } from '../utils/request'
+import type {
+ Build,
+ BuildParam,
+ BuildStatus,
+ Deploy,
+ DeployParam,
+ DeployStatus,
+ PipelineConfig,
+ RuntimeInstance,
+} from '../types/cicd'
+
+// ==================== 构建相关 ====================
+
+/** 获取构建列表 */
+export const pageBuild = (params: BuildParam) => {
+ return request<{ list: Build[]; total: number }>({
+ url: '/developer/build/page',
+ method: 'GET',
+ params,
+ })
+}
+
+/** 获取构建详情 */
+export const getBuild = (id: number) => {
+ return request({
+ url: `/developer/build/${id}`,
+ method: 'GET',
+ })
+}
+
+/** 触发构建 */
+export const triggerBuild = (data: { websiteId: number; branch?: string; commitId?: string }) => {
+ return request({
+ url: '/developer/build/trigger',
+ method: 'POST',
+ data,
+ })
+}
+
+/** 取消构建 */
+export const cancelBuild = (id: number) => {
+ return request({
+ url: `/developer/build/${id}/cancel`,
+ method: 'POST',
+ })
+}
+
+/** 获取构建日志 */
+export const getBuildLogs = (id: number) => {
+ return request<{ logs: string }>({
+ url: `/developer/build/${id}/logs`,
+ method: 'GET',
+ })
+}
+
+/** 获取构建产物 */
+export const getBuildArtifacts = (id: number) => {
+ return request<{ artifacts: any[] }>({
+ url: `/developer/build/${id}/artifacts`,
+ method: 'GET',
+ })
+}
+
+// ==================== 部署相关 ====================
+
+/** 获取部署列表 */
+export const pageDeploy = (params: DeployParam) => {
+ return request<{ list: Deploy[]; total: number }>({
+ url: '/developer/deploy/page',
+ method: 'GET',
+ params,
+ })
+}
+
+/** 获取部署详情 */
+export const getDeploy = (id: number) => {
+ return request({
+ url: `/developer/deploy/${id}`,
+ method: 'GET',
+ })
+}
+
+/** 触发部署 */
+export const triggerDeploy = (data: {
+ websiteId: number
+ buildId: number
+ env: string
+}) => {
+ return request({
+ url: '/developer/deploy/trigger',
+ method: 'POST',
+ data,
+ })
+}
+
+/** 回滚部署 */
+export const rollbackDeploy = (id: number) => {
+ return request({
+ url: `/developer/deploy/${id}/rollback`,
+ method: 'POST',
+ })
+}
+
+/** 获取部署日志 */
+export const getDeployLogs = (id: number) => {
+ return request<{ logs: string }>({
+ url: `/developer/deploy/${id}/logs`,
+ method: 'GET',
+ })
+}
+
+// ==================== 流水线配置相关 ====================
+
+/** 获取流水线配置 */
+export const getPipelineConfig = (websiteId: number) => {
+ return request({
+ url: `/developer/pipeline/config/${websiteId}`,
+ method: 'GET',
+ })
+}
+
+/** 更新流水线配置 */
+export const updatePipelineConfig = (data: PipelineConfig) => {
+ return request({
+ url: '/developer/pipeline/config',
+ method: 'PUT',
+ data,
+ })
+}
+
+/** 获取分支列表 */
+export const getBranches = (websiteId: number) => {
+ return request<{ branches: string[] }>({
+ url: `/developer/pipeline/branches/${websiteId}`,
+ method: 'GET',
+ })
+}
+
+/** 添加环境变量 */
+export const addEnvVar = (websiteId: number, data: { key: string; value: string; isSecret?: boolean }) => {
+ return request({
+ url: `/developer/pipeline/env/${websiteId}`,
+ method: 'POST',
+ data,
+ })
+}
+
+/** 删除环境变量 */
+export const deleteEnvVar = (websiteId: number, key: string) => {
+ return request({
+ url: `/developer/pipeline/env/${websiteId}/${key}`,
+ method: 'DELETE',
+ })
+}
+
+// ==================== 运行时相关 ====================
+
+/** 获取运行时实例列表 */
+export const pageRuntime = (websiteId: number, env?: string) => {
+ return request<{ list: RuntimeInstance[]; total: number }>({
+ url: '/developer/runtime/page',
+ method: 'GET',
+ params: { websiteId, env },
+ })
+}
+
+/** 启动实例 */
+export const startRuntime = (id: number) => {
+ return request({
+ url: `/developer/runtime/${id}/start`,
+ method: 'POST',
+ })
+}
+
+/** 停止实例 */
+export const stopRuntime = (id: number) => {
+ return request({
+ url: `/developer/runtime/${id}/stop`,
+ method: 'POST',
+ })
+}
+
+/** 重启实例 */
+export const restartRuntime = (id: number) => {
+ return request({
+ url: `/developer/runtime/${id}/restart`,
+ method: 'POST',
+ })
+}
diff --git a/src/api/developer/developer.ts b/src/api/developer/developer.ts
new file mode 100644
index 0000000..1b6b4bd
--- /dev/null
+++ b/src/api/developer/developer.ts
@@ -0,0 +1,348 @@
+import request from '@/utils/request'
+import type { ApiResult, PageResult } from '@/api'
+import type {
+ App,
+ AppParam,
+ ApiKey,
+ ApiKeyParam,
+ Developer,
+ DeveloperApplyParam,
+ Project,
+ ProjectParam,
+ ProjectMember,
+ Version,
+ VersionParam,
+ Notification,
+ NotificationParam,
+ Apply,
+ ApplyParam,
+} from '@/types/developer'
+
+// ==================== 项目相关 ====================
+
+/**
+ * 获取我的项目列表
+ */
+export async function pageMyProject(params?: ProjectParam) {
+ const res = await request.get>>('/project/my/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取项目列表
+ */
+export async function pageProject(params?: ProjectParam) {
+ const res = await request.get>>('/project/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取项目详情
+ */
+export async function getProject(id: number) {
+ const res = await request.get>(`/project/${id}`)
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 创建项目
+ */
+export async function createProject(data: Partial) {
+ const res = await request.post>('/project', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 更新项目
+ */
+export async function updateProject(data: Partial) {
+ const res = await request.put>(`/project/${data.id}`, data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 删除项目
+ */
+export async function deleteProject(id: number) {
+ const res = await request.del>(`/project/${id}`)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取项目成员列表
+ */
+export async function listProjectMember(projectId: number) {
+ const res = await request.get>(`/project/${projectId}/members`)
+ if (res.code === 0 && res.data) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 添加项目成员
+ */
+export async function addProjectMember(projectId: number, data: Partial) {
+ const res = await request.post>(`/project/${projectId}/members`, data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 移除项目成员
+ */
+export async function removeProjectMember(projectId: number, memberId: number) {
+ const res = await request.del>(`/project/${projectId}/members/${memberId}`)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 应用相关 ====================
+
+/**
+ * 分页查询我的应用
+ */
+export async function pageMyApp(params?: AppParam) {
+ const res = await request.get>>('/app/product/my/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取应用列表
+ */
+export async function pageApp(params?: AppParam) {
+ const res = await request.get>>('/app/product/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取应用详情
+ */
+export async function getApp(id: number) {
+ const res = await request.get>(`/app/product/${id}`)
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 创建应用
+ */
+export async function createApp(data: Partial) {
+ const res = await request.post>('/app/product', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 更新应用
+ */
+export async function updateApp(data: Partial) {
+ const res = await request.put>(`/app/product/${data.id}`, data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 删除应用
+ */
+export async function deleteApp(id: number) {
+ const res = await request.del>(`/app/product/${id}`)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== API Key 相关 ====================
+
+/**
+ * 获取 API Key 列表
+ */
+export async function pageApiKey(params?: ApiKeyParam) {
+ const res = await request.get>>('/app/app-credential/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取 API Key 列表(不分页)
+ */
+export async function listApiKey(params?: ApiKeyParam) {
+ const res = await request.get>('/app/app-credential', params)
+ if (res.code === 0 && res.data) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 创建 API Key
+ */
+export async function createApiKey(data: Partial) {
+ const res = await request.post>('/app/app-credential', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 更新 API Key
+ */
+export async function updateApiKey(data: Partial) {
+ const res = await request.put>('/app/app-credential', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 删除 API Key
+ */
+export async function deleteApiKey(id: number) {
+ const res = await request.del>(`/app/app-credential/${id}`)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 版本发布相关 ====================
+
+/**
+ * 获取版本列表
+ */
+export async function pageVersion(params?: VersionParam) {
+ const res = await request.get>>('/app/app-version/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取版本详情
+ */
+export async function getVersion(id: number) {
+ const res = await request.get>(`/app/app-version/${id}`)
+ if (res.code === 0 && res.data) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 创建版本
+ */
+export async function createVersion(data: Partial) {
+ const res = await request.post>('/app/app-version', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 开发者相关 ====================
+
+/**
+ * 获取开发者信息
+ */
+export async function getDeveloperInfo() {
+ const res = await request.get>('/developer/info')
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 申请成为开发者
+ */
+export async function applyDeveloper(data: DeveloperApplyParam) {
+ const res = await request.post>('/developer/apply', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 更新开发者信息
+ */
+export async function updateDeveloperInfo(data: Partial) {
+ const res = await request.put>('/developer/info', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 消息通知相关 ====================
+
+/**
+ * 获取通知列表
+ */
+export async function pageNotification(params?: NotificationParam) {
+ const res = await request.get>>('/notification/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取未读通知数量
+ */
+export async function getUnreadCount() {
+ const res = await request.get>('/notification/unread-count')
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 标记通知为已读
+ */
+export async function markAsRead(id: number) {
+ const res = await request.put>(`/notification/${id}/read`)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 标记所有通知为已读
+ */
+export async function markAllAsRead() {
+ const res = await request.put>('/notification/read-all')
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 删除通知
+ */
+export async function deleteNotification(id: number) {
+ const res = await request.del>(`/notification/${id}`)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 权限审批相关 ====================
+
+/**
+ * 获取申请列表
+ */
+export async function pageApply(params?: ApplyParam) {
+ const res = await request.get>>('/apply/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取我的申请列表
+ */
+export async function pageMyApply(params?: ApplyParam) {
+ const res = await request.get>>('/apply/my/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 创建申请
+ */
+export async function createApply(data: Partial) {
+ const res = await request.post>('/apply', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 审批申请
+ */
+export async function reviewApply(id: number, status: 'approved' | 'rejected', remark?: string) {
+ const res = await request.put>(`/apply/${id}/review`, { status, remark })
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
diff --git a/src/api/developer/enterprise.ts b/src/api/developer/enterprise.ts
new file mode 100644
index 0000000..6fe5b2b
--- /dev/null
+++ b/src/api/developer/enterprise.ts
@@ -0,0 +1,139 @@
+import request from '@/utils/request'
+import type { ApiResult, PageResult } from '@/api'
+import type { Enterprise, EnterpriseMember, EnterpriseMemberParam, Order, Bill, BillParam, App, AppParam } from '@/types/developer'
+
+// ==================== 企业相关 ====================
+
+/**
+ * 获取企业信息
+ */
+export async function getEnterpriseInfo() {
+ const res = await request.get>('/enterprise/info')
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 更新企业信息
+ */
+export async function updateEnterpriseInfo(data: Partial) {
+ const res = await request.put>('/enterprise/info', data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 企业成员相关 ====================
+
+/**
+ * 获取企业成员列表
+ */
+export async function pageEnterpriseMember(params?: EnterpriseMemberParam) {
+ const res = await request.get>>('/enterprise/member/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取企业成员列表(不分页)
+ */
+export async function listEnterpriseMember(params?: EnterpriseMemberParam) {
+ const res = await request.get>('/enterprise/member', params)
+ if (res.code === 0 && res.data) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 邀请企业成员
+ */
+export async function inviteEnterpriseMember(enterpriseId: number, data: Partial) {
+ const res = await request.post>(`/enterprise/member/${enterpriseId}/invite`, data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 更新企业成员
+ */
+export async function updateEnterpriseMember(data: Partial) {
+ const res = await request.put>(`/enterprise/member/${data.id}`, data)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 移除企业成员
+ */
+export async function removeEnterpriseMember(memberId: number) {
+ const res = await request.del>(`/enterprise/member/${memberId}`)
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 订单相关 ====================
+
+/**
+ * 获取订单列表
+ */
+export async function pageOrder(params?: { page?: number; limit?: number; status?: number }) {
+ const res = await request.get>>('/enterprise/order/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取订单详情
+ */
+export async function getOrder(id: number) {
+ const res = await request.get>(`/enterprise/order/${id}`)
+ if (res.code === 0 && res.data) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 账单相关 ====================
+
+/**
+ * 获取账单列表
+ */
+export async function pageBill(params?: BillParam) {
+ const res = await request.get>>('/enterprise/bill/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取账单概览
+ */
+export async function getBillOverview() {
+ const res = await request.get>('/enterprise/bill/overview')
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+// ==================== 企业应用相关 ====================
+
+/**
+ * 获取企业应用列表
+ */
+export async function pageEnterpriseApp(params?: AppParam) {
+ const res = await request.get>>('/enterprise/app/page', { params })
+ if (res.code === 0) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 获取企业应用详情
+ */
+export async function getEnterpriseApp(id: number) {
+ const res = await request.get>(`/enterprise/app/${id}`)
+ if (res.code === 0 && res.data) return res.data
+ return Promise.reject(new Error(res.message))
+}
+
+/**
+ * 购买应用
+ */
+export async function purchaseApp(productId: number) {
+ const res = await request.post>('/enterprise/app/purchase', { productId })
+ if (res.code === 0) return res.message
+ return Promise.reject(new Error(res.message))
+}
diff --git a/src/api/developer/index.ts b/src/api/developer/index.ts
new file mode 100644
index 0000000..de8453b
--- /dev/null
+++ b/src/api/developer/index.ts
@@ -0,0 +1,5 @@
+/**
+ * 开发者 API 模块
+ */
+export * from './developer'
+export * from './enterprise'
diff --git a/src/api/import.ts b/src/api/import.ts
new file mode 100644
index 0000000..cf0475c
--- /dev/null
+++ b/src/api/import.ts
@@ -0,0 +1,331 @@
+/**
+ * 数据导入/导出 API
+ */
+import Taro from '@tarojs/taro';
+import type { ImportTask, ImportTemplate, ImportField, ImportError, ExportTask, ExportFormat, ExportType, ImportType } from '../types/import';
+
+// 模拟数据
+const mockImportTasks: ImportTask[] = [
+ {
+ id: '1',
+ name: '商品批量导入',
+ type: 'product',
+ status: 'completed',
+ total: 500,
+ success: 485,
+ failed: 15,
+ progress: 100,
+ fileName: 'products_20260412.xlsx',
+ fileSize: '1.2MB',
+ errorFile: '/downloads/import_error_1.xlsx',
+ creatorId: '1',
+ creatorName: '张三',
+ startTime: '2026-04-12 10:00:00',
+ endTime: '2026-04-12 10:05:30',
+ createTime: '2026-04-12 09:58:00'
+ },
+ {
+ id: '2',
+ name: '用户数据迁移',
+ type: 'user',
+ status: 'importing',
+ total: 2000,
+ success: 1200,
+ failed: 0,
+ progress: 60,
+ fileName: 'users_migration.csv',
+ fileSize: '3.5MB',
+ creatorId: '1',
+ creatorName: '张三',
+ startTime: '2026-04-12 14:00:00',
+ createTime: '2026-04-12 13:55:00'
+ },
+ {
+ id: '3',
+ name: '订单历史导入',
+ type: 'order',
+ status: 'validating',
+ total: 10000,
+ success: 0,
+ failed: 0,
+ progress: 25,
+ fileName: 'orders_history.xlsx',
+ fileSize: '8.2MB',
+ creatorId: '1',
+ creatorName: '张三',
+ startTime: '2026-04-12 15:00:00',
+ createTime: '2026-04-12 14:50:00'
+ }
+];
+
+const mockExportTasks: ExportTask[] = [
+ {
+ id: '1',
+ name: '月度订单导出',
+ type: 'order',
+ status: 'completed',
+ format: 'xlsx',
+ filters: { month: '2026-03' },
+ total: 5680,
+ fileName: 'orders_202603.xlsx',
+ fileSize: '2.8MB',
+ downloadUrl: '/downloads/orders_202603.xlsx',
+ expiresAt: '2026-04-19 15:00:00',
+ creatorId: '1',
+ creatorName: '张三',
+ createTime: '2026-04-12 10:00:00',
+ completeTime: '2026-04-12 10:02:30'
+ },
+ {
+ id: '2',
+ name: '用户数据导出',
+ type: 'user',
+ status: 'processing',
+ format: 'csv',
+ filters: { registeredAfter: '2026-01-01' },
+ total: 0,
+ creatorId: '1',
+ creatorName: '张三',
+ createTime: '2026-04-12 14:30:00'
+ }
+];
+
+// 导入模板字段定义
+const mockTemplates: ImportTemplate[] = [
+ {
+ id: 't1',
+ name: '商品导入模板',
+ type: 'product',
+ description: '用于批量导入商品信息,支持商品名称、价格、库存等字段',
+ downloadUrl: '/templates/product_import.xlsx',
+ fields: [
+ { key: 'name', name: '商品名称', type: 'string', required: true, maxLength: 100, example: 'iPhone 15 Pro' },
+ { key: 'price', name: '价格', type: 'number', required: true, example: '8999.00' },
+ { key: 'stock', name: '库存', type: 'number', required: true, example: '100' },
+ { key: 'category', name: '分类', type: 'select', required: true, options: ['手机', '电脑', '配件', '其他'], example: '手机' },
+ { key: 'description', name: '描述', type: 'string', required: false, maxLength: 500, example: '最新款苹果手机' },
+ { key: 'status', name: '状态', type: 'select', required: true, options: ['上架', '下架'], example: '上架' },
+ { key: 'images', name: '图片URL', type: 'string', required: false, example: 'https://example.com/img.jpg' }
+ ],
+ rules: [
+ { field: 'name', rule: 'required', message: '商品名称不能为空' },
+ { field: 'price', rule: 'range', message: '价格必须大于0', params: { min: 0.01 } },
+ { field: 'stock', rule: 'required', message: '库存不能为空' }
+ ],
+ exampleUrl: '/templates/product_example.xlsx',
+ createTime: '2026-01-01'
+ },
+ {
+ id: 't2',
+ name: '用户导入模板',
+ type: 'user',
+ description: '用于批量导入用户信息,包含用户名、手机、邮箱等',
+ downloadUrl: '/templates/user_import.xlsx',
+ fields: [
+ { key: 'username', name: '用户名', type: 'string', required: true, maxLength: 50, example: 'zhangsan' },
+ { key: 'phone', name: '手机号', type: 'string', required: true, pattern: '^1[3-9]\\d{9}$', example: '13800138000' },
+ { key: 'email', name: '邮箱', type: 'string', required: false, pattern: '^\\w+@\\w+\\.\\w+$', example: 'user@example.com' },
+ { key: 'nickname', name: '昵称', type: 'string', required: false, maxLength: 30, example: '小张' },
+ { key: 'role', name: '角色', type: 'select', required: true, options: ['user', 'admin'], example: 'user' }
+ ],
+ rules: [
+ { field: 'username', rule: 'unique', message: '用户名已存在' },
+ { field: 'phone', rule: 'pattern', message: '手机号格式不正确' }
+ ],
+ createTime: '2026-01-01'
+ },
+ {
+ id: 't3',
+ name: '订单导入模板',
+ type: 'order',
+ description: '用于导入历史订单数据',
+ downloadUrl: '/templates/order_import.xlsx',
+ fields: [
+ { key: 'orderNo', name: '订单号', type: 'string', required: true, example: 'ORD202604120001' },
+ { key: 'userId', name: '用户ID', type: 'string', required: true, example: '10001' },
+ { key: 'amount', name: '订单金额', type: 'number', required: true, example: '299.00' },
+ { key: 'status', name: '订单状态', type: 'select', required: true, options: ['pending', 'paid', 'shipped', 'completed', 'cancelled'], example: 'completed' },
+ { key: 'createTime', name: '创建时间', type: 'date', required: true, example: '2026-04-01 10:30:00' }
+ ],
+ rules: [],
+ createTime: '2026-01-01'
+ }
+];
+
+/**
+ * 获取导入任务列表
+ */
+export async function pageImportTask(params: {
+ type?: ImportType;
+ status?: string;
+ page?: number;
+ pageSize?: number;
+}): Promise<{ list: ImportTask[]; total: number }> {
+ const { type, status, page = 1, pageSize = 10 } = params;
+
+ let filtered = [...mockImportTasks];
+ if (type) filtered = filtered.filter(t => t.type === type);
+ if (status) filtered = filtered.filter(t => t.status === status);
+
+ const start = (page - 1) * pageSize;
+ return { list: filtered.slice(start, start + pageSize), total: filtered.length };
+}
+
+/**
+ * 获取导入任务详情
+ */
+export async function getImportTaskDetail(id: string): Promise {
+ return mockImportTasks.find(t => t.id === id) || null;
+}
+
+/**
+ * 获取导入错误详情
+ */
+export async function getImportErrors(id: string): Promise {
+ return [
+ { row: 15, field: 'price', value: '-100', error: '价格不能为负数', suggestion: '请输入正数' },
+ { row: 28, field: 'name', value: '', error: '商品名称不能为空', suggestion: '请填写商品名称' },
+ { row: 56, field: 'stock', value: 'abc', error: '库存必须是数字', suggestion: '请输入整数' }
+ ];
+}
+
+/**
+ * 获取导入模板列表
+ */
+export async function listImportTemplate(): Promise {
+ return mockTemplates;
+}
+
+/**
+ * 获取模板字段详情
+ */
+export async function getTemplateFields(type: ImportType): Promise {
+ const template = mockTemplates.find(t => t.type === type);
+ return template?.fields || [];
+}
+
+/**
+ * 创建导入任务
+ */
+export async function createImportTask(data: {
+ name: string;
+ type: ImportType;
+ fileName: string;
+ fileSize: string;
+}): Promise {
+ const task: ImportTask = {
+ id: String(Date.now()),
+ name: data.name,
+ type: data.type,
+ status: 'uploading',
+ total: 0,
+ success: 0,
+ failed: 0,
+ progress: 0,
+ fileName: data.fileName,
+ fileSize: data.fileSize,
+ creatorId: '1',
+ creatorName: '张三',
+ startTime: new Date().toISOString().replace('T', ' ').slice(0, 19),
+ createTime: new Date().toISOString().replace('T', ' ').slice(0, 19)
+ };
+
+ mockImportTasks.unshift(task);
+ return task;
+}
+
+/**
+ * 取消导入任务
+ */
+export async function cancelImportTask(id: string): Promise {
+ const task = mockImportTasks.find(t => t.id === id);
+ if (task) task.status = 'cancelled';
+}
+
+/**
+ * 删除导入任务
+ */
+export async function deleteImportTask(id: string): Promise {
+ const index = mockImportTasks.findIndex(t => t.id === id);
+ if (index > -1) mockImportTasks.splice(index, 1);
+}
+
+/**
+ * 获取导出任务列表
+ */
+export async function pageExportTask(params: {
+ type?: ExportType;
+ status?: string;
+ page?: number;
+ pageSize?: number;
+}): Promise<{ list: ExportTask[]; total: number }> {
+ const { type, status, page = 1, pageSize = 10 } = params;
+
+ let filtered = [...mockExportTasks];
+ if (type) filtered = filtered.filter(t => t.type === type);
+ if (status) filtered = filtered.filter(t => t.status === status);
+
+ const start = (page - 1) * pageSize;
+ return { list: filtered.slice(start, start + pageSize), total: filtered.length };
+}
+
+/**
+ * 创建导出任务
+ */
+export async function createExportTask(data: {
+ name: string;
+ type: ExportType;
+ format: ExportFormat;
+ filters?: Record;
+}): Promise {
+ const task: ExportTask = {
+ id: String(Date.now()),
+ name: data.name,
+ type: data.type,
+ status: 'pending',
+ format: data.format,
+ filters: data.filters || {},
+ total: 0,
+ creatorId: '1',
+ creatorName: '张三',
+ createTime: new Date().toISOString().replace('T', ' ').slice(0, 19)
+ };
+
+ mockExportTasks.unshift(task);
+ return task;
+}
+
+/**
+ * 删除导出任务
+ */
+export async function deleteExportTask(id: string): Promise {
+ const index = mockExportTasks.findIndex(t => t.id === id);
+ if (index > -1) mockExportTasks.splice(index, 1);
+}
+
+/**
+ * 下载导入模板
+ */
+export function downloadTemplate(type: ImportType): void {
+ const template = mockTemplates.find(t => t.type === type);
+ if (template) {
+ Taro.showToast({ title: '开始下载模板', icon: 'success' });
+ console.log(`Downloading: ${template.downloadUrl}`);
+ }
+}
+
+/**
+ * 导出账单
+ */
+export async function exportBill(params: {
+ startDate: string;
+ endDate: string;
+ format: ExportFormat;
+}): Promise {
+ return createExportTask({
+ name: `账单导出 ${params.startDate} ~ ${params.endDate}`,
+ type: 'bill',
+ format: params.format,
+ filters: { startDate: params.startDate, endDate: params.endDate }
+ });
+}
diff --git a/src/api/invoice.ts b/src/api/invoice.ts
new file mode 100644
index 0000000..006bee5
--- /dev/null
+++ b/src/api/invoice.ts
@@ -0,0 +1,109 @@
+/**
+ * 发票管理 API
+ */
+import { request } from '../utils/request'
+import type {
+ Invoice,
+ InvoiceTitle,
+ InvoiceApplyParam,
+ InvoiceParam,
+ InvoiceStatus,
+ InvoiceType,
+} from '../types/invoice'
+
+// ==================== 发票抬头 ====================
+
+/** 获取发票抬头列表 */
+export const listInvoiceTitles = () => {
+ return request<{ list: InvoiceTitle[] }>({
+ url: '/enterprise/invoice/titles',
+ method: 'GET',
+ })
+}
+
+/** 创建发票抬头 */
+export const createInvoiceTitle = (data: Omit) => {
+ return request({
+ url: '/enterprise/invoice/titles',
+ method: 'POST',
+ data,
+ })
+}
+
+/** 更新发票抬头 */
+export const updateInvoiceTitle = (id: number, data: Partial) => {
+ return request({
+ url: `/enterprise/invoice/titles/${id}`,
+ method: 'PUT',
+ data,
+ })
+}
+
+/** 删除发票抬头 */
+export const deleteInvoiceTitle = (id: number) => {
+ return request({
+ url: `/enterprise/invoice/titles/${id}`,
+ method: 'DELETE',
+ })
+}
+
+/** 设置默认发票抬头 */
+export const setDefaultInvoiceTitle = (id: number) => {
+ return request({
+ url: `/enterprise/invoice/titles/${id}/default`,
+ method: 'POST',
+ })
+}
+
+// ==================== 发票申请 ====================
+
+/** 获取发票列表 */
+export const pageInvoice = (params: InvoiceParam) => {
+ return request<{ list: Invoice[]; total: number }>({
+ url: '/enterprise/invoice/page',
+ method: 'GET',
+ params,
+ })
+}
+
+/** 获取发票详情 */
+export const getInvoice = (id: number) => {
+ return request({
+ url: `/enterprise/invoice/${id}`,
+ method: 'GET',
+ })
+}
+
+/** 申请发票 */
+export const applyInvoice = (data: InvoiceApplyParam) => {
+ return request({
+ url: '/enterprise/invoice/apply',
+ method: 'POST',
+ data,
+ })
+}
+
+/** 取消发票申请 */
+export const cancelInvoice = (id: number) => {
+ return request({
+ url: `/enterprise/invoice/${id}/cancel`,
+ method: 'POST',
+ })
+}
+
+/** 重新申请发票 */
+export const reapplyInvoice = (id: number, data: Partial) => {
+ return request({
+ url: `/enterprise/invoice/${id}/reapply`,
+ method: 'POST',
+ data,
+ })
+}
+
+/** 获取可开票金额 */
+export const getInvoiceableAmount = () => {
+ return request<{ amount: number }>({
+ url: '/enterprise/invoice/amount',
+ method: 'GET',
+ })
+}
diff --git a/src/api/sdk.ts b/src/api/sdk.ts
new file mode 100644
index 0000000..41aa5cf
--- /dev/null
+++ b/src/api/sdk.ts
@@ -0,0 +1,287 @@
+/**
+ * SDK 管理 API
+ */
+import type { SDK, SDKVersion, SDKDownloadRecord, SDKCategory } from '../types/sdk';
+
+// 模拟数据
+const mockSDKs: SDK[] = [
+ {
+ id: '1',
+ name: 'JavaScript SDK',
+ icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/javascript@1.0.0/javascript.png',
+ description: '适用于 Web 浏览器和 Node.js 的 JavaScript SDK',
+ version: '2.8.0',
+ language: 'JavaScript',
+ category: 'web',
+ downloadCount: 125680,
+ stars: 3420,
+ docsUrl: '/docs/sdk/javascript',
+ npmPackage: '@websopy/sdk-js',
+ downloadUrl: 'https://www.npmjs.com/package/@websopy/sdk-js',
+ changelog: '- 优化性能 20%\n- 新增 TypeScript 支持\n- 修复若干 bug',
+ dependencies: ['axios'],
+ supportedVersions: ['ES6+', 'Node.js 14+'],
+ lastUpdated: '2026-04-10'
+ },
+ {
+ id: '2',
+ name: 'TypeScript SDK',
+ icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/typescript@3.0.3/typescript.png',
+ description: '完整的 TypeScript 类型支持,适用于现代 Web 开发',
+ version: '2.8.0',
+ language: 'TypeScript',
+ category: 'web',
+ downloadCount: 98650,
+ stars: 2890,
+ docsUrl: '/docs/sdk/typescript',
+ npmPackage: '@websopy/sdk-ts',
+ downloadUrl: 'https://www.npmjs.com/package/@websopy/sdk-ts',
+ changelog: '- 完整的类型定义\n- 更好的 IDE 支持\n- 新增装饰器支持',
+ dependencies: [],
+ supportedVersions: ['TypeScript 4.0+'],
+ lastUpdated: '2026-04-10'
+ },
+ {
+ id: '3',
+ name: '微信小程序 SDK',
+ icon: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico',
+ description: '专为微信小程序优化的 SDK,支持插件机制',
+ version: '2.7.5',
+ language: 'JavaScript',
+ category: 'mini-program',
+ downloadCount: 78540,
+ stars: 1560,
+ docsUrl: '/docs/sdk/wx-mini-program',
+ npmPackage: '@websopy/sdk-wx',
+ downloadUrl: 'https://www.npmjs.com/package/@websopy/sdk-wx',
+ changelog: '- 适配最新微信版本\n- 新增分享能力\n- 优化包体积',
+ dependencies: ['wechat-miniprogram'],
+ supportedVersions: ['微信小程序 2.0+'],
+ lastUpdated: '2026-04-08'
+ },
+ {
+ id: '4',
+ name: 'Flutter SDK',
+ icon: 'https://cdn.jsdelivr.net/gh/flutter/website@main/src/assets/images/flutter-icon.svg',
+ description: 'Flutter 跨平台 SDK,支持 iOS 和 Android',
+ version: '1.5.2',
+ language: 'Flutter',
+ category: 'mobile',
+ downloadCount: 45230,
+ stars: 980,
+ docsUrl: '/docs/sdk/flutter',
+ downloadUrl: 'https://pub.dev/packages/websopy_flutter',
+ changelog: '- 支持 Flutter 3.0\n- 新增状态管理\n- 优化首屏加载',
+ dependencies: ['http', 'shared_preferences'],
+ supportedVersions: ['Flutter 2.0+', 'Dart 2.12+'],
+ lastUpdated: '2026-04-05'
+ },
+ {
+ id: '5',
+ name: 'React Native SDK',
+ icon: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/512px-React-icon.svg.png',
+ description: 'React Native 原生模块,完美的性能体验',
+ version: '2.4.0',
+ language: 'React Native',
+ category: 'mobile',
+ downloadCount: 38560,
+ stars: 870,
+ docsUrl: '/docs/sdk/react-native',
+ downloadUrl: 'https://www.npmjs.com/package/@websopy/sdk-rn',
+ changelog: '- 新增 Hooks API\n- 支持 TypeScript\n- 修复内存泄漏',
+ dependencies: ['react-native'],
+ supportedVersions: ['React Native 0.65+', 'React 17+'],
+ lastUpdated: '2026-04-03'
+ },
+ {
+ id: '6',
+ name: 'Java SDK',
+ icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/java@1.0.0/java-original.svg',
+ description: '企业级 Java SDK,支持 Spring Boot 集成',
+ version: '3.2.0',
+ language: 'Java',
+ category: 'server',
+ downloadCount: 65420,
+ stars: 1230,
+ docsUrl: '/docs/sdk/java',
+ downloadUrl: '/downloads/sdk-java-3.2.0.jar',
+ changelog: '- 支持 Spring Boot 3.0\n- 新增响应式编程\n- 优化连接池',
+ dependencies: ['slf4j', 'jackson'],
+ supportedVersions: ['Java 8+', 'Spring Boot 2.0+'],
+ lastUpdated: '2026-04-01'
+ },
+ {
+ id: '7',
+ name: 'Go SDK',
+ icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/go@1.0.0/go-original.svg',
+ description: '高性能 Go SDK,支持 goroutine 并发',
+ version: '2.5.0',
+ language: 'Go',
+ category: 'server',
+ downloadCount: 32180,
+ stars: 760,
+ docsUrl: '/docs/sdk/go',
+ downloadUrl: 'go get github.com/websopy/sdk-go@latest',
+ changelog: '- 支持 Go 1.18 泛型\n- 新增中间件支持\n- 优化性能',
+ dependencies: [],
+ supportedVersions: ['Go 1.16+'],
+ lastUpdated: '2026-03-28'
+ },
+ {
+ id: '8',
+ name: 'Python SDK',
+ icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/python@3.10.0/python-original.svg',
+ description: 'Pythonic 的 Python SDK,支持异步编程',
+ version: '2.6.0',
+ language: 'Python',
+ category: 'server',
+ downloadCount: 52100,
+ stars: 980,
+ docsUrl: '/docs/sdk/python',
+ downloadUrl: 'pip install websopy-sdk',
+ changelog: '- 支持 asyncio\n- 新增类型提示\n- 兼容 Python 3.11',
+ dependencies: ['requests', 'typing-extensions'],
+ supportedVersions: ['Python 3.7+'],
+ lastUpdated: '2026-03-25'
+ },
+ {
+ id: '9',
+ name: 'Swift SDK',
+ icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/swift@1.0.0/swift-original.svg',
+ description: '原生 iOS/macOS Swift SDK,支持 SwiftUI',
+ version: '1.8.0',
+ language: 'Swift',
+ category: 'mobile',
+ downloadCount: 24560,
+ stars: 560,
+ docsUrl: '/docs/sdk/swift',
+ downloadUrl: 'https://github.com/websopy/sdk-swift/releases',
+ changelog: '- 支持 Swift 5.7\n- 新增 Combine 支持\n- 优化包体积',
+ dependencies: [],
+ supportedVersions: ['iOS 13+', 'macOS 11+', 'Swift 5.0+'],
+ lastUpdated: '2026-03-20'
+ },
+ {
+ id: '10',
+ name: '.NET SDK',
+ icon: 'https://cdn.jsdelivr.net/npm/@programming-icons/cdn/dot-net@1.0.0/dot-net-original.svg',
+ description: '.NET 5/6/7 SDK,支持 Entity Framework Core',
+ version: '2.3.0',
+ language: '.NET',
+ category: 'server',
+ downloadCount: 28940,
+ stars: 450,
+ docsUrl: '/docs/sdk/dotnet',
+ downloadUrl: 'https://www.nuget.org/packages/WebsopySDK',
+ changelog: '- 支持 .NET 7\n- 新增依赖注入\n- 支持 Blazor',
+ dependencies: ['Microsoft.Extensions.DependencyInjection'],
+ supportedVersions: ['.NET 5+', '.NET Core 3.1+'],
+ lastUpdated: '2026-03-18'
+ }
+];
+
+/**
+ * 获取 SDK 分类列表
+ */
+export async function listSDKCategories(): Promise {
+ return [
+ { id: 'web', name: 'Web 开发', icon: 'globe', count: 2, description: '适用于浏览器和 Node.js 环境' },
+ { id: 'mobile', name: '移动开发', icon: 'mobile', count: 3, description: 'iOS、Android、Flutter、React Native' },
+ { id: 'server', name: '服务端', icon: 'server', count: 4, description: 'Java、Go、Python、.NET 等后端语言' },
+ { id: 'mini-program', name: '小程序', icon: 'wechat', count: 1, description: '微信小程序、支付宝小程序等' }
+ ];
+}
+
+/**
+ * 获取 SDK 列表
+ */
+export async function pageSDK(params: {
+ category?: string;
+ language?: string;
+ keyword?: string;
+ page?: number;
+ pageSize?: number;
+}): Promise<{ list: SDK[]; total: number }> {
+ const { category, language, keyword, page = 1, pageSize = 10 } = params;
+
+ let filtered = [...mockSDKs];
+
+ if (category) {
+ filtered = filtered.filter(sdk => sdk.category === category);
+ }
+ if (language) {
+ filtered = filtered.filter(sdk => sdk.language === language);
+ }
+ if (keyword) {
+ const kw = keyword.toLowerCase();
+ filtered = filtered.filter(sdk =>
+ sdk.name.toLowerCase().includes(kw) ||
+ sdk.description.toLowerCase().includes(kw)
+ );
+ }
+
+ const start = (page - 1) * pageSize;
+ const list = filtered.slice(start, start + pageSize);
+
+ return { list, total: filtered.length };
+}
+
+/**
+ * 获取 SDK 详情
+ */
+export async function getSDKDetail(id: string): Promise {
+ return mockSDKs.find(sdk => sdk.id === id) || null;
+}
+
+/**
+ * 获取 SDK 版本列表
+ */
+export async function listSDKVersions(id: string): Promise {
+ const sdk = mockSDKs.find(s => s.id === id);
+ if (!sdk) return [];
+
+ return [
+ { version: sdk.version, releaseDate: sdk.lastUpdated, releaseNotes: sdk.changelog, downloadUrl: sdk.downloadUrl, size: '2.5MB', sha256: 'a1b2c3d4e5f6...' },
+ { version: '2.7.0', releaseDate: '2026-03-15', releaseNotes: '- 性能优化\n- Bug 修复', downloadUrl: '', size: '2.3MB', sha256: 'b2c3d4e5f6g7...' },
+ { version: '2.6.0', releaseDate: '2026-02-20', releaseNotes: '- 新增功能\n- 文档更新', downloadUrl: '', size: '2.1MB', sha256: 'c3d4e5f6g7h8...' }
+ ];
+}
+
+/**
+ * 获取下载记录
+ */
+export async function pageDownloadRecord(params: {
+ page?: number;
+ pageSize?: number;
+}): Promise<{ list: SDKDownloadRecord[]; total: number }> {
+ return {
+ list: [
+ { id: '1', sdkId: '1', sdkName: 'JavaScript SDK', version: '2.8.0', language: 'JavaScript', userId: '1', userName: '张三', downloadTime: '2026-04-12 14:30:00', ipAddress: '127.0.0.1' },
+ { id: '2', sdkId: '3', sdkName: '微信小程序 SDK', version: '2.7.5', language: 'JavaScript', userId: '1', userName: '张三', downloadTime: '2026-04-11 10:20:00', ipAddress: '127.0.0.1' },
+ { id: '3', sdkId: '6', sdkName: 'Java SDK', version: '3.2.0', language: 'Java', userId: '1', userName: '张三', downloadTime: '2026-04-10 16:45:00', ipAddress: '127.0.0.1' }
+ ],
+ total: 3
+ };
+}
+
+/**
+ * 记录 SDK 下载
+ */
+export async function recordDownload(id: string, version: string): Promise {
+ console.log(`Download recorded: ${id} v${version}`);
+}
+
+/**
+ * 获取 SDK 统计
+ */
+export async function getSDKStats(): Promise<{
+ totalDownloads: number;
+ totalSDKs: number;
+ topSDKs: SDK[];
+}> {
+ return {
+ totalDownloads: mockSDKs.reduce((sum, sdk) => sum + sdk.downloadCount, 0),
+ totalSDKs: mockSDKs.length,
+ topSDKs: [...mockSDKs].sort((a, b) => b.downloadCount - a.downloadCount).slice(0, 5)
+ };
+}
diff --git a/src/api/sso.ts b/src/api/sso.ts
new file mode 100644
index 0000000..0b05e39
--- /dev/null
+++ b/src/api/sso.ts
@@ -0,0 +1,114 @@
+/**
+ * SSO 单点登录 API
+ */
+import { request } from '../utils/request'
+import type { SSOConfig, SSOSession, SSOLog, SSOProvider, SyncDirection } from '../types/sso'
+
+// ==================== SSO 配置 ====================
+
+/** 获取 SSO 配置 */
+export const getSSOConfig = (enterpriseId: number) => {
+ return request({
+ url: `/enterprise/sso/config/${enterpriseId}`,
+ method: 'GET',
+ })
+}
+
+/** 创建 SSO 配置 */
+export const createSSOConfig = (data: Omit) => {
+ return request({
+ url: '/enterprise/sso/config',
+ method: 'POST',
+ data,
+ })
+}
+
+/** 更新 SSO 配置 */
+export const updateSSOConfig = (data: Partial & { id: number }) => {
+ return request({
+ url: '/enterprise/sso/config',
+ method: 'PUT',
+ data,
+ })
+}
+
+/** 删除 SSO 配置 */
+export const deleteSSOConfig = (id: number) => {
+ return request({
+ url: `/enterprise/sso/config/${id}`,
+ method: 'DELETE',
+ })
+}
+
+/** 启用/禁用 SSO */
+export const toggleSSO = (id: number, enabled: boolean) => {
+ return request({
+ url: `/enterprise/sso/config/${id}/toggle`,
+ method: 'POST',
+ data: { enabled },
+ })
+}
+
+/** 测试 SSO 连接 */
+export const testSSOConnection = (id: number) => {
+ return request<{ success: boolean; message: string }>({
+ url: `/enterprise/sso/config/${id}/test`,
+ method: 'POST',
+ })
+}
+
+/** 生成 SSO 登录链接 */
+export const getSSOLoginUrl = (id: number, redirectUri?: string) => {
+ return request<{ url: string }>({
+ url: `/enterprise/sso/config/${id}/login-url`,
+ method: 'GET',
+ params: { redirectUri },
+ })
+}
+
+// ==================== SSO 会话 ====================
+
+/** 获取 SSO 会话列表 */
+export const pageSSOSessions = (params: { enterpriseId: number; page?: number; limit?: number }) => {
+ return request<{ list: SSOSession[]; total: number }>({
+ url: '/enterprise/sso/sessions',
+ method: 'GET',
+ params,
+ })
+}
+
+/** 强制下线 SSO 会话 */
+export const logoutSSOSession = (id: number) => {
+ return request({
+ url: `/enterprise/sso/sessions/${id}/logout`,
+ method: 'POST',
+ })
+}
+
+/** 强制下线所有 SSO 会话 */
+export const logoutAllSSOSessions = (enterpriseId: number) => {
+ return request({
+ url: `/enterprise/sso/sessions/logout-all`,
+ method: 'POST',
+ data: { enterpriseId },
+ })
+}
+
+// ==================== SSO 日志 ====================
+
+/** 获取 SSO 日志列表 */
+export const pageSSOLogs = (params: {
+ enterpriseId: number
+ page?: number
+ limit?: number
+ type?: string
+ status?: string
+ timeStart?: string
+ timeEnd?: string
+}) => {
+ return request<{ list: SSOLog[]; total: number }>({
+ url: '/enterprise/sso/logs',
+ method: 'GET',
+ params,
+ })
+}
diff --git a/src/api/ticket.ts b/src/api/ticket.ts
new file mode 100644
index 0000000..605eb22
--- /dev/null
+++ b/src/api/ticket.ts
@@ -0,0 +1,296 @@
+/**
+ * 工单/技术支持 API
+ */
+import Taro from '@tarojs/taro';
+import type { Ticket, TicketReply, TicketTemplate, TicketStats, FAQ } from '../types/ticket';
+
+// 模拟数据
+const mockTickets: Ticket[] = [
+ {
+ id: '1',
+ ticketNo: 'TK202604120001',
+ title: 'API 调用频率限制问题',
+ content: '我在使用批量接口时遇到了 429 错误,请问如何申请提高调用频率限制?',
+ type: 'technical',
+ priority: 'high',
+ status: 'processing',
+ category: 'api',
+ attachments: [],
+ creatorId: '1',
+ creatorName: '张三',
+ creatorAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
+ assigneeId: 's1',
+ assigneeName: '技术支持小王',
+ assigneeAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka',
+ responseCount: 2,
+ solution: '已为您开通企业版调用配额',
+ createTime: '2026-04-12 10:30:00',
+ updateTime: '2026-04-12 14:20:00',
+ resolveTime: 230
+ },
+ {
+ id: '2',
+ ticketNo: 'TK202604110002',
+ title: '微信支付回调异常',
+ content: '支付完成后回调地址没有收到通知,请问如何排查问题?',
+ type: 'bug',
+ priority: 'urgent',
+ status: 'resolved',
+ category: 'payment',
+ attachments: ['/uploads/error-log.png'],
+ creatorId: '1',
+ creatorName: '张三',
+ creatorAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
+ assigneeId: 's2',
+ assigneeName: '技术支持小李',
+ assigneeAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Bobby',
+ responseCount: 5,
+ solution: '回调地址需要配置白名单,已协助配置完成',
+ rating: 5,
+ feedback: '响应很快,问题解决了',
+ resolveTime: 180,
+ createTime: '2026-04-11 09:15:00',
+ updateTime: '2026-04-11 12:15:00',
+ resolveTime2: '2026-04-11 12:15:00'
+ },
+ {
+ id: '3',
+ ticketNo: 'TK202604100003',
+ title: '功能建议:支持 Webhook 重试机制',
+ content: '希望 Webhook 能够支持失败重试功能,提高消息可靠性',
+ type: 'feature',
+ priority: 'medium',
+ status: 'pending',
+ category: 'api',
+ attachments: [],
+ creatorId: '1',
+ creatorName: '张三',
+ creatorAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
+ responseCount: 0,
+ createTime: '2026-04-10 16:45:00',
+ updateTime: '2026-04-10 16:45:00'
+ }
+];
+
+const mockReplies: TicketReply[] = [
+ {
+ id: 'r1',
+ ticketId: '1',
+ content: '您好,请问您的应用日调用量是多少?企业版默认配额为 10万次/天',
+ attachments: [],
+ isInternal: false,
+ senderId: 's1',
+ senderName: '技术支持小王',
+ senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka',
+ senderRole: 'support',
+ createTime: '2026-04-12 11:00:00'
+ },
+ {
+ id: 'r2',
+ ticketId: '1',
+ content: '我的日调用量大约在 15 万次左右',
+ attachments: [],
+ isInternal: false,
+ senderId: '1',
+ senderName: '张三',
+ senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
+ senderRole: 'user',
+ createTime: '2026-04-12 11:30:00'
+ },
+ {
+ id: 'r3',
+ ticketId: '1',
+ content: '已为您升级到企业高级版,配额提升至 50万次/天',
+ attachments: [],
+ isInternal: false,
+ senderId: 's1',
+ senderName: '技术支持小王',
+ senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka',
+ senderRole: 'support',
+ createTime: '2026-04-12 14:20:00'
+ }
+];
+
+const mockTemplates: TicketTemplate[] = [
+ { id: 't1', title: 'API 调用问题', content: '【问题描述】\n\n【复现步骤】\n1.\n2.\n3.\n\n【错误信息】\n\n【环境信息】', type: 'technical', category: 'api', priority: 'medium' },
+ { id: 't2', title: '支付相关问题', content: '【问题类型】\n□ 支付失败 □ 退款 □ 发票\n\n【订单号】\n\n【问题描述】', type: 'billing', category: 'payment', priority: 'high' },
+ { id: 't3', title: 'Bug 反馈', content: '【Bug 标题】\n\n【影响范围】\n\n【复现步骤】\n1.\n2.\n\n【预期行为】\n\n【实际行为】', type: 'bug', category: 'other', priority: 'high' }
+];
+
+const mockFAQs: FAQ[] = [
+ { id: 'f1', question: '如何获取 API Key?', answer: '登录控制台 -> 开发管理 -> API Key -> 创建 Key', category: 'api', viewCount: 1256, helpful: 234, notHelpful: 12, tags: ['API', '密钥'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-04-01' },
+ { id: 'f2', question: '调用频率限制是多少?', answer: '免费版 1000次/天,个人版 1万次/天,企业版 10万次/天', category: 'api', viewCount: 2345, helpful: 456, notHelpful: 23, tags: ['限流', '配额'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-04-05' },
+ { id: 'f3', question: '如何申请发票?', answer: '控制台 -> 财务 -> 发票管理 -> 申请发票', category: 'billing', viewCount: 1890, helpful: 345, notHelpful: 15, tags: ['发票', '财务'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-03-20' },
+ { id: 'f4', question: 'Webhook 回调失败怎么办?', answer: '1. 检查回调地址是否可公网访问\n2. 确保返回 200 状态码\n3. 检查签名验证', category: 'api', viewCount: 1567, helpful: 289, notHelpful: 34, tags: ['Webhook', '回调'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-04-10' },
+ { id: 'f5', question: '如何升级账户?', answer: '控制台 -> 套餐管理 -> 选择套餐 -> 在线支付', category: 'account', viewCount: 987, helpful: 178, notHelpful: 8, tags: ['升级', '套餐'], relatedQuestions: [], createTime: '2026-01-01', updateTime: '2026-03-15' }
+];
+
+/**
+ * 获取工单统计
+ */
+export async function getTicketStats(): Promise {
+ return {
+ total: mockTickets.length,
+ pending: 1,
+ processing: 1,
+ resolved: 1,
+ avgResponseTime: 45,
+ avgResolveTime: 4.2,
+ satisfaction: 4.8
+ };
+}
+
+/**
+ * 分页获取工单列表
+ */
+export async function pageTicket(params: {
+ status?: string;
+ type?: string;
+ priority?: string;
+ keyword?: string;
+ page?: number;
+ pageSize?: number;
+}): Promise<{ list: Ticket[]; total: number }> {
+ const { status, type, priority, keyword, page = 1, pageSize = 10 } = params;
+
+ let filtered = [...mockTickets];
+
+ if (status) filtered = filtered.filter(t => t.status === status);
+ if (type) filtered = filtered.filter(t => t.type === type);
+ if (priority) filtered = filtered.filter(t => t.priority === priority);
+ if (keyword) {
+ const kw = keyword.toLowerCase();
+ filtered = filtered.filter(t => t.title.toLowerCase().includes(kw) || t.content.toLowerCase().includes(kw));
+ }
+
+ const start = (page - 1) * pageSize;
+ return { list: filtered.slice(start, start + pageSize), total: filtered.length };
+}
+
+/**
+ * 获取工单详情
+ */
+export async function getTicketDetail(id: string): Promise {
+ return mockTickets.find(t => t.id === id) || null;
+}
+
+/**
+ * 获取工单回复列表
+ */
+export async function listTicketReply(ticketId: string): Promise {
+ return mockReplies.filter(r => r.ticketId === ticketId);
+}
+
+/**
+ * 创建工单
+ */
+export async function createTicket(data: Partial): Promise {
+ const newTicket: Ticket = {
+ id: String(Date.now()),
+ ticketNo: `TK${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12)}`,
+ title: data.title || '',
+ content: data.content || '',
+ type: data.type || 'technical',
+ priority: data.priority || 'medium',
+ status: 'pending',
+ category: data.category || 'api',
+ attachments: data.attachments || [],
+ creatorId: '1',
+ creatorName: '张三',
+ creatorAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
+ responseCount: 0,
+ createTime: new Date().toISOString().replace('T', ' ').slice(0, 19),
+ updateTime: new Date().toISOString().replace('T', ' ').slice(0, 19)
+ };
+
+ mockTickets.unshift(newTicket);
+ return newTicket;
+}
+
+/**
+ * 回复工单
+ */
+export async function replyTicket(ticketId: string, content: string, attachments: string[] = []): Promise {
+ const reply: TicketReply = {
+ id: String(Date.now()),
+ ticketId,
+ content,
+ attachments,
+ isInternal: false,
+ senderId: '1',
+ senderName: '张三',
+ senderAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
+ senderRole: 'user',
+ createTime: new Date().toISOString().replace('T', ' ').slice(0, 19)
+ };
+
+ mockReplies.push(reply);
+
+ const ticket = mockTickets.find(t => t.id === ticketId);
+ if (ticket) {
+ ticket.responseCount++;
+ ticket.updateTime = reply.createTime;
+ if (ticket.status === 'pending') ticket.status = 'processing';
+ }
+
+ return reply;
+}
+
+/**
+ * 关闭工单
+ */
+export async function closeTicket(id: string): Promise {
+ const ticket = mockTickets.find(t => t.id === id);
+ if (ticket) ticket.status = 'closed';
+}
+
+/**
+ * 评价工单
+ */
+export async function rateTicket(id: string, rating: number, feedback: string): Promise {
+ const ticket = mockTickets.find(t => t.id === id);
+ if (ticket) {
+ ticket.rating = rating;
+ ticket.feedback = feedback;
+ }
+}
+
+/**
+ * 获取工单模板
+ */
+export async function listTicketTemplate(): Promise {
+ return mockTemplates;
+}
+
+/**
+ * 获取 FAQ 列表
+ */
+export async function pageFAQ(params: {
+ category?: string;
+ keyword?: string;
+ page?: number;
+ pageSize?: number;
+}): Promise<{ list: FAQ[]; total: number }> {
+ const { category, keyword, page = 1, pageSize = 10 } = params;
+
+ let filtered = [...mockFAQs];
+ if (category) filtered = filtered.filter(f => f.category === category);
+ if (keyword) {
+ const kw = keyword.toLowerCase();
+ filtered = filtered.filter(f => f.question.toLowerCase().includes(kw) || f.answer.toLowerCase().includes(kw));
+ }
+
+ const start = (page - 1) * pageSize;
+ return { list: filtered.slice(start, start + pageSize), total: filtered.length };
+}
+
+/**
+ * FAQ 反馈
+ */
+export async function feedbackFAQ(id: string, helpful: boolean): Promise {
+ const faq = mockFAQs.find(f => f.id === id);
+ if (faq) {
+ if (helpful) faq.helpful++;
+ else faq.notHelpful++;
+ }
+}
diff --git a/src/app.config.ts b/src/app.config.ts
index e0dd0c4..34625e8 100644
--- a/src/app.config.ts
+++ b/src/app.config.ts
@@ -117,6 +117,62 @@ export default {
"index",
"article/index",
]
+ },
+ {
+ "root": "developer",
+ "pages": [
+ "index",
+ "project/index",
+ "project/create",
+ "project/[id]/index",
+ "project/[id]/members",
+ "project/[id]/api-keys",
+ "project/[id]/settings",
+ "app/index",
+ "app/create",
+ "app/[id]/index",
+ "app/[id]/version",
+ "app/[id]/config",
+ "app/[id]/publish",
+ "app/api-keys/index",
+ "notification/index",
+ "developer/apply",
+ "developer/profile",
+ "docs/index",
+ "docs/quickstart",
+ "docs/api-docs",
+ "market/index"
+ ]
+ },
+ {
+ "root": "enterprise",
+ "pages": [
+ "index",
+ "apps/index",
+ "apps/[id]/index",
+ "apps/[id]/monitor",
+ "apps/[id]/analytics",
+ "apps/[id]/settings",
+ "apps/purchase",
+ "members/index",
+ "members/invite",
+ "members/roles",
+ "members/audit",
+ "orders/index",
+ "orders/detail",
+ "orders/invoice",
+ "orders/bills",
+ "billing/index",
+ "billing/consumption",
+ "billing/recharge",
+ "billing/coupons",
+ "settings/index",
+ "settings/info",
+ "settings/domain",
+ "settings/security",
+ "developer/apply",
+ "developer/index"
+ ]
}
],
window: {
diff --git a/src/app/appCredential/add.tsx b/src/app/appCredential/add.tsx
index 1326736..a1baeb7 100644
--- a/src/app/appCredential/add.tsx
+++ b/src/app/appCredential/add.tsx
@@ -2,9 +2,8 @@ import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Button, Loading, CellGroup, Input, TextArea, Form} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
-import {View} from '@tarojs/components'
import {AppCredential} from "@/api/app/appCredential/model";
-import {getAppCredential, listAppCredential, updateAppCredential, addAppCredential} from "@/api/app/appCredential";
+import {getAppCredential, updateAppCredential, addAppCredential} from "@/api/app/appCredential";
const AddAppCredential = () => {
const {params} = useRouter();
@@ -21,17 +20,14 @@ const AddAppCredential = () => {
}
}
- // 提交表单
const submitSucceed = async (values: any) => {
try {
if (params.id) {
- // 编辑模式
await updateAppCredential({
...values,
id: Number(params.id)
})
} else {
- // 新增模式
await addAppCredential(values)
}
@@ -96,3 +92,21 @@ const AddAppCredential = () => {
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default AddAppCredential;
diff --git a/src/app/appCredential/index.tsx b/src/app/appCredential/index.tsx
index d96b6b9..c0e46f2 100644
--- a/src/app/appCredential/index.tsx
+++ b/src/app/appCredential/index.tsx
@@ -2,17 +2,14 @@ import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro'
-import {View} from '@tarojs/components'
import {AppCredential} from "@/api/app/appCredential/model";
-import {listAppCredential, removeAppCredential, updateAppCredential} from "@/api/app/appCredential";
+import {listAppCredential, removeAppCredential} from "@/api/app/appCredential";
const AppCredentialList = () => {
const [list, setList] = useState([])
const reload = () => {
- listAppCredential({
- // 添加查询条件
- })
+ listAppCredential({})
.then(data => {
setList(data || [])
})
@@ -24,7 +21,6 @@ const AppCredentialList = () => {
})
}
-
const onDel = async (id?: number) => {
await removeAppCredential(id)
Taro.showToast({
@@ -59,6 +55,26 @@ const AppCredentialList = () => {
}
return (
- <>
- {list.map((item, _) => (
-
+
+ {list.map((item) => (
+ | }
+ onClick={() => Taro.navigateTo({url: `/app/appCredential/add?id=${item.id}`})}
+ />
+ ))}
+
+
+
+
+
+ );
+};
+
+export default AppCredentialList;
diff --git a/src/app/appEvent/add.tsx b/src/app/appEvent/add.tsx
index c3c264b..6fa3429 100644
--- a/src/app/appEvent/add.tsx
+++ b/src/app/appEvent/add.tsx
@@ -1,10 +1,17 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
-import {Button, Loading, CellGroup, Input, TextArea, Form} from '@nutui/nutui-react-taro'
+import {Button, Loading, CellGroup, Input, TextArea, Form, Picker} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
-import {View} from '@tarojs/components'
import {AppEvent} from "@/api/app/appEvent/model";
-import {getAppEvent, listAppEvent, updateAppEvent, addAppEvent} from "@/api/app/appEvent";
+import {getAppEvent, updateAppEvent, addAppEvent} from "@/api/app/appEvent";
+
+const EVENT_TYPES = [
+ { value: 'user_register', label: '用户注册' },
+ { value: 'user_login', label: '用户登录' },
+ { value: 'order_create', label: '订单创建' },
+ { value: 'order_pay', label: '订单支付' },
+ { value: 'custom', label: '自定义' }
+];
const AddAppEvent = () => {
const {params} = useRouter();
@@ -21,17 +28,14 @@ const AddAppEvent = () => {
}
}
- // 提交表单
const submitSucceed = async (values: any) => {
try {
if (params.id) {
- // 编辑模式
await updateAppEvent({
...values,
id: Number(params.id)
})
} else {
- // 新增模式
await addAppEvent(values)
}
@@ -95,4 +99,21 @@ const AddAppEvent = () => {
}
>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default AddAppEvent;
diff --git a/src/app/appEvent/index.tsx b/src/app/appEvent/index.tsx
index 5a0886b..86b5a57 100644
--- a/src/app/appEvent/index.tsx
+++ b/src/app/appEvent/index.tsx
@@ -1,18 +1,15 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
-import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro'
-import {View} from '@tarojs/components'
+import {ArrowRight} from '@nutui/icons-react-taro'
import {AppEvent} from "@/api/app/appEvent/model";
-import {listAppEvent, removeAppEvent, updateAppEvent} from "@/api/app/appEvent";
+import {listAppEvent, removeAppEvent} from "@/api/app/appEvent";
const AppEventList = () => {
const [list, setList] = useState([])
const reload = () => {
- listAppEvent({
- // 添加查询条件
- })
+ listAppEvent({})
.then(data => {
setList(data || [])
})
@@ -24,7 +21,6 @@ const AppEventList = () => {
})
}
-
const onDel = async (id?: number) => {
await removeAppEvent(id)
Taro.showToast({
@@ -51,7 +47,7 @@ const AppEventList = () => {
description="暂无数据"
/>
-
+
@@ -59,6 +55,26 @@ const AppEventList = () => {
}
return (
- <>
- {list.map((item, _) => (
-
+
+ {list.map((item) => (
+ | }
+ onClick={() => Taro.navigateTo({url: `/app/appEvent/add?id=${item.id}`})}
+ />
+ ))}
+
+
+
+
+
+ );
+};
+
+export default AppEventList;
diff --git a/src/app/appUser/add.tsx b/src/app/appUser/add.tsx
index 01173d0..93d37eb 100644
--- a/src/app/appUser/add.tsx
+++ b/src/app/appUser/add.tsx
@@ -1,10 +1,9 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
-import {Button, Loading, CellGroup, Input, TextArea, Form} from '@nutui/nutui-react-taro'
+import {Button, Loading, CellGroup, Input, Form} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
-import {View} from '@tarojs/components'
import {AppUser} from "@/api/app/appUser/model";
-import {getAppUser, listAppUser, updateAppUser, addAppUser} from "@/api/app/appUser";
+import {getAppUser, updateAppUser} from "@/api/app/appUser";
const AddAppUser = () => {
const {params} = useRouter();
@@ -21,18 +20,13 @@ const AddAppUser = () => {
}
}
- // 提交表单
const submitSucceed = async (values: any) => {
try {
if (params.id) {
- // 编辑模式
await updateAppUser({
...values,
id: Number(params.id)
})
- } else {
- // 新增模式
- await addAppUser(values)
}
Taro.showToast({
@@ -89,10 +83,25 @@ const AddAppUser = () => {
className={'w-full'}
block
>
- {params.id ? '更新' : '保存'}
+ 更新
}
>
-
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default AddAppUser;
diff --git a/src/app/appUser/index.tsx b/src/app/appUser/index.tsx
index 792d37c..d574f25 100644
--- a/src/app/appUser/index.tsx
+++ b/src/app/appUser/index.tsx
@@ -1,18 +1,15 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
-import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
-import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro'
-import {View} from '@tarojs/components'
+import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Avatar} from '@nutui/nutui-react-taro'
+import {ArrowRight} from '@nutui/icons-react-taro'
import {AppUser} from "@/api/app/appUser/model";
-import {listAppUser, removeAppUser, updateAppUser} from "@/api/app/appUser";
+import {listAppUser} from "@/api/app/appUser";
const AppUserList = () => {
const [list, setList] = useState([])
const reload = () => {
- listAppUser({
- // 添加查询条件
- })
+ listAppUser({})
.then(data => {
setList(data || [])
})
@@ -24,16 +21,6 @@ const AppUserList = () => {
})
}
-
- const onDel = async (id?: number) => {
- await removeAppUser(id)
- Taro.showToast({
- title: '删除成功',
- icon: 'success'
- });
- reload();
- }
-
useDidShow(() => {
reload()
});
@@ -50,15 +37,27 @@ const AppUserList = () => {
}}
description="暂无数据"
/>
-
-
-
)
}
return (
- <>
- {list.map((item, _) => (
-
+
+ {list.map((item) => (
+ | }
+ onClick={() => Taro.navigateTo({url: `/app/appUser/add?id=${item.id}`})}
+ />
+ ))}
+
+
+ );
+};
+
+export default AppUserList;
diff --git a/src/app/appVersion/add.tsx b/src/app/appVersion/add.tsx
index 4e7eae9..90f4587 100644
--- a/src/app/appVersion/add.tsx
+++ b/src/app/appVersion/add.tsx
@@ -2,9 +2,8 @@ import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Button, Loading, CellGroup, Input, TextArea, Form} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
-import {View} from '@tarojs/components'
import {AppVersion} from "@/api/app/appVersion/model";
-import {getAppVersion, listAppVersion, updateAppVersion, addAppVersion} from "@/api/app/appVersion";
+import {getAppVersion, updateAppVersion} from "@/api/app/appVersion";
const AddAppVersion = () => {
const {params} = useRouter();
@@ -21,18 +20,13 @@ const AddAppVersion = () => {
}
}
- // 提交表单
const submitSucceed = async (values: any) => {
try {
if (params.id) {
- // 编辑模式
await updateAppVersion({
...values,
id: Number(params.id)
})
- } else {
- // 新增模式
- await addAppVersion(values)
}
Taro.showToast({
@@ -89,10 +83,25 @@ const AddAppVersion = () => {
className={'w-full'}
block
>
- {params.id ? '更新' : '保存'}
+ 更新
}
>
-
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default AddAppVersion;
diff --git a/src/app/appVersion/index.tsx b/src/app/appVersion/index.tsx
index 9d9fd3d..61c4b6c 100644
--- a/src/app/appVersion/index.tsx
+++ b/src/app/appVersion/index.tsx
@@ -1,18 +1,21 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
-import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
-import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro'
-import {View} from '@tarojs/components'
+import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Tag} from '@nutui/nutui-react-taro'
+import {ArrowRight} from '@nutui/icons-react-taro'
import {AppVersion} from "@/api/app/appVersion/model";
-import {listAppVersion, removeAppVersion, updateAppVersion} from "@/api/app/appVersion";
+import {listAppVersion} from "@/api/app/appVersion";
+
+const STATUS_MAP: Record = {
+ pending: { label: '待发布', color: 'warning' },
+ published: { label: '已发布', color: 'success' },
+ offline: { label: '已下架', color: 'default' }
+};
const AppVersionList = () => {
const [list, setList] = useState([])
const reload = () => {
- listAppVersion({
- // 添加查询条件
- })
+ listAppVersion({})
.then(data => {
setList(data || [])
})
@@ -24,16 +27,6 @@ const AppVersionList = () => {
})
}
-
- const onDel = async (id?: number) => {
- await removeAppVersion(id)
- Taro.showToast({
- title: '删除成功',
- icon: 'success'
- });
- reload();
- }
-
useDidShow(() => {
reload()
});
@@ -48,10 +41,10 @@ const AppVersionList = () => {
style={{
backgroundColor: 'transparent'
}}
- description="暂无数据"
+ description="暂无版本"
/>
-
+
@@ -59,6 +52,30 @@ const AppVersionList = () => {
}
return (
- <>
- {list.map((item, _) => (
-
+
+ {list.map((item) => (
+ | }
+ onClick={() => Taro.navigateTo({url: `/app/appVersion/add?id=${item.id}`})}
+ >
+
+ {STATUS_MAP[item.status || 'pending']?.label}
+
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default AppVersionList;
diff --git a/src/developer/app/[id]/analytics.config.ts b/src/developer/app/[id]/analytics.config.ts
new file mode 100644
index 0000000..21e95bc
--- /dev/null
+++ b/src/developer/app/[id]/analytics.config.ts
@@ -0,0 +1,6 @@
+/**
+ * 运营监控页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '运营监控',
+})
diff --git a/src/developer/app/[id]/analytics.scss b/src/developer/app/[id]/analytics.scss
new file mode 100644
index 0000000..eca0ba6
--- /dev/null
+++ b/src/developer/app/[id]/analytics.scss
@@ -0,0 +1,457 @@
+.analytics-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+}
+
+.loading-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 300px;
+
+ .loading-text {
+ margin-top: 16px;
+ font-size: 28px;
+ color: #999;
+ }
+}
+
+// 实时数据卡片
+.realtime-card {
+ background: linear-gradient(135deg, #1890ff, #52c41a);
+ padding: 24px;
+ margin: 24px;
+ border-radius: 16px;
+ color: #fff;
+
+ .realtime-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+
+ .title {
+ font-size: 32px;
+ font-weight: 600;
+ }
+
+ .live-dot {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 22px;
+
+ .dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: #fff;
+ animation: pulse 1.5s infinite;
+ }
+ }
+ }
+
+ .realtime-stats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 16px;
+
+ .stat-item {
+ text-align: center;
+
+ .stat-value {
+ display: block;
+ font-size: 36px;
+ font-weight: 600;
+ margin-bottom: 4px;
+
+ &.error {
+ color: #ff6b6b;
+ }
+ }
+
+ .stat-label {
+ font-size: 22px;
+ opacity: 0.8;
+ }
+ }
+ }
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+// 今日概览
+.overview-stats {
+ display: flex;
+ padding: 0 24px;
+ gap: 16px;
+
+ .stat-card {
+ flex: 1;
+ background: #fff;
+ padding: 20px;
+ border-radius: 12px;
+ text-align: center;
+
+ .stat-value {
+ display: block;
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 4px;
+ }
+
+ .stat-label {
+ font-size: 22px;
+ color: #999;
+ }
+ }
+}
+
+// Tab 内容
+.tab-content {
+ height: calc(100vh - 550px);
+ padding: 24px;
+}
+
+.chart-section,
+.metrics-section,
+.region-section,
+.source-section,
+.page-section,
+.error-section {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+
+ .section-title {
+ display: block;
+ font-size: 30px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 20px;
+ }
+}
+
+// 图表占位
+.chart-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 300px;
+ background: #fafafa;
+ border-radius: 12px;
+
+ .iconfont {
+ font-size: 64px;
+ color: #ccc;
+ margin-bottom: 16px;
+ }
+
+ .placeholder-text {
+ font-size: 28px;
+ color: #666;
+ margin-bottom: 8px;
+ }
+
+ .placeholder-hint {
+ font-size: 22px;
+ color: #999;
+ }
+}
+
+// 饼图
+.pie-chart {
+ .pie-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .pie-color {
+ width: 24px;
+ height: 24px;
+ border-radius: 4px;
+ margin-right: 12px;
+ }
+
+ .pie-label {
+ flex: 1;
+ font-size: 26px;
+ color: #333;
+ }
+
+ .pie-value {
+ font-size: 26px;
+ color: #666;
+ }
+ }
+}
+
+// 性能指标
+.metric-item {
+ padding: 16px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .metric-info {
+ .metric-name {
+ display: block;
+ font-size: 26px;
+ color: #666;
+ margin-bottom: 8px;
+ }
+
+ .metric-value-row {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+
+ .metric-value {
+ font-size: 40px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .metric-unit {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+ }
+}
+
+// 错误统计
+.error-summary {
+ display: flex;
+ gap: 24px;
+
+ .error-item {
+ flex: 1;
+ text-align: center;
+ padding: 20px;
+ background: #fafafa;
+ border-radius: 12px;
+
+ .error-value {
+ display: block;
+ font-size: 40px;
+ font-weight: 600;
+ margin-bottom: 8px;
+
+ &.error {
+ color: #F44336;
+ }
+
+ &.warning {
+ color: #FF9800;
+ }
+ }
+
+ .error-label {
+ font-size: 24px;
+ color: #666;
+ }
+ }
+}
+
+// 用户统计
+.user-stats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ margin-bottom: 24px;
+
+ .user-card {
+ background: #fff;
+ padding: 24px 16px;
+ border-radius: 12px;
+ text-align: center;
+
+ .user-value {
+ display: block;
+ font-size: 36px;
+ font-weight: 600;
+ color: #1890ff;
+ margin-bottom: 8px;
+ }
+
+ .user-label {
+ font-size: 24px;
+ color: #666;
+ }
+ }
+}
+
+// 区域分布
+.region-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .region-rank {
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+ text-align: center;
+ background: #1890ff;
+ color: #fff;
+ border-radius: 50%;
+ font-size: 22px;
+ margin-right: 12px;
+ }
+
+ .region-name {
+ width: 100px;
+ font-size: 26px;
+ color: #333;
+ }
+
+ .region-bar {
+ flex: 1;
+ height: 16px;
+ background: #f0f0f0;
+ border-radius: 8px;
+ margin: 0 12px;
+ overflow: hidden;
+
+ .bar-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #1890ff, #52c41a);
+ border-radius: 8px;
+ }
+ }
+
+ .region-value {
+ width: 80px;
+ text-align: right;
+ font-size: 26px;
+ color: #666;
+ }
+}
+
+// 来源分布
+.source-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .source-rank {
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+ text-align: center;
+ background: #f0f0f0;
+ color: #666;
+ border-radius: 50%;
+ font-size: 22px;
+ margin-right: 12px;
+ }
+
+ .source-name {
+ flex: 1;
+ font-size: 26px;
+ color: #333;
+ }
+
+ .source-value {
+ font-size: 26px;
+ color: #666;
+ }
+}
+
+// 页面排行
+.page-item {
+ display: flex;
+ align-items: center;
+ padding: 16px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .page-rank {
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+ text-align: center;
+ background: #1890ff;
+ color: #fff;
+ border-radius: 50%;
+ font-size: 22px;
+ margin-right: 12px;
+ }
+
+ .page-info {
+ flex: 1;
+
+ .page-title {
+ display: block;
+ font-size: 26px;
+ color: #333;
+ margin-bottom: 4px;
+ }
+
+ .page-path {
+ font-size: 22px;
+ color: #999;
+ }
+ }
+
+ .page-stats {
+ display: flex;
+ gap: 16px;
+
+ .page-stat {
+ text-align: center;
+
+ .stat-num {
+ display: block;
+ font-size: 26px;
+ font-weight: 500;
+ color: #333;
+ }
+
+ .stat-label {
+ font-size: 20px;
+ color: #999;
+ }
+ }
+ }
+}
+
+// 空数据
+.empty-data {
+ text-align: center;
+ padding: 48px;
+ font-size: 26px;
+ color: #999;
+}
diff --git a/src/developer/app/[id]/analytics.tsx b/src/developer/app/[id]/analytics.tsx
new file mode 100644
index 0000000..0c88c75
--- /dev/null
+++ b/src/developer/app/[id]/analytics.tsx
@@ -0,0 +1,425 @@
+/**
+ * 运营监控页面 - 数据分析看板
+ */
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { AtTabs, AtTabsPane, AtTag, AtActivityIndicator } from 'taro-ui'
+import {
+ getOverviewStats,
+ getRealtimeData,
+ getPerformanceMetrics,
+ getDeviceStats,
+ getSourceStats,
+ getPageStats,
+ getRegionStats,
+} from '../../../../api/analytics'
+import type { OverviewStats, PerformanceMetric, DeviceStat, SourceStat, PageStat, RegionStat } from '../../../../types/analytics'
+import './analytics.scss'
+
+// 格式化数字
+const formatNumber = (num?: number) => {
+ if (!num) return '0'
+ if (num >= 100000000) return (num / 100000000).toFixed(1) + '亿'
+ if (num >= 10000) return (num / 10000).toFixed(1) + '万'
+ return num.toLocaleString()
+}
+
+// 格式化百分比
+const formatPercent = (num?: number) => {
+ if (!num) return '0%'
+ return num.toFixed(1) + '%'
+}
+
+export default function Analytics() {
+ const [loading, setLoading] = useState(true)
+ const [realtime, setRealtime] = useState({
+ uv: 0,
+ pv: 0,
+ apiCalls: 0,
+ errors: 0,
+ activeUsers: 0,
+ })
+ const [overview, setOverview] = useState({})
+ const [metrics, setMetrics] = useState([])
+ const [devices, setDevices] = useState([])
+ const [sources, setSources] = useState([])
+ const [pages, setPages] = useState([])
+ const [regions, setRegions] = useState([])
+ const [activeTab, setActiveTab] = useState(0)
+ const [appId, setAppId] = useState('')
+
+ useEffect(() => {
+ const pages = Taro.getCurrentPages()
+ const current = pages[pages.length - 1]
+ if (current?.options?.appId) {
+ setAppId(current.options.appId)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (appId) {
+ fetchData()
+ // 实时数据每 30 秒刷新
+ const timer = setInterval(() => {
+ fetchRealtimeData()
+ }, 30000)
+ return () => clearInterval(timer)
+ }
+ }, [appId])
+
+ // 获取所有数据
+ const fetchData = async () => {
+ try {
+ setLoading(true)
+ await Promise.all([
+ fetchRealtimeData(),
+ fetchOverviewData(),
+ fetchMetricsData(),
+ fetchDeviceData(),
+ fetchSourceData(),
+ fetchPageData(),
+ fetchRegionData(),
+ ])
+ } catch (err) {
+ console.error('获取数据失败', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 获取实时数据
+ const fetchRealtimeData = async () => {
+ try {
+ const res = await getRealtimeData(Number(appId))
+ if (res.data) {
+ setRealtime(res.data)
+ }
+ } catch (err) {
+ console.error('获取实时数据失败', err)
+ }
+ }
+
+ // 获取概览数据
+ const fetchOverviewData = async () => {
+ try {
+ const res = await getOverviewStats(Number(appId))
+ if (res.data) {
+ setOverview(res.data)
+ }
+ } catch (err) {
+ console.error('获取概览数据失败', err)
+ }
+ }
+
+ // 获取性能指标
+ const fetchMetricsData = async () => {
+ try {
+ const res = await getPerformanceMetrics(Number(appId))
+ if (res.data) {
+ setMetrics(res.data.list || [])
+ }
+ } catch (err) {
+ console.error('获取性能指标失败', err)
+ }
+ }
+
+ // 获取设备分布
+ const fetchDeviceData = async () => {
+ try {
+ const res = await getDeviceStats(Number(appId))
+ if (res.data) {
+ setDevices(res.data.list || [])
+ }
+ } catch (err) {
+ console.error('获取设备数据失败', err)
+ }
+ }
+
+ // 获取来源分布
+ const fetchSourceData = async () => {
+ try {
+ const res = await getSourceStats(Number(appId))
+ if (res.data) {
+ setSources(res.data.list || [])
+ }
+ } catch (err) {
+ console.error('获取来源数据失败', err)
+ }
+ }
+
+ // 获取页面访问
+ const fetchPageData = async () => {
+ try {
+ const res = await getPageStats({
+ websiteId: Number(appId),
+ limit: 10,
+ })
+ if (res.data) {
+ setPages(res.data.list || [])
+ }
+ } catch (err) {
+ console.error('获取页面数据失败', err)
+ }
+ }
+
+ // 获取区域分布
+ const fetchRegionData = async () => {
+ try {
+ const res = await getRegionStats(Number(appId))
+ if (res.data) {
+ setRegions(res.data.list || [])
+ }
+ } catch (err) {
+ console.error('获取区域数据失败', err)
+ }
+ }
+
+ const tabs = [
+ { title: '概览' },
+ { title: '性能' },
+ { title: '用户' },
+ { title: '页面' },
+ ]
+
+ return (
+
+ {loading && realtime.uv === 0 ? (
+
+
+ 加载中...
+
+ ) : (
+ <>
+ {/* 实时数据 */}
+
+
+ 实时数据
+
+
+ LIVE
+
+
+
+
+ {formatNumber(realtime.uv)}
+ 实时 UV
+
+
+ {formatNumber(realtime.pv)}
+ 实时 PV
+
+
+ {formatNumber(realtime.apiCalls)}
+ API 调用
+
+
+ 0 ? 'error' : ''}`}>
+ {formatNumber(realtime.errors)}
+
+ 错误数
+
+
+
+
+ {/* 今日概览 */}
+
+
+ {formatNumber(overview.todayUv)}
+ 今日 UV
+
+
+ {formatNumber(overview.todayPv)}
+ 今日 PV
+
+
+ {formatNumber(overview.todayApiCalls)}
+ API 调用
+
+
+
+ {/* Tab 切换 */}
+ setActiveTab(index)}
+ >
+
+
+ {/* 趋势图表区 */}
+
+ 数据趋势
+
+
+ 趋势图表
+ 可集成 ECharts 展示数据趋势
+
+
+
+ {/* 设备分布 */}
+
+ 设备分布
+ {devices.length > 0 ? (
+
+ {devices.map((device, index) => (
+
+
+ {device.device}
+ {formatPercent(device.percentage)}
+
+ ))}
+
+ ) : (
+ 暂无数据
+ )}
+
+
+
+
+
+
+ {/* 性能指标 */}
+
+ 核心指标
+ {metrics.length > 0 ? (
+ metrics.map((metric, index) => (
+
+
+ {metric.metric}
+
+ {metric.value?.toFixed(2)}
+ {metric.unit}
+
+ {metric.trend === 'up' ? '↑' : metric.trend === 'down' ? '↓' : '→'}
+ {Math.abs(metric.change || 0)}%
+
+
+
+
+ ))
+ ) : (
+ 暂无数据
+ )}
+
+
+ {/* 错误统计 */}
+
+ 错误统计
+
+
+ {overview.todayErrors || 0}
+ 今日错误
+
+
+ 0
+ 严重错误
+
+
+
+
+
+
+
+
+ {/* 用户统计 */}
+
+
+ {formatNumber(overview.activeUsers)}
+ 活跃用户
+
+
+ {formatNumber(overview.newUsers)}
+ 新增用户
+
+
+ {formatPercent(overview.retention)}
+ 留存率
+
+
+
+ {/* 区域分布 */}
+
+ 地域分布
+ {regions.length > 0 ? (
+ regions.slice(0, 5).map((region, index) => (
+
+ {index + 1}
+ {region.region}
+
+
+
+ {formatPercent(region.percentage)}
+
+ ))
+ ) : (
+ 暂无数据
+ )}
+
+
+ {/* 来源分布 */}
+
+ 流量来源
+ {sources.length > 0 ? (
+ sources.map((source, index) => (
+
+ {index + 1}
+ {source.source}
+ {formatPercent(source.percentage)}
+
+ ))
+ ) : (
+ 暂无数据
+ )}
+
+
+
+
+
+
+ {/* 页面排行 */}
+
+ 页面访问排行
+ {pages.length > 0 ? (
+ pages.map((page, index) => (
+
+ {index + 1}
+
+ {page.title || page.path}
+ {page.path}
+
+
+
+ {formatNumber(page.pv)}
+ PV
+
+
+ {formatNumber(page.uv)}
+ UV
+
+
+
+ ))
+ ) : (
+ 暂无数据
+ )}
+
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/src/developer/app/[id]/build/[id].config.ts b/src/developer/app/[id]/build/[id].config.ts
new file mode 100644
index 0000000..6dc5312
--- /dev/null
+++ b/src/developer/app/[id]/build/[id].config.ts
@@ -0,0 +1,6 @@
+/**
+ * 构建详情页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '构建详情',
+})
diff --git a/src/developer/app/[id]/build/[id].scss b/src/developer/app/[id]/build/[id].scss
new file mode 100644
index 0000000..c4c490d
--- /dev/null
+++ b/src/developer/app/[id]/build/[id].scss
@@ -0,0 +1,261 @@
+.build-detail {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 24px;
+
+ &.loading-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 300px;
+
+ .loading-text {
+ margin-top: 16px;
+ font-size: 28px;
+ color: #999;
+ }
+ }
+}
+
+.status-card {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+
+ .status-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+
+ .build-no {
+ font-size: 36px;
+ font-weight: 600;
+ color: #333;
+ }
+ }
+
+ .status-info {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+ margin-bottom: 24px;
+
+ .info-item {
+ .label {
+ display: block;
+ font-size: 24px;
+ color: #999;
+ margin-bottom: 4px;
+ }
+
+ .value {
+ font-size: 28px;
+ color: #333;
+
+ &.commit {
+ font-family: monospace;
+ color: #2196F3;
+ }
+ }
+ }
+ }
+
+ .commit-message {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ margin-bottom: 16px;
+
+ .iconfont {
+ color: #2196F3;
+ flex-shrink: 0;
+ }
+
+ .msg {
+ flex: 1;
+ font-size: 26px;
+ color: #666;
+ line-height: 1.5;
+ }
+ }
+
+ .meta {
+ display: flex;
+ justify-content: space-between;
+ font-size: 24px;
+ color: #999;
+ }
+}
+
+.actions {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 24px;
+
+ .at-button {
+ flex: 1;
+ }
+}
+
+.tabs {
+ display: flex;
+ background: #fff;
+ border-radius: 16px;
+ overflow: hidden;
+ margin-bottom: 24px;
+
+ .tab {
+ flex: 1;
+ text-align: center;
+ padding: 24px;
+ font-size: 28px;
+ color: #666;
+ position: relative;
+
+ &.active {
+ color: #1890ff;
+ font-weight: 500;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 60px;
+ height: 4px;
+ background: #1890ff;
+ border-radius: 2px;
+ }
+ }
+ }
+}
+
+.tab-content {
+ height: calc(100vh - 600px);
+}
+
+.info-panel {
+ .info-section {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+
+ .section-title {
+ display: block;
+ font-size: 30px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 20px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #eee;
+ }
+
+ .info-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 0;
+
+ .row-label {
+ font-size: 26px;
+ color: #666;
+ }
+
+ .row-value {
+ font-size: 26px;
+ color: #333;
+
+ &.commit {
+ font-family: monospace;
+ color: #2196F3;
+ max-width: 400px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+ }
+}
+
+.logs-panel {
+ background: #1e1e1e;
+ border-radius: 16px;
+ padding: 24px;
+ min-height: 500px;
+
+ .logs-content {
+ .log-line {
+ display: block;
+ font-family: 'Courier New', monospace;
+ font-size: 22px;
+ color: #d4d4d4;
+ line-height: 1.8;
+ word-break: break-all;
+ }
+ }
+
+ .loading-logs {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 300px;
+ color: #d4d4d4;
+ font-size: 26px;
+ gap: 16px;
+ }
+}
+
+.artifacts-panel {
+ .artifact-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 16px;
+
+ .artifact-info {
+ flex: 1;
+
+ .artifact-name {
+ display: block;
+ font-size: 28px;
+ color: #333;
+ margin-bottom: 8px;
+ }
+
+ .artifact-size {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+ }
+
+ .empty-artifacts {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 100px 0;
+ color: #999;
+
+ .iconfont {
+ font-size: 96px;
+ margin-bottom: 24px;
+ }
+
+ text {
+ font-size: 28px;
+ }
+ }
+}
diff --git a/src/developer/app/[id]/build/[id].tsx b/src/developer/app/[id]/build/[id].tsx
new file mode 100644
index 0000000..2be78c4
--- /dev/null
+++ b/src/developer/app/[id]/build/[id].tsx
@@ -0,0 +1,325 @@
+/**
+ * 构建详情页面
+ */
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView, Button } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { AtButton, AtTag, AtActivityIndicator, AtTimeline } from 'taro-ui'
+import { getBuild, getBuildLogs, triggerBuild } from '../../../../../api/cicd'
+import type { Build, BuildStatus } from '../../../../../types/cicd'
+import './build-detail.scss'
+
+// 状态映射
+const STATUS_MAP: Record = {
+ pending: { text: '等待中', color: '#FFC107' },
+ running: { text: '构建中', color: '#2196F3' },
+ success: { text: '构建成功', color: '#4CAF50' },
+ failed: { text: '构建失败', color: '#F44336' },
+ cancelled: { text: '已取消', color: '#9E9E9E' },
+}
+
+export default function BuildDetail() {
+ const [loading, setLoading] = useState(true)
+ const [build, setBuild] = useState({})
+ const [logs, setLogs] = useState('')
+ const [activeTab, setActiveTab] = useState('info')
+ const [buildId, setBuildId] = useState('')
+
+ useEffect(() => {
+ const pages = Taro.getCurrentPages()
+ const current = pages[pages.length - 1]
+ if (current?.options?.id) {
+ setBuildId(current.options.id)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (buildId) {
+ fetchBuildDetail()
+ }
+ }, [buildId])
+
+ // 获取构建详情
+ const fetchBuildDetail = async () => {
+ try {
+ const res = await getBuild(Number(buildId))
+ if (res.data) {
+ setBuild(res.data)
+ }
+ } catch (err) {
+ console.error('获取构建详情失败', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 获取构建日志
+ const fetchBuildLogs = async () => {
+ try {
+ const res = await getBuildLogs(Number(buildId))
+ if (res.data) {
+ setLogs(res.data.logs || '')
+ }
+ } catch (err) {
+ console.error('获取构建日志失败', err)
+ }
+ }
+
+ // 切换 Tab
+ useEffect(() => {
+ if (activeTab === 'logs' && !logs) {
+ fetchBuildLogs()
+ }
+ }, [activeTab])
+
+ // 重新触发构建
+ const handleRetryBuild = async () => {
+ Taro.showModal({
+ title: '提示',
+ content: '确定要重新构建吗?',
+ success: async (res) => {
+ if (res.confirm && build.websiteId) {
+ try {
+ Taro.showLoading({ title: '重新构建中...' })
+ await triggerBuild({
+ websiteId: build.websiteId!,
+ commitId: build.commitId,
+ branch: build.branch,
+ })
+ Taro.showToast({ title: '构建已触发', icon: 'success' })
+ fetchBuildDetail()
+ } catch (err) {
+ Taro.showToast({ title: '触发失败', icon: 'none' })
+ } finally {
+ Taro.hideLoading()
+ }
+ }
+ },
+ })
+ }
+
+ // 部署
+ const handleDeploy = () => {
+ Taro.navigateTo({
+ url: `/developer/app/${build.websiteId}/deploys?buildId=${buildId}`,
+ })
+ }
+
+ // 格式化时间
+ const formatTime = (time?: string) => {
+ if (!time) return '-'
+ const date = new Date(time)
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
+ }
+
+ // 格式化时长
+ const formatDuration = (seconds?: number) => {
+ if (!seconds) return '-'
+ if (seconds < 60) return `${seconds} 秒`
+ const mins = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return `${mins} 分 ${secs} 秒`
+ }
+
+ if (loading) {
+ return (
+
+
+ 加载中...
+
+ )
+ }
+
+ const statusInfo = STATUS_MAP[build.status || 'pending']
+
+ return (
+
+ {/* 构建状态 */}
+
+
+ #{build.buildNo || build.id}
+
+ {statusInfo.text}
+
+
+
+
+
+ 分支
+ {build.branch || 'main'}
+
+
+ 提交
+ {build.commitId?.slice(0, 7)}
+
+
+ 触发方式
+ {build.trigger || 'manual'}
+
+
+ 耗时
+ {formatDuration(build.duration)}
+
+
+
+
+
+ {build.commitMessage}
+
+
+
+ {build.author}
+ {formatTime(build.startTime)}
+
+
+
+ {/* 操作按钮 */}
+
+ {build.status === 'failed' && (
+
+ 重新构建
+
+ )}
+ {build.status === 'success' && (
+
+ 部署
+
+ )}
+
+
+ {/* Tab 切换 */}
+
+ setActiveTab('info')}
+ >
+ 构建信息
+
+ setActiveTab('logs')}
+ >
+ 构建日志
+
+ setActiveTab('artifacts')}
+ >
+ 构建产物
+
+
+
+ {/* Tab 内容 */}
+
+ {activeTab === 'info' && (
+
+
+ 基本信息
+
+ 构建编号
+ {build.buildNo}
+
+
+ 构建 ID
+ {build.id}
+
+
+ 项目 ID
+ {build.websiteId}
+
+
+
+
+ Git 信息
+
+ 分支
+ {build.branch}
+
+
+ Commit ID
+ {build.commitId}
+
+
+ 提交者
+ {build.author}
+
+
+
+
+ 时间信息
+
+ 开始时间
+ {formatTime(build.startTime)}
+
+
+ 结束时间
+ {formatTime(build.endTime)}
+
+
+ 总耗时
+ {formatDuration(build.duration)}
+
+
+
+ )}
+
+ {activeTab === 'logs' && (
+
+ {logs ? (
+
+ {logs.split('\n').map((line, index) => (
+
+ {line}
+
+ ))}
+
+ ) : (
+
+
+ 加载日志中...
+
+ )}
+
+ )}
+
+ {activeTab === 'artifacts' && (
+
+ {build.artifacts && build.artifacts.length > 0 ? (
+ build.artifacts.map((artifact, index) => (
+
+
+ {artifact.name}
+
+ {(artifact.size || 0) > 1024 * 1024
+ ? `${((artifact.size || 0) / 1024 / 1024).toFixed(2)} MB`
+ : `${((artifact.size || 0) / 1024).toFixed(2)} KB`}
+
+
+ {artifact.downloadUrl && (
+ {
+ if (artifact.downloadUrl) {
+ Taro.setClipboardData({ data: artifact.downloadUrl })
+ Taro.showToast({ title: '链接已复制', icon: 'success' })
+ }
+ }}
+ >
+ 复制链接
+
+ )}
+
+ ))
+ ) : (
+
+
+ 暂无构建产物
+
+ )}
+
+ )}
+
+
+ )
+}
diff --git a/src/developer/app/[id]/builds.config.ts b/src/developer/app/[id]/builds.config.ts
new file mode 100644
index 0000000..1fe2e14
--- /dev/null
+++ b/src/developer/app/[id]/builds.config.ts
@@ -0,0 +1,6 @@
+/**
+ * 构建列表页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '构建历史',
+})
diff --git a/src/developer/app/[id]/builds.scss b/src/developer/app/[id]/builds.scss
new file mode 100644
index 0000000..ff2370d
--- /dev/null
+++ b/src/developer/app/[id]/builds.scss
@@ -0,0 +1,154 @@
+.builds-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+}
+
+.search-bar {
+ display: flex;
+ align-items: center;
+ padding: 24px;
+ gap: 16px;
+ background: #fff;
+ border-bottom: 1px solid #eee;
+
+ .search-input-wrap {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ height: 64px;
+ padding: 0 24px;
+ background: #f5f5f5;
+ border-radius: 32px;
+
+ .iconfont {
+ margin-right: 12px;
+ color: #999;
+ }
+
+ .search-input {
+ flex: 1;
+ font-size: 28px;
+ }
+ }
+}
+
+.build-list {
+ height: calc(100vh - 280px);
+ padding: 24px;
+}
+
+.build-item {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+
+ .build-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+
+ .build-no {
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ }
+ }
+
+ .build-info {
+ margin-bottom: 16px;
+
+ .build-branch {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ font-size: 26px;
+ color: #666;
+
+ .iconfont {
+ color: #4CAF50;
+ }
+ }
+
+ .build-commit {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 24px;
+ color: #999;
+
+ .iconfont {
+ color: #2196F3;
+ }
+
+ .commit-id {
+ font-family: monospace;
+ color: #666;
+ }
+
+ .commit-msg {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ }
+
+ .build-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 16px;
+ border-top: 1px solid #eee;
+
+ .build-meta {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ font-size: 24px;
+ color: #999;
+
+ .author {
+ color: #666;
+ }
+ }
+ }
+}
+
+.loading-wrap,
+.empty-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 100px 0;
+
+ .iconfont {
+ font-size: 96px;
+ color: #ccc;
+ }
+
+ .loading-text,
+ .empty-text {
+ margin-top: 24px;
+ font-size: 28px;
+ color: #999;
+ }
+}
+
+.loading-more {
+ display: flex;
+ justify-content: center;
+ padding: 24px;
+}
+
+.no-more {
+ text-align: center;
+ padding: 24px;
+ font-size: 24px;
+ color: #999;
+}
diff --git a/src/developer/app/[id]/builds.tsx b/src/developer/app/[id]/builds.tsx
new file mode 100644
index 0000000..8e4137d
--- /dev/null
+++ b/src/developer/app/[id]/builds.tsx
@@ -0,0 +1,269 @@
+/**
+ * 构建列表页面
+ */
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView, Input } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { AtButton, AtTabs, AtTabsPane, AtTag, AtActivityIndicator } from 'taro-ui'
+import { pageBuild, triggerBuild, cancelBuild } from '../../../../api/cicd'
+import type { Build, BuildStatus } from '../../../../types/cicd'
+import './builds.scss'
+
+// 状态映射
+const STATUS_MAP: Record = {
+ pending: { text: '等待中', color: '#FFC107' },
+ running: { text: '构建中', color: '#2196F3' },
+ success: { text: '构建成功', color: '#4CAF50' },
+ failed: { text: '构建失败', color: '#F44336' },
+ cancelled: { text: '已取消', color: '#9E9E9E' },
+}
+
+export default function Builds() {
+ const [loading, setLoading] = useState(true)
+ const [builds, setBuilds] = useState([])
+ const [total, setTotal] = useState(0)
+ const [page, setPage] = useState(1)
+ const [hasMore, setHasMore] = useState(true)
+ const [status, setStatus] = useState('')
+ const [keyword, setKeyword] = useState('')
+ const [appId, setAppId] = useState('')
+
+ // 获取构建列表
+ const fetchBuilds = async (pageNum = 1, reset = false) => {
+ try {
+ const res = await pageBuild({
+ page: pageNum,
+ limit: 20,
+ websiteId: Number(appId),
+ status: status || undefined,
+ })
+
+ if (res.data) {
+ if (reset) {
+ setBuilds(res.data.list || [])
+ } else {
+ setBuilds(prev => [...prev, ...(res.data?.list || [])])
+ }
+ setTotal(res.data.total || 0)
+ setHasMore((res.data.list || []).length === 20)
+ setPage(pageNum)
+ }
+ } catch (err) {
+ console.error('获取构建列表失败', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ const pages = Taro.getCurrentPages()
+ const current = pages[pages.length - 1]
+ if (current?.options?.appId) {
+ setAppId(current.options.appId)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (appId) {
+ fetchBuilds(1, true)
+ }
+ }, [appId, status])
+
+ // 触发构建
+ const handleTriggerBuild = async () => {
+ if (!appId) return
+
+ Taro.showModal({
+ title: '提示',
+ content: '确定要触发新的构建吗?',
+ success: async (res) => {
+ if (res.confirm) {
+ try {
+ Taro.showLoading({ title: '触发中...' })
+ await triggerBuild({ websiteId: Number(appId) })
+ Taro.showToast({ title: '构建已触发', icon: 'success' })
+ fetchBuilds(1, true)
+ } catch (err) {
+ Taro.showToast({ title: '触发失败', icon: 'none' })
+ } finally {
+ Taro.hideLoading()
+ }
+ }
+ },
+ })
+ }
+
+ // 取消构建
+ const handleCancelBuild = async (buildId: number) => {
+ Taro.showModal({
+ title: '提示',
+ content: '确定要取消该构建吗?',
+ success: async (res) => {
+ if (res.confirm) {
+ try {
+ Taro.showLoading({ title: '取消中...' })
+ await cancelBuild(buildId)
+ Taro.showToast({ title: '已取消', icon: 'success' })
+ fetchBuilds(page, true)
+ } catch (err) {
+ Taro.showToast({ title: '取消失败', icon: 'none' })
+ } finally {
+ Taro.hideLoading()
+ }
+ }
+ },
+ })
+ }
+
+ // 查看构建详情
+ const goToBuildDetail = (buildId: number) => {
+ Taro.navigateTo({
+ url: `/developer/app/${appId}/build/${buildId}`,
+ })
+ }
+
+ // 加载更多
+ const handleLoadMore = () => {
+ if (hasMore && !loading) {
+ fetchBuilds(page + 1)
+ }
+ }
+
+ // 格式化时间
+ const formatTime = (time?: string) => {
+ if (!time) return '-'
+ const date = new Date(time)
+ return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
+ }
+
+ // 格式化时长
+ const formatDuration = (seconds?: number) => {
+ if (!seconds) return '-'
+ if (seconds < 60) return `${seconds}s`
+ const mins = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return `${mins}m ${secs}s`
+ }
+
+ const tabs = [
+ { title: '全部' },
+ { title: '等待中' },
+ { title: '构建中' },
+ { title: '成功' },
+ { title: '失败' },
+ ]
+
+ return (
+
+ {/* 搜索栏 */}
+
+
+
+ setKeyword(e.detail.value)}
+ />
+
+
+ 触发构建
+
+
+
+ {/* 状态筛选 */}
+ {
+ if (status === '') return t.title === '全部'
+ return STATUS_MAP[status]?.text === t.title
+ })}
+ tabList={tabs}
+ scroll
+ onClick={(index) => {
+ const statusMap: (BuildStatus | '')[] = ['', 'pending', 'running', 'success', 'failed']
+ setStatus(statusMap[index])
+ }}
+ >
+
+
+ {loading && builds.length === 0 ? (
+
+
+ 加载中...
+
+ ) : builds.length === 0 ? (
+
+
+ 暂无构建记录
+
+ ) : (
+ builds.map((build) => (
+ build.id && goToBuildDetail(build.id)}
+ >
+
+ #{build.buildNo || build.id}
+
+ {STATUS_MAP[build.status || 'pending'].text}
+
+
+
+
+
+
+ {build.branch || 'main'}
+
+
+
+ {build.commitId?.slice(0, 7)}
+ {build.commitMessage}
+
+
+
+
+
+ {build.author}
+ {formatTime(build.createTime)}
+ {formatDuration(build.duration)}
+
+ {build.status === 'pending' || build.status === 'running' ? (
+ {
+ e.stopPropagation()
+ build.id && handleCancelBuild(build.id)
+ }}
+ >
+ 取消
+
+ ) : null}
+
+
+ ))
+ )}
+
+ {loading && builds.length > 0 && (
+
+
+
+ )}
+
+ {!hasMore && builds.length > 0 && (
+ 没有更多了
+ )}
+
+
+
+
+ )
+}
diff --git a/src/developer/app/[id]/config.config.ts b/src/developer/app/[id]/config.config.ts
new file mode 100644
index 0000000..ea9112e
--- /dev/null
+++ b/src/developer/app/[id]/config.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '应用配置',
+})
diff --git a/src/developer/app/[id]/config.scss b/src/developer/app/[id]/config.scss
new file mode 100644
index 0000000..ad5dcc5
--- /dev/null
+++ b/src/developer/app/[id]/config.scss
@@ -0,0 +1,10 @@
+.config-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/developer/app/[id]/config.tsx b/src/developer/app/[id]/config.tsx
new file mode 100644
index 0000000..6aa8c62
--- /dev/null
+++ b/src/developer/app/[id]/config.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './config.scss'
+
+const ConfigPage: React.FC = () => {
+ return (
+
+
+ 应用配置功能开发中...
+
+
+ )
+}
+
+export default ConfigPage
diff --git a/src/developer/app/[id]/deploy/[id].config.ts b/src/developer/app/[id]/deploy/[id].config.ts
new file mode 100644
index 0000000..b2e408e
--- /dev/null
+++ b/src/developer/app/[id]/deploy/[id].config.ts
@@ -0,0 +1,6 @@
+/**
+ * 部署详情页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '部署详情',
+})
diff --git a/src/developer/app/[id]/deploy/[id].scss b/src/developer/app/[id]/deploy/[id].scss
new file mode 100644
index 0000000..ce55f54
--- /dev/null
+++ b/src/developer/app/[id]/deploy/[id].scss
@@ -0,0 +1,237 @@
+.deploy-detail {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 24px;
+
+ &.loading-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 300px;
+
+ .loading-text {
+ margin-top: 16px;
+ font-size: 28px;
+ color: #999;
+ }
+ }
+}
+
+.status-card {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+
+ .status-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 32px;
+
+ .version-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .version {
+ font-size: 40px;
+ font-weight: 600;
+ color: #333;
+ }
+ }
+ }
+
+ .status-progress {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 32px;
+
+ .progress-step {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .step-dot {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: #e0e0e0;
+ margin-bottom: 8px;
+
+ &.active {
+ background: #4CAF50;
+ }
+ }
+
+ .step-label {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+
+ .progress-line {
+ width: 100px;
+ height: 4px;
+ background: #e0e0e0;
+ margin: 0 16px;
+ margin-bottom: 32px;
+
+ &.active {
+ background: #4CAF50;
+ }
+ }
+ }
+
+ .deploy-meta {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+
+ .meta-item {
+ .label {
+ display: block;
+ font-size: 24px;
+ color: #999;
+ margin-bottom: 4px;
+ }
+
+ .value {
+ font-size: 28px;
+ color: #333;
+ }
+ }
+ }
+
+ .rollback-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 24px;
+ padding: 16px;
+ background: #FFF3E0;
+ border-radius: 8px;
+ font-size: 26px;
+ color: #FF9800;
+
+ .iconfont {
+ font-size: 28px;
+ }
+ }
+}
+
+.actions {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 24px;
+
+ .at-button {
+ flex: 1;
+ }
+}
+
+.tabs {
+ display: flex;
+ background: #fff;
+ border-radius: 16px;
+ overflow: hidden;
+ margin-bottom: 24px;
+
+ .tab {
+ flex: 1;
+ text-align: center;
+ padding: 24px;
+ font-size: 28px;
+ color: #666;
+ position: relative;
+
+ &.active {
+ color: #1890ff;
+ font-weight: 500;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 60px;
+ height: 4px;
+ background: #1890ff;
+ border-radius: 2px;
+ }
+ }
+ }
+}
+
+.tab-content {
+ height: calc(100vh - 600px);
+}
+
+.info-panel {
+ .info-section {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+
+ .section-title {
+ display: block;
+ font-size: 30px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 20px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #eee;
+ }
+
+ .info-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 0;
+
+ .row-label {
+ font-size: 26px;
+ color: #666;
+ }
+
+ .row-value {
+ font-size: 26px;
+ color: #333;
+ }
+ }
+ }
+}
+
+.logs-panel {
+ background: #1e1e1e;
+ border-radius: 16px;
+ padding: 24px;
+ min-height: 500px;
+
+ .logs-content {
+ .log-line {
+ display: block;
+ font-family: 'Courier New', monospace;
+ font-size: 22px;
+ color: #d4d4d4;
+ line-height: 1.8;
+ word-break: break-all;
+ }
+ }
+
+ .loading-logs {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 300px;
+ color: #d4d4d4;
+ font-size: 26px;
+ gap: 16px;
+ }
+}
diff --git a/src/developer/app/[id]/deploy/[id].tsx b/src/developer/app/[id]/deploy/[id].tsx
new file mode 100644
index 0000000..cff8a03
--- /dev/null
+++ b/src/developer/app/[id]/deploy/[id].tsx
@@ -0,0 +1,304 @@
+/**
+ * 部署详情页面
+ */
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { AtButton, AtTag, AtActivityIndicator } from 'taro-ui'
+import { getDeploy, getDeployLogs, rollbackDeploy } from '../../../../../api/cicd'
+import type { Deploy, DeployStatus, DeployEnv } from '../../../../../types/cicd'
+import './deploy-detail.scss'
+
+// 状态映射
+const STATUS_MAP: Record = {
+ pending: { text: '等待中', color: '#FFC107' },
+ deploying: { text: '部署中', color: '#2196F3' },
+ success: { text: '部署成功', color: '#4CAF50' },
+ failed: { text: '部署失败', color: '#F44336' },
+ rollback: { text: '回滚中', color: '#FF9800' },
+}
+
+// 环境映射
+const ENV_MAP: Record = {
+ development: { text: '开发环境', color: '#9E9E9E' },
+ staging: { text: '预发布环境', color: '#FF9800' },
+ production: { text: '生产环境', color: '#4CAF50' },
+}
+
+export default function DeployDetail() {
+ const [loading, setLoading] = useState(true)
+ const [deploy, setDeploy] = useState({})
+ const [logs, setLogs] = useState('')
+ const [activeTab, setActiveTab] = useState('info')
+ const [deployId, setDeployId] = useState('')
+
+ useEffect(() => {
+ const pages = Taro.getCurrentPages()
+ const current = pages[pages.length - 1]
+ if (current?.options?.id) {
+ setDeployId(current.options.id)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (deployId) {
+ fetchDeployDetail()
+ }
+ }, [deployId])
+
+ // 获取部署详情
+ const fetchDeployDetail = async () => {
+ try {
+ const res = await getDeploy(Number(deployId))
+ if (res.data) {
+ setDeploy(res.data)
+ }
+ } catch (err) {
+ console.error('获取部署详情失败', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 获取部署日志
+ const fetchDeployLogs = async () => {
+ try {
+ const res = await getDeployLogs(Number(deployId))
+ if (res.data) {
+ setLogs(res.data.logs || '')
+ }
+ } catch (err) {
+ console.error('获取部署日志失败', err)
+ }
+ }
+
+ // 切换 Tab
+ useEffect(() => {
+ if (activeTab === 'logs' && !logs) {
+ fetchDeployLogs()
+ }
+ }, [activeTab])
+
+ // 回滚部署
+ const handleRollback = async () => {
+ Taro.showModal({
+ title: '确认回滚',
+ content: `确定要回滚到版本 ${deploy.previousVersion || '上一个版本'} 吗?`,
+ success: async (res) => {
+ if (res.confirm) {
+ try {
+ Taro.showLoading({ title: '回滚中...' })
+ await rollbackDeploy(Number(deployId))
+ Taro.showToast({ title: '回滚成功', icon: 'success' })
+ fetchDeployDetail()
+ } catch (err) {
+ Taro.showToast({ title: '回滚失败', icon: 'none' })
+ } finally {
+ Taro.hideLoading()
+ }
+ }
+ },
+ })
+ }
+
+ // 格式化时间
+ const formatTime = (time?: string) => {
+ if (!time) return '-'
+ const date = new Date(time)
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
+ }
+
+ // 格式化时长
+ const formatDuration = (seconds?: number) => {
+ if (!seconds) return '-'
+ if (seconds < 60) return `${seconds} 秒`
+ const mins = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return `${mins} 分 ${secs} 秒`
+ }
+
+ if (loading) {
+ return (
+
+
+ 加载中...
+
+ )
+ }
+
+ const statusInfo = STATUS_MAP[deploy.status || 'pending']
+ const envInfo = ENV_MAP[deploy.env || 'staging']
+
+ return (
+
+ {/* 部署状态卡片 */}
+
+
+
+ v{deploy.version}
+
+ {envInfo.text}
+
+
+
+ {statusInfo.text}
+
+
+
+
+
+
+ 等待
+
+
+
+
+ 部署
+
+
+
+
+ 完成
+
+
+
+
+
+ 构建版本
+ #{deploy.buildNo}
+
+
+ 部署人
+ {deploy.deployer}
+
+
+ 开始时间
+ {formatTime(deploy.startTime)}
+
+
+ 总耗时
+ {formatDuration(deploy.duration)}
+
+
+
+ {deploy.previousVersion && (
+
+
+ 从 v{deploy.previousVersion} 回滚
+
+ )}
+
+
+ {/* 操作按钮 */}
+
+ {deploy.status === 'success' && (
+
+ 回滚到此版本
+
+ )}
+ {
+ Taro.navigateBack()
+ }}
+ >
+ 返回列表
+
+
+
+ {/* Tab 切换 */}
+
+ setActiveTab('info')}
+ >
+ 部署信息
+
+ setActiveTab('logs')}
+ >
+ 部署日志
+
+
+
+ {/* Tab 内容 */}
+
+ {activeTab === 'info' && (
+
+
+ 基本信息
+
+ 部署 ID
+ {deploy.id}
+
+
+ 项目 ID
+ {deploy.websiteId}
+
+
+ 构建 ID
+ {deploy.buildId}
+
+
+ 版本号
+ v{deploy.version}
+
+
+
+
+ 时间信息
+
+ 开始时间
+ {formatTime(deploy.startTime)}
+
+
+ 结束时间
+ {formatTime(deploy.endTime)}
+
+
+ 总耗时
+ {formatDuration(deploy.duration)}
+
+
+
+ {deploy.previousVersion && (
+
+ 回滚信息
+
+ 回滚源版本
+ v{deploy.previousVersion}
+
+
+ 回滚至版本
+ v{deploy.version}
+
+
+ )}
+
+ )}
+
+ {activeTab === 'logs' && (
+
+ {logs ? (
+
+ {logs.split('\n').map((line, index) => (
+
+ {line}
+
+ ))}
+
+ ) : (
+
+
+ 加载日志中...
+
+ )}
+
+ )}
+
+
+ )
+}
diff --git a/src/developer/app/[id]/deploys.config.ts b/src/developer/app/[id]/deploys.config.ts
new file mode 100644
index 0000000..7e340d2
--- /dev/null
+++ b/src/developer/app/[id]/deploys.config.ts
@@ -0,0 +1,6 @@
+/**
+ * 部署列表页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '部署历史',
+})
diff --git a/src/developer/app/[id]/deploys.scss b/src/developer/app/[id]/deploys.scss
new file mode 100644
index 0000000..d297b0c
--- /dev/null
+++ b/src/developer/app/[id]/deploys.scss
@@ -0,0 +1,229 @@
+.deploys-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+}
+
+.stats-bar {
+ display: flex;
+ justify-content: space-around;
+ padding: 24px;
+ background: #fff;
+ border-bottom: 1px solid #eee;
+
+ .stat-item {
+ text-align: center;
+
+ .stat-num {
+ display: block;
+ font-size: 40px;
+ font-weight: 600;
+ color: #333;
+
+ &.success {
+ color: #4CAF50;
+ }
+
+ &.failed {
+ color: #F44336;
+ }
+ }
+
+ .stat-label {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+}
+
+.deploy-list {
+ height: calc(100vh - 300px);
+ padding: 24px;
+}
+
+.deploy-item {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+
+ .deploy-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+
+ .deploy-version {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .version {
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ }
+ }
+ }
+
+ .deploy-info {
+ margin-bottom: 16px;
+
+ .deploy-build,
+ .rollback-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 26px;
+ color: #666;
+ margin-bottom: 8px;
+
+ .iconfont {
+ color: #2196F3;
+ }
+ }
+
+ .rollback-info .iconfont {
+ color: #FF9800;
+ }
+ }
+
+ .deploy-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 16px;
+ border-top: 1px solid #eee;
+
+ .deploy-meta {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ font-size: 24px;
+ color: #999;
+
+ .deployer {
+ color: #666;
+ }
+ }
+ }
+}
+
+.loading-wrap,
+.empty-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 100px 0;
+
+ .iconfont {
+ font-size: 96px;
+ color: #ccc;
+ margin-bottom: 24px;
+ }
+
+ .loading-text,
+ .empty-text {
+ font-size: 28px;
+ color: #999;
+ margin-bottom: 24px;
+ }
+}
+
+.loading-more {
+ display: flex;
+ justify-content: center;
+ padding: 24px;
+}
+
+.no-more {
+ text-align: center;
+ padding: 24px;
+ font-size: 24px;
+ color: #999;
+}
+
+// 部署弹窗样式
+.deploy-modal-content {
+ .select-label {
+ display: block;
+ font-size: 28px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 16px;
+ }
+
+ .env-select {
+ margin-bottom: 24px;
+
+ .env-options {
+ display: flex;
+ gap: 16px;
+
+ .env-option {
+ flex: 1;
+ padding: 16px;
+ text-align: center;
+ background: #f5f5f5;
+ border-radius: 8px;
+ font-size: 26px;
+ color: #666;
+
+ &.active {
+ background: #1890ff;
+ color: #fff;
+ }
+ }
+ }
+ }
+
+ .build-select {
+ .build-list {
+ max-height: 400px;
+ }
+
+ .build-option {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ margin-bottom: 12px;
+
+ &.active {
+ background: rgba(24, 144, 255, 0.1);
+ border: 1px solid #1890ff;
+ }
+
+ .build-info {
+ flex: 1;
+
+ .build-no {
+ font-size: 28px;
+ font-weight: 500;
+ color: #333;
+ margin-right: 12px;
+ }
+
+ .build-branch {
+ font-size: 24px;
+ color: #4CAF50;
+ margin-right: 12px;
+ }
+
+ .build-commit {
+ font-size: 24px;
+ color: #2196F3;
+ font-family: monospace;
+ }
+ }
+
+ .build-time {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+ }
+}
diff --git a/src/developer/app/[id]/deploys.tsx b/src/developer/app/[id]/deploys.tsx
new file mode 100644
index 0000000..8d50e4a
--- /dev/null
+++ b/src/developer/app/[id]/deploys.tsx
@@ -0,0 +1,371 @@
+/**
+ * 部署列表页面
+ */
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { AtButton, AtTabs, AtTabsPane, AtTag, AtActivityIndicator, AtModal } from 'taro-ui'
+import { pageDeploy, triggerDeploy, rollbackDeploy } from '../../../../api/cicd'
+import { pageBuild } from '../../../../api/cicd'
+import type { Deploy, DeployStatus, DeployEnv, Build } from '../../../../types/cicd'
+import './deploys.scss'
+
+// 状态映射
+const STATUS_MAP: Record = {
+ pending: { text: '等待中', color: '#FFC107' },
+ deploying: { text: '部署中', color: '#2196F3' },
+ success: { text: '部署成功', color: '#4CAF50' },
+ failed: { text: '部署失败', color: '#F44336' },
+ rollback: { text: '回滚中', color: '#FF9800' },
+}
+
+// 环境映射
+const ENV_MAP: Record = {
+ development: { text: '开发', color: '#9E9E9E' },
+ staging: { text: '预发布', color: '#FF9800' },
+ production: { text: '生产', color: '#4CAF50' },
+}
+
+export default function Deploys() {
+ const [loading, setLoading] = useState(true)
+ const [deploys, setDeploys] = useState([])
+ const [total, setTotal] = useState(0)
+ const [page, setPage] = useState(1)
+ const [hasMore, setHasMore] = useState(true)
+ const [env, setEnv] = useState('')
+ const [appId, setAppId] = useState('')
+ const [buildId, setBuildId] = useState('')
+ const [showDeployModal, setShowDeployModal] = useState(false)
+ const [selectBuild, setSelectBuild] = useState(null)
+ const [selectEnv, setSelectEnv] = useState('staging')
+ const [builds, setBuilds] = useState([])
+
+ // 获取部署列表
+ const fetchDeploys = async (pageNum = 1, reset = false) => {
+ try {
+ const res = await pageDeploy({
+ page: pageNum,
+ limit: 20,
+ websiteId: Number(appId),
+ env: env || undefined,
+ })
+
+ if (res.data) {
+ if (reset) {
+ setDeploys(res.data.list || [])
+ } else {
+ setDeploys(prev => [...prev, ...(res.data?.list || [])])
+ }
+ setTotal(res.data.total || 0)
+ setHasMore((res.data.list || []).length === 20)
+ setPage(pageNum)
+ }
+ } catch (err) {
+ console.error('获取部署列表失败', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 获取成功构建列表(用于部署选择)
+ const fetchBuilds = async () => {
+ try {
+ const res = await pageBuild({
+ page: 1,
+ limit: 20,
+ websiteId: Number(appId),
+ status: 'success',
+ })
+ if (res.data) {
+ setBuilds(res.data.list || [])
+ }
+ } catch (err) {
+ console.error('获取构建列表失败', err)
+ }
+ }
+
+ useEffect(() => {
+ const pages = Taro.getCurrentPages()
+ const current = pages[pages.length - 1]
+ if (current?.options?.appId) {
+ setAppId(current.options.appId)
+ }
+ if (current?.options?.buildId) {
+ setBuildId(current.options.buildId)
+ // 如果有 buildId,自动打开部署弹窗
+ fetchBuilds().then(() => {
+ if (current.options?.buildId && builds.length > 0) {
+ const build = builds.find(b => b.id === Number(current.options.buildId))
+ if (build) {
+ setSelectBuild(build)
+ setShowDeployModal(true)
+ }
+ }
+ })
+ }
+ }, [])
+
+ useEffect(() => {
+ if (appId) {
+ fetchDeploys(1, true)
+ }
+ }, [appId, env])
+
+ // 触发部署
+ const handleTriggerDeploy = async () => {
+ if (!selectBuild || !appId) return
+
+ try {
+ Taro.showLoading({ title: '部署中...' })
+ await triggerDeploy({
+ websiteId: Number(appId),
+ buildId: selectBuild.id!,
+ env: selectEnv,
+ })
+ Taro.showToast({ title: '部署已触发', icon: 'success' })
+ setShowDeployModal(false)
+ fetchDeploys(1, true)
+ } catch (err) {
+ Taro.showToast({ title: '部署失败', icon: 'none' })
+ } finally {
+ Taro.hideLoading()
+ }
+ }
+
+ // 回滚部署
+ const handleRollback = async (deployId: number) => {
+ Taro.showModal({
+ title: '确认回滚',
+ content: '确定要回滚到上一个版本吗?',
+ success: async (res) => {
+ if (res.confirm) {
+ try {
+ Taro.showLoading({ title: '回滚中...' })
+ await rollbackDeploy(deployId)
+ Taro.showToast({ title: '回滚成功', icon: 'success' })
+ fetchDeploys(page, true)
+ } catch (err) {
+ Taro.showToast({ title: '回滚失败', icon: 'none' })
+ } finally {
+ Taro.hideLoading()
+ }
+ }
+ },
+ })
+ }
+
+ // 查看部署详情
+ const goToDeployDetail = (deployId: number) => {
+ Taro.navigateTo({
+ url: `/developer/app/${appId}/deploy/${deployId}`,
+ })
+ }
+
+ // 加载更多
+ const handleLoadMore = () => {
+ if (hasMore && !loading) {
+ fetchDeploys(page + 1)
+ }
+ }
+
+ // 格式化时间
+ const formatTime = (time?: string) => {
+ if (!time) return '-'
+ const date = new Date(time)
+ return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
+ }
+
+ // 格式化时长
+ const formatDuration = (seconds?: number) => {
+ if (!seconds) return '-'
+ if (seconds < 60) return `${seconds}s`
+ const mins = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return `${mins}m ${secs}s`
+ }
+
+ const tabs = [
+ { title: '全部' },
+ { title: '开发' },
+ { title: '预发布' },
+ { title: '生产' },
+ ]
+
+ const envList: (DeployEnv | '')[] = ['', 'development', 'staging', 'production']
+
+ return (
+
+ {/* 顶部统计 */}
+
+
+ {total}
+ 总部署
+
+
+ {deploys.filter(d => d.status === 'success').length}
+ 成功
+
+
+ {deploys.filter(d => d.status === 'failed').length}
+ 失败
+
+
+
+ {/* 环境筛选 */}
+ setEnv(envList[index])}
+ >
+
+
+ {loading && deploys.length === 0 ? (
+
+
+ 加载中...
+
+ ) : deploys.length === 0 ? (
+
+
+ 暂无部署记录
+ {
+ fetchBuilds()
+ setShowDeployModal(true)
+ }}>
+ 发起部署
+
+
+ ) : (
+ deploys.map((deploy) => (
+ deploy.id && goToDeployDetail(deploy.id)}
+ >
+
+
+ v{deploy.version}
+
+ {ENV_MAP[deploy.env || 'staging'].text}
+
+
+
+ {STATUS_MAP[deploy.status || 'pending'].text}
+
+
+
+
+
+
+ #{deploy.buildNo}
+
+ {deploy.previousVersion && (
+
+
+ 从 v{deploy.previousVersion} 回滚
+
+ )}
+
+
+
+
+ {deploy.deployer}
+ {formatTime(deploy.startTime)}
+ {formatDuration(deploy.duration)}
+
+ {deploy.status === 'success' && (
+ {
+ e.stopPropagation()
+ deploy.id && handleRollback(deploy.id)
+ }}
+ >
+ 回滚
+
+ )}
+
+
+ ))
+ )}
+
+ {loading && deploys.length > 0 && (
+
+
+
+ )}
+
+ {!hasMore && deploys.length > 0 && (
+ 没有更多了
+ )}
+
+
+
+
+ {/* 部署弹窗 */}
+ setShowDeployModal(false)}
+ onCancel={() => setShowDeployModal(false)}
+ onConfirm={handleTriggerDeploy}
+ content={
+
+ {/* 环境选择 */}
+
+ 部署环境
+
+ {(['development', 'staging', 'production'] as DeployEnv[]).map((e) => (
+ setSelectEnv(e)}
+ >
+ {ENV_MAP[e].text}
+
+ ))}
+
+
+
+ {/* 构建选择 */}
+
+ 选择构建版本
+
+ {builds.map((build) => (
+ setSelectBuild(build)}
+ >
+
+ #{build.buildNo}
+ {build.branch}
+ {build.commitId?.slice(0, 7)}
+
+
+ {formatTime(build.endTime)}
+
+
+ ))}
+
+
+
+ }
+ />
+
+ )
+}
diff --git a/src/developer/app/[id]/index.config.ts b/src/developer/app/[id]/index.config.ts
new file mode 100644
index 0000000..0a667ca
--- /dev/null
+++ b/src/developer/app/[id]/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '应用详情',
+})
diff --git a/src/developer/app/[id]/index.scss b/src/developer/app/[id]/index.scss
new file mode 100644
index 0000000..7b2999c
--- /dev/null
+++ b/src/developer/app/[id]/index.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.app-detail-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #666666;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/developer/app/[id]/index.tsx b/src/developer/app/[id]/index.tsx
new file mode 100644
index 0000000..21bb704
--- /dev/null
+++ b/src/developer/app/[id]/index.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import './index.scss'
+
+const AppDetail: React.FC = () => {
+ const id = Taro.getCurrentInstance()?.router?.params?.id
+
+ return (
+
+
+ 📱 应用详情
+
+
+ 应用 ID: {id}
+ 应用详情功能开发中...
+
+
+ )
+}
+
+export default AppDetail
diff --git a/src/developer/app/[id]/pipeline.config.ts b/src/developer/app/[id]/pipeline.config.ts
new file mode 100644
index 0000000..0fdfc26
--- /dev/null
+++ b/src/developer/app/[id]/pipeline.config.ts
@@ -0,0 +1,6 @@
+/**
+ * 流水线配置页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '流水线配置',
+})
diff --git a/src/developer/app/[id]/pipeline.scss b/src/developer/app/[id]/pipeline.scss
new file mode 100644
index 0000000..99f0c02
--- /dev/null
+++ b/src/developer/app/[id]/pipeline.scss
@@ -0,0 +1,254 @@
+.pipeline-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 120px;
+
+ &.loading-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 300px;
+
+ .loading-text {
+ margin-top: 16px;
+ font-size: 28px;
+ color: #999;
+ }
+ }
+}
+
+.pipeline-content {
+ height: calc(100vh - 120px);
+ padding: 24px;
+}
+
+.config-section {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+
+ .section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+ }
+
+ .section-title {
+ display: block;
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 24px;
+ }
+}
+
+.config-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .item-info {
+ flex: 1;
+
+ .item-label {
+ display: block;
+ font-size: 28px;
+ color: #333;
+ margin-bottom: 4px;
+ }
+
+ .item-desc {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+}
+
+// 分支规则
+.rule-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ margin-bottom: 12px;
+
+ .rule-info {
+ flex: 1;
+
+ .rule-branch {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ font-size: 28px;
+ color: #333;
+
+ .iconfont {
+ color: #4CAF50;
+ }
+ }
+
+ .rule-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+ }
+
+ .rule-env {
+ font-size: 24px;
+ color: #666;
+ }
+}
+
+// 环境变量
+.env-list {
+ .env-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ margin-bottom: 12px;
+
+ .env-info {
+ flex: 1;
+
+ .env-key {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+
+ .key {
+ font-size: 28px;
+ font-weight: 500;
+ color: #333;
+ font-family: monospace;
+ }
+ }
+
+ .env-value {
+ font-size: 24px;
+ color: #666;
+ font-family: monospace;
+ }
+ }
+
+ .env-delete {
+ font-size: 32px;
+ color: #F44336;
+ padding: 8px;
+ }
+ }
+}
+
+// Webhook
+.webhook-list {
+ .webhook-item {
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ margin-bottom: 12px;
+
+ .webhook-info {
+ .webhook-url {
+ display: block;
+ font-size: 26px;
+ color: #2196F3;
+ margin-bottom: 8px;
+ word-break: break-all;
+ }
+
+ .webhook-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+ }
+ }
+}
+
+.empty-rules,
+.empty-envs,
+.empty-webhooks {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 48px 0;
+ color: #999;
+
+ .iconfont {
+ font-size: 64px;
+ margin-bottom: 16px;
+ }
+
+ text {
+ font-size: 26px;
+ }
+}
+
+// 保存按钮
+.save-bar {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 24px;
+ background: #fff;
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
+
+ .at-button {
+ width: 100%;
+ }
+}
+
+// 环境变量弹窗
+.env-modal-content {
+ .modal-item {
+ margin-bottom: 24px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .modal-label {
+ display: block;
+ font-size: 26px;
+ color: #666;
+ margin-bottom: 8px;
+ }
+
+ .modal-input {
+ width: 100%;
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ font-size: 28px;
+ }
+
+ .modal-switch {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .modal-hint {
+ font-size: 22px;
+ color: #999;
+ margin-top: 8px;
+ }
+ }
+}
diff --git a/src/developer/app/[id]/pipeline.tsx b/src/developer/app/[id]/pipeline.tsx
new file mode 100644
index 0000000..a546d05
--- /dev/null
+++ b/src/developer/app/[id]/pipeline.tsx
@@ -0,0 +1,346 @@
+/**
+ * 流水线配置页面
+ */
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView, Input, Switch } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { AtButton, AtSwitch, AtTag, AtActivityIndicator, AtInput, AtModal } from 'taro-ui'
+import {
+ getPipelineConfig,
+ updatePipelineConfig,
+ addEnvVar,
+ deleteEnvVar,
+ getBranches,
+} from '../../../../api/cicd'
+import type { PipelineConfig, BranchRule, EnvVar } from '../../../../types/cicd'
+import './pipeline.scss'
+
+export default function Pipeline() {
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [config, setConfig] = useState({})
+ const [branches, setBranches] = useState([])
+ const [showEnvModal, setShowEnvModal] = useState(false)
+ const [newEnvKey, setNewEnvKey] = useState('')
+ const [newEnvValue, setNewEnvValue] = useState('')
+ const [newEnvSecret, setNewEnvSecret] = useState(false)
+ const [appId, setAppId] = useState('')
+
+ useEffect(() => {
+ const pages = Taro.getCurrentPages()
+ const current = pages[pages.length - 1]
+ if (current?.options?.appId) {
+ setAppId(current.options.appId)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (appId) {
+ fetchConfig()
+ fetchBranches()
+ }
+ }, [appId])
+
+ // 获取流水线配置
+ const fetchConfig = async () => {
+ try {
+ const res = await getPipelineConfig(Number(appId))
+ if (res.data) {
+ setConfig(res.data)
+ }
+ } catch (err) {
+ console.error('获取流水线配置失败', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 获取分支列表
+ const fetchBranches = async () => {
+ try {
+ const res = await getBranches(Number(appId))
+ if (res.data) {
+ setBranches(res.data.branches || [])
+ }
+ } catch (err) {
+ console.error('获取分支列表失败', err)
+ }
+ }
+
+ // 更新配置
+ const handleUpdateConfig = async () => {
+ try {
+ setSaving(true)
+ await updatePipelineConfig({
+ ...config,
+ websiteId: Number(appId),
+ })
+ Taro.showToast({ title: '保存成功', icon: 'success' })
+ } catch (err) {
+ Taro.showToast({ title: '保存失败', icon: 'none' })
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ // 添加环境变量
+ const handleAddEnvVar = async () => {
+ if (!newEnvKey.trim()) {
+ Taro.showToast({ title: '请输入变量名', icon: 'none' })
+ return
+ }
+
+ try {
+ await addEnvVar(Number(appId), {
+ key: newEnvKey,
+ value: newEnvValue,
+ isSecret: newEnvSecret,
+ })
+ Taro.showToast({ title: '添加成功', icon: 'success' })
+ setShowEnvModal(false)
+ setNewEnvKey('')
+ setNewEnvValue('')
+ setNewEnvSecret(false)
+ fetchConfig()
+ } catch (err) {
+ Taro.showToast({ title: '添加失败', icon: 'none' })
+ }
+ }
+
+ // 删除环境变量
+ const handleDeleteEnvVar = (key: string) => {
+ Taro.showModal({
+ title: '确认删除',
+ content: `确定要删除环境变量 ${key} 吗?`,
+ success: async (res) => {
+ if (res.confirm) {
+ try {
+ await deleteEnvVar(Number(appId), key)
+ Taro.showToast({ title: '删除成功', icon: 'success' })
+ fetchConfig()
+ } catch (err) {
+ Taro.showToast({ title: '删除失败', icon: 'none' })
+ }
+ }
+ },
+ })
+ }
+
+ // 复制环境变量值
+ const handleCopyEnvValue = (value: string) => {
+ Taro.setClipboardData({ data: value })
+ Taro.showToast({ title: '已复制', icon: 'success' })
+ }
+
+ if (loading) {
+ return (
+
+
+ 加载中...
+
+ )
+ }
+
+ return (
+
+
+ {/* 基础配置 */}
+
+ 基础配置
+
+
+
+ 启用流水线
+ 开启后支持自动构建和部署
+
+ setConfig({ ...config, enabled: e.detail.value })}
+ color="#1890ff"
+ />
+
+
+
+
+ 自动部署
+ 构建成功后自动部署到对应环境
+
+ setConfig({ ...config, autoDeploy: e.detail.value })}
+ color="#1890ff"
+ />
+
+
+
+ {/* 分支规则 */}
+
+
+ 分支规则
+
+ 添加规则
+
+
+
+ {config.branchRules && config.branchRules.length > 0 ? (
+ config.branchRules.map((rule, index) => (
+
+
+
+
+ {rule.branch || 'main'}
+
+
+ {rule.autoBuild && 自动构建}
+ {rule.autoDeploy && 自动部署}
+ {rule.requireApproval && 需要审批}
+
+
+
+ {rule.deployEnv === 'development' ? '开发' : rule.deployEnv === 'staging' ? '预发布' : '生产'}
+
+
+ ))
+ ) : (
+
+
+ 暂无分支规则
+
+ )}
+
+
+ {/* 环境变量 */}
+
+
+ 环境变量
+ setShowEnvModal(true)}>
+ 添加变量
+
+
+
+ {config.environmentVars && config.environmentVars.length > 0 ? (
+
+ {config.environmentVars.map((env, index) => (
+
+
+
+ {env.key}
+ {env.isSecret && 加密}
+
+ !env.isSecret && env.value && handleCopyEnvValue(env.value)}
+ >
+ {env.isSecret ? '******' : env.value}
+
+
+ env.key && handleDeleteEnvVar(env.key)}
+ />
+
+ ))}
+
+ ) : (
+
+
+ 暂无环境变量
+
+ )}
+
+
+ {/* Webhook 配置 */}
+
+
+ Webhook 配置
+
+ 添加 Webhook
+
+
+
+ {config.webhooks && config.webhooks.length > 0 ? (
+
+ {config.webhooks.map((webhook, index) => (
+
+
+ {webhook.url}
+
+ {webhook.enabled ? (
+ 启用
+ ) : (
+ 禁用
+ )}
+ {webhook.events?.map((event, i) => (
+ {event}
+ ))}
+
+
+
+ ))}
+
+ ) : (
+
+
+ 暂无 Webhook
+
+ )}
+
+
+
+ {/* 保存按钮 */}
+
+
+ 保存配置
+
+
+
+ {/* 添加环境变量弹窗 */}
+ setShowEnvModal(false)}
+ onCancel={() => setShowEnvModal(false)}
+ onConfirm={handleAddEnvVar}
+ content={
+
+
+ 变量名 *
+ setNewEnvKey(e.detail.value)}
+ />
+
+
+ 变量值
+ setNewEnvValue(e.detail.value)}
+ />
+
+
+
+ 加密存储
+ setNewEnvSecret(e.detail.value)}
+ size="small"
+ />
+
+ 加密后值将不可见
+
+
+ }
+ />
+
+ )
+}
diff --git a/src/developer/app/[id]/publish.config.ts b/src/developer/app/[id]/publish.config.ts
new file mode 100644
index 0000000..bce2d99
--- /dev/null
+++ b/src/developer/app/[id]/publish.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '发布管理',
+})
diff --git a/src/developer/app/[id]/publish.scss b/src/developer/app/[id]/publish.scss
new file mode 100644
index 0000000..1453253
--- /dev/null
+++ b/src/developer/app/[id]/publish.scss
@@ -0,0 +1,10 @@
+.publish-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/developer/app/[id]/publish.tsx b/src/developer/app/[id]/publish.tsx
new file mode 100644
index 0000000..7c67901
--- /dev/null
+++ b/src/developer/app/[id]/publish.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './publish.scss'
+
+const PublishPage: React.FC = () => {
+ return (
+
+
+ 发布管理功能开发中...
+
+
+ )
+}
+
+export default PublishPage
diff --git a/src/developer/app/[id]/version.scss b/src/developer/app/[id]/version.scss
new file mode 100644
index 0000000..f76efef
--- /dev/null
+++ b/src/developer/app/[id]/version.scss
@@ -0,0 +1,335 @@
+.version-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 24rpx;
+ padding-bottom: 120rpx;
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__publish-btn {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ border: none;
+ border-radius: 32rpx;
+ font-size: 26rpx;
+ padding: 0 28rpx;
+ height: 60rpx;
+ line-height: 60rpx;
+
+ &::after {
+ border: none;
+ }
+ }
+
+ // 环境 Tab
+ &__tabs {
+ display: flex;
+ background: #fff;
+ border-radius: 12rpx;
+ padding: 8rpx;
+ margin-bottom: 24rpx;
+ }
+
+ &__tab {
+ flex: 1;
+ height: 72rpx;
+ line-height: 72rpx;
+ text-align: center;
+ font-size: 28rpx;
+ color: #666;
+ border-radius: 8rpx;
+
+ &.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ }
+ }
+
+ // 列表
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+ }
+
+ &__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 120rpx 0;
+
+ text {
+ font-size: 30rpx;
+ color: #999;
+ }
+
+ &-hint {
+ margin-top: 16rpx;
+ font-size: 26rpx !important;
+ color: #ccc !important;
+ }
+ }
+
+ &__loading,
+ &__no-more {
+ display: flex;
+ justify-content: center;
+ padding: 32rpx 0;
+
+ text {
+ font-size: 26rpx;
+ color: #999;
+ }
+ }
+}
+
+// 版本卡片
+.version-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 16rpx;
+ }
+
+ &__info {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ }
+
+ &__name {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__status {
+ font-size: 24rpx;
+ padding: 4rpx 12rpx;
+ border-radius: 8rpx;
+ }
+
+ &__env {
+ font-size: 24rpx;
+ padding: 4rpx 16rpx;
+ border-radius: 8rpx;
+ background: #f5f5f5;
+ }
+
+ &__current {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ font-size: 24rpx;
+ padding: 4rpx 16rpx;
+ border-radius: 8rpx;
+ margin-bottom: 16rpx;
+ display: inline-block;
+ }
+
+ &__body {
+ background: #fafafa;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ margin-bottom: 16rpx;
+ }
+
+ &__row {
+ display: flex;
+ margin-bottom: 12rpx;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__label {
+ font-size: 26rpx;
+ color: #666;
+ width: 140rpx;
+ flex-shrink: 0;
+ }
+
+ &__value {
+ font-size: 26rpx;
+ color: #333;
+ flex: 1;
+ }
+
+ &__changelog {
+ background: #f0f7ff;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ margin-bottom: 16rpx;
+
+ &-title {
+ display: block;
+ font-size: 26rpx;
+ color: #1890ff;
+ margin-bottom: 8rpx;
+ }
+
+ &-content {
+ display: block;
+ font-size: 26rpx;
+ color: #333;
+ line-height: 1.6;
+ white-space: pre-wrap;
+ }
+ }
+
+ &__footer {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ &__time {
+ font-size: 24rpx;
+ color: #999;
+ }
+}
+
+// 发布弹窗
+.version-page__modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+
+ &-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ &-content {
+ position: relative;
+ width: 100%;
+ max-height: 85vh;
+ background: #fff;
+ border-radius: 32rpx 32rpx 0 0;
+ padding: 48rpx 32rpx;
+ padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
+ overflow-y: auto;
+ }
+
+ &-title {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #333;
+ text-align: center;
+ margin-bottom: 48rpx;
+ }
+}
+
+.version-page__form {
+ &-item {
+ margin-bottom: 32rpx;
+ }
+
+ &-label {
+ display: block;
+ font-size: 28rpx;
+ color: #333;
+ margin-bottom: 16rpx;
+ }
+
+ &-input {
+ width: 100%;
+ height: 88rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 0 24rpx;
+ font-size: 30rpx;
+ }
+
+ &-radio {
+ display: flex;
+ gap: 24rpx;
+
+ &-item {
+ flex: 1;
+ height: 72rpx;
+ line-height: 72rpx;
+ text-align: center;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ font-size: 28rpx;
+ color: #666;
+
+ &.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ }
+ }
+ }
+
+ &-textarea {
+ width: 100%;
+ }
+}
+
+.version-page__textarea {
+ width: 100%;
+ min-height: 180rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ font-size: 30rpx;
+ line-height: 1.6;
+ box-sizing: border-box;
+}
+
+.version-page__modal-actions {
+ display: flex;
+ gap: 24rpx;
+ margin-top: 48rpx;
+}
+
+.version-page__modal-btn {
+ flex: 1;
+ height: 88rpx;
+ line-height: 88rpx;
+ border-radius: 44rpx;
+ font-size: 32rpx;
+
+ &--cancel {
+ background: #f5f5f5;
+ color: #666;
+ }
+
+ &--confirm {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+
+ &::after {
+ border: none;
+ }
+ }
+}
diff --git a/src/developer/app/[id]/version.tsx b/src/developer/app/[id]/version.tsx
new file mode 100644
index 0000000..7ff2678
--- /dev/null
+++ b/src/developer/app/[id]/version.tsx
@@ -0,0 +1,364 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, Button, Input } from '@tarojs/components'
+import Taro, { useRouter } from '@tarojs/taro'
+import { usePullDownRefresh } from '@tarojs/taro'
+import { pageVersion, createVersion } from '@/api/developer/developer'
+import type { Version, VersionParam, VersionStatus, PublishEnv } from '@/types/developer'
+import './version.scss'
+
+// 状态配置
+const STATUS_CONFIG: Record = {
+ 0: { label: '构建中', color: '#faad14', bgColor: '#fffbe6' },
+ 1: { label: '已发布', color: '#52c41a', bgColor: '#f6ffed' },
+ 2: { label: '已回滚', color: '#ff4d4f', bgColor: '#fff1f0' },
+ 3: { label: '构建失败', color: '#f5222d', bgColor: '#fff1f0' },
+}
+
+// 环境配置
+const ENV_CONFIG: Record = {
+ development: { label: '开发环境', color: '#722ed1' },
+ staging: { label: '预发布环境', color: '#1890ff' },
+ production: { label: '生产环境', color: '#52c41a' },
+}
+
+const VersionPage: React.FC = () => {
+ const router = useRouter()
+ const appId = Number(router.params.id)
+
+ const [loading, setLoading] = useState(false)
+ const [refreshing, setRefreshing] = useState(false)
+ const [list, setList] = useState([])
+ const [page, setPage] = useState(1)
+ const [hasMore, setHasMore] = useState(true)
+ const [showCreate, setShowCreate] = useState(false)
+ const [createForm, setCreateForm] = useState({
+ versionName: '',
+ versionNo: '',
+ changelog: '',
+ env: 'staging' as PublishEnv,
+ })
+ const [creating, setCreating] = useState(false)
+ const [currentTab, setCurrentTab] = useState('all')
+
+ // 加载数据
+ const loadData = async (pageNum: number = 1, isRefresh = false) => {
+ if (loading) return
+ setLoading(true)
+ if (isRefresh) setRefreshing(true)
+
+ try {
+ const params: VersionParam = {
+ page: pageNum,
+ limit: 20,
+ websiteId: appId,
+ env: currentTab === 'all' ? undefined : currentTab,
+ }
+ const data = await pageVersion(params)
+
+ if (pageNum === 1) {
+ setList(data?.records || [])
+ } else {
+ setList(prev => [...prev, ...(data?.records || [])])
+ }
+
+ const total = data?.total || 0
+ const records = data?.records || []
+ setHasMore(records.length > 0 && (pageNum * 20) < total)
+ setPage(pageNum)
+ } catch (err) {
+ console.error('加载失败', err)
+ Taro.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => {
+ loadData(1)
+ }, [appId, currentTab])
+
+ // 下拉刷新
+ usePullDownRefresh(() => {
+ loadData(1, true)
+ })
+
+ // 加载更多
+ const onReachBottom = () => {
+ if (hasMore && !loading) {
+ loadData(page + 1)
+ }
+ }
+
+ // 创建版本
+ const handleCreate = async () => {
+ if (!createForm.versionName.trim()) {
+ Taro.showToast({ title: '请输入版本名称', icon: 'none' })
+ return
+ }
+ if (!createForm.versionNo.trim()) {
+ Taro.showToast({ title: '请输入版本号', icon: 'none' })
+ return
+ }
+
+ setCreating(true)
+ try {
+ await createVersion({
+ websiteId: appId,
+ versionName: createForm.versionName,
+ versionNo: createForm.versionNo,
+ changelog: createForm.changelog,
+ env: createForm.env,
+ } as Partial)
+ Taro.showToast({ title: '发布成功', icon: 'success' })
+ setShowCreate(false)
+ setCreateForm({ versionName: '', versionNo: '', changelog: '', env: 'staging' })
+ loadData(1)
+ } catch (err) {
+ console.error('发布失败', err)
+ Taro.showToast({ title: '发布失败', icon: 'none' })
+ } finally {
+ setCreating(false)
+ }
+ }
+
+ // 发布新版本
+ const handlePublish = () => {
+ if (list.some(v => v.status === 0)) {
+ Taro.showToast({ title: '有版本正在构建中', icon: 'none' })
+ return
+ }
+ setShowCreate(true)
+ }
+
+ // 格式化日期
+ const formatDate = (dateStr?: string) => {
+ if (!dateStr) return '-'
+ const date = new Date(dateStr)
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
+ }
+
+ // 格式化文件大小
+ const formatSize = (size?: string) => {
+ if (!size) return '-'
+ if (size.length < 4) return size
+ const num = parseFloat(size)
+ if (num >= 1024 * 1024) {
+ return (num / (1024 * 1024)).toFixed(2) + ' MB'
+ } else if (num >= 1024) {
+ return (num / 1024).toFixed(2) + ' KB'
+ }
+ return size + ' B'
+ }
+
+ // Tab 配置
+ const tabs: { key: PublishEnv | 'all'; label: string }[] = [
+ { key: 'all', label: '全部' },
+ { key: 'development', label: '开发' },
+ { key: 'staging', label: '预发布' },
+ { key: 'production', label: '生产' },
+ ]
+
+ return (
+
+ {/* 头部 */}
+
+ 📦 版本管理
+
+
+
+ {/* 环境 Tab */}
+
+ {tabs.map((tab) => (
+ setCurrentTab(tab.key)}
+ >
+ {tab.label}
+
+ ))}
+
+
+ {/* 列表 */}
+
+ {list.length === 0 && !loading ? (
+
+ 暂无版本记录
+ 点击「发布版本」创建新版本
+
+ ) : (
+ list.map((item) => (
+
+
+
+
+ {item.versionName || `v${item.versionNo}`}
+
+
+ {STATUS_CONFIG[item.status as VersionStatus]?.label}
+
+
+
+ {ENV_CONFIG[item.env as PublishEnv]?.label}
+
+
+
+ {item.isCurrent && (
+
+ 当前版本
+
+ )}
+
+
+ {item.versionNo && (
+
+ 版本号:
+ {item.versionNo}
+
+ )}
+ {item.packageSize && (
+
+ 包大小:
+ {formatSize(item.packageSize)}
+
+ )}
+ {item.publishBy && (
+
+ 发布人:
+ {item.publishBy}
+
+ )}
+ {item.publishTime && (
+
+ 发布时间:
+ {formatDate(item.publishTime)}
+
+ )}
+
+
+ {item.changelog && (
+
+ 更新日志:
+ {item.changelog}
+
+ )}
+
+
+
+ 创建于 {formatDate(item.createTime)}
+
+
+
+ ))
+ )}
+
+ {/* 加载状态 */}
+ {loading && list.length > 0 && (
+
+ 加载中...
+
+ )}
+
+ {!hasMore && list.length > 0 && (
+
+ 没有更多了
+
+ )}
+
+
+ {/* 发布弹窗 */}
+ {showCreate && (
+
+ setShowCreate(false)} />
+
+ 发布新版本
+
+
+
+ 版本名称 *
+ setCreateForm(prev => ({ ...prev, versionName: e.detail.value }))}
+ />
+
+
+
+ 版本号 *
+ setCreateForm(prev => ({ ...prev, versionNo: e.detail.value }))}
+ />
+
+
+
+ 发布环境
+
+ {(['staging', 'production'] as const).map((env) => (
+ setCreateForm(prev => ({ ...prev, env }))}
+ >
+ {ENV_CONFIG[env].label}
+
+ ))}
+
+
+
+
+ 更新日志
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default VersionPage
diff --git a/src/developer/app/api-keys/index.config.ts b/src/developer/app/api-keys/index.config.ts
new file mode 100644
index 0000000..7ce54f5
--- /dev/null
+++ b/src/developer/app/api-keys/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'API Key 管理',
+})
diff --git a/src/developer/app/api-keys/index.scss b/src/developer/app/api-keys/index.scss
new file mode 100644
index 0000000..f0ac176
--- /dev/null
+++ b/src/developer/app/api-keys/index.scss
@@ -0,0 +1,305 @@
+.api-keys-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 24rpx;
+ padding-bottom: 120rpx;
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__create-btn {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ border: none;
+ border-radius: 32rpx;
+ font-size: 26rpx;
+ padding: 0 28rpx;
+ height: 60rpx;
+ line-height: 60rpx;
+
+ &::after {
+ border: none;
+ }
+ }
+
+ &__tips {
+ background: #fffbeb;
+ border: 1px solid #ffe58f;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ margin-bottom: 24rpx;
+
+ text {
+ font-size: 26rpx;
+ color: #d48806;
+ line-height: 1.6;
+ }
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+ }
+
+ &__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 120rpx 0;
+
+ text {
+ font-size: 30rpx;
+ color: #999;
+ }
+
+ &-hint {
+ margin-top: 16rpx;
+ font-size: 26rpx !important;
+ color: #ccc !important;
+ }
+ }
+
+ &__loading,
+ &__no-more {
+ display: flex;
+ justify-content: center;
+ padding: 32rpx 0;
+
+ text {
+ font-size: 26rpx;
+ color: #999;
+ }
+ }
+}
+
+// API Key 卡片
+.api-key-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24rpx;
+ }
+
+ &__info {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ }
+
+ &__name {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__status {
+ font-size: 24rpx;
+ padding: 4rpx 12rpx;
+ border-radius: 8rpx;
+ background: #f5f5f5;
+ }
+
+ &__type {
+ font-size: 24rpx;
+ color: #666;
+ background: #f0f0f0;
+ padding: 4rpx 16rpx;
+ border-radius: 8rpx;
+ }
+
+ &__body {
+ padding: 24rpx;
+ background: #fafafa;
+ border-radius: 12rpx;
+ margin-bottom: 24rpx;
+ }
+
+ &__row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16rpx;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__label {
+ font-size: 26rpx;
+ color: #666;
+ width: 140rpx;
+ flex-shrink: 0;
+ }
+
+ &__value {
+ font-size: 26rpx;
+ color: #333;
+ flex: 1;
+ word-break: break-all;
+
+ &--monospace {
+ font-family: 'Monaco', 'Menlo', monospace;
+ font-size: 24rpx;
+ }
+ }
+
+ &__footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__time {
+ font-size: 24rpx;
+ color: #999;
+ }
+
+ &__actions {
+ display: flex;
+ gap: 32rpx;
+ }
+
+ &__action {
+ font-size: 26rpx;
+ padding: 8rpx 16rpx;
+
+ &--primary {
+ color: #667eea;
+ }
+
+ &--danger {
+ color: #ff4d4f;
+ }
+ }
+}
+
+// 创建弹窗
+.api-keys-page__modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+
+ &-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ &-content {
+ position: relative;
+ width: 100%;
+ max-height: 80vh;
+ background: #fff;
+ border-radius: 32rpx 32rpx 0 0;
+ padding: 48rpx 32rpx;
+ padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
+ overflow-y: auto;
+ }
+
+ &-title {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #333;
+ text-align: center;
+ margin-bottom: 48rpx;
+ }
+}
+
+.api-keys-page__form {
+ &-item {
+ margin-bottom: 32rpx;
+ }
+
+ &-label {
+ display: block;
+ font-size: 28rpx;
+ color: #333;
+ margin-bottom: 16rpx;
+ }
+
+ &-input {
+ width: 100%;
+ height: 88rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 0 24rpx;
+ font-size: 30rpx;
+ }
+
+ &-radio {
+ display: flex;
+ gap: 24rpx;
+
+ &-item {
+ flex: 1;
+ height: 72rpx;
+ line-height: 72rpx;
+ text-align: center;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ font-size: 28rpx;
+ color: #666;
+
+ &.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ }
+ }
+ }
+}
+
+.api-keys-page__modal-actions {
+ display: flex;
+ gap: 24rpx;
+ margin-top: 48rpx;
+}
+
+.api-keys-page__modal-btn {
+ flex: 1;
+ height: 88rpx;
+ line-height: 88rpx;
+ border-radius: 44rpx;
+ font-size: 32rpx;
+
+ &--cancel {
+ background: #f5f5f5;
+ color: #666;
+ }
+
+ &--confirm {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+
+ &::after {
+ border: none;
+ }
+ }
+}
diff --git a/src/developer/app/api-keys/index.tsx b/src/developer/app/api-keys/index.tsx
new file mode 100644
index 0000000..3ec0a15
--- /dev/null
+++ b/src/developer/app/api-keys/index.tsx
@@ -0,0 +1,335 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, Input, Button, actionSheet } from '@tarojs/components'
+import Taro, { useRouter } from '@tarojs/taro'
+import { usePullDownRefresh } from '@tarojs/taro'
+import { pageApiKey, createApiKey, deleteApiKey } from '@/api/developer/developer'
+import type { ApiKey, ApiKeyParam } from '@/types/developer'
+import './index.scss'
+
+// API Key 状态映射
+const STATUS_MAP: Record = {
+ 0: { text: '禁用', color: '#999' },
+ 1: { text: '正常', color: '#52c41a' },
+}
+
+const TYPE_MAP: Record = {
+ server: '服务端',
+ client: '客户端',
+ webhook: 'Webhook',
+}
+
+const ApiKeysPage: React.FC = () => {
+ const router = useRouter()
+ const projectId = Number(router.params.projectId) || undefined
+
+ const [loading, setLoading] = useState(false)
+ const [refreshing, setRefreshing] = useState(false)
+ const [list, setList] = useState([])
+ const [page, setPage] = useState(1)
+ const [hasMore, setHasMore] = useState(true)
+ const [showCreate, setShowCreate] = useState(false)
+ const [createForm, setCreateForm] = useState({
+ name: '',
+ type: 'server' as const,
+ remark: '',
+ })
+ const [creating, setCreating] = useState(false)
+
+ // 加载数据
+ const loadData = async (pageNum: number = 1, isRefresh = false) => {
+ if (loading) return
+ setLoading(true)
+ if (isRefresh) setRefreshing(true)
+
+ try {
+ const params: ApiKeyParam = {
+ page: pageNum,
+ limit: 20,
+ websiteId: projectId,
+ }
+ const data = await pageApiKey(params)
+
+ if (pageNum === 1) {
+ setList(data?.records || [])
+ } else {
+ setList(prev => [...prev, ...(data?.records || [])])
+ }
+
+ const total = data?.total || 0
+ const records = data?.records || []
+ setHasMore(records.length > 0 && (pageNum * 20) < total)
+ setPage(pageNum)
+ } catch (err) {
+ console.error('加载失败', err)
+ Taro.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => {
+ loadData()
+ }, [projectId])
+
+ // 下拉刷新
+ usePullDownRefresh(() => {
+ loadData(1, true)
+ })
+
+ // 加载更多
+ const onReachBottom = () => {
+ if (hasMore && !loading) {
+ loadData(page + 1)
+ }
+ }
+
+ // 创建 API Key
+ const handleCreate = async () => {
+ if (!createForm.name.trim()) {
+ Taro.showToast({ title: '请输入名称', icon: 'none' })
+ return
+ }
+
+ setCreating(true)
+ try {
+ await createApiKey({
+ name: createForm.name,
+ type: createForm.type,
+ remark: createForm.remark,
+ websiteId: projectId,
+ } as Partial)
+ Taro.showToast({ title: '创建成功', icon: 'success' })
+ setShowCreate(false)
+ setCreateForm({ name: '', type: 'server', remark: '' })
+ loadData(1)
+ } catch (err) {
+ console.error('创建失败', err)
+ Taro.showToast({ title: '创建失败', icon: 'none' })
+ } finally {
+ setCreating(false)
+ }
+ }
+
+ // 删除 API Key
+ const handleDelete = (item: ApiKey) => {
+ actionSheet({
+ alertText: `确定删除 "${item.name}" 吗?此操作不可恢复。`,
+ actions: [
+ { name: '删除', color: '#ff4d4f', type: 'warn' as const },
+ ],
+ confirmText: '取消',
+ }).then(res => {
+ if (res.confirm) {
+ doDelete(item.id!)
+ }
+ }).catch(() => {})
+ }
+
+ const doDelete = async (id: number) => {
+ try {
+ await deleteApiKey(id)
+ Taro.showToast({ title: '已删除', icon: 'success' })
+ loadData(1)
+ } catch (err) {
+ console.error('删除失败', err)
+ Taro.showToast({ title: '删除失败', icon: 'none' })
+ }
+ }
+
+ // 复制内容
+ const handleCopy = (text: string, label: string) => {
+ Taro.setClipboardData({
+ data: text,
+ success: () => {
+ Taro.showToast({ title: `${label}已复制`, icon: 'success' })
+ },
+ })
+ }
+
+ // 格式化日期
+ const formatDate = (dateStr?: string) => {
+ if (!dateStr) return '-'
+ const date = new Date(dateStr)
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+ }
+
+ return (
+
+ {/* 头部 */}
+
+ 🔑 API Key 管理
+
+
+
+ {/* 提示信息 */}
+
+ API Key 是用于调用接口的凭证,请妥善保管,切勿泄露。
+
+
+ {/* 列表 */}
+
+ {list.length === 0 && !loading ? (
+
+ 暂无 API Key
+ 点击上方「创建」按钮生成新的 Key
+
+ ) : (
+ list.map((item) => (
+
+
+
+ {item.name}
+
+ {STATUS_MAP[item.status || 1]?.text}
+
+
+
+ {TYPE_MAP[item.type || 'server']}
+
+
+
+
+
+ AppId:
+ handleCopy(item.appId || '', 'AppId')}
+ >
+ {item.appId || '-'}
+
+
+
+ AppSecret:
+ handleCopy(item.appSecret || '', 'AppSecret')}
+ >
+ ••••••••••••••••
+
+
+ {item.remark && (
+
+ 备注:
+ {item.remark}
+
+ )}
+
+
+
+
+ 创建于 {formatDate(item.createTime)}
+
+
+ {item.appSecret && (
+ handleCopy(item.appSecret || '', 'AppSecret')}
+ >
+ 复制密钥
+
+ )}
+ handleDelete(item)}
+ >
+ 删除
+
+
+
+
+ ))
+ )}
+
+ {/* 加载状态 */}
+ {loading && list.length > 0 && (
+
+ 加载中...
+
+ )}
+
+ {!hasMore && list.length > 0 && (
+
+ 没有更多了
+
+ )}
+
+
+ {/* 创建弹窗 */}
+ {showCreate && (
+
+ setShowCreate(false)} />
+
+ 创建 API Key
+
+
+
+ 名称 *
+ setCreateForm(prev => ({ ...prev, name: e.detail.value }))}
+ />
+
+
+
+ 类型
+
+ {(['server', 'client', 'webhook'] as const).map((type) => (
+ setCreateForm(prev => ({ ...prev, type }))}
+ >
+ {TYPE_MAP[type]}
+
+ ))}
+
+
+
+
+ 备注
+ setCreateForm(prev => ({ ...prev, remark: e.detail.value }))}
+ />
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default ApiKeysPage
diff --git a/src/developer/app/create.scss b/src/developer/app/create.scss
new file mode 100644
index 0000000..d559463
--- /dev/null
+++ b/src/developer/app/create.scss
@@ -0,0 +1,75 @@
+page {
+ background: #f5f6f7;
+}
+
+.app-create-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ display: flex;
+ flex-direction: column;
+
+ &__scroll {
+ flex: 1;
+ height: 0;
+ }
+}
+
+/* 表单 */
+.form {
+ padding: 24rpx;
+
+ &__group {
+ margin-bottom: 32rpx;
+ }
+
+ &__label {
+ margin-bottom: 16rpx;
+ }
+
+ &__labelText {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #111111;
+ }
+
+ &__input {
+ padding: 24rpx;
+ background: #ffffff;
+ border-radius: 16rpx;
+ font-size: 30rpx;
+ }
+
+ &__textarea {
+ padding: 24rpx;
+ background: #ffffff;
+ border-radius: 16rpx;
+ font-size: 30rpx;
+ min-height: 160rpx;
+ }
+
+ &__footer {
+ padding: 24rpx 32rpx;
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+ background: #ffffff;
+ box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
+ }
+}
+
+/* 类型选择网格 */
+.type-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16rpx;
+}
+
+.type-item {
+ background: #ffffff;
+ border-radius: 16rpx;
+ padding: 20rpx;
+ position: relative;
+
+ &__text {
+ font-size: 28rpx;
+ color: #111111;
+ }
+}
diff --git a/src/developer/app/create.tsx b/src/developer/app/create.tsx
new file mode 100644
index 0000000..9db55e3
--- /dev/null
+++ b/src/developer/app/create.tsx
@@ -0,0 +1,118 @@
+import React, { useState } from 'react'
+import { View, Text, ScrollView, Input, Textarea } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Radio, RadioGroup, Picker } from '@nutui/nutui-react-taro'
+import { createApp } from '@/api/developer'
+import { APP_TYPE_NAME } from '@/types/developer'
+import './create.scss'
+
+const AppCreate: React.FC = () => {
+ const [formData, setFormData] = useState({
+ name: '',
+ type: 20 as number, // 微信小程序
+ description: '',
+ })
+ const [loading, setLoading] = useState(false)
+
+ const appTypes = [
+ { value: 10, label: '网站' },
+ { value: 20, label: '微信小程序' },
+ { value: 30, label: '抖音小程序' },
+ { value: 40, label: '百度小程序' },
+ { value: 50, label: '支付宝小程序' },
+ { value: 60, label: 'Android APP' },
+ { value: 70, label: 'iOS APP' },
+ { value: 100, label: '插件' },
+ ]
+
+ // 更新表单数据
+ const updateField = (field: string, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }))
+ }
+
+ // 提交表单
+ const handleSubmit = async () => {
+ if (!formData.name.trim()) {
+ Taro.showToast({ title: '请输入应用名称', icon: 'none' })
+ return
+ }
+
+ try {
+ setLoading(true)
+ await createApp(formData)
+ Taro.showToast({ title: '创建成功', icon: 'success' })
+ setTimeout(() => {
+ Taro.navigateBack()
+ }, 1500)
+ } catch (error) {
+ console.error('创建失败', error)
+ Taro.showToast({ title: '创建失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ {/* 应用名称 */}
+
+
+ 应用名称 *
+
+ updateField('name', e.detail.value)}
+ maxlength={50}
+ />
+
+
+ {/* 应用类型 */}
+
+
+ 应用类型 *
+
+ updateField('type', Number(v))}>
+
+ {appTypes.map((type) => (
+
+
+ {type.label}
+
+
+ ))}
+
+
+
+
+ {/* 应用描述 */}
+
+
+ 应用描述
+
+
+
+
+
+ {/* 底部按钮 */}
+
+
+
+
+ )
+}
+
+export default AppCreate
diff --git a/src/developer/app/index.scss b/src/developer/app/index.scss
new file mode 100644
index 0000000..619bf1b
--- /dev/null
+++ b/src/developer/app/index.scss
@@ -0,0 +1,159 @@
+page {
+ background: #f5f6f7;
+}
+
+.app-list-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding-bottom: 140rpx;
+
+ &__scroll {
+ height: calc(100vh - 100rpx);
+ }
+}
+
+.tabs-wrapper {
+ background: #ffffff;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+/* 应用列表 */
+.app-list {
+ padding: 24rpx;
+
+ &__loading {
+ padding: 120rpx 0;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+ }
+}
+
+/* 应用卡片 */
+.app-card {
+ display: flex;
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 24rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
+
+ &__icon {
+ width: 100rpx;
+ height: 100rpx;
+ border-radius: 20rpx;
+ background: #f3f4f6;
+ overflow: hidden;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &__iconImg {
+ width: 100%;
+ height: 100%;
+ }
+
+ &__iconDefault {
+ font-size: 48rpx;
+ }
+
+ &__info {
+ flex: 1;
+ margin-left: 20rpx;
+ min-width: 0;
+ }
+
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12rpx;
+ }
+
+ &__name {
+ font-size: 32rpx;
+ font-weight: 800;
+ color: #111111;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__status {
+ display: flex;
+ align-items: center;
+ gap: 6rpx;
+ flex-shrink: 0;
+ }
+
+ &__statusDot {
+ width: 8rpx;
+ height: 8rpx;
+ border-radius: 999rpx;
+ }
+
+ &__statusText {
+ font-size: 22rpx;
+ font-weight: 600;
+ }
+
+ &__type {
+ display: block;
+ margin-top: 6rpx;
+ font-size: 24rpx;
+ color: #666666;
+ }
+
+ &__desc {
+ display: block;
+ margin-top: 10rpx;
+ font-size: 24rpx;
+ color: #999999;
+ line-height: 1.5;
+ }
+
+ &__meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 12rpx;
+ }
+
+ &__version {
+ font-size: 22rpx;
+ color: #3b82f6;
+ font-weight: 600;
+ }
+
+ &__time {
+ font-size: 22rpx;
+ color: #999999;
+ }
+}
+
+/* 创建按钮 */
+.create-btn-wrapper {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 24rpx 32rpx;
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+ background: #ffffff;
+ box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
+}
+
+/* 底部安全区域 */
+.safe-area-bottom {
+ height: 40rpx;
+}
diff --git a/src/developer/app/index.tsx b/src/developer/app/index.tsx
new file mode 100644
index 0000000..2488c61
--- /dev/null
+++ b/src/developer/app/index.tsx
@@ -0,0 +1,149 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, ScrollView, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Empty, Tabs, PullToRefresh } from '@nutui/nutui-react-taro'
+import { pageMyApp } from '@/api/developer'
+import type { App } from '@/types/developer'
+import { APP_TYPE_NAME, STATUS_NAME, STATUS_COLOR } from '@/types/developer'
+import './index.scss'
+
+const AppList: React.FC = () => {
+ const [activeTab, setActiveTab] = useState('all')
+ const [apps, setApps] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+
+ // 加载数据
+ const loadData = async () => {
+ try {
+ setLoading(true)
+ const result = await pageMyApp({ current: 1, size: 20 })
+ if (result) {
+ setApps(result.list || [])
+ }
+ } catch (error) {
+ console.error('加载失败', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 下拉刷新
+ const onRefresh = async () => {
+ setRefreshing(true)
+ await loadData()
+ setRefreshing(false)
+ }
+
+ useEffect(() => {
+ loadData()
+ }, [])
+
+ // 筛选应用
+ const filteredApps = activeTab === 'all'
+ ? apps
+ : apps.filter((app) => {
+ if (activeTab === 'running') return app.status === 1
+ if (activeTab === 'stopped') return app.status !== 1
+ return true
+ })
+
+ // 跳转创建页面
+ const handleCreate = () => {
+ Taro.navigateTo({ url: '/developer/app/create' })
+ }
+
+ // 跳转应用详情
+ const handleAppClick = (id: number) => {
+ Taro.navigateTo({ url: `/developer/app/${id}` })
+ }
+
+ // 获取状态颜色
+ const getStatusColor = (status?: number) => {
+ return STATUS_COLOR[status || 0] || '#6b7280'
+ }
+
+ // 获取状态文本
+ const getStatusText = (status?: number) => {
+ return STATUS_NAME[status || 0] || '未知'
+ }
+
+ return (
+
+
+ {/* 标签页 */}
+
+ setActiveTab(v as string)}>
+
+
+
+
+
+
+
+ {/* 应用列表 */}
+
+ {loading ? (
+
+ 加载中...
+
+ ) : filteredApps.length === 0 ? (
+
+ ) : (
+
+ {filteredApps.map((app) => (
+ handleAppClick(app.productId!)}>
+
+ {app.icon ? (
+
+ ) : (
+ 📱
+ )}
+
+
+
+
+ {app.productName}
+
+
+ {getStatusText(app.status)}
+
+
+
+
+ {APP_TYPE_NAME[app.appType as keyof typeof APP_TYPE_NAME] || '未知'}
+
+
+
+ {app.description || '暂无描述'}
+
+
+
+ v{app.version || '1.0.0'}
+
+ 更新于 {app.updateTime ? app.updateTime.split(' ')[0] : '-'}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* 底部安全区域 */}
+
+
+
+ {/* 创建按钮 */}
+
+
+
+
+
+ )
+}
+
+export default AppList
diff --git a/src/developer/developer/apply.scss b/src/developer/developer/apply.scss
new file mode 100644
index 0000000..2f5bc25
--- /dev/null
+++ b/src/developer/developer/apply.scss
@@ -0,0 +1,223 @@
+.apply-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 120rpx;
+
+ // Tab
+ &__tabs {
+ display: flex;
+ background: #fff;
+ padding: 0 32rpx;
+ }
+
+ &__tab {
+ flex: 1;
+ height: 96rpx;
+ line-height: 96rpx;
+ text-align: center;
+ font-size: 30rpx;
+ color: #666;
+ border-bottom: 4rpx solid transparent;
+
+ &.active {
+ color: #667eea;
+ border-bottom-color: #667eea;
+ font-weight: 600;
+ }
+ }
+
+ // 表单
+ &__form {
+ padding: 32rpx;
+ }
+
+ &__section {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ margin-bottom: 24rpx;
+
+ &-title {
+ display: block;
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 32rpx;
+ padding-left: 16rpx;
+ border-left: 6rpx solid #667eea;
+ }
+ }
+
+ &__field {
+ margin-bottom: 32rpx;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__label {
+ display: block;
+ font-size: 28rpx;
+ color: #333;
+ margin-bottom: 16rpx;
+ }
+
+ &__input {
+ width: 100%;
+ height: 88rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 0 24rpx;
+ font-size: 30rpx;
+ }
+
+ &__textarea {
+ width: 100%;
+ min-height: 200rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ font-size: 30rpx;
+ line-height: 1.6;
+ box-sizing: border-box;
+ }
+
+ &__agreement {
+ padding: 24rpx 0;
+ text-align: center;
+
+ &-text {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+
+ &__submit {
+ height: 96rpx;
+ line-height: 96rpx;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ border-radius: 48rpx;
+ font-size: 32rpx;
+ font-weight: 600;
+
+ &::after {
+ border: none;
+ }
+ }
+
+ // 列表
+ &__list {
+ padding: 24rpx 32rpx;
+ }
+
+ &__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 120rpx 0;
+
+ &-icon {
+ font-size: 120rpx;
+ margin-bottom: 32rpx;
+ }
+
+ &-text {
+ font-size: 30rpx;
+ color: #999;
+ }
+ }
+
+ &__loading,
+ &__no-more {
+ display: flex;
+ justify-content: center;
+ padding: 32rpx 0;
+
+ text {
+ font-size: 26rpx;
+ color: #999;
+ }
+ }
+}
+
+// 申请卡片
+.apply-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ margin-bottom: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+ }
+
+ &__type {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__status {
+ font-size: 24rpx;
+ padding: 8rpx 20rpx;
+ border-radius: 8rpx;
+ }
+
+ &__reason {
+ background: #fafafa;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ margin-bottom: 24rpx;
+
+ &-label {
+ display: block;
+ font-size: 26rpx;
+ color: #666;
+ margin-bottom: 8rpx;
+ }
+
+ &-content {
+ font-size: 28rpx;
+ color: #333;
+ line-height: 1.5;
+ }
+ }
+
+ &__footer {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16rpx;
+ }
+
+ &__time,
+ &__review {
+ font-size: 24rpx;
+ color: #999;
+ }
+
+ &__remark {
+ background: #fff7e6;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ margin-top: 16rpx;
+
+ &-label {
+ display: block;
+ font-size: 24rpx;
+ color: #fa8c16;
+ margin-bottom: 8rpx;
+ }
+
+ &-content {
+ font-size: 26rpx;
+ color: #333;
+ }
+ }
+}
diff --git a/src/developer/developer/apply.tsx b/src/developer/developer/apply.tsx
new file mode 100644
index 0000000..a74acd6
--- /dev/null
+++ b/src/developer/developer/apply.tsx
@@ -0,0 +1,301 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, Input, Button, Textarea } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { useReachBottom } from '@tarojs/taro'
+import { pageMyApply, createApply } from '@/api/developer/developer'
+import type { Apply, ApplyParam, ApplyStatus } from '@/types/developer'
+import './apply.scss'
+
+// 状态配置
+const STATUS_CONFIG: Record = {
+ pending: { label: '待审核', color: '#faad14', bgColor: '#fffbe6' },
+ approved: { label: '已通过', color: '#52c41a', bgColor: '#f6ffed' },
+ rejected: { label: '已拒绝', color: '#ff4d4f', bgColor: '#fff1f0' },
+}
+
+const DeveloperApplyPage: React.FC = () => {
+ const [tab, setTab] = useState<'apply' | 'record'>('apply')
+ const [loading, setLoading] = useState(false)
+ const [refreshing, setRefreshing] = useState(false)
+ const [list, setList] = useState([])
+ const [page, setPage] = useState(1)
+ const [hasMore, setHasMore] = useState(true)
+
+ // 申请表单
+ const [form, setForm] = useState({
+ realName: '',
+ phone: '',
+ email: '',
+ company: '',
+ position: '',
+ experience: '',
+ })
+ const [submitting, setSubmitting] = useState(false)
+
+ // 加载申请记录
+ const loadData = async (pageNum: number = 1, isRefresh = false) => {
+ if (loading) return
+ setLoading(true)
+ if (isRefresh) setRefreshing(true)
+
+ try {
+ const params: ApplyParam = { page: pageNum, limit: 20 }
+ const data = await pageMyApply(params)
+
+ if (pageNum === 1) {
+ setList(data?.records || [])
+ } else {
+ setList(prev => [...prev, ...(data?.records || [])])
+ }
+
+ const total = data?.total || 0
+ const records = data?.records || []
+ setHasMore(records.length > 0 && (pageNum * 20) < total)
+ setPage(pageNum)
+ } catch (err) {
+ console.error('加载失败', err)
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => {
+ if (tab === 'record') {
+ loadData()
+ }
+ }, [tab])
+
+ // 提交申请
+ const handleSubmit = async () => {
+ if (!form.realName.trim()) {
+ Taro.showToast({ title: '请输入真实姓名', icon: 'none' })
+ return
+ }
+ if (!form.phone.trim()) {
+ Taro.showToast({ title: '请输入手机号码', icon: 'none' })
+ return
+ }
+
+ setSubmitting(true)
+ try {
+ await createApply({
+ type: 'developer',
+ reason: JSON.stringify(form),
+ } as Partial)
+ Taro.showToast({ title: '申请已提交', icon: 'success' })
+ setForm({
+ realName: '',
+ phone: '',
+ email: '',
+ company: '',
+ position: '',
+ experience: '',
+ })
+ setTab('record')
+ loadData(1)
+ } catch (err) {
+ console.error('提交失败', err)
+ Taro.showToast({ title: '提交失败', icon: 'none' })
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ // 格式化日期
+ const formatDate = (dateStr?: string) => {
+ if (!dateStr) return '-'
+ const date = new Date(dateStr)
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+ }
+
+ return (
+
+ {/* Tab */}
+
+ setTab('apply')}
+ >
+ 申请成为开发者
+
+ setTab('record')}
+ >
+ 申请记录
+
+
+
+ {/* 申请表单 */}
+ {tab === 'apply' && (
+
+
+ 基本信息
+
+
+ 真实姓名 *
+ setForm(prev => ({ ...prev, realName: e.detail.value }))}
+ />
+
+
+
+ 手机号码 *
+ setForm(prev => ({ ...prev, phone: e.detail.value }))}
+ />
+
+
+
+ 电子邮箱
+ setForm(prev => ({ ...prev, email: e.detail.value }))}
+ />
+
+
+
+
+ 职业信息
+
+
+ 公司/组织
+ setForm(prev => ({ ...prev, company: e.detail.value }))}
+ />
+
+
+
+ 职位
+ setForm(prev => ({ ...prev, position: e.detail.value }))}
+ />
+
+
+
+ 开发经验
+
+
+
+
+
+ 点击提交即表示您同意《开发者协议》和《隐私政策》
+
+
+
+
+
+ )}
+
+ {/* 申请记录 */}
+ {tab === 'record' && (
+
+ {list.length === 0 && !loading ? (
+
+ 📋
+ 暂无申请记录
+
+ ) : (
+ list.map((item) => (
+
+
+
+ 开发者申请
+
+
+ {STATUS_CONFIG[item.status as ApplyStatus]?.label}
+
+
+
+ {item.reason && (
+
+ 申请信息:
+
+ {(() => {
+ try {
+ const info = JSON.parse(item.reason)
+ return `姓名: ${info.realName} | 手机: ${info.phone}`
+ } catch {
+ return item.reason
+ }
+ })()}
+
+
+ )}
+
+
+
+ 申请时间: {formatDate(item.createTime)}
+
+ {item.reviewTime && (
+
+ 审核时间: {formatDate(item.reviewTime)}
+
+ )}
+
+
+ {item.reviewRemark && (
+
+ 审核备注:
+ {item.reviewRemark}
+
+ )}
+
+ ))
+ )}
+
+ {loading && list.length === 0 && (
+
+ 加载中...
+
+ )}
+
+ {!hasMore && list.length > 0 && (
+
+ 没有更多了
+
+ )}
+
+ )}
+
+ )
+}
+
+export default DeveloperApplyPage
diff --git a/src/developer/developer/profile.config.ts b/src/developer/developer/profile.config.ts
new file mode 100644
index 0000000..a67d588
--- /dev/null
+++ b/src/developer/developer/profile.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '开发者资料',
+})
diff --git a/src/developer/developer/profile.scss b/src/developer/developer/profile.scss
new file mode 100644
index 0000000..1dad64b
--- /dev/null
+++ b/src/developer/developer/profile.scss
@@ -0,0 +1,10 @@
+.profile-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/developer/developer/profile.tsx b/src/developer/developer/profile.tsx
new file mode 100644
index 0000000..17891ae
--- /dev/null
+++ b/src/developer/developer/profile.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './profile.scss'
+
+const ProfilePage: React.FC = () => {
+ return (
+
+
+ 开发者资料设置开发中...
+
+
+ )
+}
+
+export default ProfilePage
diff --git a/src/developer/docs/api-docs.config.ts b/src/developer/docs/api-docs.config.ts
new file mode 100644
index 0000000..c0332cc
--- /dev/null
+++ b/src/developer/docs/api-docs.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'API 文档',
+})
diff --git a/src/developer/docs/api-docs.scss b/src/developer/docs/api-docs.scss
new file mode 100644
index 0000000..c2a6c06
--- /dev/null
+++ b/src/developer/docs/api-docs.scss
@@ -0,0 +1,10 @@
+.api-docs-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/developer/docs/api-docs.tsx b/src/developer/docs/api-docs.tsx
new file mode 100644
index 0000000..3307ee6
--- /dev/null
+++ b/src/developer/docs/api-docs.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './api-docs.scss'
+
+const ApiDocsPage: React.FC = () => {
+ return (
+
+
+ API 文档开发中...
+
+
+ )
+}
+
+export default ApiDocsPage
diff --git a/src/developer/docs/index.config.ts b/src/developer/docs/index.config.ts
new file mode 100644
index 0000000..4de3616
--- /dev/null
+++ b/src/developer/docs/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '开发者文档',
+})
diff --git a/src/developer/docs/index.scss b/src/developer/docs/index.scss
new file mode 100644
index 0000000..9059c01
--- /dev/null
+++ b/src/developer/docs/index.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.docs-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/developer/docs/index.tsx b/src/developer/docs/index.tsx
new file mode 100644
index 0000000..75f2197
--- /dev/null
+++ b/src/developer/docs/index.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './index.scss'
+
+const DeveloperDocs: React.FC = () => {
+ return (
+
+
+ 📖 开发者文档
+
+
+ 开发者文档功能开发中...
+
+
+ )
+}
+
+export default DeveloperDocs
diff --git a/src/developer/docs/quickstart.config.ts b/src/developer/docs/quickstart.config.ts
new file mode 100644
index 0000000..9a631b5
--- /dev/null
+++ b/src/developer/docs/quickstart.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '快速开始',
+})
diff --git a/src/developer/docs/quickstart.scss b/src/developer/docs/quickstart.scss
new file mode 100644
index 0000000..f15d572
--- /dev/null
+++ b/src/developer/docs/quickstart.scss
@@ -0,0 +1,10 @@
+.quickstart-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/developer/docs/quickstart.tsx b/src/developer/docs/quickstart.tsx
new file mode 100644
index 0000000..4fbf684
--- /dev/null
+++ b/src/developer/docs/quickstart.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './quickstart.scss'
+
+const QuickstartPage: React.FC = () => {
+ return (
+
+
+ 快速开始文档开发中...
+
+
+ )
+}
+
+export default QuickstartPage
diff --git a/src/developer/index.config.ts b/src/developer/index.config.ts
index 1293fcb..8574af0 100644
--- a/src/developer/index.config.ts
+++ b/src/developer/index.config.ts
@@ -1,3 +1,5 @@
export default definePageConfig({
- navigationBarTitleText: '配送中心'
+ navigationBarTitleText: '开发者中心',
+ navigationStyle: 'custom',
+ usingComponents: {},
})
diff --git a/src/developer/index.scss b/src/developer/index.scss
index e69de29..f56ac9a 100644
--- a/src/developer/index.scss
+++ b/src/developer/index.scss
@@ -0,0 +1,285 @@
+page {
+ background: #f5f6f7;
+}
+
+.developer-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+
+ &__scroll {
+ height: 100vh;
+ }
+}
+
+/* 头部区域 */
+.developer-header {
+ position: relative;
+ padding: 40rpx 32rpx 60rpx;
+ background: linear-gradient(180deg, #1a56db 0%, #3b82f6 100%);
+ overflow: hidden;
+
+ &__glow {
+ position: absolute;
+ left: 50%;
+ top: -100rpx;
+ width: 500rpx;
+ height: 300rpx;
+ transform: translateX(-50%);
+ border-radius: 999rpx;
+ background: rgba(255, 255, 255, 0.15);
+ filter: blur(60rpx);
+ pointer-events: none;
+ }
+
+ &__content {
+ position: relative;
+ z-index: 1;
+ }
+
+ &__title {
+ margin-bottom: 12rpx;
+ }
+
+ &__titleText {
+ font-size: 44rpx;
+ font-weight: 900;
+ color: #ffffff;
+ }
+
+ &__subtitle {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.85);
+ }
+}
+
+/* 统计卡片 */
+.stats-card {
+ margin: -40rpx 24rpx 24rpx;
+ padding: 32rpx 24rpx;
+ background: #ffffff;
+ border-radius: 20rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
+
+ &__row {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ }
+
+ &__item {
+ flex: 1;
+ text-align: center;
+ }
+
+ &__value {
+ display: block;
+ font-size: 48rpx;
+ font-weight: 900;
+ color: #1a56db;
+ line-height: 1.2;
+ }
+
+ &__label {
+ display: block;
+ margin-top: 8rpx;
+ font-size: 24rpx;
+ color: #666666;
+ }
+
+ &__divider {
+ width: 2rpx;
+ height: 60rpx;
+ background: #e5e7eb;
+ }
+}
+
+/* 快捷操作 */
+.quick-actions {
+ margin: 0 24rpx 24rpx;
+ padding: 24rpx;
+ background: #ffffff;
+ border-radius: 20rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
+
+ &__title {
+ margin-bottom: 20rpx;
+ }
+
+ &__titleText {
+ font-size: 32rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20rpx;
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12rpx;
+ padding: 20rpx 10rpx;
+ border-radius: 16rpx;
+ background: #f9fafb;
+ }
+
+ &__icon {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ }
+
+ &__icon--blue {
+ background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
+ }
+
+ &__icon--green {
+ background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+ }
+
+ &__icon--purple {
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
+ }
+
+ &__icon--orange {
+ background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
+ }
+
+ &__text {
+ font-size: 24rpx;
+ color: #374151;
+ font-weight: 600;
+ }
+}
+
+/* 应用列表 */
+.app-list {
+ margin: 0 24rpx 24rpx;
+ padding: 24rpx;
+ background: #ffffff;
+ border-radius: 20rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
+
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20rpx;
+ }
+
+ &__title {
+ font-size: 32rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__more {
+ padding: 8rpx 16rpx;
+ }
+
+ &__moreText {
+ font-size: 26rpx;
+ color: #666666;
+ }
+
+ &__loading {
+ padding: 60rpx 0;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+ }
+
+ &__footer {
+ margin-top: 24rpx;
+ }
+}
+
+/* 应用项 */
+.app-item {
+ display: flex;
+ align-items: center;
+ padding: 20rpx;
+ border-radius: 16rpx;
+ background: #f9fafb;
+
+ &__icon {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 16rpx;
+ background: #e5e7eb;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40rpx;
+ overflow: hidden;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ &__info {
+ flex: 1;
+ margin-left: 20rpx;
+ min-width: 0;
+ }
+
+ &__name {
+ display: block;
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #111111;
+ line-height: 1.3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__type {
+ display: block;
+ margin-top: 6rpx;
+ font-size: 24rpx;
+ color: #666666;
+ }
+
+ &__status {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ padding: 8rpx 16rpx;
+ border-radius: 999rpx;
+ background: #f3f4f6;
+ }
+
+ &__statusDot {
+ width: 8rpx;
+ height: 8rpx;
+ border-radius: 999rpx;
+ }
+
+ &__statusText {
+ font-size: 22rpx;
+ font-weight: 600;
+ }
+}
+
+/* 底部安全区域 */
+.safe-area-bottom {
+ height: calc(40rpx + env(safe-area-inset-bottom));
+}
diff --git a/src/developer/index.tsx b/src/developer/index.tsx
index ca85d76..29fbc76 100644
--- a/src/developer/index.tsx
+++ b/src/developer/index.tsx
@@ -1,295 +1,197 @@
-import React from 'react'
-import {View, Text} from '@tarojs/components'
-import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
-import {
- User,
- Shopping,
- Dongdong,
- ArrowRight,
- Purse,
- People
-} from '@nutui/icons-react-taro'
-import {useDealerUser} from '@/hooks/useDealerUser'
-import { useThemeStyles } from '@/hooks/useTheme'
-import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
+import React, { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
+import { Button, Empty } from '@nutui/nutui-react-taro'
+import { useThemeStyles } from '@/hooks/useTheme'
+import { pageMyApp } from '@/api/developer'
+import type { App } from '@/types/developer'
+import { APP_TYPE_NAME } from '@/types/developer'
+import './index.scss'
-const DealerIndex: React.FC = () => {
- const {
- dealerUser,
- error,
- refresh,
- } = useDealerUser()
-
- // 使用主题样式
+const DeveloperIndex: React.FC = () => {
const themeStyles = useThemeStyles()
+ const [apps, setApps] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [stats, setStats] = useState({
+ totalApps: 0,
+ runningApps: 0,
+ totalCalls: 0,
+ })
- // 导航到各个功能页面
- const navigateToPage = (url: string) => {
- Taro.navigateTo({url})
- }
-
- // 格式化金额
- const formatMoney = (money?: string) => {
- if (!money) return '0.00'
- return parseFloat(money).toFixed(2)
- }
-
- // 格式化时间
- const formatTime = (time?: string) => {
- if (!time) return '-'
- return new Date(time).toLocaleDateString()
- }
-
- // 获取用户主题
- const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
-
- // 获取渐变背景
- const getGradientBackground = (themeColor?: string) => {
- if (themeColor) {
- const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
- return gradientUtils.createGradient(themeColor, darkerColor)
+ // 加载数据
+ const loadData = async () => {
+ try {
+ setLoading(true)
+ const result = await pageMyApp({ current: 1, size: 10 })
+ if (result) {
+ setApps(result.list || [])
+ setStats({
+ totalApps: result.count || 0,
+ runningApps: result.list?.filter((a) => a.status === 1).length || 0,
+ totalCalls: 0,
+ })
+ }
+ } catch (error) {
+ console.error('加载数据失败', error)
+ Taro.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ setLoading(false)
}
- return userTheme.background
}
- console.log(getGradientBackground(),'getGradientBackground()')
+ useEffect(() => {
+ loadData()
+ }, [])
- if (error) {
- return (
-
-
- {error}
-
-
-
- )
+ // 跳转页面
+ const navigateTo = (url: string) => {
+ Taro.navigateTo({ url })
+ }
+
+ // 获取状态颜色
+ const getStatusColor = (status?: number) => {
+ const colors: Record = {
+ 0: '#6b7280',
+ 1: '#10b981',
+ 2: '#f59e0b',
+ 3: '#ef4444',
+ 4: '#ef4444',
+ 5: '#ef4444',
+ }
+ return colors[status || 0] || '#6b7280'
+ }
+
+ // 获取状态文本
+ const getStatusText = (status?: number) => {
+ const texts: Record = {
+ 0: '未开通',
+ 1: '运行中',
+ 2: '维护中',
+ 3: '已关闭',
+ 4: '已欠费',
+ 5: '违规停机',
+ }
+ return texts[status || 0] || '未知'
}
return (
-
-
- {/*头部信息*/}
- {dealerUser && (
-
- {/* 装饰性背景元素 - 小程序兼容版本 */}
-
-
-
-
- }
- className="mr-4"
- style={{
- border: '2px solid rgba(255, 255, 255, 0.3)'
- }}
- />
-
-
- {dealerUser?.realName || '分销商'}
-
-
- ID: {dealerUser.userId}
-
-
-
- 加入时间
-
- {formatTime(dealerUser.createTime)}
-
-
+
+
+ {/* 头部区域 */}
+
+
+
+
+ 🛠️ 开发者中心
+ 管理你的项目和应用
- )}
-
- {/* 佣金统计卡片 */}
- {dealerUser && (
-
-
- 工资统计
-
-
-
-
- {formatMoney(dealerUser.money)}
-
- 工资收入
-
-
-
- {formatMoney(dealerUser.freezeMoney)}
-
- 桶数
-
-
-
- {formatMoney(dealerUser.totalMoney)}
-
- 累计收入
-
-
-
- )}
-
- {/* 团队统计 */}
- {dealerUser && (
-
-
- 我的邀请
- navigateToPage('/dealer/team/index')}
- >
- 查看详情
-
-
-
-
-
-
- {dealerUser.firstNum || 0}
-
- 一级成员
-
-
-
- {dealerUser.secondNum || 0}
-
- 二级成员
-
-
-
- {dealerUser.thirdNum || 0}
-
- 三级成员
-
-
-
- )}
-
- {/* 功能导航 */}
-
- 配送工具
-
-
- navigateToPage('/rider/orders/index')}>
-
-
-
-
-
-
-
- navigateToPage('/rider/withdraw/index')}>
-
-
-
-
-
-
-
- navigateToPage('/rider/team/index')}>
-
-
-
-
-
-
-
- navigateToPage('/rider/qrcode/index')}>
-
-
-
-
-
-
-
-
- {/* 第二行功能 */}
- {/**/}
- {/* navigateToPage('/dealer/invite-stats/index')}>*/}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
-
- {/* /!* 预留其他功能位置 *!/*/}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
-
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
-
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
- {/**/}
-
-
- {/* 底部安全区域 */}
-
+ {/* 统计卡片 */}
+
+
+
+ {stats.totalApps}
+ 我的应用
+
+
+
+ {stats.runningApps}
+ 运行中
+
+
+
+ {stats.totalCalls}
+ API 调用
+
+
+
+
+ {/* 快捷操作 */}
+
+
+ 快捷操作
+
+
+ navigateTo('/developer/project/index')}>
+
+ 📁
+
+ 项目管理
+
+ navigateTo('/developer/app/index')}>
+
+ 📱
+
+ 应用管理
+
+ navigateTo('/developer/app/api-keys')}>
+
+ 🔑
+
+ API Key
+
+ navigateTo('/developer/docs/index')}>
+
+ 📖
+
+ 开发者文档
+
+
+
+
+ {/* 应用列表 */}
+
+
+ 我的应用
+ navigateTo('/developer/app/index')}>
+ 查看全部 ›
+
+
+
+ {loading ? (
+
+ 加载中...
+
+ ) : apps.length === 0 ? (
+
+ ) : (
+
+ {apps.map((app) => (
+ navigateTo(`/developer/app/${app.productId}`)}>
+
+ {app.icon ? (
+
+ ) : (
+ 📱
+ )}
+
+
+ {app.productName}
+ {APP_TYPE_NAME[app.appType as keyof typeof APP_TYPE_NAME] || '未知'}
+
+
+
+ {getStatusText(app.status)}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ {/* 底部安全区域 */}
+
+
)
}
-export default DealerIndex
+export default DeveloperIndex
diff --git a/src/developer/market/index.config.ts b/src/developer/market/index.config.ts
new file mode 100644
index 0000000..5c76527
--- /dev/null
+++ b/src/developer/market/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '开发者市场',
+})
diff --git a/src/developer/market/index.scss b/src/developer/market/index.scss
new file mode 100644
index 0000000..6fec1dc
--- /dev/null
+++ b/src/developer/market/index.scss
@@ -0,0 +1,20 @@
+.market-page {
+ padding: 32rpx;
+
+ &__header {
+ margin-bottom: 32rpx;
+ }
+
+ &__title {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: #333;
+ }
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/developer/market/index.tsx b/src/developer/market/index.tsx
new file mode 100644
index 0000000..a38c7f6
--- /dev/null
+++ b/src/developer/market/index.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './index.scss'
+
+const MarketPage: React.FC = () => {
+ return (
+
+
+ 🛒 开发者市场
+
+
+ 模板和插件市场开发中...
+
+
+ )
+}
+
+export default MarketPage
diff --git a/src/developer/notification/index.scss b/src/developer/notification/index.scss
new file mode 100644
index 0000000..ff85f3e
--- /dev/null
+++ b/src/developer/notification/index.scss
@@ -0,0 +1,183 @@
+.notification-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 120rpx;
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32rpx 32rpx 24rpx;
+ background: #fff;
+
+ &-left {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ }
+ }
+
+ &__title {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: #333;
+ }
+
+ &__badge {
+ background: #ff4d4f;
+ color: #fff;
+ font-size: 22rpx;
+ padding: 4rpx 12rpx;
+ border-radius: 20rpx;
+ min-width: 36rpx;
+ text-align: center;
+ }
+
+ &__mark-read {
+ font-size: 28rpx;
+ color: #667eea;
+ }
+
+ // Tab
+ &__tabs {
+ display: flex;
+ padding: 0 32rpx 24rpx;
+ background: #fff;
+ gap: 24rpx;
+ }
+
+ &__tab {
+ font-size: 28rpx;
+ color: #666;
+ padding-bottom: 8rpx;
+ border-bottom: 4rpx solid transparent;
+
+ &.active {
+ color: #667eea;
+ border-bottom-color: #667eea;
+ }
+ }
+
+ // 列表
+ &__list {
+ padding: 24rpx 32rpx;
+ }
+
+ &__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 120rpx 0;
+
+ &-icon {
+ font-size: 120rpx;
+ margin-bottom: 32rpx;
+ }
+
+ &-text {
+ font-size: 30rpx;
+ color: #999;
+ }
+ }
+
+ &__loading,
+ &__no-more {
+ display: flex;
+ justify-content: center;
+ padding: 32rpx 0;
+
+ text {
+ font-size: 26rpx;
+ color: #999;
+ }
+ }
+}
+
+// 通知卡片
+.notification-card {
+ display: flex;
+ align-items: flex-start;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 28rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+ position: relative;
+
+ &.unread {
+ background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%);
+ }
+
+ &.read {
+ opacity: 0.8;
+ }
+
+ &__icon {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 50%;
+ background: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40rpx;
+ margin-right: 24rpx;
+ flex-shrink: 0;
+ }
+
+ &__content {
+ flex: 1;
+ min-width: 0;
+ }
+
+ &__header {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ margin-bottom: 8rpx;
+ }
+
+ &__title {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__dot {
+ width: 16rpx;
+ height: 16rpx;
+ border-radius: 50%;
+ background: #ff4d4f;
+ flex-shrink: 0;
+ }
+
+ &__body {
+ font-size: 28rpx;
+ color: #666;
+ line-height: 1.5;
+ margin-bottom: 12rpx;
+ }
+
+ &__time {
+ font-size: 24rpx;
+ color: #999;
+ }
+
+ &__delete {
+ position: absolute;
+ top: 16rpx;
+ right: 16rpx;
+ width: 48rpx;
+ height: 48rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36rpx;
+ color: #ccc;
+
+ &:active {
+ color: #999;
+ }
+ }
+}
diff --git a/src/developer/notification/index.tsx b/src/developer/notification/index.tsx
new file mode 100644
index 0000000..e54ea00
--- /dev/null
+++ b/src/developer/notification/index.tsx
@@ -0,0 +1,288 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, ActionSheet } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { usePullDownRefresh } from '@tarojs/taro'
+import {
+ pageNotification,
+ getUnreadCount,
+ markAsRead,
+ markAllAsRead,
+ deleteNotification,
+} from '@/api/developer/developer'
+import type { Notification, NotificationParam, NotificationType } from '@/types/developer'
+import './index.scss'
+
+// 通知类型配置
+const TYPE_CONFIG: Record = {
+ system: { label: '系统通知', icon: '🔔', color: '#1890ff' },
+ app: { label: '应用通知', icon: '📱', color: '#52c41a' },
+ member: { label: '成员通知', icon: '👥', color: '#722ed1' },
+ order: { label: '订单通知', icon: '💰', color: '#fa8c16' },
+ developer: { label: '开发通知', icon: '🛠️', color: '#13c2c2' },
+}
+
+const NotificationPage: React.FC = () => {
+ const [loading, setLoading] = useState(false)
+ const [refreshing, setRefreshing] = useState(false)
+ const [list, setList] = useState([])
+ const [page, setPage] = useState(1)
+ const [hasMore, setHasMore] = useState(true)
+ const [unreadCount, setUnreadCount] = useState(0)
+ const [currentTab, setCurrentTab] = useState('all')
+
+ // 加载数据
+ const loadData = async (pageNum: number = 1, isRefresh = false) => {
+ if (loading) return
+ setLoading(true)
+ if (isRefresh) setRefreshing(true)
+
+ try {
+ const params: NotificationParam = {
+ page: pageNum,
+ limit: 20,
+ type: currentTab === 'all' ? undefined : currentTab,
+ }
+ const data = await pageNotification(params)
+
+ if (pageNum === 1) {
+ setList(data?.records || [])
+ } else {
+ setList(prev => [...prev, ...(data?.records || [])])
+ }
+
+ const total = data?.total || 0
+ const records = data?.records || []
+ setHasMore(records.length > 0 && (pageNum * 20) < total)
+ setPage(pageNum)
+ } catch (err) {
+ console.error('加载失败', err)
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ // 加载未读数
+ const loadUnreadCount = async () => {
+ try {
+ const data = await getUnreadCount()
+ setUnreadCount(data?.count || 0)
+ } catch (err) {
+ console.error('获取未读数失败', err)
+ }
+ }
+
+ useEffect(() => {
+ loadData(1)
+ loadUnreadCount()
+ }, [currentTab])
+
+ // 下拉刷新
+ usePullDownRefresh(() => {
+ loadData(1, true)
+ loadUnreadCount()
+ })
+
+ // 加载更多
+ const onReachBottom = () => {
+ if (hasMore && !loading) {
+ loadData(page + 1)
+ }
+ }
+
+ // 标记已读
+ const handleRead = async (item: Notification) => {
+ if (item.isRead) return
+
+ try {
+ await markAsRead(item.id!)
+ setList(prev =>
+ prev.map(n => (n.id === item.id ? { ...n, isRead: true } : n))
+ )
+ setUnreadCount(prev => Math.max(0, prev - 1))
+ } catch (err) {
+ console.error('标记已读失败', err)
+ }
+ }
+
+ // 全部已读
+ const handleMarkAllRead = async () => {
+ try {
+ await markAllAsRead()
+ setList(prev => prev.map(n => ({ ...n, isRead: true })))
+ setUnreadCount(0)
+ Taro.showToast({ title: '已全部已读', icon: 'success' })
+ } catch (err) {
+ console.error('标记全部已读失败', err)
+ Taro.showToast({ title: '操作失败', icon: 'none' })
+ }
+ }
+
+ // 删除
+ const handleDelete = (item: Notification) => {
+ ActionSheet({
+ alertText: '确定删除这条通知吗?',
+ actions: [{ name: '删除', color: '#ff4d4f', type: 'warn' as const }],
+ confirmText: '取消',
+ }).then(res => {
+ if (res.confirm) {
+ doDelete(item)
+ }
+ }).catch(() => {})
+ }
+
+ const doDelete = async (item: Notification) => {
+ try {
+ await deleteNotification(item.id!)
+ setList(prev => prev.filter(n => n.id !== item.id))
+ if (!item.isRead) {
+ setUnreadCount(prev => Math.max(0, prev - 1))
+ }
+ Taro.showToast({ title: '已删除', icon: 'success' })
+ } catch (err) {
+ console.error('删除失败', err)
+ Taro.showToast({ title: '删除失败', icon: 'none' })
+ }
+ }
+
+ // 点击通知
+ const handleClick = (item: Notification) => {
+ handleRead(item)
+
+ // 根据通知类型和内容跳转
+ if (item.data?.url) {
+ Taro.navigateTo({ url: item.data.url })
+ } else if (item.data?.appId) {
+ Taro.navigateTo({ url: `/developer/app/${item.data.appId}/index` })
+ } else if (item.data?.projectId) {
+ Taro.navigateTo({ url: `/developer/project/${item.data.projectId}/index` })
+ }
+ }
+
+ // 格式化日期
+ const formatDate = (dateStr?: string) => {
+ if (!dateStr) return ''
+ const date = new Date(dateStr)
+ const now = new Date()
+ const diff = now.getTime() - date.getTime()
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24))
+
+ if (days === 0) {
+ return `今天 ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
+ } else if (days === 1) {
+ return '昨天'
+ } else if (days < 7) {
+ return `${days}天前`
+ } else {
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+ }
+ }
+
+ // Tab 配置
+ const tabs: { key: NotificationType | 'all'; label: string }[] = [
+ { key: 'all', label: '全部' },
+ { key: 'system', label: '系统' },
+ { key: 'app', label: '应用' },
+ { key: 'member', label: '成员' },
+ { key: 'order', label: '订单' },
+ ]
+
+ return (
+
+ {/* 头部 */}
+
+
+ 🔔 消息通知
+ {unreadCount > 0 && (
+
+ {unreadCount > 99 ? '99+' : unreadCount}
+
+ )}
+
+ {unreadCount > 0 && (
+
+ 全部已读
+
+ )}
+
+
+ {/* Tab */}
+
+ {tabs.map((tab) => (
+ setCurrentTab(tab.key)}
+ >
+ {tab.label}
+
+ ))}
+
+
+ {/* 列表 */}
+
+ {list.length === 0 && !loading ? (
+
+ 📭
+ 暂无通知
+
+ ) : (
+ list.map((item) => (
+ handleClick(item)}
+ >
+
+
+ {TYPE_CONFIG[item.type || 'system']?.icon}
+
+
+
+
+
+ {item.title}
+ {!item.isRead && }
+
+
+ {item.content}
+
+
+ {formatDate(item.createTime)}
+
+
+
+ {
+ e.stopPropagation()
+ handleDelete(item)
+ }}
+ >
+ ×
+
+
+ ))
+ )}
+
+ {/* 加载状态 */}
+ {loading && list.length > 0 && (
+
+ 加载中...
+
+ )}
+
+ {!hasMore && list.length > 0 && (
+
+ 没有更多了
+
+ )}
+
+
+ )
+}
+
+export default NotificationPage
diff --git a/src/developer/project/[id]/api-keys.config.ts b/src/developer/project/[id]/api-keys.config.ts
new file mode 100644
index 0000000..7ce54f5
--- /dev/null
+++ b/src/developer/project/[id]/api-keys.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'API Key 管理',
+})
diff --git a/src/developer/project/[id]/api-keys.scss b/src/developer/project/[id]/api-keys.scss
new file mode 100644
index 0000000..803e7d2
--- /dev/null
+++ b/src/developer/project/[id]/api-keys.scss
@@ -0,0 +1,2 @@
+// 复用 app/api-keys/index.scss 的样式
+@import '../../app/api-keys/index';
diff --git a/src/developer/project/[id]/api-keys.tsx b/src/developer/project/[id]/api-keys.tsx
new file mode 100644
index 0000000..2508bb7
--- /dev/null
+++ b/src/developer/project/[id]/api-keys.tsx
@@ -0,0 +1,139 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, Input, Button, actionSheet } from '@tarojs/components'
+import Taro, { useRouter } from '@tarojs/taro'
+import { usePullDownRefresh } from '@tarojs/taro'
+import { listApiKey, createApiKey, deleteApiKey } from '@/api/developer/developer'
+import type { ApiKey, ApiKeyParam } from '@/types/developer'
+import './api-keys.scss'
+
+const ApiKeysPage: React.FC = () => {
+ const router = useRouter()
+ const projectId = Number(router.params.id)
+
+ const [loading, setLoading] = useState(false)
+ const [list, setList] = useState([])
+ const [showCreate, setShowCreate] = useState(false)
+ const [createForm, setCreateForm] = useState({ name: '', remark: '' })
+ const [creating, setCreating] = useState(false)
+
+ const loadData = async () => {
+ setLoading(true)
+ try {
+ const params: ApiKeyParam = { websiteId: projectId }
+ const data = await listApiKey(params)
+ setList(data || [])
+ } catch (err) {
+ console.error('加载失败', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => { loadData() }, [projectId])
+ usePullDownRefresh(() => { loadData() })
+
+ const handleCreate = async () => {
+ if (!createForm.name.trim()) {
+ Taro.showToast({ title: '请输入名称', icon: 'none' })
+ return
+ }
+ setCreating(true)
+ try {
+ await createApiKey({ name: createForm.name, remark: createForm.remark, websiteId: projectId } as Partial)
+ Taro.showToast({ title: '创建成功', icon: 'success' })
+ setShowCreate(false)
+ setCreateForm({ name: '', remark: '' })
+ loadData()
+ } catch {
+ Taro.showToast({ title: '创建失败', icon: 'none' })
+ } finally {
+ setCreating(false)
+ }
+ }
+
+ const handleDelete = (item: ApiKey) => {
+ actionSheet({
+ alertText: `确定删除 "${item.name}" 吗?`,
+ actions: [{ name: '删除', color: '#ff4d4f', type: 'warn' as const }],
+ confirmText: '取消',
+ }).then(res => {
+ if (res.confirm) {
+ deleteApiKey(item.id!).then(() => {
+ Taro.showToast({ title: '已删除', icon: 'success' })
+ loadData()
+ })
+ }
+ }).catch(() => {})
+ }
+
+ const handleCopy = (text: string) => {
+ Taro.setClipboardData({ data: text, success: () => Taro.showToast({ title: '已复制', icon: 'success' }) })
+ }
+
+ return (
+
+
+ 🔑 API Key 管理
+
+
+
+
+ {list.length === 0 && !loading ? (
+ 暂无 API Key
+ ) : (
+ list.map((item) => (
+
+
+ {item.name}
+
+ {item.status ? '正常' : '禁用'}
+
+
+
+ AppId:
+ handleCopy(item.appId || '')}>
+ {item.appId || '-'}
+
+
+
+ AppSecret:
+ handleCopy(item.appSecret || '')}>
+ ••••••••••••••••
+
+
+
+ {item.appSecret && handleCopy(item.appSecret || '')}>复制密钥}
+ handleDelete(item)}>删除
+
+
+ ))
+ )}
+
+
+ {showCreate && (
+
+ setShowCreate(false)} />
+
+ 创建 API Key
+
+
+ 名称 *
+ setCreateForm(prev => ({ ...prev, name: e.detail.value }))} />
+
+
+ 备注
+ setCreateForm(prev => ({ ...prev, remark: e.detail.value }))} />
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default ApiKeysPage
diff --git a/src/developer/project/[id]/index.config.ts b/src/developer/project/[id]/index.config.ts
new file mode 100644
index 0000000..463c9fb
--- /dev/null
+++ b/src/developer/project/[id]/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '项目详情',
+})
diff --git a/src/developer/project/[id]/index.scss b/src/developer/project/[id]/index.scss
new file mode 100644
index 0000000..a0be6a1
--- /dev/null
+++ b/src/developer/project/[id]/index.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.project-detail-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #666666;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/developer/project/[id]/index.tsx b/src/developer/project/[id]/index.tsx
new file mode 100644
index 0000000..c51c6b6
--- /dev/null
+++ b/src/developer/project/[id]/index.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import './index.scss'
+
+const ProjectDetail: React.FC = () => {
+ const id = Taro.getCurrentInstance()?.router?.params?.id
+
+ return (
+
+
+ 📁 项目详情
+
+
+ 项目 ID: {id}
+ 项目详情功能开发中...
+
+
+ )
+}
+
+export default ProjectDetail
diff --git a/src/developer/project/[id]/members.scss b/src/developer/project/[id]/members.scss
new file mode 100644
index 0000000..3154444
--- /dev/null
+++ b/src/developer/project/[id]/members.scss
@@ -0,0 +1,307 @@
+.members-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 24rpx;
+ padding-bottom: 120rpx;
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__invite-btn {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ border: none;
+ border-radius: 32rpx;
+ font-size: 26rpx;
+ padding: 0 28rpx;
+ height: 60rpx;
+ line-height: 60rpx;
+
+ &::after {
+ border: none;
+ }
+ }
+
+ // 统计卡片
+ &__stats {
+ display: flex;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ margin-bottom: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+ }
+
+ &__stat {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &-num {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: #333;
+ margin-bottom: 8rpx;
+ }
+
+ &-label {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+
+ // 列表
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+ }
+
+ &__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 120rpx 0;
+
+ text {
+ font-size: 30rpx;
+ color: #999;
+ }
+
+ &-hint {
+ margin-top: 16rpx;
+ font-size: 26rpx !important;
+ color: #ccc !important;
+ }
+ }
+
+ &__loading {
+ display: flex;
+ justify-content: center;
+ padding: 32rpx 0;
+
+ text {
+ font-size: 26rpx;
+ color: #999;
+ }
+ }
+}
+
+// 成员卡片
+.member-card {
+ display: flex;
+ align-items: flex-start;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ &__avatar {
+ width: 96rpx;
+ height: 96rpx;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 24rpx;
+ flex-shrink: 0;
+ overflow: hidden;
+
+ &-img {
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ }
+
+ &-text {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #fff;
+ }
+ }
+
+ &__info {
+ flex: 1;
+ min-width: 0;
+ }
+
+ &__name-row {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ margin-bottom: 8rpx;
+ }
+
+ &__name {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__role {
+ font-size: 22rpx;
+ padding: 4rpx 12rpx;
+ border-radius: 8rpx;
+ background: #f5f5f5;
+ }
+
+ &__desc {
+ font-size: 26rpx;
+ color: #666;
+ margin-bottom: 8rpx;
+ }
+
+ &__time {
+ font-size: 24rpx;
+ color: #999;
+ }
+
+ &__actions {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+ margin-left: 24rpx;
+ }
+
+ &__action {
+ font-size: 26rpx;
+ color: #667eea;
+ white-space: nowrap;
+
+ &--danger {
+ color: #ff4d4f;
+ }
+ }
+}
+
+// 邀请弹窗
+.members-page__modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+
+ &-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ &-content {
+ position: relative;
+ width: 100%;
+ background: #fff;
+ border-radius: 32rpx 32rpx 0 0;
+ padding: 48rpx 32rpx;
+ padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
+ }
+
+ &-title {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #333;
+ text-align: center;
+ margin-bottom: 48rpx;
+ }
+}
+
+.members-page__form {
+ &-item {
+ margin-bottom: 32rpx;
+ }
+
+ &-label {
+ display: block;
+ font-size: 28rpx;
+ color: #333;
+ margin-bottom: 16rpx;
+ }
+
+ &-input {
+ width: 100%;
+ height: 88rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 0 24rpx;
+ font-size: 30rpx;
+ }
+
+ &-radio {
+ display: flex;
+ gap: 24rpx;
+
+ &-item {
+ flex: 1;
+ height: 72rpx;
+ line-height: 72rpx;
+ text-align: center;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ font-size: 28rpx;
+ color: #666;
+
+ &.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ }
+ }
+ }
+
+ &-hint {
+ display: block;
+ font-size: 24rpx;
+ color: #999;
+ margin-top: 12rpx;
+ }
+}
+
+.members-page__modal-actions {
+ display: flex;
+ gap: 24rpx;
+ margin-top: 48rpx;
+}
+
+.members-page__modal-btn {
+ flex: 1;
+ height: 88rpx;
+ line-height: 88rpx;
+ border-radius: 44rpx;
+ font-size: 32rpx;
+
+ &--cancel {
+ background: #f5f5f5;
+ color: #666;
+ }
+
+ &--confirm {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+
+ &::after {
+ border: none;
+ }
+ }
+}
diff --git a/src/developer/project/[id]/members.tsx b/src/developer/project/[id]/members.tsx
new file mode 100644
index 0000000..95a3c22
--- /dev/null
+++ b/src/developer/project/[id]/members.tsx
@@ -0,0 +1,324 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, Button, Input, actionSheet } from '@tarojs/components'
+import Taro, { useRouter } from '@tarojs/taro'
+import { usePullDownRefresh } from '@tarojs/taro'
+import { listProjectMember, addProjectMember, removeProjectMember } from '@/api/developer/developer'
+import type { ProjectMember } from '@/types/developer'
+import './members.scss'
+
+// 角色配置
+const ROLE_CONFIG: Record = {
+ owner: { label: '所有者', color: '#722ed1', desc: '拥有所有权限' },
+ admin: { label: '管理员', color: '#1890ff', desc: '管理项目设置和成员' },
+ developer: { label: '开发者', color: '#52c41a', desc: '开发和管理应用' },
+ viewer: { label: '查看者', color: '#999', desc: '仅可查看' },
+}
+
+const MembersPage: React.FC = () => {
+ const router = useRouter()
+ const projectId = Number(router.params.id)
+
+ const [loading, setLoading] = useState(false)
+ const [refreshing, setRefreshing] = useState(false)
+ const [list, setList] = useState([])
+ const [showInvite, setShowInvite] = useState(false)
+ const [inviteForm, setInviteForm] = useState({
+ username: '',
+ role: 'developer' as ProjectMember['role'],
+ })
+ const [inviting, setInviting] = useState(false)
+
+ // 加载数据
+ const loadData = async (isRefresh = false) => {
+ if (loading) return
+ setLoading(true)
+ if (isRefresh) setRefreshing(true)
+
+ try {
+ const data = await listProjectMember(projectId)
+ setList(data || [])
+ } catch (err) {
+ console.error('加载失败', err)
+ Taro.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => {
+ loadData()
+ }, [projectId])
+
+ // 下拉刷新
+ usePullDownRefresh(() => {
+ loadData(true)
+ })
+
+ // 邀请成员
+ const handleInvite = async () => {
+ if (!inviteForm.username.trim()) {
+ Taro.showToast({ title: '请输入用户名', icon: 'none' })
+ return
+ }
+
+ setInviting(true)
+ try {
+ await addProjectMember(projectId, {
+ username: inviteForm.username,
+ role: inviteForm.role,
+ } as Partial)
+ Taro.showToast({ title: '邀请成功', icon: 'success' })
+ setShowInvite(false)
+ setInviteForm({ username: '', role: 'developer' })
+ loadData()
+ } catch (err) {
+ console.error('邀请失败', err)
+ Taro.showToast({ title: '邀请失败', icon: 'none' })
+ } finally {
+ setInviting(false)
+ }
+ }
+
+ // 移除成员
+ const handleRemove = (member: ProjectMember) => {
+ if (member.role === 'owner') {
+ Taro.showToast({ title: '无法移除项目所有者', icon: 'none' })
+ return
+ }
+
+ actionSheet({
+ alertText: `确定将 "${member.username}" 从项目中移除吗?`,
+ actions: [
+ { name: '移除', color: '#ff4d4f', type: 'warn' as const },
+ ],
+ confirmText: '取消',
+ }).then(res => {
+ if (res.confirm) {
+ doRemove(member.id!)
+ }
+ }).catch(() => {})
+ }
+
+ const doRemove = async (memberId: number) => {
+ try {
+ await removeProjectMember(projectId, memberId)
+ Taro.showToast({ title: '已移除', icon: 'success' })
+ loadData()
+ } catch (err) {
+ console.error('移除失败', err)
+ Taro.showToast({ title: '移除失败', icon: 'none' })
+ }
+ }
+
+ // 修改角色
+ const handleChangeRole = (member: ProjectMember) => {
+ if (member.role === 'owner') {
+ Taro.showToast({ title: '无法修改所有者角色', icon: 'none' })
+ return
+ }
+
+ const roles = ['admin', 'developer', 'viewer']
+ const options = roles.map(role => ({
+ name: ROLE_CONFIG[role].label,
+ }))
+
+ actionSheet({
+ title: `修改 "${member.username}" 的角色`,
+ actions: options,
+ confirmText: '取消',
+ }).then(res => {
+ if (res.confirm === false && res.errMsg?.includes('cancel')) return
+ const role = roles[res.confirm as number]
+ if (role) {
+ updateRole(member.id!, role as ProjectMember['role'])
+ }
+ }).catch(() => {})
+ }
+
+ const updateRole = async (memberId: number, role: ProjectMember['role']) => {
+ try {
+ await addProjectMember(projectId, { id: memberId, role } as Partial)
+ Taro.showToast({ title: '已更新角色', icon: 'success' })
+ loadData()
+ } catch (err) {
+ console.error('更新失败', err)
+ Taro.showToast({ title: '更新失败', icon: 'none' })
+ }
+ }
+
+ // 格式化日期
+ const formatDate = (dateStr?: string) => {
+ if (!dateStr) return '-'
+ const date = new Date(dateStr)
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+ }
+
+ return (
+
+ {/* 头部 */}
+
+ 👥 项目成员
+
+
+
+ {/* 统计 */}
+
+
+ {list.length}
+ 总成员
+
+
+
+ {list.filter(m => m.role === 'admin' || m.role === 'owner').length}
+
+ 管理员
+
+
+
+ {list.filter(m => m.role === 'developer').length}
+
+ 开发者
+
+
+
+ {list.filter(m => m.role === 'viewer').length}
+
+ 查看者
+
+
+
+ {/* 成员列表 */}
+
+ {list.length === 0 && !loading ? (
+
+ 暂无成员
+ 点击「邀请」添加项目成员
+
+ ) : (
+ list.map((member) => (
+
+
+ {member.avatar ? (
+
+ ) : (
+
+ {(member.username || '?').charAt(0).toUpperCase()}
+
+ )}
+
+
+
+
+ {member.username}
+
+ {ROLE_CONFIG[member.role || 'viewer']?.label}
+
+
+
+ {ROLE_CONFIG[member.role || 'viewer']?.desc}
+
+
+ 加入于 {formatDate(member.joinedAt)}
+
+
+
+ {member.role !== 'owner' && (
+
+ handleChangeRole(member)}
+ >
+ 修改角色
+
+ handleRemove(member)}
+ >
+ 移除
+
+
+ )}
+
+ ))
+ )}
+
+ {loading && list.length === 0 && (
+
+ 加载中...
+
+ )}
+
+
+ {/* 邀请弹窗 */}
+ {showInvite && (
+
+ setShowInvite(false)} />
+
+ 邀请项目成员
+
+
+
+ 用户名 *
+ setInviteForm(prev => ({ ...prev, username: e.detail.value }))}
+ />
+
+
+
+ 角色
+
+ {(['admin', 'developer', 'viewer'] as const).map((role) => (
+ setInviteForm(prev => ({ ...prev, role }))}
+ >
+ {ROLE_CONFIG[role].label}
+
+ ))}
+
+
+ {ROLE_CONFIG[inviteForm.role]?.desc}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default MembersPage
diff --git a/src/developer/project/[id]/settings.config.ts b/src/developer/project/[id]/settings.config.ts
new file mode 100644
index 0000000..0b62f63
--- /dev/null
+++ b/src/developer/project/[id]/settings.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '项目设置',
+})
diff --git a/src/developer/project/[id]/settings.scss b/src/developer/project/[id]/settings.scss
new file mode 100644
index 0000000..df3d005
--- /dev/null
+++ b/src/developer/project/[id]/settings.scss
@@ -0,0 +1,10 @@
+.settings-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/developer/project/[id]/settings.tsx b/src/developer/project/[id]/settings.tsx
new file mode 100644
index 0000000..c68c32e
--- /dev/null
+++ b/src/developer/project/[id]/settings.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './settings.scss'
+
+const SettingsPage: React.FC = () => {
+ return (
+
+
+ 项目设置功能开发中...
+
+
+ )
+}
+
+export default SettingsPage
diff --git a/src/developer/project/create.scss b/src/developer/project/create.scss
new file mode 100644
index 0000000..e945ac8
--- /dev/null
+++ b/src/developer/project/create.scss
@@ -0,0 +1,88 @@
+page {
+ background: #f5f6f7;
+}
+
+.project-create-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ display: flex;
+ flex-direction: column;
+
+ &__scroll {
+ flex: 1;
+ height: 0;
+ }
+}
+
+/* 表单 */
+.form {
+ padding: 24rpx;
+
+ &__group {
+ margin-bottom: 32rpx;
+ }
+
+ &__label {
+ margin-bottom: 16rpx;
+ }
+
+ &__labelText {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #111111;
+ }
+
+ &__input {
+ padding: 24rpx;
+ background: #ffffff;
+ border-radius: 16rpx;
+ font-size: 30rpx;
+ }
+
+ &__textarea {
+ padding: 24rpx;
+ background: #ffffff;
+ border-radius: 16rpx;
+ font-size: 30rpx;
+ min-height: 160rpx;
+ }
+
+ &__footer {
+ padding: 24rpx 32rpx;
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+ background: #ffffff;
+ box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
+ }
+}
+
+/* 类型选项 */
+.type-options {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.type-option {
+ background: #ffffff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ position: relative;
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 8rpx;
+ }
+
+ &__title {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #111111;
+ }
+
+ &__desc {
+ font-size: 24rpx;
+ color: #666666;
+ line-height: 1.5;
+ }
+}
diff --git a/src/developer/project/create.tsx b/src/developer/project/create.tsx
new file mode 100644
index 0000000..7808f70
--- /dev/null
+++ b/src/developer/project/create.tsx
@@ -0,0 +1,133 @@
+import React, { useState } from 'react'
+import { View, Text, ScrollView, Input, Textarea } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Radio, RadioGroup } from '@nutui/nutui-react-taro'
+import { createProject } from '@/api/developer'
+import './create.scss'
+
+const ProjectCreate: React.FC = () => {
+ const [formData, setFormData] = useState({
+ name: '',
+ type: 'basic' as 'basic' | 'pro' | 'enterprise',
+ description: '',
+ })
+ const [loading, setLoading] = useState(false)
+
+ // 更新表单数据
+ const updateField = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }))
+ }
+
+ // 提交表单
+ const handleSubmit = async () => {
+ if (!formData.name.trim()) {
+ Taro.showToast({ title: '请输入项目名称', icon: 'none' })
+ return
+ }
+
+ try {
+ setLoading(true)
+ await createProject(formData)
+ Taro.showToast({ title: '创建成功', icon: 'success' })
+ setTimeout(() => {
+ Taro.navigateBack()
+ }, 1500)
+ } catch (error) {
+ console.error('创建失败', error)
+ Taro.showToast({ title: '创建失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 获取类型说明
+ const getTypeDesc = (type: string) => {
+ const descs: Record = {
+ basic: '免费版本,包含基础功能,适合个人开发者',
+ pro: '专业版本,功能完整,适合中小企业',
+ enterprise: '企业版本,全功能支持,适合大型企业',
+ }
+ return descs[type] || ''
+ }
+
+ return (
+
+
+
+ {/* 项目名称 */}
+
+
+ 项目名称 *
+
+ updateField('name', e.detail.value)}
+ maxlength={50}
+ />
+
+
+ {/* 项目类型 */}
+
+
+ 项目类型 *
+
+ updateField('type', e)}>
+
+
+
+
+ 基础版
+ {getTypeDesc('basic')}
+
+
+
+
+
+
+ 专业版
+ {getTypeDesc('pro')}
+
+
+
+
+
+
+ 企业版
+ {getTypeDesc('enterprise')}
+
+
+
+
+
+
+
+ {/* 项目描述 */}
+
+
+ 项目描述
+
+
+
+
+
+ {/* 底部按钮 */}
+
+
+
+
+ )
+}
+
+export default ProjectCreate
diff --git a/src/developer/project/index.scss b/src/developer/project/index.scss
new file mode 100644
index 0000000..47fdf35
--- /dev/null
+++ b/src/developer/project/index.scss
@@ -0,0 +1,147 @@
+page {
+ background: #f5f6f7;
+}
+
+.project-list-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding-bottom: 140rpx;
+
+ &__scroll {
+ height: calc(100vh - 100rpx);
+ }
+}
+
+.tabs-wrapper {
+ background: #ffffff;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+/* 项目列表 */
+.project-list {
+ padding: 24rpx;
+
+ &__loading {
+ padding: 120rpx 0;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+ }
+}
+
+/* 项目卡片 */
+.project-card {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 28rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title-row {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ margin-bottom: 10rpx;
+ }
+
+ &__name {
+ font-size: 34rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__badge {
+ padding: 4rpx 12rpx;
+ border-radius: 999rpx;
+ font-size: 22rpx;
+ font-weight: 600;
+ }
+
+ &__badgeText {
+ line-height: 1;
+ }
+
+ &__desc {
+ font-size: 26rpx;
+ color: #666666;
+ line-height: 1.5;
+ }
+
+ &__stats {
+ display: flex;
+ align-items: center;
+ padding: 20rpx 0;
+ border-top: 2rpx solid #f3f4f6;
+ border-bottom: 2rpx solid #f3f4f6;
+ }
+
+ &__stat {
+ flex: 1;
+ text-align: center;
+ }
+
+ &__statValue {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 900;
+ color: #3b82f6;
+ line-height: 1.2;
+ }
+
+ &__statLabel {
+ display: block;
+ margin-top: 6rpx;
+ font-size: 22rpx;
+ color: #666666;
+ }
+
+ &__footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 20rpx;
+ }
+
+ &__time {
+ font-size: 24rpx;
+ color: #999999;
+ }
+
+ &__action {
+ padding: 8rpx 16rpx;
+ }
+
+ &__actionText {
+ font-size: 26rpx;
+ color: #3b82f6;
+ font-weight: 600;
+ }
+}
+
+/* 创建按钮 */
+.create-btn-wrapper {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 24rpx 32rpx;
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+ background: #ffffff;
+ box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
+}
+
+/* 底部安全区域 */
+.safe-area-bottom {
+ height: 40rpx;
+}
diff --git a/src/developer/project/index.tsx b/src/developer/project/index.tsx
new file mode 100644
index 0000000..4a18cc6
--- /dev/null
+++ b/src/developer/project/index.tsx
@@ -0,0 +1,178 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Empty, Tabs, PullToRefresh } from '@nutui/nutui-react-taro'
+import './index.scss'
+
+const ProjectList: React.FC = () => {
+ const [activeTab, setActiveTab] = useState('all')
+ const [projects, setProjects] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+
+ // 模拟数据
+ const mockProjects = [
+ {
+ id: 1,
+ name: '我的企业官网',
+ type: 'pro',
+ description: '企业品牌展示官网',
+ appCount: 2,
+ memberCount: 3,
+ apiCallCount: 12580,
+ status: 'active',
+ updatedAt: '2026-04-10',
+ },
+ {
+ id: 2,
+ name: '电商小程序',
+ type: 'enterprise',
+ description: '多端电商解决方案',
+ appCount: 5,
+ memberCount: 8,
+ apiCallCount: 98650,
+ status: 'active',
+ updatedAt: '2026-04-12',
+ },
+ {
+ id: 3,
+ name: '内部管理系统',
+ type: 'basic',
+ description: 'OA办公系统',
+ appCount: 1,
+ memberCount: 5,
+ apiCallCount: 3200,
+ status: 'active',
+ updatedAt: '2026-04-08',
+ },
+ ]
+
+ // 加载数据
+ const loadData = async () => {
+ try {
+ setLoading(true)
+ // TODO: 替换为真实 API 调用
+ // const result = await pageMyProject({ type: activeTab === 'all' ? undefined : activeTab })
+ await new Promise((resolve) => setTimeout(resolve, 500))
+ setProjects(mockProjects)
+ } catch (error) {
+ console.error('加载失败', error)
+ Taro.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 下拉刷新
+ const onRefresh = async () => {
+ setRefreshing(true)
+ await loadData()
+ setRefreshing(false)
+ }
+
+ useEffect(() => {
+ loadData()
+ }, [activeTab])
+
+ // 获取项目类型标签
+ const getTypeBadge = (type: string) => {
+ const badges: Record = {
+ basic: { text: '基础', color: '#6b7280' },
+ pro: { text: '专业', color: '#3b82f6' },
+ enterprise: { text: '企业', color: '#f59e0b' },
+ }
+ return badges[type] || badges.basic
+ }
+
+ // 跳转创建页面
+ const handleCreate = () => {
+ Taro.navigateTo({ url: '/developer/project/create' })
+ }
+
+ // 跳转项目详情
+ const handleProjectClick = (id: number) => {
+ Taro.navigateTo({ url: `/developer/project/${id}` })
+ }
+
+ return (
+
+
+ {/* 标签页 */}
+
+ setActiveTab(v as string)}>
+
+
+
+
+
+
+
+
+ {/* 项目列表 */}
+
+ {loading ? (
+
+ 加载中...
+
+ ) : projects.length === 0 ? (
+
+ ) : (
+
+ {projects.map((project) => {
+ const badge = getTypeBadge(project.type)
+ return (
+ handleProjectClick(project.id)}>
+
+
+ {project.name}
+
+ {badge.text}
+
+
+ {project.description}
+
+
+
+
+ {project.appCount}
+ 应用
+
+
+ {project.memberCount}
+ 成员
+
+
+ {project.apiCallCount}
+ API调用
+
+
+
+
+ 更新于 {project.updatedAt}
+
+ 查看详情 ›
+
+
+
+ )
+ })}
+
+ )}
+
+
+ {/* 底部安全区域 */}
+
+
+
+ {/* 创建按钮 */}
+
+
+
+
+
+ )
+}
+
+export default ProjectList
diff --git a/src/developer/sdk/index.config.ts b/src/developer/sdk/index.config.ts
new file mode 100644
index 0000000..740c291
--- /dev/null
+++ b/src/developer/sdk/index.config.ts
@@ -0,0 +1,8 @@
+/**
+ * SDK 下载中心 - 页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: 'SDK 下载中心',
+ enableShareAppMessage: true,
+ enableShareTimeline: true
+});
diff --git a/src/developer/sdk/index.scss b/src/developer/sdk/index.scss
new file mode 100644
index 0000000..8da6368
--- /dev/null
+++ b/src/developer/sdk/index.scss
@@ -0,0 +1,342 @@
+// SDK 下载中心样式
+.sdk-center {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: env(safe-area-inset-bottom);
+}
+
+// 搜索栏
+.search-bar {
+ display: flex;
+ align-items: center;
+ padding: 24px;
+ background: #fff;
+
+ .search-input {
+ flex: 1;
+ height: 72px;
+ padding: 0 24px;
+ background: #f5f5f5;
+ border-radius: 36px;
+ font-size: 28px;
+ }
+
+ .search-btn {
+ margin-left: 20px;
+ padding: 0 32px;
+ height: 72px;
+ line-height: 72px;
+ background: #007aff;
+ color: #fff;
+ border-radius: 36px;
+ font-size: 28px;
+
+ &::after { border: none; }
+ }
+}
+
+// 分类滚动
+.category-scroll {
+ background: #fff;
+ white-space: nowrap;
+
+ .category-tabs {
+ display: inline-flex;
+ padding: 0 16px 24px;
+ }
+
+ .category-tab {
+ display: inline-flex;
+ align-items: center;
+ padding: 16px 32px;
+ margin-right: 16px;
+ background: #f5f5f5;
+ border-radius: 32px;
+ font-size: 26px;
+ color: #666;
+ white-space: nowrap;
+
+ &.active {
+ background: #007aff;
+ color: #fff;
+ }
+ }
+}
+
+// 统计栏
+.stats-bar {
+ padding: 24px;
+ background: #fff;
+
+ .stats-text {
+ font-size: 26px;
+ color: #999;
+ }
+}
+
+// SDK 列表
+.sdk-list {
+ height: calc(100vh - 400px);
+ padding: 24px;
+
+ .loading, .empty {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 300px;
+ color: #999;
+ font-size: 28px;
+ }
+}
+
+.sdk-card {
+ background: #fff;
+ border-radius: 24px;
+ padding: 32px;
+ margin-bottom: 24px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
+
+ .sdk-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20px;
+ }
+
+ .sdk-icon-wrap {
+ width: 88px;
+ height: 88px;
+ background: #f5f5f5;
+ border-radius: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .sdk-icon {
+ font-size: 48px;
+ }
+ }
+
+ .sdk-info {
+ flex: 1;
+ margin-left: 24px;
+
+ .sdk-name {
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ }
+
+ .sdk-version {
+ font-size: 24px;
+ color: #999;
+ margin-top: 8px;
+ display: block;
+ }
+ }
+
+ .sdk-actions {
+ .action-btn {
+ padding: 16px 32px;
+ font-size: 26px;
+ border-radius: 24px;
+ background: #f5f5f5;
+ color: #333;
+
+ &.primary {
+ background: #007aff;
+ color: #fff;
+ }
+
+ &::after { border: none; }
+ }
+ }
+
+ .sdk-desc {
+ font-size: 26px;
+ color: #666;
+ line-height: 1.6;
+ display: block;
+ margin-bottom: 20px;
+ }
+
+ .sdk-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .sdk-tags {
+ display: flex;
+
+ .tag {
+ padding: 8px 16px;
+ background: #f0f7ff;
+ color: #007aff;
+ border-radius: 8px;
+ font-size: 22px;
+ margin-right: 12px;
+ }
+ }
+
+ .sdk-stats {
+ display: flex;
+
+ .stat {
+ font-size: 24px;
+ color: #999;
+ margin-left: 20px;
+ }
+ }
+ }
+}
+
+// SDK 详情弹窗
+.sdk-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-end;
+ z-index: 1000;
+
+ .sdk-modal-content {
+ width: 100%;
+ max-height: 85vh;
+ background: #fff;
+ border-radius: 32px 32px 0 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32px;
+ border-bottom: 1px solid #eee;
+
+ .modal-title {
+ font-size: 36px;
+ font-weight: 600;
+ }
+
+ .modal-close {
+ width: 64px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36px;
+ color: #999;
+ }
+ }
+
+ .modal-body {
+ flex: 1;
+ padding: 32px;
+ max-height: calc(85vh - 200px);
+ }
+
+ .modal-section {
+ margin-bottom: 40px;
+
+ .section-title {
+ font-size: 30px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 20px;
+ display: block;
+ }
+
+ .section-content {
+ font-size: 28px;
+ color: #666;
+ line-height: 1.6;
+
+ &.changelog {
+ white-space: pre-wrap;
+ background: #f5f5f5;
+ padding: 24px;
+ border-radius: 16px;
+ }
+ }
+ }
+
+ .info-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 24px;
+
+ .info-item {
+ .info-label {
+ font-size: 24px;
+ color: #999;
+ display: block;
+ margin-bottom: 8px;
+ }
+
+ .info-value {
+ font-size: 28px;
+ color: #333;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .code-block {
+ background: #1e1e1e;
+ padding: 24px;
+ border-radius: 16px;
+
+ .code-text {
+ color: #a5d6ff;
+ font-size: 26px;
+ font-family: monospace;
+ }
+ }
+
+ .deps-list {
+ display: flex;
+ flex-wrap: wrap;
+
+ .dep-tag {
+ padding: 8px 16px;
+ background: #f0f7ff;
+ color: #007aff;
+ border-radius: 8px;
+ font-size: 24px;
+ margin-right: 12px;
+ margin-bottom: 12px;
+ }
+ }
+
+ .modal-footer {
+ display: flex;
+ padding: 32px;
+ padding-bottom: calc(32px + env(safe-area-inset-bottom));
+ border-top: 1px solid #eee;
+
+ .modal-btn {
+ flex: 1;
+ height: 88px;
+ line-height: 88px;
+ font-size: 32px;
+ border-radius: 44px;
+ margin: 0 12px;
+ background: #f5f5f5;
+ color: #333;
+
+ &.primary {
+ background: #007aff;
+ color: #fff;
+ }
+
+ &:first-child { margin-left: 0; }
+ &:last-child { margin-right: 0; }
+ &::after { border: none; }
+ }
+ }
+}
diff --git a/src/developer/sdk/index.tsx b/src/developer/sdk/index.tsx
new file mode 100644
index 0000000..0ac9b40
--- /dev/null
+++ b/src/developer/sdk/index.tsx
@@ -0,0 +1,284 @@
+/**
+ * SDK 下载中心
+ */
+import { useState, useEffect } from 'react';
+import { View, Text, Image, Input, ScrollView, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { pageSDK, listSDKCategories, recordDownload } from '../../api/sdk';
+import type { SDK, SDKCategory } from '../../types/sdk';
+import './index.scss';
+
+export default function SDKCenter() {
+ const [categories, setCategories] = useState([]);
+ const [sdks, setSDKs] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [loading, setLoading] = useState(true);
+ const [activeCategory, setActiveCategory] = useState('all');
+ const [keyword, setKeyword] = useState('');
+ const [selectedSDK, setSelectedSDK] = useState(null);
+
+ useEffect(() => {
+ loadCategories();
+ loadSDKs();
+ }, []);
+
+ // 加载分类
+ const loadCategories = async () => {
+ try {
+ const data = await listSDKCategories();
+ setCategories(data);
+ } catch (e) {
+ console.error('加载分类失败', e);
+ }
+ };
+
+ // 加载 SDK 列表
+ const loadSDKs = async (category = activeCategory, kw = keyword) => {
+ setLoading(true);
+ try {
+ const params: any = { page: 1, pageSize: 20 };
+ if (category !== 'all') params.category = category;
+ if (kw) params.keyword = kw;
+
+ const data = await pageSDK(params);
+ setSDKs(data.list);
+ setTotal(data.total);
+ } catch (e) {
+ console.error('加载 SDK 失败', e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 搜索
+ const handleSearch = () => {
+ loadSDKs(activeCategory, keyword);
+ };
+
+ // 切换分类
+ const handleCategoryChange = (categoryId: string) => {
+ setActiveCategory(categoryId);
+ loadSDKs(categoryId, keyword);
+ };
+
+ // 查看详情
+ const handleViewDetail = (sdk: SDK) => {
+ setSelectedSDK(sdk);
+ };
+
+ // 下载 SDK
+ const handleDownload = async (sdk: SDK) => {
+ try {
+ await recordDownload(sdk.id, sdk.version);
+
+ if (sdk.npmPackage) {
+ Taro.setClipboardData({
+ data: `npm install ${sdk.npmPackage}`,
+ success: () => {
+ Taro.showToast({ title: '安装命令已复制', icon: 'success' });
+ }
+ });
+ } else {
+ Taro.showToast({ title: '开始下载...', icon: 'loading' });
+ }
+ } catch (e) {
+ Taro.showToast({ title: '下载失败', icon: 'none' });
+ }
+ };
+
+ // 访问文档
+ const handleViewDocs = (sdk: SDK) => {
+ Taro.navigateTo({ url: `/developer/docs/sdk-detail?id=${sdk.id}` });
+ };
+
+ // 获取语言图标
+ const getLanguageIcon = (language: string) => {
+ const icons: Record = {
+ 'JavaScript': '🟨',
+ 'TypeScript': '🔷',
+ 'Python': '🐍',
+ 'Java': '☕',
+ 'Go': '🔵',
+ 'Swift': '🍎',
+ 'Flutter': '🎨',
+ 'React Native': '⚛️',
+ '.NET': '🔷',
+ 'PHP': '🐘',
+ 'Ruby': '💎',
+ 'Kotlin': '🟣'
+ };
+ return icons[language] || '📦';
+ };
+
+ // 获取分类图标
+ const getCategoryIcon = (icon: string) => {
+ const icons: Record = {
+ 'globe': '🌐',
+ 'mobile': '📱',
+ 'server': '🖥️',
+ 'wechat': '💬'
+ };
+ return icons[icon] || '📦';
+ };
+
+ return (
+
+ {/* 搜索栏 */}
+
+ setKeyword(e.detail.value)}
+ onConfirm={handleSearch}
+ />
+
+
+
+ {/* 分类标签 */}
+
+
+ handleCategoryChange('all')}
+ >
+ 全部
+
+ {categories.map(cat => (
+ handleCategoryChange(cat.id)}
+ >
+ {getCategoryIcon(cat.icon)} {cat.name}
+
+ ))}
+
+
+
+ {/* 统计信息 */}
+
+ 共 {total} 个 SDK
+
+
+ {/* SDK 列表 */}
+
+ {loading ? (
+
+ 加载中...
+
+ ) : sdks.length === 0 ? (
+
+ 未找到相关 SDK
+
+ ) : (
+ sdks.map(sdk => (
+ handleViewDetail(sdk)}>
+
+
+ {getLanguageIcon(sdk.language)}
+
+
+ {sdk.name}
+ v{sdk.version}
+
+
+
+
+
+
+ {sdk.description}
+
+
+
+ {sdk.language}
+ {sdk.category === 'web' ? 'Web' : sdk.category === 'mobile' ? '移动' : sdk.category === 'server' ? '服务端' : '小程序'}
+
+
+ ⬇️ {sdk.downloadCount > 1000 ? (sdk.downloadCount / 1000).toFixed(1) + 'k' : sdk.downloadCount}
+ ⭐ {sdk.stars > 1000 ? (sdk.stars / 1000).toFixed(1) + 'k' : sdk.stars}
+
+
+
+ ))
+ )}
+
+
+ {/* SDK 详情弹窗 */}
+ {selectedSDK && (
+ setSelectedSDK(null)}>
+ e.stopPropagation()}>
+
+ {selectedSDK.name}
+ setSelectedSDK(null)}>✕
+
+
+
+
+ 基本信息
+
+
+ 最新版本
+ {selectedSDK.version}
+
+
+ 语言
+ {selectedSDK.language}
+
+
+ 下载量
+ {selectedSDK.downloadCount.toLocaleString()}
+
+
+ 更新时间
+ {selectedSDK.lastUpdated}
+
+
+
+
+
+ 描述
+ {selectedSDK.description}
+
+
+ {selectedSDK.npmPackage && (
+
+ NPM 包名
+
+ npm install {selectedSDK.npmPackage}
+
+
+ )}
+
+ {selectedSDK.dependencies.length > 0 && (
+
+ 依赖
+
+ {selectedSDK.dependencies.map(dep => (
+ {dep}
+ ))}
+
+
+ )}
+
+
+ 更新日志
+ {selectedSDK.changelog}
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/developer/ticket/detail.config.ts b/src/developer/ticket/detail.config.ts
new file mode 100644
index 0000000..b9693c4
--- /dev/null
+++ b/src/developer/ticket/detail.config.ts
@@ -0,0 +1,7 @@
+/**
+ * 工单详情 - 页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '工单详情',
+ enableShareAppMessage: true
+});
diff --git a/src/developer/ticket/detail.scss b/src/developer/ticket/detail.scss
new file mode 100644
index 0000000..b0877e2
--- /dev/null
+++ b/src/developer/ticket/detail.scss
@@ -0,0 +1,262 @@
+// 工单详情样式
+.ticket-detail {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: calc(160px + env(safe-area-inset-bottom));
+}
+
+.loading, .empty {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 400px;
+ color: #999;
+ font-size: 28px;
+}
+
+// 工单信息
+.ticket-info {
+ background: #fff;
+ padding: 32px;
+ margin-bottom: 16px;
+
+ .info-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+
+ .ticket-no {
+ font-size: 24px;
+ color: #999;
+ }
+
+ .status-tag {
+ padding: 8px 20px;
+ border-radius: 20px;
+ font-size: 24px;
+ }
+ }
+
+ .ticket-title {
+ font-size: 36px;
+ font-weight: 600;
+ color: #333;
+ line-height: 1.4;
+ display: block;
+ margin-bottom: 20px;
+ }
+
+ .info-meta {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+
+ .priority-badge {
+ font-size: 24px;
+ font-weight: 600;
+ margin-right: 24px;
+ }
+
+ .type-text, .time-text {
+ font-size: 24px;
+ color: #999;
+ margin-right: 24px;
+ }
+ }
+}
+
+// 内容区域
+.content-section, .replies-section, .solution-section {
+ background: #fff;
+ padding: 32px;
+ margin-bottom: 16px;
+}
+
+.section-label {
+ font-size: 28px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 24px;
+}
+
+// 用户消息
+.content-body {
+ .user-message {
+ display: flex;
+
+ .user-avatar {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ margin-right: 20px;
+ flex-shrink: 0;
+ }
+
+ .message-content {
+ flex: 1;
+
+ .message-header {
+ margin-bottom: 12px;
+
+ .user-name {
+ font-size: 28px;
+ font-weight: 600;
+ color: #333;
+ }
+ }
+
+ .message-body {
+ background: #f5f5f5;
+ padding: 24px;
+ border-radius: 20px;
+ border-top-left-radius: 8px;
+
+ .message-text {
+ font-size: 28px;
+ color: #333;
+ line-height: 1.6;
+ white-space: pre-wrap;
+ }
+ }
+ }
+ }
+}
+
+// 回复列表
+.replies-list {
+ .reply-item {
+ display: flex;
+ margin-bottom: 32px;
+
+ &:last-child { margin-bottom: 0; }
+
+ &.support {
+ flex-direction: row-reverse;
+
+ .reply-content {
+ align-items: flex-end;
+
+ .reply-body {
+ background: #e6f0ff;
+ border-radius: 20px;
+ border-top-right-radius: 8px;
+ }
+ }
+
+ .reply-header {
+ flex-direction: row-reverse;
+
+ .reply-name {
+ display: flex;
+ align-items: center;
+ }
+ }
+ }
+
+ .reply-avatar {
+ width: 72px;
+ height: 72px;
+ border-radius: 50%;
+ margin-right: 16px;
+ flex-shrink: 0;
+ }
+
+ .reply-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ max-width: calc(100% - 200px);
+
+ .reply-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12px;
+
+ .reply-name {
+ font-size: 28px;
+ font-weight: 600;
+ color: #333;
+
+ .support-tag {
+ margin-left: 8px;
+ padding: 4px 12px;
+ background: #007aff;
+ color: #fff;
+ font-size: 20px;
+ border-radius: 8px;
+ font-weight: normal;
+ }
+ }
+
+ .reply-time {
+ font-size: 22px;
+ color: #999;
+ }
+ }
+
+ .reply-body {
+ background: #f5f5f5;
+ padding: 20px;
+ border-radius: 20px;
+ border-top-left-radius: 8px;
+
+ .reply-text {
+ font-size: 28px;
+ color: #333;
+ line-height: 1.5;
+ }
+ }
+ }
+ }
+}
+
+// 解决方案
+.solution-content {
+ background: #f0fff0;
+ padding: 24px;
+ border-radius: 16px;
+ border-left: 8px solid #34c759;
+
+ Text {
+ font-size: 28px;
+ color: #333;
+ line-height: 1.6;
+ }
+}
+
+// 回复输入
+.reply-input {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ align-items: center;
+ padding: 20px 24px;
+ padding-bottom: calc(20px + env(safe-area-inset-bottom));
+ background: #fff;
+ border-top: 1px solid #eee;
+
+ .input-field {
+ flex: 1;
+ height: 80px;
+ padding: 0 24px;
+ background: #f5f5f5;
+ border-radius: 40px;
+ font-size: 28px;
+ }
+
+ .send-btn {
+ margin-left: 20px;
+ padding: 0 36px;
+ height: 80px;
+ line-height: 80px;
+ background: #007aff;
+ color: #fff;
+ font-size: 28px;
+ border-radius: 40px;
+
+ &::after { border: none; }
+ }
+}
diff --git a/src/developer/ticket/detail.tsx b/src/developer/ticket/detail.tsx
new file mode 100644
index 0000000..dc9d4ed
--- /dev/null
+++ b/src/developer/ticket/detail.tsx
@@ -0,0 +1,225 @@
+/**
+ * 工单详情页
+ */
+import { useState, useEffect } from 'react';
+import { View, Text, Image, Input, ScrollView, Button } from '@tarojs/components';
+import Taro, { useRouter } from '@tarojs/taro';
+import { getTicketDetail, listTicketReply, replyTicket } from '../../api/ticket';
+import type { Ticket, TicketReply } from '../../types/ticket';
+import './detail.scss';
+
+const STATUS_MAP: Record = {
+ pending: { label: '待处理', color: '#ff9500' },
+ processing: { label: '处理中', color: '#007aff' },
+ waiting: { label: '等待回复', color: '#5856d6' },
+ resolved: { label: '已解决', color: '#34c759' },
+ closed: { label: '已关闭', color: '#999' },
+ rejected: { label: '已拒绝', color: '#ff3b30' }
+};
+
+const PRIORITY_MAP: Record = {
+ low: { label: '低', color: '#34c759' },
+ medium: { label: '中', color: '#ff9500' },
+ high: { label: '高', color: '#ff3b30' },
+ urgent: { label: '紧急', color: '#af52de' }
+};
+
+const TYPE_MAP: Record = {
+ technical: '技术问题',
+ billing: '账单问题',
+ account: '账户问题',
+ feature: '功能建议',
+ bug: 'Bug 反馈',
+ other: '其他'
+};
+
+export default function TicketDetail() {
+ const router = useRouter();
+ const [ticket, setTicket] = useState(null);
+ const [replies, setReplies] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [replyContent, setReplyContent] = useState('');
+ const [sending, setSending] = useState(false);
+
+ useEffect(() => {
+ loadData();
+ }, []);
+
+ const loadData = async () => {
+ const { id } = router.params;
+ if (!id) return;
+
+ setLoading(true);
+ try {
+ const [ticketData, repliesData] = await Promise.all([
+ getTicketDetail(id),
+ listTicketReply(id)
+ ]);
+ setTicket(ticketData);
+ setReplies(repliesData);
+ } catch (e) {
+ console.error('加载失败', e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 发送回复
+ const handleReply = async () => {
+ if (!replyContent.trim()) {
+ Taro.showToast({ title: '请输入回复内容', icon: 'none' });
+ return;
+ }
+
+ if (!ticket) return;
+
+ setSending(true);
+ try {
+ const reply = await replyTicket(ticket.id, replyContent);
+ setReplies([...replies, reply]);
+ setReplyContent('');
+ Taro.showToast({ title: '发送成功', icon: 'success' });
+ } catch (e) {
+ Taro.showToast({ title: '发送失败', icon: 'none' });
+ } finally {
+ setSending(false);
+ }
+ };
+
+ // 格式化时间
+ const formatTime = (time: string) => {
+ const date = new Date(time.replace(' ', 'T'));
+ return `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
+ };
+
+ if (loading) {
+ return (
+
+ 加载中...
+
+ );
+ }
+
+ if (!ticket) {
+ return (
+
+ 工单不存在
+
+ );
+ }
+
+ return (
+
+ {/* 工单信息 */}
+
+
+ {ticket.ticketNo}
+
+ {STATUS_MAP[ticket.status]?.label}
+
+
+
+ {ticket.title}
+
+
+
+ 优先级:{PRIORITY_MAP[ticket.priority]?.label}
+
+ 类型:{TYPE_MAP[ticket.type]}
+ {formatTime(ticket.createTime)}
+
+
+
+ {/* 问题内容 */}
+
+ 问题描述
+
+
+
+
+
+ {ticket.creatorName}
+
+
+ {ticket.content}
+
+
+
+
+
+
+ {/* 回复列表 */}
+ {replies.length > 0 && (
+
+ 沟通记录 ({replies.length})
+
+ {replies.map(reply => (
+
+
+
+
+
+ {reply.senderName}
+ {reply.senderRole === 'support' && (
+ 客服
+ )}
+
+ {formatTime(reply.createTime)}
+
+
+ {reply.content}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* 解决方案 */}
+ {ticket.status === 'resolved' && ticket.solution && (
+
+ 解决方案
+
+ {ticket.solution}
+
+
+ )}
+
+ {/* 回复输入框 */}
+ {ticket.status !== 'closed' && ticket.status !== 'resolved' && (
+
+ setReplyContent(e.detail.value)}
+ onConfirm={handleReply}
+ />
+
+
+ )}
+
+ );
+}
diff --git a/src/developer/ticket/faq.config.ts b/src/developer/ticket/faq.config.ts
new file mode 100644
index 0000000..6311127
--- /dev/null
+++ b/src/developer/ticket/faq.config.ts
@@ -0,0 +1,7 @@
+/**
+ * FAQ 常见问题 - 页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '常见问题',
+ enableShareAppMessage: true
+});
diff --git a/src/developer/ticket/faq.scss b/src/developer/ticket/faq.scss
new file mode 100644
index 0000000..c66bb96
--- /dev/null
+++ b/src/developer/ticket/faq.scss
@@ -0,0 +1,266 @@
+// FAQ 样式
+.faq-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: calc(140px + env(safe-area-inset-bottom));
+}
+
+// 搜索栏
+.search-bar {
+ display: flex;
+ align-items: center;
+ padding: 24px;
+ background: #fff;
+
+ .search-input {
+ flex: 1;
+ height: 72px;
+ padding: 0 24px;
+ background: #f5f5f5;
+ border-radius: 36px;
+ font-size: 28px;
+ }
+
+ .search-btn {
+ margin-left: 20px;
+ padding: 0 32px;
+ height: 72px;
+ line-height: 72px;
+ background: #007aff;
+ color: #fff;
+ border-radius: 36px;
+ font-size: 28px;
+
+ &::after { border: none; }
+ }
+}
+
+// 分类标签
+.category-scroll {
+ background: #fff;
+ white-space: nowrap;
+
+ .category-tabs {
+ display: inline-flex;
+ padding: 0 16px 24px;
+ }
+
+ .category-tab {
+ display: inline-flex;
+ align-items: center;
+ padding: 16px 28px;
+ margin-right: 16px;
+ background: #f5f5f5;
+ border-radius: 32px;
+ font-size: 26px;
+ color: #666;
+ white-space: nowrap;
+
+ &.active {
+ background: #007aff;
+ color: #fff;
+ }
+ }
+}
+
+// 统计栏
+.stats-bar {
+ padding: 24px;
+ background: #fff;
+
+ .stats-text {
+ font-size: 26px;
+ color: #999;
+ }
+}
+
+// FAQ 列表
+.faq-list {
+ height: calc(100vh - 450px);
+ padding: 0 24px;
+}
+
+.loading, .empty {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 300px;
+ color: #999;
+ font-size: 28px;
+}
+
+.faq-item {
+ background: #fff;
+ border-radius: 20px;
+ margin-bottom: 20px;
+ overflow: hidden;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
+
+ &.expanded {
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ }
+
+ .faq-question {
+ display: flex;
+ align-items: center;
+ padding: 28px;
+
+ .question-icon {
+ width: 48px;
+ height: 48px;
+ background: #007aff;
+ color: #fff;
+ font-size: 24px;
+ font-weight: 700;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-right: 16px;
+ }
+
+ .question-text {
+ flex: 1;
+ font-size: 30px;
+ font-weight: 600;
+ color: #333;
+ line-height: 1.4;
+ }
+
+ .expand-icon {
+ width: 48px;
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36px;
+ color: #999;
+ flex-shrink: 0;
+ }
+ }
+
+ .faq-answer {
+ padding: 0 28px 28px;
+ padding-left: 92px;
+
+ .answer-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16px;
+
+ .answer-icon {
+ width: 40px;
+ height: 40px;
+ background: #34c759;
+ color: #fff;
+ font-size: 22px;
+ font-weight: 700;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 12px;
+ }
+
+ .answer-label {
+ font-size: 24px;
+ color: #34c759;
+ font-weight: 600;
+ }
+ }
+
+ .answer-text {
+ font-size: 28px;
+ color: #666;
+ line-height: 1.8;
+ white-space: pre-wrap;
+ }
+
+ .answer-tags {
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: 20px;
+
+ .tag {
+ padding: 8px 16px;
+ background: #f0f7ff;
+ color: #007aff;
+ font-size: 22px;
+ border-radius: 8px;
+ margin-right: 12px;
+ margin-bottom: 12px;
+ }
+ }
+
+ .answer-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 24px;
+ padding-top: 20px;
+ border-top: 1px solid #f0f0f0;
+
+ .helpful-info {
+ display: flex;
+
+ .helpful-text {
+ font-size: 24px;
+ color: #34c759;
+ margin-right: 16px;
+ }
+
+ .unhelpful-text {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+
+ .helpful-actions {
+ display: flex;
+
+ .helpful-btn {
+ padding: 12px 20px;
+ font-size: 22px;
+ background: #f5f5f5;
+ color: #666;
+ border-radius: 8px;
+ margin-left: 12px;
+
+ &::after { border: none; }
+ }
+ }
+ }
+ }
+}
+
+// 底部提示
+.footer-tip {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 24px;
+ padding-bottom: calc(24px + env(safe-area-inset-bottom));
+ background: #fff;
+ border-top: 1px solid #eee;
+
+ .tip-text {
+ font-size: 28px;
+ color: #666;
+ margin-right: 16px;
+ }
+
+ .ticket-btn {
+ padding: 16px 32px;
+ background: #007aff;
+ color: #fff;
+ font-size: 28px;
+ border-radius: 32px;
+
+ &::after { border: none; }
+ }
+}
diff --git a/src/developer/ticket/faq.tsx b/src/developer/ticket/faq.tsx
new file mode 100644
index 0000000..b353bb0
--- /dev/null
+++ b/src/developer/ticket/faq.tsx
@@ -0,0 +1,187 @@
+/**
+ * FAQ 常见问题页面
+ */
+import { useState, useEffect } from 'react';
+import { View, Text, Input, ScrollView, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { pageFAQ, feedbackFAQ } from '../../api/ticket';
+import type { FAQ } from '../../types/ticket';
+import './faq.scss';
+
+const CATEGORY_MAP: Record = {
+ api: { label: 'API 相关', icon: '🔌' },
+ billing: { label: '账单财务', icon: '💰' },
+ account: { label: '账户安全', icon: '🔐' },
+ sdk: { label: 'SDK 问题', icon: '📦' },
+ payment: { label: '支付问题', icon: '💳' },
+ other: { label: '其他', icon: '📝' }
+};
+
+export default function FAQPage() {
+ const [faqs, setFaqs] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [loading, setLoading] = useState(true);
+ const [keyword, setKeyword] = useState('');
+ const [category, setCategory] = useState('all');
+ const [expandedId, setExpandedId] = useState(null);
+
+ useEffect(() => {
+ loadFAQs();
+ }, []);
+
+ const loadFAQs = async () => {
+ setLoading(true);
+ try {
+ const params: any = { page: 1, pageSize: 50 };
+ if (category !== 'all') params.category = category;
+ if (keyword) params.keyword = keyword;
+
+ const data = await pageFAQ(params);
+ setFaqs(data.list);
+ setTotal(data.total);
+ } catch (e) {
+ console.error('加载失败', e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSearch = () => {
+ loadFAQs();
+ };
+
+ const handleCategoryChange = (cat: string) => {
+ setCategory(cat);
+ loadFAQs();
+ };
+
+ const toggleExpand = (id: string) => {
+ setExpandedId(expandedId === id ? null : id);
+ };
+
+ const handleFeedback = async (faq: FAQ, helpful: boolean) => {
+ await feedbackFAQ(faq.id, helpful);
+ Taro.showToast({ title: helpful ? '感谢您的好评' : '我们会改进', icon: 'success' });
+ loadFAQs();
+ };
+
+ return (
+
+ {/* 搜索栏 */}
+
+ setKeyword(e.detail.value)}
+ onConfirm={handleSearch}
+ />
+
+
+
+ {/* 分类标签 */}
+
+
+ handleCategoryChange('all')}
+ >
+ 全部
+
+ {Object.entries(CATEGORY_MAP).map(([key, { label, icon }]) => (
+ handleCategoryChange(key)}
+ >
+ {icon} {label}
+
+ ))}
+
+
+
+ {/* 统计 */}
+
+ 共 {total} 个常见问题
+
+
+ {/* FAQ 列表 */}
+
+ {loading ? (
+ 加载中...
+ ) : faqs.length === 0 ? (
+
+ 未找到相关问题
+
+ ) : (
+ faqs.map(faq => (
+
+ toggleExpand(faq.id)}>
+ Q
+ {faq.question}
+ {expandedId === faq.id ? '−' : '+'}
+
+
+ {expandedId === faq.id && (
+
+
+ A
+ 回答
+
+ {faq.answer}
+
+ {faq.tags.length > 0 && (
+
+ {faq.tags.map(tag => (
+ {tag}
+ ))}
+
+ )}
+
+
+
+
+ 有帮助 ({faq.helpful})
+
+
+ 没帮助 ({faq.notHelpful})
+
+
+
+
+
+
+
+
+ )}
+
+ ))
+ )}
+
+
+ {/* 底部提示 */}
+
+ 没有找到答案?
+
+
+
+ );
+}
diff --git a/src/developer/ticket/index.config.ts b/src/developer/ticket/index.config.ts
new file mode 100644
index 0000000..cdf70d2
--- /dev/null
+++ b/src/developer/ticket/index.config.ts
@@ -0,0 +1,7 @@
+/**
+ * 工单系统 - 页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '工单中心',
+ enableShareAppMessage: true
+});
diff --git a/src/developer/ticket/index.scss b/src/developer/ticket/index.scss
new file mode 100644
index 0000000..2f7b721
--- /dev/null
+++ b/src/developer/ticket/index.scss
@@ -0,0 +1,376 @@
+// 工单中心样式
+.ticket-center {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: env(safe-area-inset-bottom);
+}
+
+// 统计卡片
+.stats-cards {
+ display: flex;
+ padding: 24px;
+ background: linear-gradient(135deg, #007aff 0%, #5856d6 100%);
+
+ .stats-card {
+ flex: 1;
+ text-align: center;
+ padding: 24px 12px;
+
+ .stats-num {
+ font-size: 40px;
+ font-weight: 700;
+ color: #fff;
+ display: block;
+ }
+
+ .stats-label {
+ font-size: 24px;
+ color: rgba(255, 255, 255, 0.8);
+ margin-top: 8px;
+ display: block;
+ }
+ }
+}
+
+// 筛选栏
+.filter-bar {
+ display: flex;
+ align-items: center;
+ padding: 24px;
+ background: #fff;
+ border-bottom: 1px solid #eee;
+
+ .status-tabs {
+ flex: 1;
+ white-space: nowrap;
+
+ .status-tab {
+ display: inline-flex;
+ align-items: center;
+ padding: 16px 28px;
+ margin-right: 16px;
+ font-size: 28px;
+ color: #666;
+ position: relative;
+
+ &.active {
+ color: #007aff;
+ font-weight: 600;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 48px;
+ height: 4px;
+ background: #007aff;
+ border-radius: 2px;
+ }
+ }
+
+ .tab-badge {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ min-width: 32px;
+ height: 32px;
+ padding: 0 8px;
+ background: #ff3b30;
+ color: #fff;
+ font-size: 20px;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ }
+
+ .create-btn {
+ padding: 16px 32px;
+ background: #007aff;
+ color: #fff;
+ font-size: 28px;
+ border-radius: 32px;
+
+ &::after { border: none; }
+ }
+}
+
+// 工单列表
+.ticket-list {
+ height: calc(100vh - 400px);
+ padding: 24px;
+
+ .loading, .empty {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 300px;
+ color: #999;
+ font-size: 28px;
+ }
+}
+
+.ticket-card {
+ background: #fff;
+ border-radius: 24px;
+ padding: 32px;
+ margin-bottom: 24px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
+
+ .ticket-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+
+ .ticket-no {
+ font-size: 24px;
+ color: #999;
+ }
+
+ .status-tag {
+ padding: 8px 20px;
+ border-radius: 20px;
+ font-size: 24px;
+ }
+ }
+
+ .ticket-title {
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ line-height: 1.4;
+ display: block;
+ margin-bottom: 20px;
+ }
+
+ .ticket-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .meta-left {
+ display: flex;
+ align-items: center;
+
+ .priority-tag {
+ font-size: 24px;
+ font-weight: 600;
+ margin-right: 16px;
+ }
+
+ .type-text {
+ font-size: 24px;
+ color: #666;
+ margin-right: 16px;
+ }
+
+ .reply-count {
+ font-size: 24px;
+ color: #007aff;
+ }
+ }
+
+ .time-text {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+
+ .ticket-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 20px;
+ padding-top: 20px;
+ border-top: 1px solid #f0f0f0;
+
+ .solution-text {
+ flex: 1;
+ font-size: 26px;
+ color: #34c759;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .footer-actions {
+ display: flex;
+ align-items: center;
+ }
+
+ .rating-text {
+ font-size: 26px;
+ color: #ff9500;
+ }
+
+ .action-btn {
+ padding: 12px 24px;
+ font-size: 24px;
+ border-radius: 20px;
+ background: #f5f5f5;
+ color: #666;
+
+ &.close {
+ background: #fff0f0;
+ color: #ff3b30;
+ }
+
+ &::after { border: none; }
+ }
+ }
+}
+
+// 创建工单弹窗
+.modal-mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-end;
+ z-index: 1000;
+
+ .create-modal {
+ width: 100%;
+ max-height: 85vh;
+ background: #fff;
+ border-radius: 32px 32px 0 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32px;
+ border-bottom: 1px solid #eee;
+
+ .modal-title {
+ font-size: 36px;
+ font-weight: 600;
+ }
+
+ .modal-close {
+ width: 64px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36px;
+ color: #999;
+ }
+ }
+
+ .modal-body {
+ flex: 1;
+ padding: 32px;
+ max-height: calc(85vh - 200px);
+ }
+
+ .form-item {
+ margin-bottom: 32px;
+
+ .form-label {
+ font-size: 28px;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ margin-bottom: 16px;
+
+ .required {
+ color: #ff3b30;
+ }
+ }
+
+ .form-input {
+ width: 100%;
+ height: 88px;
+ padding: 0 24px;
+ background: #f5f5f5;
+ border-radius: 16px;
+ font-size: 28px;
+ }
+
+ .textarea-wrap {
+ background: #f5f5f5;
+ border-radius: 16px;
+ padding: 24px;
+
+ .form-textarea {
+ width: 100%;
+ min-height: 300px;
+ font-size: 28px;
+ line-height: 1.6;
+ }
+ }
+ }
+
+ .type-selector {
+ display: flex;
+ flex-wrap: wrap;
+
+ .type-option {
+ padding: 16px 28px;
+ margin-right: 16px;
+ margin-bottom: 16px;
+ background: #f5f5f5;
+ border-radius: 12px;
+ font-size: 26px;
+ color: #666;
+
+ &.active {
+ background: #e6f0ff;
+ color: #007aff;
+ font-weight: 600;
+ }
+ }
+ }
+
+ .priority-selector {
+ display: flex;
+
+ .priority-option {
+ flex: 1;
+ padding: 20px;
+ margin-right: 16px;
+ background: #fff;
+ border: 2px solid #ddd;
+ border-radius: 12px;
+ font-size: 26px;
+ text-align: center;
+
+ &:last-child { margin-right: 0; }
+
+ &.active {
+ background: #f0f7ff;
+ font-weight: 600;
+ }
+ }
+ }
+
+ .modal-footer {
+ padding: 32px;
+ padding-bottom: calc(32px + env(safe-area-inset-bottom));
+ border-top: 1px solid #eee;
+
+ .submit-btn {
+ width: 100%;
+ height: 88px;
+ line-height: 88px;
+ background: #007aff;
+ color: #fff;
+ font-size: 32px;
+ border-radius: 44px;
+
+ &::after { border: none; }
+ }
+ }
+}
diff --git a/src/developer/ticket/index.tsx b/src/developer/ticket/index.tsx
new file mode 100644
index 0000000..c370fdf
--- /dev/null
+++ b/src/developer/ticket/index.tsx
@@ -0,0 +1,359 @@
+/**
+ * 工单中心
+ */
+import { useState, useEffect } from 'react';
+import { View, Text, Image, Input, ScrollView, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { pageTicket, getTicketStats, createTicket, closeTicket, rateTicket } from '../../api/ticket';
+import type { Ticket, TicketStats } from '../../types/ticket';
+import './index.scss';
+
+const STATUS_MAP: Record = {
+ pending: { label: '待处理', color: '#ff9500' },
+ processing: { label: '处理中', color: '#007aff' },
+ waiting: { label: '等待回复', color: '#5856d6' },
+ resolved: { label: '已解决', color: '#34c759' },
+ closed: { label: '已关闭', color: '#999' },
+ rejected: { label: '已拒绝', color: '#ff3b30' }
+};
+
+const PRIORITY_MAP: Record = {
+ low: { label: '低', color: '#34c759' },
+ medium: { label: '中', color: '#ff9500' },
+ high: { label: '高', color: '#ff3b30' },
+ urgent: { label: '紧急', color: '#af52de' }
+};
+
+const TYPE_MAP: Record = {
+ technical: '技术问题',
+ billing: '账单问题',
+ account: '账户问题',
+ feature: '功能建议',
+ bug: 'Bug 反馈',
+ other: '其他'
+};
+
+export default function TicketCenter() {
+ const [tickets, setTickets] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [status, setStatus] = useState('all');
+ const [keyword, setKeyword] = useState('');
+ const [showCreate, setShowCreate] = useState(false);
+ const [showDetail, setShowDetail] = useState(null);
+
+ // 创建表单
+ const [formData, setFormData] = useState({
+ title: '',
+ content: '',
+ type: 'technical' as Ticket['type'],
+ priority: 'medium' as Ticket['priority'],
+ category: 'api' as Ticket['category']
+ });
+
+ useEffect(() => {
+ loadStats();
+ loadTickets();
+ }, []);
+
+ // 加载统计
+ const loadStats = async () => {
+ try {
+ const data = await getTicketStats();
+ setStats(data);
+ } catch (e) {
+ console.error('加载统计失败', e);
+ }
+ };
+
+ // 加载工单列表
+ const loadTickets = async () => {
+ setLoading(true);
+ try {
+ const params: any = { page: 1, pageSize: 20 };
+ if (status !== 'all') params.status = status;
+ if (keyword) params.keyword = keyword;
+
+ const data = await pageTicket(params);
+ setTickets(data.list);
+ setTotal(data.total);
+ } catch (e) {
+ console.error('加载工单失败', e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 创建工单
+ const handleCreate = async () => {
+ if (!formData.title.trim()) {
+ Taro.showToast({ title: '请填写标题', icon: 'none' });
+ return;
+ }
+ if (!formData.content.trim()) {
+ Taro.showToast({ title: '请填写内容', icon: 'none' });
+ return;
+ }
+
+ try {
+ await createTicket(formData);
+ Taro.showToast({ title: '提交成功', icon: 'success' });
+ setShowCreate(false);
+ setFormData({ title: '', content: '', type: 'technical', priority: 'medium', category: 'api' });
+ loadTickets();
+ loadStats();
+ } catch (e) {
+ Taro.showToast({ title: '提交失败', icon: 'none' });
+ }
+ };
+
+ // 关闭工单
+ const handleClose = async (id: string) => {
+ Taro.showModal({
+ title: '确认关闭',
+ content: '确定要关闭这个工单吗?',
+ success: async (res) => {
+ if (res.confirm) {
+ await closeTicket(id);
+ Taro.showToast({ title: '已关闭', icon: 'success' });
+ loadTickets();
+ }
+ }
+ });
+ };
+
+ // 评价工单
+ const handleRate = (ticket: Ticket) => {
+ Taro.showModal({
+ title: '工单评价',
+ editable: true,
+ placeholderText: '请输入您的反馈(选填)',
+ success: async (res) => {
+ if (res.confirm) {
+ await rateTicket(ticket.id, 5, res.content || '');
+ Taro.showToast({ title: '感谢您的反馈', icon: 'success' });
+ loadTickets();
+ }
+ }
+ });
+ };
+
+ // 查看详情
+ const handleViewDetail = (ticket: Ticket) => {
+ Taro.navigateTo({ url: `/developer/ticket/detail?id=${ticket.id}` });
+ };
+
+ // 格式化时间
+ const formatTime = (time: string) => {
+ const date = new Date(time.replace(' ', 'T'));
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+
+ if (diff < 60000) return '刚刚';
+ if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
+ return time.slice(0, 10);
+ };
+
+ // 格式化解决时间
+ const formatResolveTime = (minutes?: number) => {
+ if (!minutes) return '-';
+ if (minutes < 60) return `${minutes} 分钟`;
+ return `${(minutes / 60).toFixed(1)} 小时`;
+ };
+
+ return (
+
+ {/* 统计卡片 */}
+ {stats && (
+
+
+ {stats.total}
+ 全部工单
+
+
+ {stats.pending}
+ 待处理
+
+
+ {stats.processing}
+ 处理中
+
+
+ {stats.resolved}
+ 已解决
+
+
+ )}
+
+ {/* 筛选栏 */}
+
+
+ {[
+ { value: 'all', label: '全部' },
+ { value: 'pending', label: '待处理' },
+ { value: 'processing', label: '处理中' },
+ { value: 'resolved', label: '已解决' },
+ { value: 'closed', label: '已关闭' }
+ ].map(tab => (
+ { setStatus(tab.value); loadTickets(); }}
+ >
+ {tab.label}
+ {tab.value !== 'all' && stats && tab.value === 'pending' && stats.pending > 0 && (
+ {stats.pending}
+ )}
+
+ ))}
+
+
+
+
+
+ {/* 工单列表 */}
+
+ {loading ? (
+ 加载中...
+ ) : tickets.length === 0 ? (
+
+ 暂无工单
+
+ ) : (
+ tickets.map(ticket => (
+ handleViewDetail(ticket)}>
+
+ {ticket.ticketNo}
+
+ {STATUS_MAP[ticket.status]?.label}
+
+
+
+ {ticket.title}
+
+
+
+
+ {PRIORITY_MAP[ticket.priority]?.label}
+
+ {TYPE_MAP[ticket.type]}
+ {ticket.responseCount > 0 && (
+ {ticket.responseCount} 条回复
+ )}
+
+ {formatTime(ticket.createTime)}
+
+
+ {ticket.status === 'resolved' && (
+
+ {ticket.solution && (
+ 解决方案:{ticket.solution}
+ )}
+
+ {!ticket.rating && (
+
+ )}
+ {ticket.rating && (
+ ⭐ {ticket.rating}分
+ )}
+
+
+ )}
+
+ {ticket.status === 'pending' && (
+
+
+
+ )}
+
+ ))
+ )}
+
+
+ {/* 创建工单弹窗 */}
+ {showCreate && (
+ setShowCreate(false)}>
+ e.stopPropagation()}>
+
+ 提交工单
+ setShowCreate(false)}>✕
+
+
+
+
+ 问题类型
+
+ {Object.entries(TYPE_MAP).map(([key, label]) => (
+ setFormData({ ...formData, type: key as Ticket['type'], category: key as Ticket['category'] })}
+ >
+ {label}
+
+ ))}
+
+
+
+
+ 优先级
+
+ {Object.entries(PRIORITY_MAP).map(([key, { label, color }]) => (
+ setFormData({ ...formData, priority: key as Ticket['priority'] })}
+ >
+ {label}
+
+ ))}
+
+
+
+
+ 标题 *
+ setFormData({ ...formData, title: e.detail.value })}
+ />
+
+
+
+ 详细描述 *
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/enterprise/apps/[id]/analytics.config.ts b/src/enterprise/apps/[id]/analytics.config.ts
new file mode 100644
index 0000000..7a2152f
--- /dev/null
+++ b/src/enterprise/apps/[id]/analytics.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '数据分析',
+})
diff --git a/src/enterprise/apps/[id]/analytics.scss b/src/enterprise/apps/[id]/analytics.scss
new file mode 100644
index 0000000..159f9cc
--- /dev/null
+++ b/src/enterprise/apps/[id]/analytics.scss
@@ -0,0 +1,10 @@
+.analytics-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/apps/[id]/analytics.tsx b/src/enterprise/apps/[id]/analytics.tsx
new file mode 100644
index 0000000..489d420
--- /dev/null
+++ b/src/enterprise/apps/[id]/analytics.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './analytics.scss'
+
+const AnalyticsPage: React.FC = () => {
+ return (
+
+
+ 数据分析功能开发中...
+
+
+ )
+}
+
+export default AnalyticsPage
diff --git a/src/enterprise/apps/[id]/index.config.ts b/src/enterprise/apps/[id]/index.config.ts
new file mode 100644
index 0000000..0a667ca
--- /dev/null
+++ b/src/enterprise/apps/[id]/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '应用详情',
+})
diff --git a/src/enterprise/apps/[id]/index.scss b/src/enterprise/apps/[id]/index.scss
new file mode 100644
index 0000000..2c6fa45
--- /dev/null
+++ b/src/enterprise/apps/[id]/index.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-app-detail-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #666666;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/enterprise/apps/[id]/index.tsx b/src/enterprise/apps/[id]/index.tsx
new file mode 100644
index 0000000..eb18434
--- /dev/null
+++ b/src/enterprise/apps/[id]/index.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import './index.scss'
+
+const EnterpriseAppDetail: React.FC = () => {
+ const id = Taro.getStorageSync('id')
+
+ return (
+
+
+ 应用详情
+
+
+ 应用 ID: {id}
+ 企业应用详情功能开发中...
+
+
+ )
+}
+
+export default EnterpriseAppDetail
diff --git a/src/enterprise/apps/[id]/monitor.config.ts b/src/enterprise/apps/[id]/monitor.config.ts
new file mode 100644
index 0000000..833a79b
--- /dev/null
+++ b/src/enterprise/apps/[id]/monitor.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '运营监控',
+})
diff --git a/src/enterprise/apps/[id]/monitor.scss b/src/enterprise/apps/[id]/monitor.scss
new file mode 100644
index 0000000..858c52e
--- /dev/null
+++ b/src/enterprise/apps/[id]/monitor.scss
@@ -0,0 +1,10 @@
+.monitor-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/apps/[id]/monitor.tsx b/src/enterprise/apps/[id]/monitor.tsx
new file mode 100644
index 0000000..85c0b1e
--- /dev/null
+++ b/src/enterprise/apps/[id]/monitor.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './monitor.scss'
+
+const MonitorPage: React.FC = () => {
+ return (
+
+
+ 运营监控功能开发中...
+
+
+ )
+}
+
+export default MonitorPage
diff --git a/src/enterprise/apps/[id]/settings.config.ts b/src/enterprise/apps/[id]/settings.config.ts
new file mode 100644
index 0000000..9750000
--- /dev/null
+++ b/src/enterprise/apps/[id]/settings.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '应用设置',
+})
diff --git a/src/enterprise/apps/[id]/settings.scss b/src/enterprise/apps/[id]/settings.scss
new file mode 100644
index 0000000..df3d005
--- /dev/null
+++ b/src/enterprise/apps/[id]/settings.scss
@@ -0,0 +1,10 @@
+.settings-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/apps/[id]/settings.tsx b/src/enterprise/apps/[id]/settings.tsx
new file mode 100644
index 0000000..f81c695
--- /dev/null
+++ b/src/enterprise/apps/[id]/settings.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './settings.scss'
+
+const SettingsPage: React.FC = () => {
+ return (
+
+
+ 应用设置功能开发中...
+
+
+ )
+}
+
+export default SettingsPage
diff --git a/src/enterprise/apps/index.scss b/src/enterprise/apps/index.scss
new file mode 100644
index 0000000..649659e
--- /dev/null
+++ b/src/enterprise/apps/index.scss
@@ -0,0 +1,152 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-apps-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding-bottom: 140rpx;
+
+ &__scroll {
+ height: calc(100vh - 100rpx);
+ }
+}
+
+.tabs-wrapper {
+ background: #ffffff;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+/* 应用列表 */
+.app-list {
+ padding: 24rpx;
+
+ &__loading {
+ padding: 120rpx 0;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+ }
+}
+
+/* 应用卡片 */
+.app-card {
+ display: flex;
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 24rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
+
+ &__icon {
+ width: 100rpx;
+ height: 100rpx;
+ border-radius: 20rpx;
+ background: #f3f4f6;
+ overflow: hidden;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &__iconImg {
+ width: 100%;
+ height: 100%;
+ }
+
+ &__iconDefault {
+ font-size: 48rpx;
+ }
+
+ &__info {
+ flex: 1;
+ margin-left: 20rpx;
+ min-width: 0;
+ }
+
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12rpx;
+ }
+
+ &__name {
+ font-size: 32rpx;
+ font-weight: 800;
+ color: #111111;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__status {
+ display: flex;
+ align-items: center;
+ gap: 6rpx;
+ flex-shrink: 0;
+ }
+
+ &__statusDot {
+ width: 8rpx;
+ height: 8rpx;
+ border-radius: 999rpx;
+ }
+
+ &__statusText {
+ font-size: 22rpx;
+ font-weight: 600;
+ }
+
+ &__desc {
+ display: block;
+ margin-top: 10rpx;
+ font-size: 24rpx;
+ color: #999999;
+ line-height: 1.5;
+ }
+
+ &__meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 12rpx;
+ }
+
+ &__version {
+ font-size: 22rpx;
+ color: #d97706;
+ font-weight: 600;
+ }
+
+ &__time {
+ font-size: 22rpx;
+ color: #666666;
+ }
+}
+
+/* 购买按钮 */
+.purchase-btn-wrapper {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 24rpx 32rpx;
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+ background: #ffffff;
+ box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
+}
+
+/* 底部安全区域 */
+.safe-area-bottom {
+ height: 40rpx;
+}
diff --git a/src/enterprise/apps/index.tsx b/src/enterprise/apps/index.tsx
new file mode 100644
index 0000000..1db4ce7
--- /dev/null
+++ b/src/enterprise/apps/index.tsx
@@ -0,0 +1,140 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, ScrollView, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Empty, Tabs, PullToRefresh } from '@nutui/nutui-react-taro'
+import { pageEnterpriseApp } from '@/api/developer/enterprise'
+import type { App } from '@/types/developer'
+import { STATUS_NAME, STATUS_COLOR } from '@/types/developer'
+import './index.scss'
+
+const EnterpriseApps: React.FC = () => {
+ const [activeTab, setActiveTab] = useState('all')
+ const [apps, setApps] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+
+ // 加载数据
+ const loadData = async () => {
+ try {
+ setLoading(true)
+ const result = await pageEnterpriseApp({ current: 1, size: 20 })
+ if (result) {
+ setApps(result.list || [])
+ }
+ } catch (error) {
+ console.error('加载失败', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 下拉刷新
+ const onRefresh = async () => {
+ setRefreshing(true)
+ await loadData()
+ setRefreshing(false)
+ }
+
+ useEffect(() => {
+ loadData()
+ }, [])
+
+ // 筛选应用
+ const filteredApps = activeTab === 'all'
+ ? apps
+ : apps.filter((app) => {
+ if (activeTab === 'running') return app.status === 1
+ if (activeTab === 'stopped') return app.status !== 1
+ return true
+ })
+
+ // 跳转应用详情
+ const handleAppClick = (id: number) => {
+ Taro.navigateTo({ url: `/enterprise/apps/${id}` })
+ }
+
+ // 获取状态颜色
+ const getStatusColor = (status?: number) => {
+ return STATUS_COLOR[status || 0] || '#6b7280'
+ }
+
+ // 获取状态文本
+ const getStatusText = (status?: number) => {
+ return STATUS_NAME[status || 0] || '未知'
+ }
+
+ return (
+
+
+ {/* 标签页 */}
+
+ setActiveTab(v as string)}>
+
+
+
+
+
+
+
+ {/* 应用列表 */}
+
+ {loading ? (
+
+ 加载中...
+
+ ) : filteredApps.length === 0 ? (
+
+ ) : (
+
+ {filteredApps.map((app) => (
+ handleAppClick(app.productId!)}>
+
+ {app.icon ? (
+
+ ) : (
+ 📱
+ )}
+
+
+
+
+ {app.productName}
+
+
+ {getStatusText(app.status)}
+
+
+
+
+ {app.description || '暂无描述'}
+
+
+
+ v{app.version || '1.0.0'}
+
+ {app.expirationTime ? `到期: ${app.expirationTime.split(' ')[0]}` : '永久有效'}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* 底部安全区域 */}
+
+
+
+ {/* 底部按钮 */}
+
+
+
+
+
+ )
+}
+
+export default EnterpriseApps
diff --git a/src/enterprise/apps/purchase.config.ts b/src/enterprise/apps/purchase.config.ts
new file mode 100644
index 0000000..ace27d2
--- /dev/null
+++ b/src/enterprise/apps/purchase.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '购买应用',
+})
diff --git a/src/enterprise/apps/purchase.scss b/src/enterprise/apps/purchase.scss
new file mode 100644
index 0000000..589aedc
--- /dev/null
+++ b/src/enterprise/apps/purchase.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-app-purchase-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/enterprise/apps/purchase.tsx b/src/enterprise/apps/purchase.tsx
new file mode 100644
index 0000000..bd43f14
--- /dev/null
+++ b/src/enterprise/apps/purchase.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './purchase.scss'
+
+const EnterpriseAppPurchase: React.FC = () => {
+ return (
+
+
+ 购买应用
+
+
+ 应用购买功能开发中...
+
+
+ )
+}
+
+export default EnterpriseAppPurchase
diff --git a/src/enterprise/billing/consumption.config.ts b/src/enterprise/billing/consumption.config.ts
new file mode 100644
index 0000000..4346fa7
--- /dev/null
+++ b/src/enterprise/billing/consumption.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '消费明细',
+})
diff --git a/src/enterprise/billing/consumption.scss b/src/enterprise/billing/consumption.scss
new file mode 100644
index 0000000..9172cb5
--- /dev/null
+++ b/src/enterprise/billing/consumption.scss
@@ -0,0 +1,10 @@
+.consumption-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/billing/consumption.tsx b/src/enterprise/billing/consumption.tsx
new file mode 100644
index 0000000..46d7922
--- /dev/null
+++ b/src/enterprise/billing/consumption.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './consumption.scss'
+
+const ConsumptionPage: React.FC = () => {
+ return (
+
+
+ 消费明细功能开发中...
+
+
+ )
+}
+
+export default ConsumptionPage
diff --git a/src/enterprise/billing/coupons.config.ts b/src/enterprise/billing/coupons.config.ts
new file mode 100644
index 0000000..b45e4e9
--- /dev/null
+++ b/src/enterprise/billing/coupons.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '优惠券',
+})
diff --git a/src/enterprise/billing/coupons.scss b/src/enterprise/billing/coupons.scss
new file mode 100644
index 0000000..1ff74c4
--- /dev/null
+++ b/src/enterprise/billing/coupons.scss
@@ -0,0 +1,10 @@
+.coupons-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/billing/coupons.tsx b/src/enterprise/billing/coupons.tsx
new file mode 100644
index 0000000..4a5b204
--- /dev/null
+++ b/src/enterprise/billing/coupons.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './coupons.scss'
+
+const CouponsPage: React.FC = () => {
+ return (
+
+
+ 优惠券功能开发中...
+
+
+ )
+}
+
+export default CouponsPage
diff --git a/src/enterprise/billing/index.config.ts b/src/enterprise/billing/index.config.ts
new file mode 100644
index 0000000..45471f0
--- /dev/null
+++ b/src/enterprise/billing/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '费用中心',
+})
diff --git a/src/enterprise/billing/index.scss b/src/enterprise/billing/index.scss
new file mode 100644
index 0000000..c2ec005
--- /dev/null
+++ b/src/enterprise/billing/index.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-billing-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/enterprise/billing/index.tsx b/src/enterprise/billing/index.tsx
new file mode 100644
index 0000000..489bd56
--- /dev/null
+++ b/src/enterprise/billing/index.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './index.scss'
+
+const EnterpriseBilling: React.FC = () => {
+ return (
+
+
+ 💰 费用中心
+
+
+ 费用中心功能开发中...
+
+
+ )
+}
+
+export default EnterpriseBilling
diff --git a/src/enterprise/billing/recharge.config.ts b/src/enterprise/billing/recharge.config.ts
new file mode 100644
index 0000000..5b3d7d6
--- /dev/null
+++ b/src/enterprise/billing/recharge.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '充值中心',
+})
diff --git a/src/enterprise/billing/recharge.scss b/src/enterprise/billing/recharge.scss
new file mode 100644
index 0000000..8e4c59e
--- /dev/null
+++ b/src/enterprise/billing/recharge.scss
@@ -0,0 +1,10 @@
+.recharge-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/billing/recharge.tsx b/src/enterprise/billing/recharge.tsx
new file mode 100644
index 0000000..a433a41
--- /dev/null
+++ b/src/enterprise/billing/recharge.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './recharge.scss'
+
+const RechargePage: React.FC = () => {
+ return (
+
+
+ 充值中心功能开发中...
+
+
+ )
+}
+
+export default RechargePage
diff --git a/src/enterprise/developer/apply.config.ts b/src/enterprise/developer/apply.config.ts
new file mode 100644
index 0000000..692fe60
--- /dev/null
+++ b/src/enterprise/developer/apply.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '申请开发者',
+})
diff --git a/src/enterprise/developer/apply.scss b/src/enterprise/developer/apply.scss
new file mode 100644
index 0000000..951167d
--- /dev/null
+++ b/src/enterprise/developer/apply.scss
@@ -0,0 +1,108 @@
+.apply-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 32rpx;
+ padding-bottom: 200rpx;
+
+ &__header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 24rpx;
+ padding: 48rpx 32rpx;
+ text-align: center;
+ margin-bottom: 32rpx;
+ }
+
+ &__icon {
+ font-size: 100rpx;
+ display: block;
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: #fff;
+ display: block;
+ margin-bottom: 16rpx;
+ }
+
+ &__desc {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.8);
+ display: block;
+ line-height: 1.6;
+ }
+
+ &__benefits {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ margin-bottom: 32rpx;
+
+ &-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ margin-bottom: 32rpx;
+ }
+ }
+
+ &__benefit {
+ display: flex;
+ align-items: center;
+ padding: 24rpx 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &-icon {
+ font-size: 48rpx;
+ margin-right: 24rpx;
+ }
+
+ &-info {
+ flex: 1;
+ }
+
+ &-name {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ margin-bottom: 8rpx;
+ }
+
+ &-desc {
+ font-size: 26rpx;
+ color: #999;
+ display: block;
+ }
+ }
+
+ &__actions {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 32rpx;
+ padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
+ background: #fff;
+ }
+
+ &__btn {
+ height: 96rpx;
+ line-height: 96rpx;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ border-radius: 48rpx;
+ font-size: 32rpx;
+ font-weight: 600;
+
+ &::after {
+ border: none;
+ }
+ }
+}
diff --git a/src/enterprise/developer/apply.tsx b/src/enterprise/developer/apply.tsx
new file mode 100644
index 0000000..d7e366c
--- /dev/null
+++ b/src/enterprise/developer/apply.tsx
@@ -0,0 +1,62 @@
+import React from 'react'
+import { View, Text, Button } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import './apply.scss'
+
+const ApplyPage: React.FC = () => {
+ const handleApply = () => {
+ Taro.navigateTo({ url: '/developer/developer/apply' })
+ }
+
+ return (
+
+
+ 🛠️
+ 成为开发者
+
+ 开通开发者权限,您将可以创建项目、开发应用、发布产品等功能
+
+
+
+
+ 开发者权益
+
+ 📁
+
+ 项目管理
+ 创建和管理多个项目
+
+
+
+ 📱
+
+ 应用开发
+ 开发、测试和发布应用
+
+
+
+ 🔑
+
+ API Key
+ 生成和管理 API 凭证
+
+
+
+ 📊
+
+ 数据分析
+ 查看应用使用数据
+
+
+
+
+
+
+
+
+ )
+}
+
+export default ApplyPage
diff --git a/src/enterprise/developer/index.config.ts b/src/enterprise/developer/index.config.ts
new file mode 100644
index 0000000..0c44677
--- /dev/null
+++ b/src/enterprise/developer/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '开发者入口',
+})
diff --git a/src/enterprise/developer/index.scss b/src/enterprise/developer/index.scss
new file mode 100644
index 0000000..7215a39
--- /dev/null
+++ b/src/enterprise/developer/index.scss
@@ -0,0 +1,94 @@
+.developer-entry-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 32rpx;
+ padding-bottom: 200rpx;
+
+ &__header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 24rpx;
+ padding: 64rpx 32rpx;
+ text-align: center;
+ margin-bottom: 32rpx;
+ }
+
+ &__icon {
+ font-size: 120rpx;
+ display: block;
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 48rpx;
+ font-weight: 700;
+ color: #fff;
+ display: block;
+ margin-bottom: 16rpx;
+ }
+
+ &__desc {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.8);
+ display: block;
+ line-height: 1.6;
+ }
+
+ &__features {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 24rpx;
+ margin-bottom: 32rpx;
+ }
+
+ &__feature {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ text-align: center;
+
+ &-icon {
+ font-size: 56rpx;
+ margin-bottom: 16rpx;
+ }
+
+ &-name {
+ font-size: 28rpx;
+ color: #333;
+ }
+ }
+
+ &__actions {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 32rpx;
+ padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+ }
+
+ &__btn {
+ height: 88rpx;
+ line-height: 88rpx;
+ border-radius: 44rpx;
+ font-size: 30rpx;
+
+ &--primary {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+
+ &::after {
+ border: none;
+ }
+ }
+
+ &--secondary {
+ background: #fff;
+ color: #667eea;
+ border: 1px solid #667eea;
+ }
+ }
+}
diff --git a/src/enterprise/developer/index.tsx b/src/enterprise/developer/index.tsx
new file mode 100644
index 0000000..1551dcd
--- /dev/null
+++ b/src/enterprise/developer/index.tsx
@@ -0,0 +1,56 @@
+import React from 'react'
+import { View, Text, Button } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import './index.scss'
+
+const DeveloperEntryPage: React.FC = () => {
+ const handleEntry = () => {
+ Taro.switchTab({ url: '/pages/index/index' })
+ }
+
+ const handleApply = () => {
+ Taro.navigateTo({ url: '/enterprise/developer/apply' })
+ }
+
+ return (
+
+
+ 🛠️
+ 开发者中心
+
+ 为开发者提供完整的项目管理和应用开发能力
+
+
+
+
+
+ 📁
+ 项目管理
+
+
+ 📱
+ 应用管理
+
+
+ 🔑
+ API Key
+
+
+ 📊
+ 数据分析
+
+
+
+
+
+
+
+
+ )
+}
+
+export default DeveloperEntryPage
diff --git a/src/enterprise/index.config.ts b/src/enterprise/index.config.ts
new file mode 100644
index 0000000..3f36a54
--- /dev/null
+++ b/src/enterprise/index.config.ts
@@ -0,0 +1,5 @@
+export default definePageConfig({
+ navigationBarTitleText: '企业控制台',
+ navigationStyle: 'custom',
+ usingComponents: {},
+})
diff --git a/src/enterprise/index.scss b/src/enterprise/index.scss
new file mode 100644
index 0000000..78adc4c
--- /dev/null
+++ b/src/enterprise/index.scss
@@ -0,0 +1,285 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+
+ &__scroll {
+ height: 100vh;
+ }
+}
+
+/* 头部区域 */
+.enterprise-header {
+ position: relative;
+ padding: 40rpx 32rpx 60rpx;
+ background: linear-gradient(180deg, #d97706 0%, #f59e0b 100%);
+ overflow: hidden;
+
+ &__glow {
+ position: absolute;
+ left: 50%;
+ top: -100rpx;
+ width: 500rpx;
+ height: 300rpx;
+ transform: translateX(-50%);
+ border-radius: 999rpx;
+ background: rgba(255, 255, 255, 0.15);
+ filter: blur(60rpx);
+ pointer-events: none;
+ }
+
+ &__content {
+ position: relative;
+ z-index: 1;
+ }
+
+ &__title {
+ margin-bottom: 12rpx;
+ }
+
+ &__titleText {
+ font-size: 44rpx;
+ font-weight: 900;
+ color: #ffffff;
+ }
+
+ &__subtitle {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.85);
+ }
+}
+
+/* 统计卡片 */
+.stats-card {
+ margin: -40rpx 24rpx 24rpx;
+ padding: 32rpx 24rpx;
+ background: #ffffff;
+ border-radius: 20rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
+
+ &__row {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ }
+
+ &__item {
+ flex: 1;
+ text-align: center;
+ }
+
+ &__value {
+ display: block;
+ font-size: 48rpx;
+ font-weight: 900;
+ color: #d97706;
+ line-height: 1.2;
+ }
+
+ &__label {
+ display: block;
+ margin-top: 8rpx;
+ font-size: 24rpx;
+ color: #666666;
+ }
+
+ &__divider {
+ width: 2rpx;
+ height: 60rpx;
+ background: #e5e7eb;
+ }
+}
+
+/* 快捷操作 */
+.quick-actions {
+ margin: 0 24rpx 24rpx;
+ padding: 24rpx;
+ background: #ffffff;
+ border-radius: 20rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
+
+ &__title {
+ margin-bottom: 20rpx;
+ }
+
+ &__titleText {
+ font-size: 32rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20rpx;
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12rpx;
+ padding: 20rpx 10rpx;
+ border-radius: 16rpx;
+ background: #f9fafb;
+ }
+
+ &__icon {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ }
+
+ &__icon--blue {
+ background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
+ }
+
+ &__icon--green {
+ background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+ }
+
+ &__icon--purple {
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
+ }
+
+ &__icon--orange {
+ background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
+ }
+
+ &__text {
+ font-size: 24rpx;
+ color: #374151;
+ font-weight: 600;
+ }
+}
+
+/* 应用列表 */
+.app-list {
+ margin: 0 24rpx 24rpx;
+ padding: 24rpx;
+ background: #ffffff;
+ border-radius: 20rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
+
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20rpx;
+ }
+
+ &__title {
+ font-size: 32rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__more {
+ padding: 8rpx 16rpx;
+ }
+
+ &__moreText {
+ font-size: 26rpx;
+ color: #666666;
+ }
+
+ &__loading {
+ padding: 60rpx 0;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+ }
+
+ &__footer {
+ margin-top: 24rpx;
+ }
+}
+
+/* 应用项 */
+.app-item {
+ display: flex;
+ align-items: center;
+ padding: 20rpx;
+ border-radius: 16rpx;
+ background: #f9fafb;
+
+ &__icon {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 16rpx;
+ background: #e5e7eb;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40rpx;
+ overflow: hidden;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ &__info {
+ flex: 1;
+ margin-left: 20rpx;
+ min-width: 0;
+ }
+
+ &__name {
+ display: block;
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #111111;
+ line-height: 1.3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__type {
+ display: block;
+ margin-top: 6rpx;
+ font-size: 24rpx;
+ color: #666666;
+ }
+
+ &__status {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ padding: 8rpx 16rpx;
+ border-radius: 999rpx;
+ background: #f3f4f6;
+ }
+
+ &__statusDot {
+ width: 8rpx;
+ height: 8rpx;
+ border-radius: 999rpx;
+ }
+
+ &__statusText {
+ font-size: 22rpx;
+ font-weight: 600;
+ }
+}
+
+/* 底部安全区域 */
+.safe-area-bottom {
+ height: calc(40rpx + env(safe-area-inset-bottom));
+}
diff --git a/src/enterprise/index.tsx b/src/enterprise/index.tsx
new file mode 100644
index 0000000..570d0da
--- /dev/null
+++ b/src/enterprise/index.tsx
@@ -0,0 +1,188 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Empty } from '@nutui/nutui-react-taro'
+import { useThemeStyles } from '@/hooks/useTheme'
+import { pageEnterpriseApp } from '@/api/developer/enterprise'
+import type { App } from '@/types/developer'
+import { STATUS_NAME, STATUS_COLOR } from '@/types/developer'
+import './index.scss'
+
+const EnterpriseIndex: React.FC = () => {
+ const themeStyles = useThemeStyles()
+ const [apps, setApps] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [stats, setStats] = useState({
+ totalApps: 0,
+ runningApps: 0,
+ totalMembers: 0,
+ pendingBills: 0,
+ })
+
+ // 加载数据
+ const loadData = async () => {
+ try {
+ setLoading(true)
+ const result = await pageEnterpriseApp({ current: 1, size: 10 })
+ if (result) {
+ setApps(result.list || [])
+ setStats({
+ totalApps: result.count || 0,
+ runningApps: result.list?.filter((a) => a.status === 1).length || 0,
+ totalMembers: 0,
+ pendingBills: 0,
+ })
+ }
+ } catch (error) {
+ console.error('加载数据失败', error)
+ Taro.showToast({ title: '加载失败', icon: 'none' })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ loadData()
+ }, [])
+
+ // 跳转页面
+ const navigateTo = (url: string) => {
+ Taro.navigateTo({ url })
+ }
+
+ // 获取状态颜色
+ const getStatusColor = (status?: number) => {
+ return STATUS_COLOR[status || 0] || '#6b7280'
+ }
+
+ // 获取状态文本
+ const getStatusText = (status?: number) => {
+ return STATUS_NAME[status || 0] || '未知'
+ }
+
+ return (
+
+
+ {/* 头部区域 */}
+
+
+
+
+ 🏢 企业控制台
+
+ 管理企业的应用、成员和资源
+
+
+
+ {/* 统计卡片 */}
+
+
+
+ {stats.totalApps}
+ 企业应用
+
+
+
+ {stats.runningApps}
+ 运行中
+
+
+
+ {stats.totalMembers}
+ 成员
+
+
+
+ {stats.pendingBills}
+ 待支付
+
+
+
+
+ {/* 快捷操作 */}
+
+
+ 快捷操作
+
+
+ navigateTo('/enterprise/apps/index')}>
+
+ 📱
+
+ 企业应用
+
+ navigateTo('/enterprise/members/index')}>
+
+ 👥
+
+ 成员管理
+
+ navigateTo('/enterprise/orders/index')}>
+
+ 📋
+
+ 订单账单
+
+ navigateTo('/enterprise/settings/index')}>
+
+ ⚙️
+
+ 企业设置
+
+
+
+
+ {/* 应用列表 */}
+
+
+ 企业应用
+ navigateTo('/enterprise/apps/index')}>
+ 查看全部 ›
+
+
+
+ {loading ? (
+
+ 加载中...
+
+ ) : apps.length === 0 ? (
+
+ ) : (
+
+ {apps.map((app) => (
+ navigateTo(`/enterprise/apps/${app.productId}`)}>
+
+ {app.icon ? (
+
+ ) : (
+ 📱
+ )}
+
+
+ {app.productName}
+ {app.appType}
+
+
+
+ {getStatusText(app.status)}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ {/* 底部安全区域 */}
+
+
+
+ )
+}
+
+export default EnterpriseIndex
diff --git a/src/enterprise/members/audit.config.ts b/src/enterprise/members/audit.config.ts
new file mode 100644
index 0000000..e7a38ad
--- /dev/null
+++ b/src/enterprise/members/audit.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '权限审批',
+})
diff --git a/src/enterprise/members/audit.scss b/src/enterprise/members/audit.scss
new file mode 100644
index 0000000..6b72777
--- /dev/null
+++ b/src/enterprise/members/audit.scss
@@ -0,0 +1,230 @@
+.audit-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 120rpx;
+
+ &__header {
+ padding: 32rpx 32rpx 24rpx;
+ background: #fff;
+ }
+
+ &__title {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: #333;
+ }
+
+ // Tab
+ &__tabs {
+ display: flex;
+ padding: 0 32rpx 24rpx;
+ background: #fff;
+ gap: 24rpx;
+ }
+
+ &__tab {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ font-size: 28rpx;
+ color: #666;
+ padding-bottom: 8rpx;
+ border-bottom: 4rpx solid transparent;
+
+ &.active {
+ color: #667eea;
+ border-bottom-color: #667eea;
+ }
+
+ &-badge {
+ background: #ff4d4f;
+ color: #fff;
+ font-size: 20rpx;
+ padding: 2rpx 10rpx;
+ border-radius: 20rpx;
+ min-width: 32rpx;
+ text-align: center;
+ }
+ }
+
+ // 列表
+ &__list {
+ padding: 24rpx 32rpx;
+ }
+
+ &__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 120rpx 0;
+
+ &-icon {
+ font-size: 120rpx;
+ margin-bottom: 32rpx;
+ }
+
+ &-text {
+ font-size: 30rpx;
+ color: #999;
+ }
+ }
+
+ &__loading,
+ &__no-more {
+ display: flex;
+ justify-content: center;
+ padding: 32rpx 0;
+
+ text {
+ font-size: 26rpx;
+ color: #999;
+ }
+ }
+}
+
+// 审批卡片
+.audit-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 32rpx;
+ margin-bottom: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24rpx;
+ }
+
+ &__applicant {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ }
+
+ &__avatar {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+
+ &-img {
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center;
+ }
+
+ &-text {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #fff;
+ }
+ }
+
+ &__applicant-info {
+ display: flex;
+ flex-direction: column;
+ gap: 8rpx;
+ }
+
+ &__name {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ &__type {
+ font-size: 24rpx;
+ color: #999;
+ }
+
+ &__status {
+ font-size: 24rpx;
+ padding: 8rpx 20rpx;
+ border-radius: 8rpx;
+ }
+
+ &__body {
+ background: #fafafa;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ margin-bottom: 16rpx;
+ }
+
+ &__row {
+ display: flex;
+ margin-bottom: 12rpx;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__label {
+ font-size: 26rpx;
+ color: #666;
+ width: 140rpx;
+ flex-shrink: 0;
+ }
+
+ &__value {
+ font-size: 26rpx;
+ color: #333;
+ flex: 1;
+ }
+
+ &__remark {
+ background: #fff7e6;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ margin-bottom: 16rpx;
+
+ &-label {
+ display: block;
+ font-size: 24rpx;
+ color: #fa8c16;
+ margin-bottom: 8rpx;
+ }
+
+ &-content {
+ font-size: 26rpx;
+ color: #333;
+ }
+ }
+
+ &__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 24rpx;
+ padding-top: 16rpx;
+ border-top: 1px solid #f0f0f0;
+ }
+
+ &__btn {
+ border-radius: 32rpx;
+ font-size: 28rpx;
+ padding: 0 40rpx;
+
+ &--approve {
+ background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
+ color: #fff;
+
+ &::after {
+ border: none;
+ }
+ }
+
+ &--reject {
+ background: #fff;
+ color: #ff4d4f;
+ border: 1px solid #ff4d4f;
+ }
+ }
+}
diff --git a/src/enterprise/members/audit.tsx b/src/enterprise/members/audit.tsx
new file mode 100644
index 0000000..98fa6ab
--- /dev/null
+++ b/src/enterprise/members/audit.tsx
@@ -0,0 +1,253 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, Button } from '@tarojs/components'
+import Taro, { usePullDownRefresh } from '@tarojs/taro'
+import { pageApply, reviewApply } from '@/api/developer/developer'
+import type { Apply, ApplyParam, ApplyStatus, ApplyType } from '@/types/developer'
+import './audit.scss'
+
+// 类型配置
+const TYPE_CONFIG: Record = {
+ developer: { label: '开发者申请', icon: '🛠️' },
+ project: { label: '项目权限', icon: '📁' },
+ member: { label: '成员权限', icon: '👥' },
+}
+
+// 状态配置
+const STATUS_CONFIG: Record = {
+ pending: { label: '待审核', color: '#faad14', bgColor: '#fffbe6' },
+ approved: { label: '已通过', color: '#52c41a', bgColor: '#f6ffed' },
+ rejected: { label: '已拒绝', color: '#ff4d4f', bgColor: '#fff1f0' },
+}
+
+const AuditPage: React.FC = () => {
+ const [loading, setLoading] = useState(false)
+ const [refreshing, setRefreshing] = useState(false)
+ const [list, setList] = useState([])
+ const [page, setPage] = useState(1)
+ const [hasMore, setHasMore] = useState(true)
+ const [currentTab, setCurrentTab] = useState<'pending' | 'approved' | 'rejected' | 'all'>('pending')
+ const [processing, setProcessing] = useState(null)
+
+ // 加载数据
+ const loadData = async (pageNum: number = 1, isRefresh = false) => {
+ if (loading) return
+ setLoading(true)
+ if (isRefresh) setRefreshing(true)
+
+ try {
+ const params: ApplyParam = {
+ page: pageNum,
+ limit: 20,
+ status: currentTab === 'all' ? undefined : currentTab,
+ }
+ const data = await pageApply(params)
+
+ if (pageNum === 1) {
+ setList(data?.records || [])
+ } else {
+ setList(prev => [...prev, ...(data?.records || [])])
+ }
+
+ const total = data?.total || 0
+ const records = data?.records || []
+ setHasMore(records.length > 0 && (pageNum * 20) < total)
+ setPage(pageNum)
+ } catch (err) {
+ console.error('加载失败', err)
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => {
+ loadData(1)
+ }, [currentTab])
+
+ // 下拉刷新
+ usePullDownRefresh(() => {
+ loadData(1, true)
+ })
+
+ // 加载更多
+ const onReachBottom = () => {
+ if (hasMore && !loading) {
+ loadData(page + 1)
+ }
+ }
+
+ // 审批
+ const handleReview = async (item: Apply, status: 'approved' | 'rejected') => {
+ setProcessing(item.id!)
+ try {
+ await reviewApply(item.id!, status)
+ Taro.showToast({ title: status === 'approved' ? '已通过' : '已拒绝', icon: 'success' })
+ loadData(1)
+ } catch (err) {
+ console.error('审批失败', err)
+ Taro.showToast({ title: '操作失败', icon: 'none' })
+ } finally {
+ setProcessing(null)
+ }
+ }
+
+ // 格式化日期
+ const formatDate = (dateStr?: string) => {
+ if (!dateStr) return '-'
+ const date = new Date(dateStr)
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
+ }
+
+ // Tab 配置
+ const tabs: { key: 'pending' | 'approved' | 'rejected' | 'all'; label: string }[] = [
+ { key: 'pending', label: '待审核' },
+ { key: 'approved', label: '已通过' },
+ { key: 'rejected', label: '已拒绝' },
+ { key: 'all', label: '全部' },
+ ]
+
+ return (
+
+ {/* 头部 */}
+
+ 📋 权限审批
+
+
+ {/* Tab */}
+
+ {tabs.map((tab) => (
+ setCurrentTab(tab.key)}
+ >
+ {tab.label}
+ {tab.key === 'pending' && (
+
+ {list.filter(i => i.status === 'pending').length}
+
+ )}
+
+ ))}
+
+
+ {/* 列表 */}
+
+ {list.length === 0 && !loading ? (
+
+ 📭
+ 暂无申请记录
+
+ ) : (
+ list.map((item) => (
+
+
+
+
+ {item.applicantAvatar ? (
+
+ ) : (
+
+ {(item.applicantName || '?').charAt(0).toUpperCase()}
+
+ )}
+
+
+ {item.applicantName}
+
+ {TYPE_CONFIG[item.type as ApplyType]?.icon} {TYPE_CONFIG[item.type as ApplyType]?.label}
+
+
+
+
+ {STATUS_CONFIG[item.status as ApplyStatus]?.label}
+
+
+
+
+
+ 申请目标:
+ {item.targetName || '-'}
+
+ {item.reason && (
+
+ 申请理由:
+ {item.reason}
+
+ )}
+
+ 申请时间:
+ {formatDate(item.createTime)}
+
+ {item.reviewerName && (
+
+ 审核人:
+ {item.reviewerName}
+
+ )}
+ {item.reviewTime && (
+
+ 审核时间:
+ {formatDate(item.reviewTime)}
+
+ )}
+
+
+ {item.reviewRemark && (
+
+ 审核备注:
+ {item.reviewRemark}
+
+ )}
+
+ {item.status === 'pending' && (
+
+
+
+
+ )}
+
+ ))
+ )}
+
+ {/* 加载状态 */}
+ {loading && list.length > 0 && (
+
+ 加载中...
+
+ )}
+
+ {!hasMore && list.length > 0 && (
+
+ 没有更多了
+
+ )}
+
+
+ )
+}
+
+export default AuditPage
diff --git a/src/enterprise/members/index.scss b/src/enterprise/members/index.scss
new file mode 100644
index 0000000..2e607dd
--- /dev/null
+++ b/src/enterprise/members/index.scss
@@ -0,0 +1,116 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-members-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding-bottom: 140rpx;
+
+ &__scroll {
+ height: calc(100vh - 100rpx);
+ }
+}
+
+.tabs-wrapper {
+ background: #ffffff;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+/* 成员列表 */
+.member-list {
+ padding: 24rpx;
+
+ &__loading {
+ padding: 120rpx 0;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+ }
+}
+
+/* 成员卡片 */
+.member-card {
+ display: flex;
+ align-items: center;
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 24rpx;
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
+
+ &__avatar {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 50%;
+ background: #f3f4f6;
+ flex-shrink: 0;
+ }
+
+ &__info {
+ flex: 1;
+ margin-left: 20rpx;
+ min-width: 0;
+ }
+
+ &__header {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ }
+
+ &__name {
+ font-size: 32rpx;
+ font-weight: 700;
+ color: #111111;
+ }
+
+ &__badge {
+ padding: 4rpx 12rpx;
+ border-radius: 999rpx;
+ font-size: 22rpx;
+ font-weight: 600;
+ }
+
+ &__badgeText {
+ line-height: 1;
+ }
+
+ &__dept {
+ display: block;
+ margin-top: 6rpx;
+ font-size: 24rpx;
+ color: #666666;
+ }
+
+ &__time {
+ display: block;
+ margin-top: 8rpx;
+ font-size: 22rpx;
+ color: #999999;
+ }
+}
+
+/* 邀请按钮 */
+.invite-btn-wrapper {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 24rpx 32rpx;
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+ background: #ffffff;
+ box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
+}
+
+/* 底部安全区域 */
+.safe-area-bottom {
+ height: 40rpx;
+}
diff --git a/src/enterprise/members/index.tsx b/src/enterprise/members/index.tsx
new file mode 100644
index 0000000..18b4ae8
--- /dev/null
+++ b/src/enterprise/members/index.tsx
@@ -0,0 +1,131 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, ScrollView, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Empty, Tabs, PullToRefresh, Avatar } from '@nutui/nutui-react-taro'
+import { pageEnterpriseMember } from '@/api/developer/enterprise'
+import type { EnterpriseMember } from '@/types/developer'
+import './index.scss'
+
+const EnterpriseMembers: React.FC = () => {
+ const [activeTab, setActiveTab] = useState('all')
+ const [members, setMembers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+
+ // 加载数据
+ const loadData = async () => {
+ try {
+ setLoading(true)
+ const result = await pageEnterpriseMember({ page: 1, limit: 20 })
+ if (result) {
+ setMembers(result.list || [])
+ }
+ } catch (error) {
+ console.error('加载失败', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 下拉刷新
+ const onRefresh = async () => {
+ setRefreshing(true)
+ await loadData()
+ setRefreshing(false)
+ }
+
+ useEffect(() => {
+ loadData()
+ }, [])
+
+ // 筛选成员
+ const filteredMembers = activeTab === 'all'
+ ? members
+ : members.filter((m) => {
+ if (activeTab === 'owner') return m.role === 'owner'
+ if (activeTab === 'admin') return m.role === 'admin'
+ if (activeTab === 'member') return m.role === 'member'
+ return true
+ })
+
+ // 获取角色标签
+ const getRoleBadge = (role?: string) => {
+ const badges: Record = {
+ owner: { text: '所有者', color: '#d97706', bg: 'rgba(217, 119, 6, 0.1)' },
+ admin: { text: '管理员', color: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
+ member: { text: '成员', color: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
+ }
+ return badges[role || 'member'] || badges.member
+ }
+
+ return (
+
+
+ {/* 标签页 */}
+
+ setActiveTab(v as string)}>
+
+
+
+
+
+
+
+
+ {/* 成员列表 */}
+
+ {loading ? (
+
+ 加载中...
+
+ ) : filteredMembers.length === 0 ? (
+
+ ) : (
+
+ {filteredMembers.map((member) => {
+ const badge = getRoleBadge(member.role)
+ return (
+
+
+ {member.username?.charAt(0) || 'U'}
+
+
+
+
+ {member.username || '未知用户'}
+
+ {badge.text}
+
+
+
+ {member.department && (
+ {member.department}
+ )}
+
+
+ 加入于 {member.inviteTime ? member.inviteTime.split(' ')[0] : '-'}
+
+
+
+ )
+ })}
+
+ )}
+
+
+ {/* 底部安全区域 */}
+
+
+
+ {/* 底部按钮 */}
+
+
+
+
+
+ )
+}
+
+export default EnterpriseMembers
diff --git a/src/enterprise/members/invite.config.ts b/src/enterprise/members/invite.config.ts
new file mode 100644
index 0000000..09c6ddf
--- /dev/null
+++ b/src/enterprise/members/invite.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '邀请成员',
+})
diff --git a/src/enterprise/members/invite.scss b/src/enterprise/members/invite.scss
new file mode 100644
index 0000000..bbb3637
--- /dev/null
+++ b/src/enterprise/members/invite.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-member-invite-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/enterprise/members/invite.tsx b/src/enterprise/members/invite.tsx
new file mode 100644
index 0000000..1c44210
--- /dev/null
+++ b/src/enterprise/members/invite.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './invite.scss'
+
+const EnterpriseMemberInvite: React.FC = () => {
+ return (
+
+
+ 邀请成员
+
+
+ 成员邀请功能开发中...
+
+
+ )
+}
+
+export default EnterpriseMemberInvite
diff --git a/src/enterprise/members/roles.config.ts b/src/enterprise/members/roles.config.ts
new file mode 100644
index 0000000..b46ac0e
--- /dev/null
+++ b/src/enterprise/members/roles.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '角色权限',
+})
diff --git a/src/enterprise/members/roles.scss b/src/enterprise/members/roles.scss
new file mode 100644
index 0000000..bb08b0d
--- /dev/null
+++ b/src/enterprise/members/roles.scss
@@ -0,0 +1,10 @@
+.roles-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/members/roles.tsx b/src/enterprise/members/roles.tsx
new file mode 100644
index 0000000..bf8e533
--- /dev/null
+++ b/src/enterprise/members/roles.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './roles.scss'
+
+const RolesPage: React.FC = () => {
+ return (
+
+
+ 角色权限功能开发中...
+
+
+ )
+}
+
+export default RolesPage
diff --git a/src/enterprise/orders/bills.config.ts b/src/enterprise/orders/bills.config.ts
new file mode 100644
index 0000000..bac9ffb
--- /dev/null
+++ b/src/enterprise/orders/bills.config.ts
@@ -0,0 +1,7 @@
+/**
+ * 账单导出 - 页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '账单管理',
+ enableShareAppMessage: true
+});
diff --git a/src/enterprise/orders/bills.scss b/src/enterprise/orders/bills.scss
new file mode 100644
index 0000000..5ea908a
--- /dev/null
+++ b/src/enterprise/orders/bills.scss
@@ -0,0 +1,437 @@
+// 账单导出样式
+.bill-export {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: env(safe-area-inset-bottom);
+}
+
+// 导出横幅
+.export-banner {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32px;
+ background: linear-gradient(135deg, #34c759 0%, #30d158 100%);
+
+ .banner-info {
+ .banner-title {
+ font-size: 36px;
+ font-weight: 700;
+ color: #fff;
+ display: block;
+ margin-bottom: 8px;
+ }
+
+ .banner-desc {
+ font-size: 26px;
+ color: rgba(255, 255, 255, 0.8);
+ }
+ }
+
+ .export-btn {
+ padding: 20px 36px;
+ background: #fff;
+ color: #34c759;
+ font-size: 28px;
+ font-weight: 600;
+ border-radius: 40px;
+
+ &::after { border: none; }
+ }
+}
+
+// 列表头部
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32px 24px 16px;
+
+ .section-title {
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .section-count {
+ font-size: 26px;
+ color: #999;
+ }
+}
+
+// 任务列表
+.task-list {
+ height: calc(100vh - 300px);
+ padding: 0 24px 24px;
+}
+
+.loading, .empty {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 400px;
+ color: #999;
+ font-size: 28px;
+
+ .empty-btn {
+ margin-top: 24px;
+ padding: 20px 48px;
+ background: #007aff;
+ color: #fff;
+ font-size: 28px;
+ border-radius: 40px;
+
+ &::after { border: none; }
+ }
+}
+
+.task-card {
+ background: #fff;
+ border-radius: 24px;
+ padding: 32px;
+ margin-bottom: 24px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
+
+ &.expired {
+ opacity: 0.6;
+ }
+
+ .task-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 20px;
+
+ .task-info {
+ display: flex;
+ align-items: flex-start;
+
+ .task-icon {
+ font-size: 48px;
+ margin-right: 16px;
+ }
+
+ .task-meta {
+ .task-name {
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ margin-bottom: 12px;
+ }
+
+ .task-tags {
+ display: flex;
+
+ .tag {
+ padding: 6px 16px;
+ background: #f5f5f5;
+ color: #666;
+ font-size: 22px;
+ border-radius: 8px;
+ margin-right: 12px;
+
+ &.count {
+ background: #e6f0ff;
+ color: #007aff;
+ }
+ }
+ }
+ }
+ }
+
+ .task-status {
+ font-size: 26px;
+ font-weight: 500;
+ }
+ }
+
+ .download-section {
+ background: #f9f9f9;
+ padding: 24px;
+ border-radius: 16px;
+ margin-bottom: 20px;
+
+ .file-info {
+ margin-bottom: 16px;
+
+ .file-name {
+ font-size: 28px;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ margin-bottom: 8px;
+ }
+
+ .file-size {
+ font-size: 24px;
+ color: #999;
+ margin-right: 16px;
+ }
+
+ .expires-info {
+ font-size: 24px;
+ color: #666;
+
+ .expired-tag {
+ color: #ff3b30;
+ }
+ }
+ }
+
+ .download-actions {
+ .download-btn {
+ padding: 16px 36px;
+ background: #34c759;
+ color: #fff;
+ font-size: 26px;
+ border-radius: 24px;
+
+ &::after { border: none; }
+ }
+ }
+ }
+
+ .task-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 20px;
+ border-top: 1px solid #f0f0f0;
+
+ .task-time {
+ font-size: 24px;
+ color: #999;
+ }
+
+ .footer-actions {
+ .action-btn {
+ padding: 12px 24px;
+ font-size: 24px;
+ background: #f5f5f5;
+ color: #666;
+ border-radius: 8px;
+
+ &.delete {
+ background: #fff0f0;
+ color: #ff3b30;
+ }
+
+ &::after { border: none; }
+ }
+ }
+ }
+}
+
+// 导出弹窗
+.modal-mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-end;
+ z-index: 1000;
+
+ .export-modal {
+ width: 100%;
+ max-height: 85vh;
+ background: #fff;
+ border-radius: 32px 32px 0 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32px;
+ border-bottom: 1px solid #eee;
+
+ .modal-title {
+ font-size: 36px;
+ font-weight: 600;
+ }
+
+ .modal-close {
+ width: 64px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36px;
+ color: #999;
+ }
+ }
+
+ .modal-body {
+ flex: 1;
+ padding: 32px;
+ max-height: calc(85vh - 200px);
+ }
+
+ .form-item {
+ margin-bottom: 32px;
+
+ .form-label {
+ font-size: 28px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 20px;
+ display: block;
+ }
+ }
+
+ .type-selector {
+ display: flex;
+
+ .type-option {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 24px 16px;
+ margin-right: 16px;
+ background: #f5f5f5;
+ border-radius: 16px;
+ border: 2px solid transparent;
+
+ &:last-child { margin-right: 0; }
+
+ &.active {
+ background: #e6f0ff;
+ border-color: #007aff;
+ }
+
+ .option-icon {
+ font-size: 40px;
+ margin-bottom: 12px;
+ }
+
+ .option-label {
+ font-size: 26px;
+ color: #333;
+ }
+ }
+ }
+
+ .format-selector {
+ display: flex;
+
+ .format-option {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 24px;
+ margin-right: 16px;
+ background: #f5f5f5;
+ border-radius: 16px;
+ border: 2px solid transparent;
+
+ &:last-child { margin-right: 0; }
+
+ &.active {
+ background: #e6f0ff;
+ border-color: #007aff;
+ }
+
+ .format-icon {
+ font-size: 48px;
+ margin-bottom: 12px;
+ }
+
+ .format-label {
+ font-size: 28px;
+ color: #333;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .date-range {
+ display: flex;
+ align-items: center;
+
+ .date-picker {
+ flex: 1;
+
+ .date-label {
+ font-size: 24px;
+ color: #999;
+ margin-bottom: 8px;
+ display: block;
+ }
+
+ .picker-value {
+ padding: 20px 24px;
+ background: #f5f5f5;
+ border-radius: 12px;
+ font-size: 28px;
+ color: #333;
+ }
+ }
+
+ .date-separator {
+ padding: 0 24px;
+ font-size: 28px;
+ color: #999;
+ }
+ }
+
+ .tip-box {
+ display: flex;
+ background: #fffbe6;
+ padding: 24px;
+ border-radius: 16px;
+ margin-top: 24px;
+
+ .tip-icon {
+ font-size: 32px;
+ margin-right: 16px;
+ }
+
+ .tip-text {
+ flex: 1;
+ font-size: 26px;
+ color: #666;
+ line-height: 1.6;
+ }
+ }
+
+ .modal-footer {
+ display: flex;
+ padding: 32px;
+ padding-bottom: calc(32px + env(safe-area-inset-bottom));
+ border-top: 1px solid #eee;
+
+ .cancel-btn {
+ flex: 1;
+ height: 88px;
+ line-height: 88px;
+ font-size: 32px;
+ background: #f5f5f5;
+ color: #333;
+ border-radius: 44px;
+ margin-right: 16px;
+
+ &::after { border: none; }
+ }
+
+ .submit-btn {
+ flex: 2;
+ height: 88px;
+ line-height: 88px;
+ font-size: 32px;
+ background: #007aff;
+ color: #fff;
+ border-radius: 44px;
+
+ &::after { border: none; }
+ }
+ }
+}
diff --git a/src/enterprise/orders/bills.tsx b/src/enterprise/orders/bills.tsx
new file mode 100644
index 0000000..d558054
--- /dev/null
+++ b/src/enterprise/orders/bills.tsx
@@ -0,0 +1,305 @@
+/**
+ * 账单管理/导出页面
+ */
+import { useState, useEffect } from 'react';
+import { View, Text, ScrollView, Button, Picker } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { pageExportTask, exportBill, deleteExportTask } from '../../api/import';
+import type { ExportTask, ExportFormat, ExportType } from '../../types/import';
+import './bills.scss';
+
+const STATUS_MAP: Record = {
+ pending: { label: '等待中', color: '#999', icon: '⏳' },
+ processing: { label: '生成中', color: '#007aff', icon: '⚙️' },
+ completed: { label: '已完成', color: '#34c759', icon: '✅' },
+ failed: { label: '失败', color: '#ff3b30', icon: '❌' }
+};
+
+const FORMAT_MAP: Record = {
+ xlsx: { label: 'Excel', icon: '📊' },
+ csv: { label: 'CSV', icon: '📋' },
+ json: { label: 'JSON', icon: '📄' },
+ pdf: { label: 'PDF', icon: '📕' }
+};
+
+const TYPE_MAP: Record = {
+ user: { label: '用户数据', icon: '👤' },
+ order: { label: '订单数据', icon: '📋' },
+ product: { label: '商品数据', icon: '📦' },
+ bill: { label: '账单数据', icon: '💰' },
+ log: { label: '日志数据', icon: '📝' },
+ custom: { label: '自定义', icon: '⚙️' }
+};
+
+export default function BillExport() {
+ const [tasks, setTasks] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [loading, setLoading] = useState(true);
+ const [showExport, setShowExport] = useState(false);
+
+ // 导出表单
+ const [formData, setFormData] = useState({
+ type: 'order' as ExportType,
+ format: 'xlsx' as ExportFormat,
+ startDate: '2026-01-01',
+ endDate: '2026-04-12'
+ });
+
+ useEffect(() => {
+ loadTasks();
+ }, []);
+
+ const loadTasks = async () => {
+ setLoading(true);
+ try {
+ const data = await pageExportTask({ page: 1, pageSize: 20 });
+ setTasks(data.list);
+ setTotal(data.total);
+ } catch (e) {
+ console.error('加载失败', e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 创建导出任务
+ const handleExport = async () => {
+ try {
+ const task = await exportBill({
+ startDate: formData.startDate,
+ endDate: formData.endDate,
+ format: formData.format
+ });
+
+ Taro.showToast({ title: '导出任务已创建', icon: 'success' });
+ setShowExport(false);
+ loadTasks();
+ } catch (e) {
+ Taro.showToast({ title: '创建失败', icon: 'none' });
+ }
+ };
+
+ // 下载
+ const handleDownload = (task: ExportTask) => {
+ if (task.downloadUrl) {
+ Taro.showToast({ title: '开始下载...', icon: 'loading' });
+ console.log(`Downloading: ${task.downloadUrl}`);
+ }
+ };
+
+ // 删除任务
+ const handleDelete = async (id: string) => {
+ Taro.showModal({
+ title: '确认删除',
+ content: '确定要删除这个导出任务吗?',
+ success: async (res) => {
+ if (res.confirm) {
+ await deleteExportTask(id);
+ Taro.showToast({ title: '已删除', icon: 'success' });
+ loadTasks();
+ }
+ }
+ });
+ };
+
+ // 格式化时间
+ const formatTime = (time: string) => {
+ return time.slice(0, 16).replace('T', ' ');
+ };
+
+ // 判断是否过期
+ const isExpired = (task: ExportTask) => {
+ if (!task.expiresAt) return false;
+ return new Date(task.expiresAt) < new Date();
+ };
+
+ return (
+
+ {/* 导出按钮 */}
+
+
+ 账单导出
+ 支持 Excel、CSV、PDF 格式导出
+
+
+
+
+ {/* 导出记录 */}
+
+ 导出记录
+ 共 {total} 条
+
+
+
+ {loading ? (
+ 加载中...
+ ) : tasks.length === 0 ? (
+
+ 暂无导出记录
+
+
+ ) : (
+ tasks.map(task => (
+
+
+
+ {TYPE_MAP[task.type]?.icon}
+
+ {task.name}
+
+ {FORMAT_MAP[task.format]?.icon} {FORMAT_MAP[task.format]?.label}
+ {task.total} 条
+
+
+
+
+ {STATUS_MAP[task.status]?.icon} {STATUS_MAP[task.status]?.label}
+
+
+
+ {task.status === 'completed' && task.downloadUrl && (
+
+
+ {task.fileName}
+ {task.fileSize}
+ {task.expiresAt && (
+
+ 过期时间: {formatTime(task.expiresAt)}
+ {isExpired(task) && 已过期}
+
+ )}
+
+
+ {!isExpired(task) && (
+
+ )}
+
+
+ )}
+
+
+ 创建于 {formatTime(task.createTime)}
+
+ {task.status === 'completed' && (
+
+ )}
+
+
+
+ ))
+ )}
+
+
+ {/* 导出弹窗 */}
+ {showExport && (
+ setShowExport(false)}>
+ e.stopPropagation()}>
+
+ 导出账单
+ setShowExport(false)}>✕
+
+
+
+
+ 导出类型
+
+ {[
+ { value: 'bill', label: '账单数据', icon: '💰' },
+ { value: 'order', label: '订单数据', icon: '📋' },
+ { value: 'user', label: '用户数据', icon: '👤' }
+ ].map(item => (
+ setFormData({ ...formData, type: item.value as ExportType })}
+ >
+ {item.icon}
+ {item.label}
+
+ ))}
+
+
+
+
+ 导出格式
+
+ {[
+ { value: 'xlsx', label: 'Excel', icon: '📊' },
+ { value: 'csv', label: 'CSV', icon: '📋' },
+ { value: 'pdf', label: 'PDF', icon: '📕' }
+ ].map(item => (
+ setFormData({ ...formData, format: item.value as ExportFormat })}
+ >
+ {item.icon}
+ {item.label}
+
+ ))}
+
+
+
+
+ 时间范围
+
+
+ 开始日期
+ setFormData({ ...formData, startDate: e.detail.value })}
+ >
+ {formData.startDate}
+
+
+ 至
+
+ 结束日期
+ setFormData({ ...formData, endDate: e.detail.value })}
+ >
+ {formData.endDate}
+
+
+
+
+
+
+ 💡
+
+ 导出文件将在 24 小时后过期,请及时下载。
+ 大数据量导出可能需要几分钟时间。
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/enterprise/orders/detail.config.ts b/src/enterprise/orders/detail.config.ts
new file mode 100644
index 0000000..e34d8fc
--- /dev/null
+++ b/src/enterprise/orders/detail.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '订单详情',
+})
diff --git a/src/enterprise/orders/detail.scss b/src/enterprise/orders/detail.scss
new file mode 100644
index 0000000..b875a9a
--- /dev/null
+++ b/src/enterprise/orders/detail.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-order-detail-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/enterprise/orders/detail.tsx b/src/enterprise/orders/detail.tsx
new file mode 100644
index 0000000..788e88c
--- /dev/null
+++ b/src/enterprise/orders/detail.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './detail.scss'
+
+const EnterpriseOrderDetail: React.FC = () => {
+ return (
+
+
+ 订单详情
+
+
+ 订单详情功能开发中...
+
+
+ )
+}
+
+export default EnterpriseOrderDetail
diff --git a/src/enterprise/orders/index.config.ts b/src/enterprise/orders/index.config.ts
new file mode 100644
index 0000000..c37c3a7
--- /dev/null
+++ b/src/enterprise/orders/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '订单账单',
+})
diff --git a/src/enterprise/orders/index.scss b/src/enterprise/orders/index.scss
new file mode 100644
index 0000000..0aab17b
--- /dev/null
+++ b/src/enterprise/orders/index.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-orders-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/enterprise/orders/index.tsx b/src/enterprise/orders/index.tsx
new file mode 100644
index 0000000..50cf3ce
--- /dev/null
+++ b/src/enterprise/orders/index.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './index.scss'
+
+const EnterpriseOrders: React.FC = () => {
+ return (
+
+
+ 📋 订单账单
+
+
+ 订单管理功能开发中...
+
+
+ )
+}
+
+export default EnterpriseOrders
diff --git a/src/enterprise/orders/invoice.config.ts b/src/enterprise/orders/invoice.config.ts
new file mode 100644
index 0000000..b00fa86
--- /dev/null
+++ b/src/enterprise/orders/invoice.config.ts
@@ -0,0 +1,6 @@
+/**
+ * 发票管理页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '发票管理',
+})
diff --git a/src/enterprise/orders/invoice.scss b/src/enterprise/orders/invoice.scss
new file mode 100644
index 0000000..ee6c160
--- /dev/null
+++ b/src/enterprise/orders/invoice.scss
@@ -0,0 +1,253 @@
+.invoice-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+}
+
+// 可开票金额卡片
+.invoiceable-card {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32px 24px;
+ background: linear-gradient(135deg, #1890ff, #52c41a);
+ color: #fff;
+
+ .card-info {
+ .label {
+ display: block;
+ font-size: 26px;
+ opacity: 0.9;
+ margin-bottom: 8px;
+ }
+
+ .amount {
+ font-size: 48px;
+ font-weight: 600;
+ }
+ }
+}
+
+// 统计栏
+.stats-bar {
+ display: flex;
+ background: #fff;
+ padding: 24px;
+ gap: 32px;
+
+ .stat-item {
+ text-align: center;
+
+ .stat-num {
+ display: block;
+ font-size: 36px;
+ font-weight: 600;
+ color: #333;
+
+ &.success {
+ color: #4CAF50;
+ }
+ }
+
+ .stat-label {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+}
+
+// 发票列表
+.invoice-list {
+ height: calc(100vh - 380px);
+ padding: 24px;
+}
+
+.invoice-item {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+
+ .invoice-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+
+ .invoice-no {
+ font-size: 30px;
+ font-weight: 600;
+ color: #333;
+ }
+ }
+
+ .invoice-info {
+ .info-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 8px 0;
+
+ .label {
+ font-size: 26px;
+ color: #999;
+ }
+
+ .value {
+ font-size: 26px;
+ color: #666;
+
+ &.amount {
+ color: #F44336;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+
+ .invoice-actions {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid #eee;
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ .express-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 16px;
+ padding: 12px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ font-size: 26px;
+ color: #2196F3;
+
+ .iconfont {
+ font-size: 28px;
+ }
+ }
+}
+
+.loading-wrap,
+.empty-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 100px 0;
+
+ .iconfont {
+ font-size: 96px;
+ color: #ccc;
+ margin-bottom: 24px;
+ }
+
+ .loading-text,
+ .empty-text {
+ font-size: 28px;
+ color: #999;
+ }
+}
+
+.loading-more {
+ display: flex;
+ justify-content: center;
+ padding: 24px;
+}
+
+.no-more {
+ text-align: center;
+ padding: 24px;
+ font-size: 24px;
+ color: #999;
+}
+
+// 申请发票弹窗
+.apply-modal-content {
+ .modal-section {
+ margin-bottom: 24px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .section-label {
+ display: block;
+ font-size: 28px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 16px;
+ }
+ }
+
+ .title-list {
+ .title-item {
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ margin-bottom: 12px;
+ border: 2px solid transparent;
+
+ &.active {
+ background: rgba(24, 144, 255, 0.1);
+ border-color: #1890ff;
+ }
+
+ .title-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 8px;
+
+ .title-name {
+ font-size: 28px;
+ color: #333;
+ }
+ }
+
+ .title-tax {
+ font-size: 22px;
+ color: #999;
+ }
+ }
+ }
+
+ .empty-titles {
+ text-align: center;
+ padding: 24px;
+ color: #999;
+
+ text {
+ display: block;
+ margin-bottom: 16px;
+ }
+ }
+
+ .amount-input {
+ display: flex;
+ align-items: center;
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+
+ .currency {
+ font-size: 36px;
+ font-weight: 600;
+ color: #333;
+ margin-right: 8px;
+ }
+
+ .input {
+ flex: 1;
+ font-size: 36px;
+ font-weight: 600;
+ }
+ }
+
+ .amount-hint {
+ font-size: 22px;
+ color: #999;
+ margin-top: 8px;
+ }
+}
diff --git a/src/enterprise/orders/invoice.tsx b/src/enterprise/orders/invoice.tsx
new file mode 100644
index 0000000..d92669e
--- /dev/null
+++ b/src/enterprise/orders/invoice.tsx
@@ -0,0 +1,120 @@
+/**
+ * 发票管理页面
+ */
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Tabs, Tag, Loading, Dialog } from '@nutui/nutui-react-taro'
+import { pageInvoice, listInvoiceTitles, applyInvoice, cancelInvoice, getInvoiceableAmount } from '@/api/invoice'
+import type { Invoice, InvoiceTitle, InvoiceStatus } from '@/types/invoice'
+import './invoice.scss'
+
+// 状态映射
+const STATUS_MAP: Record = {
+ pending: { text: '待审核', color: '#FFC107' },
+ approved: { text: '已通过', color: '#2196F3' },
+ rejected: { text: '已拒绝', color: '#F44336' },
+ issued: { text: '已开票', color: '#4CAF50' },
+ cancelled: { text: '已取消', color: '#9E9E9E' },
+}
+
+// Tab 配置
+const TABS = [
+ { key: 'all', title: '全部' },
+ { key: 'pending', title: '待审核' },
+ { key: 'approved', title: '已通过' },
+ { key: 'issued', title: '已开票' }
+]
+
+const InvoicePage: React.FC = () => {
+ const [loading, setLoading] = useState(true)
+ const [tabIndex, setTabIndex] = useState(0)
+ const [invoices, setInvoices] = useState([])
+ const [invoiceableAmount, setInvoiceableAmount] = useState(0)
+ const [showApplyModal, setShowApplyModal] = useState(false)
+
+ useEffect(() => {
+ loadData()
+ }, [tabIndex])
+
+ const loadData = async () => {
+ setLoading(true)
+ try {
+ const [invoiceData, amountData] = await Promise.all([
+ pageInvoice({ status: tabIndex > 0 ? TABS[tabIndex].key as InvoiceStatus : undefined }),
+ getInvoiceableAmount()
+ ])
+ setInvoices(invoiceData.list)
+ setInvoiceableAmount(amountData.amount)
+ } catch (e) {
+ console.error('加载失败', e)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleCancel = async (id: string) => {
+ Dialog.confirm({
+ title: '确认取消',
+ content: '确定要取消这个发票申请吗?',
+ onConfirm: async () => {
+ await cancelInvoice(id)
+ Taro.showToast({ title: '已取消', icon: 'success' })
+ loadData()
+ }
+ })
+ }
+
+ return (
+
+ {/* 可开票金额 */}
+
+ 可开票金额
+ ¥{invoiceableAmount.toLocaleString()}
+
+
+
+ {/* 发票列表 */}
+ setTabIndex(Number(val))}>
+ {TABS.map(tab => (
+
+ {loading ? (
+ 加载中...
+ ) : invoices.length === 0 ? (
+
+ 暂无发票记录
+
+ ) : (
+
+ {invoices.map(invoice => (
+
+
+ {invoice.title}
+ {STATUS_MAP[invoice.status]?.text}
+
+
+ 金额: ¥{invoice.amount.toLocaleString()}
+ 类型: {invoice.type === 'normal' ? '普通发票' : '增值税专用发票'}
+
+
+ {invoice.createTime.slice(0, 10)}
+ {invoice.status === 'pending' && (
+
+ )}
+
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ )
+}
+
+export default InvoicePage
diff --git a/src/enterprise/settings/domain.config.ts b/src/enterprise/settings/domain.config.ts
new file mode 100644
index 0000000..df9124f
--- /dev/null
+++ b/src/enterprise/settings/domain.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '域名配置',
+})
diff --git a/src/enterprise/settings/domain.scss b/src/enterprise/settings/domain.scss
new file mode 100644
index 0000000..a386191
--- /dev/null
+++ b/src/enterprise/settings/domain.scss
@@ -0,0 +1,10 @@
+.domain-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/settings/domain.tsx b/src/enterprise/settings/domain.tsx
new file mode 100644
index 0000000..f69cc52
--- /dev/null
+++ b/src/enterprise/settings/domain.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './domain.scss'
+
+const DomainPage: React.FC = () => {
+ return (
+
+
+ 域名配置功能开发中...
+
+
+ )
+}
+
+export default DomainPage
diff --git a/src/enterprise/settings/import.config.ts b/src/enterprise/settings/import.config.ts
new file mode 100644
index 0000000..61c4e10
--- /dev/null
+++ b/src/enterprise/settings/import.config.ts
@@ -0,0 +1,7 @@
+/**
+ * 数据导入 - 页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: '数据导入',
+ enableShareAppMessage: true
+});
diff --git a/src/enterprise/settings/import.scss b/src/enterprise/settings/import.scss
new file mode 100644
index 0000000..4633968
--- /dev/null
+++ b/src/enterprise/settings/import.scss
@@ -0,0 +1,482 @@
+// 数据导入样式
+.data-import {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: env(safe-area-inset-bottom);
+}
+
+// Tab 切换
+.tab-bar {
+ display: flex;
+ background: #fff;
+ border-bottom: 1px solid #eee;
+
+ .tab-item {
+ flex: 1;
+ text-align: center;
+ padding: 32px 0;
+ font-size: 30px;
+ color: #666;
+ position: relative;
+
+ &.active {
+ color: #007aff;
+ font-weight: 600;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 80px;
+ height: 4px;
+ background: #007aff;
+ border-radius: 2px;
+ }
+ }
+ }
+}
+
+.loading, .empty {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 400px;
+ color: #999;
+ font-size: 28px;
+}
+
+// 导入记录
+.task-list, .template-list {
+ height: calc(100vh - 120px);
+ padding: 24px;
+}
+
+.task-card {
+ background: #fff;
+ border-radius: 24px;
+ padding: 32px;
+ margin-bottom: 24px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
+
+ .task-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+
+ .task-info {
+ display: flex;
+ align-items: flex-start;
+
+ .task-icon {
+ font-size: 48px;
+ margin-right: 16px;
+ }
+
+ .task-meta {
+ .task-name {
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ margin-bottom: 8px;
+ }
+
+ .task-file {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+ }
+
+ .task-status {
+ font-size: 26px;
+ font-weight: 500;
+ }
+ }
+
+ .progress-section {
+ display: flex;
+ align-items: center;
+ margin-bottom: 24px;
+
+ .progress-bar {
+ flex: 1;
+ height: 12px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ overflow: hidden;
+ margin-right: 16px;
+
+ .progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #007aff, #5856d6);
+ border-radius: 6px;
+ transition: width 0.3s;
+ }
+ }
+
+ .progress-text {
+ font-size: 26px;
+ color: #007aff;
+ font-weight: 600;
+ min-width: 80px;
+ text-align: right;
+ }
+ }
+
+ .task-stats {
+ display: flex;
+ margin-bottom: 24px;
+
+ .stat-item {
+ flex: 1;
+ text-align: center;
+
+ .stat-value {
+ font-size: 36px;
+ font-weight: 700;
+ color: #333;
+ display: block;
+ }
+
+ .stat-label {
+ font-size: 24px;
+ color: #999;
+ }
+
+ &.success .stat-value { color: #34c759; }
+ &.error .stat-value { color: #ff3b30; }
+ }
+ }
+
+ .task-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 20px;
+ border-top: 1px solid #f0f0f0;
+
+ .task-time {
+ font-size: 24px;
+ color: #999;
+ }
+
+ .action-btns {
+ display: flex;
+
+ .action-btn {
+ padding: 12px 20px;
+ margin-left: 12px;
+ font-size: 24px;
+ background: #f5f5f5;
+ color: #333;
+ border-radius: 8px;
+
+ &.cancel {
+ background: #fff0f0;
+ color: #ff3b30;
+ }
+
+ &::after { border: none; }
+ }
+ }
+ }
+}
+
+// 模板列表
+.template-card {
+ background: #fff;
+ border-radius: 24px;
+ padding: 32px;
+ margin-bottom: 24px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
+
+ .template-header {
+ display: flex;
+ align-items: flex-start;
+ margin-bottom: 24px;
+
+ .template-icon {
+ font-size: 48px;
+ margin-right: 16px;
+ }
+
+ .template-info {
+ flex: 1;
+
+ .template-name {
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ margin-bottom: 8px;
+ }
+
+ .template-desc {
+ font-size: 26px;
+ color: #666;
+ line-height: 1.5;
+ }
+ }
+ }
+
+ .template-fields {
+ background: #f9f9f9;
+ padding: 24px;
+ border-radius: 16px;
+ margin-bottom: 24px;
+
+ .fields-label {
+ font-size: 26px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 16px;
+ display: block;
+ }
+
+ .field-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #eee;
+
+ &:last-child { border-bottom: none; }
+
+ .field-name {
+ flex: 1;
+ font-size: 26px;
+ color: #333;
+
+ .required { color: #ff3b30; }
+ }
+
+ .field-type {
+ font-size: 24px;
+ color: #999;
+ margin-right: 16px;
+ }
+
+ .field-example {
+ font-size: 24px;
+ color: #007aff;
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .more-fields {
+ font-size: 24px;
+ color: #007aff;
+ text-align: center;
+ display: block;
+ margin-top: 12px;
+ }
+ }
+
+ .template-actions {
+ display: flex;
+
+ .download-btn {
+ flex: 1;
+ height: 80px;
+ line-height: 80px;
+ font-size: 28px;
+ border-radius: 40px;
+ margin: 0 8px;
+ background: #f5f5f5;
+ color: #333;
+
+ &.primary {
+ background: #007aff;
+ color: #fff;
+ }
+
+ &:first-child { margin-left: 0; }
+ &:last-child { margin-right: 0; }
+ &::after { border: none; }
+ }
+ }
+}
+
+// 弹窗样式
+.modal-mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-end;
+ z-index: 1000;
+
+ .error-modal, .template-modal {
+ width: 100%;
+ max-height: 80vh;
+ background: #fff;
+ border-radius: 32px 32px 0 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32px;
+ border-bottom: 1px solid #eee;
+
+ .modal-title {
+ font-size: 36px;
+ font-weight: 600;
+ }
+
+ .modal-close {
+ width: 64px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36px;
+ color: #999;
+ }
+ }
+
+ .modal-body {
+ flex: 1;
+ padding: 32px;
+ max-height: calc(80vh - 200px);
+ }
+
+ .modal-footer {
+ padding: 32px;
+ padding-bottom: calc(32px + env(safe-area-inset-bottom));
+ border-top: 1px solid #eee;
+
+ .footer-btn {
+ width: 100%;
+ height: 88px;
+ line-height: 88px;
+ background: #007aff;
+ color: #fff;
+ font-size: 32px;
+ border-radius: 44px;
+
+ &::after { border: none; }
+ }
+ }
+}
+
+// 错误详情
+.error-item {
+ background: #fff5f5;
+ padding: 24px;
+ border-radius: 16px;
+ margin-bottom: 16px;
+
+ .error-row {
+ display: flex;
+ margin-bottom: 12px;
+
+ &:last-child { margin-bottom: 0; }
+
+ .error-label {
+ width: 100px;
+ font-size: 26px;
+ color: #999;
+ flex-shrink: 0;
+ }
+
+ .error-value {
+ flex: 1;
+ font-size: 26px;
+ color: #333;
+ }
+
+ &.suggestion {
+ background: #f0fff0;
+ padding: 12px;
+ border-radius: 8px;
+ margin-top: 8px;
+ }
+ }
+
+ .error-val {
+ color: #ff3b30;
+ }
+
+ .error-msg {
+ color: #ff3b30;
+ font-weight: 600;
+ }
+}
+
+// 模板详情
+.template-desc-full {
+ font-size: 28px;
+ color: #666;
+ line-height: 1.6;
+ margin-bottom: 32px;
+ display: block;
+}
+
+.fields-section {
+ .section-title {
+ font-size: 30px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 24px;
+ display: block;
+ }
+
+ .field-detail {
+ background: #f9f9f9;
+ padding: 24px;
+ border-radius: 16px;
+ margin-bottom: 16px;
+
+ .field-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+
+ .field-name {
+ font-size: 28px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .required-tag {
+ margin-left: 12px;
+ padding: 4px 12px;
+ background: #ff3b30;
+ color: #fff;
+ font-size: 20px;
+ border-radius: 8px;
+ }
+ }
+
+ .field-info {
+ display: flex;
+ margin-bottom: 8px;
+
+ Text {
+ font-size: 24px;
+ color: #666;
+ margin-right: 24px;
+ }
+ }
+
+ .field-options, .field-length, .field-example {
+ font-size: 24px;
+ color: #007aff;
+ display: block;
+ margin-top: 8px;
+ }
+ }
+}
diff --git a/src/enterprise/settings/import.tsx b/src/enterprise/settings/import.tsx
new file mode 100644
index 0000000..5844e3a
--- /dev/null
+++ b/src/enterprise/settings/import.tsx
@@ -0,0 +1,340 @@
+/**
+ * 数据导入页面
+ */
+import { useState, useEffect } from 'react';
+import { View, Text, Image, ScrollView, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { pageImportTask, listImportTemplate, getImportErrors, createImportTask, cancelImportTask, downloadTemplate } from '../../api/import';
+import type { ImportTask, ImportTemplate, ImportError } from '../../types/import';
+import './import.scss';
+
+const STATUS_MAP: Record = {
+ pending: { label: '等待中', color: '#999', icon: '⏳' },
+ uploading: { label: '上传中', color: '#007aff', icon: '⬆️' },
+ parsing: { label: '解析中', color: '#5856d6', icon: '📄' },
+ validating: { label: '校验中', color: '#ff9500', icon: '✓' },
+ importing: { label: '导入中', color: '#007aff', icon: '📥' },
+ completed: { label: '已完成', color: '#34c759', icon: '✅' },
+ failed: { label: '失败', color: '#ff3b30', icon: '❌' },
+ cancelled: { label: '已取消', color: '#999', icon: '🚫' }
+};
+
+const TYPE_MAP: Record = {
+ user: { label: '用户数据', icon: '👤' },
+ product: { label: '商品数据', icon: '📦' },
+ order: { label: '订单数据', icon: '📋' },
+ category: { label: '分类数据', icon: '📂' },
+ banner: { label: 'Banner', icon: '🎨' },
+ config: { label: '配置数据', icon: '⚙️' },
+ custom: { label: '自定义', icon: '📝' }
+};
+
+export default function DataImport() {
+ const [tasks, setTasks] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [templates, setTemplates] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState('task');
+ const [showErrors, setShowErrors] = useState([]);
+ const [showTemplate, setShowTemplate] = useState(null);
+
+ useEffect(() => {
+ loadData();
+ }, []);
+
+ const loadData = async () => {
+ setLoading(true);
+ try {
+ const [taskData, templateData] = await Promise.all([
+ pageImportTask({ page: 1, pageSize: 20 }),
+ listImportTemplate()
+ ]);
+ setTasks(taskData.list);
+ setTotal(taskData.total);
+ setTemplates(templateData);
+ } catch (e) {
+ console.error('加载失败', e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 查看错误详情
+ const handleViewErrors = async (taskId: string) => {
+ try {
+ const errors = await getImportErrors(taskId);
+ setShowErrors(errors);
+ } catch (e) {
+ Taro.showToast({ title: '加载失败', icon: 'none' });
+ }
+ };
+
+ // 取消任务
+ const handleCancel = async (id: string) => {
+ Taro.showModal({
+ title: '确认取消',
+ content: '确定要取消这个导入任务吗?',
+ success: async (res) => {
+ if (res.confirm) {
+ await cancelImportTask(id);
+ Taro.showToast({ title: '已取消', icon: 'success' });
+ loadData();
+ }
+ }
+ });
+ };
+
+ // 下载模板
+ const handleDownloadTemplate = (template: ImportTemplate) => {
+ downloadTemplate(template.type);
+ };
+
+ // 进度百分比
+ const getProgressWidth = (task: ImportTask) => {
+ if (task.status === 'completed') return '100%';
+ if (task.status === 'failed' || task.status === 'cancelled') return '0%';
+ return `${task.progress}%`;
+ };
+
+ return (
+
+ {/* Tab 切换 */}
+
+ setActiveTab('task')}
+ >
+ 导入记录
+
+ setActiveTab('template')}
+ >
+ 下载模板
+
+
+
+ {/* 导入记录 */}
+ {activeTab === 'task' && (
+
+ {loading ? (
+ 加载中...
+ ) : tasks.length === 0 ? (
+
+ 暂无导入记录
+
+ ) : (
+ tasks.map(task => (
+
+
+
+ {TYPE_MAP[task.type]?.icon}
+
+ {task.name}
+ {task.fileName} ({task.fileSize})
+
+
+
+ {STATUS_MAP[task.status]?.icon} {STATUS_MAP[task.status]?.label}
+
+
+
+ {/* 进度条 */}
+ {['uploading', 'parsing', 'validating', 'importing'].includes(task.status) && (
+
+
+
+
+ {task.progress}%
+
+ )}
+
+ {/* 统计 */}
+
+
+ {task.total}
+ 总数
+
+
+ {task.success}
+ 成功
+
+
+ {task.failed}
+ 失败
+
+
+
+ {/* 操作 */}
+
+
+ {task.startTime ? `开始: ${task.startTime.slice(0, 16)}` : `创建: ${task.createTime.slice(0, 16)}`}
+
+
+ {task.errorFile && (
+
+ )}
+ {task.failed > 0 && (
+
+ )}
+ {['pending', 'uploading', 'parsing', 'validating', 'importing'].includes(task.status) && (
+
+ )}
+
+
+
+ ))
+ )}
+
+ )}
+
+ {/* 模板下载 */}
+ {activeTab === 'template' && (
+
+ {templates.map(template => (
+
+
+ {TYPE_MAP[template.type]?.icon}
+
+ {template.name}
+ {template.description}
+
+
+
+
+ 字段说明
+
+ {template.fields.slice(0, 5).map(field => (
+
+
+ {field.name}
+ {field.required && *}
+
+ {field.type}
+ {field.example && (
+ 例: {field.example}
+ )}
+
+ ))}
+ {template.fields.length > 5 && (
+ 更多字段...
+ )}
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* 错误详情弹窗 */}
+ {showErrors.length > 0 && (
+ setShowErrors([])}>
+ e.stopPropagation()}>
+
+ 错误详情
+ setShowErrors([])}>✕
+
+
+ {showErrors.map((error, index) => (
+
+
+ 行号
+ {error.row}
+
+
+ 字段
+ {error.field}
+
+
+ 值
+ {error.value || '(空)'}
+
+
+ 错误
+ {error.error}
+
+ {error.suggestion && (
+
+ 建议
+ {error.suggestion}
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {/* 模板详情弹窗 */}
+ {showTemplate && (
+ setShowTemplate(null)}>
+ e.stopPropagation()}>
+
+ {showTemplate.name}
+ setShowTemplate(null)}>✕
+
+
+ {showTemplate.description}
+
+
+ 字段定义
+ {showTemplate.fields.map(field => (
+
+
+ {field.name}
+ {field.required && 必填}
+
+
+ 字段: {field.key}
+ 类型: {field.type}
+
+ {field.options && (
+ 选项: {field.options.join(', ')}
+ )}
+ {field.maxLength && (
+ 最大长度: {field.maxLength}
+ )}
+ {field.example && (
+ 示例: {field.example}
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/enterprise/settings/index.config.ts b/src/enterprise/settings/index.config.ts
new file mode 100644
index 0000000..d88b59f
--- /dev/null
+++ b/src/enterprise/settings/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '企业设置',
+})
diff --git a/src/enterprise/settings/index.scss b/src/enterprise/settings/index.scss
new file mode 100644
index 0000000..7762ac5
--- /dev/null
+++ b/src/enterprise/settings/index.scss
@@ -0,0 +1,28 @@
+page {
+ background: #f5f6f7;
+}
+
+.enterprise-settings-page {
+ min-height: 100vh;
+ background: #f5f6f7;
+ padding: 24rpx;
+
+ &__header {
+ margin-bottom: 24rpx;
+ }
+
+ &__title {
+ font-size: 36rpx;
+ font-weight: 800;
+ color: #111111;
+ }
+
+ &__content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 48rpx;
+ text-align: center;
+ color: #999999;
+ font-size: 28rpx;
+ }
+}
diff --git a/src/enterprise/settings/index.tsx b/src/enterprise/settings/index.tsx
new file mode 100644
index 0000000..177e9bc
--- /dev/null
+++ b/src/enterprise/settings/index.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './index.scss'
+
+const EnterpriseSettings: React.FC = () => {
+ return (
+
+
+ ⚙️ 企业设置
+
+
+ 企业设置功能开发中...
+
+
+ )
+}
+
+export default EnterpriseSettings
diff --git a/src/enterprise/settings/info.config.ts b/src/enterprise/settings/info.config.ts
new file mode 100644
index 0000000..23d7af0
--- /dev/null
+++ b/src/enterprise/settings/info.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '企业信息',
+})
diff --git a/src/enterprise/settings/info.scss b/src/enterprise/settings/info.scss
new file mode 100644
index 0000000..d053591
--- /dev/null
+++ b/src/enterprise/settings/info.scss
@@ -0,0 +1,10 @@
+.info-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/settings/info.tsx b/src/enterprise/settings/info.tsx
new file mode 100644
index 0000000..b46a931
--- /dev/null
+++ b/src/enterprise/settings/info.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './info.scss'
+
+const InfoPage: React.FC = () => {
+ return (
+
+
+ 企业信息功能开发中...
+
+
+ )
+}
+
+export default InfoPage
diff --git a/src/enterprise/settings/security.config.ts b/src/enterprise/settings/security.config.ts
new file mode 100644
index 0000000..9bb0be7
--- /dev/null
+++ b/src/enterprise/settings/security.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '安全设置',
+})
diff --git a/src/enterprise/settings/security.scss b/src/enterprise/settings/security.scss
new file mode 100644
index 0000000..042af0d
--- /dev/null
+++ b/src/enterprise/settings/security.scss
@@ -0,0 +1,10 @@
+.security-page {
+ padding: 32rpx;
+
+ &__content {
+ text-align: center;
+ padding: 120rpx 0;
+ font-size: 28rpx;
+ color: #999;
+ }
+}
diff --git a/src/enterprise/settings/security.tsx b/src/enterprise/settings/security.tsx
new file mode 100644
index 0000000..44c6d4a
--- /dev/null
+++ b/src/enterprise/settings/security.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import './security.scss'
+
+const SecurityPage: React.FC = () => {
+ return (
+
+
+ 安全设置功能开发中...
+
+
+ )
+}
+
+export default SecurityPage
diff --git a/src/enterprise/settings/sso.config.ts b/src/enterprise/settings/sso.config.ts
new file mode 100644
index 0000000..9b0b382
--- /dev/null
+++ b/src/enterprise/settings/sso.config.ts
@@ -0,0 +1,6 @@
+/**
+ * SSO 单点登录配置页面配置
+ */
+export default definePageConfig({
+ navigationBarTitleText: 'SSO 单点登录',
+})
diff --git a/src/enterprise/settings/sso.scss b/src/enterprise/settings/sso.scss
new file mode 100644
index 0000000..528ac08
--- /dev/null
+++ b/src/enterprise/settings/sso.scss
@@ -0,0 +1,379 @@
+.sso-page {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 120px;
+
+ &.loading-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 300px;
+
+ .loading-text {
+ margin-top: 16px;
+ font-size: 28px;
+ color: #999;
+ }
+ }
+}
+
+.sso-content {
+ height: calc(100vh - 120px);
+}
+
+// 状态卡片
+.status-card {
+ background: #fff;
+ padding: 24px;
+ margin: 24px;
+ border-radius: 16px;
+
+ .status-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+
+ .status-info {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ .status-icon {
+ font-size: 48px;
+ color: #1890ff;
+ }
+
+ .status-text {
+ .status-label {
+ display: block;
+ font-size: 32px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 8px;
+ }
+ }
+ }
+ }
+
+ .sync-info {
+ display: flex;
+ justify-content: space-between;
+ padding: 12px 0;
+ border-top: 1px solid #eee;
+
+ .label {
+ font-size: 26px;
+ color: #999;
+ }
+
+ .value {
+ font-size: 26px;
+ color: #666;
+ }
+ }
+
+ .error-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 16px;
+ padding: 12px;
+ background: #FFF2F0;
+ border-radius: 8px;
+ font-size: 26px;
+ color: #F44336;
+
+ .iconfont {
+ font-size: 28px;
+ }
+ }
+}
+
+// Tab 切换
+.tabs {
+ display: flex;
+ margin: 0 24px;
+ background: #fff;
+ border-radius: 12px;
+ overflow: hidden;
+
+ .tab {
+ flex: 1;
+ text-align: center;
+ padding: 20px;
+ font-size: 28px;
+ color: #666;
+
+ &.active {
+ color: #1890ff;
+ font-weight: 500;
+ background: rgba(24, 144, 255, 0.1);
+ }
+ }
+}
+
+// 配置内容
+.config-content {
+ padding: 24px;
+}
+
+.config-section {
+ background: #fff;
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+
+ .section-title {
+ display: block;
+ font-size: 30px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 20px;
+ }
+
+ .section-hint {
+ display: block;
+ font-size: 24px;
+ color: #999;
+ margin-bottom: 16px;
+ }
+}
+
+// 提供商选择
+.provider-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+
+ .provider-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 24px;
+ background: #f5f5f5;
+ border-radius: 12px;
+ border: 2px solid transparent;
+
+ &.active {
+ background: rgba(24, 144, 255, 0.1);
+ border-color: #1890ff;
+ }
+
+ .provider-icon {
+ font-size: 40px;
+ color: #666;
+ margin-bottom: 8px;
+ }
+
+ .provider-name {
+ font-size: 24px;
+ color: #333;
+ }
+ }
+}
+
+// 表单项
+.form-item {
+ margin-bottom: 20px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .form-label {
+ display: block;
+ font-size: 26px;
+ color: #666;
+ margin-bottom: 8px;
+ }
+
+ .form-input {
+ width: 100%;
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ font-size: 28px;
+ }
+}
+
+// 开关项
+.switch-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ margin-bottom: 20px;
+ }
+
+ .switch-info {
+ .switch-label {
+ display: block;
+ font-size: 28px;
+ color: #333;
+ margin-bottom: 4px;
+ }
+
+ .switch-desc {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+}
+
+// 服务提供商信息
+.sp-info {
+ .sp-item {
+ padding: 16px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ margin-bottom: 12px;
+
+ .sp-label {
+ display: block;
+ font-size: 24px;
+ color: #999;
+ margin-bottom: 8px;
+ }
+
+ .sp-value {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .value-text {
+ flex: 1;
+ font-size: 26px;
+ color: #333;
+ font-family: monospace;
+ }
+
+ .copy-btn {
+ font-size: 24px;
+ color: #1890ff;
+ margin-left: 16px;
+ }
+ }
+ }
+}
+
+// 日志内容
+.logs-content {
+ padding: 24px;
+
+ .log-item {
+ background: #fff;
+ border-radius: 12px;
+ padding: 16px;
+ margin-bottom: 16px;
+
+ .log-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+
+ .log-time {
+ font-size: 24px;
+ color: #999;
+ }
+ }
+
+ .log-info {
+ margin-bottom: 8px;
+
+ .log-user {
+ font-size: 28px;
+ font-weight: 500;
+ color: #333;
+ margin-right: 12px;
+ }
+
+ .log-msg {
+ font-size: 26px;
+ color: #666;
+ }
+ }
+
+ .log-ip {
+ font-size: 22px;
+ color: #999;
+ }
+ }
+}
+
+.empty-logs {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 100px 0;
+ color: #999;
+
+ .iconfont {
+ font-size: 96px;
+ margin-bottom: 24px;
+ }
+
+ text {
+ font-size: 28px;
+ }
+}
+
+// 操作按钮
+.action-bar {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ gap: 16px;
+ padding: 24px;
+ background: #fff;
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
+
+ .at-button {
+ flex: 1;
+ }
+}
+
+// 测试结果弹窗
+.test-result {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 24px;
+
+ .result-icon {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 16px;
+
+ .iconfont {
+ font-size: 40px;
+ color: #fff;
+ }
+
+ &.success {
+ background: #4CAF50;
+ }
+
+ &.error {
+ background: #F44336;
+ }
+ }
+
+ .result-message {
+ font-size: 28px;
+ color: #333;
+ text-align: center;
+ }
+}
diff --git a/src/enterprise/settings/sso.tsx b/src/enterprise/settings/sso.tsx
new file mode 100644
index 0000000..a2ec2dc
--- /dev/null
+++ b/src/enterprise/settings/sso.tsx
@@ -0,0 +1,173 @@
+/**
+ * SSO 单点登录配置页面
+ */
+import { useState, useEffect } from 'react'
+import { View, Text, ScrollView } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { Button, Switch, Tag, Loading, Dialog, Input } from '@nutui/nutui-react-taro'
+import {
+ getSSOConfig,
+ createSSOConfig,
+ updateSSOConfig,
+ toggleSSO,
+ testSSOConnection,
+ pageSSOLogs,
+} from '@/api/sso'
+import type { SSOConfig, SSOProvider, SSOLog } from '@/types/sso'
+import './sso.scss'
+
+// 提供商映射
+const PROVIDER_MAP: Record = {
+ oidc: { text: 'OIDC', icon: '🔐' },
+ saml: { text: 'SAML', icon: '🔑' },
+ cas: { text: 'CAS', icon: '🎫' },
+ ldap: { text: 'LDAP', icon: '👥' },
+ oauth2: { text: 'OAuth2', icon: '🔓' }
+}
+
+const STATUS_CONFIG = {
+ disconnected: { text: '未连接', color: '#999' },
+ connected: { text: '已连接', color: '#34c759' },
+ error: { text: '连接错误', color: '#ff3b30' }
+}
+
+const SSOConfigPage: React.FC = () => {
+ const [loading, setLoading] = useState(true)
+ const [config, setConfig] = useState(null)
+ const [logs, setLogs] = useState([])
+ const [showModal, setShowModal] = useState(false)
+
+ useEffect(() => {
+ loadData()
+ }, [])
+
+ const loadData = async () => {
+ setLoading(true)
+ try {
+ const [configData, logData] = await Promise.all([
+ getSSOConfig(),
+ pageSSOLogs({ page: 1, pageSize: 10 })
+ ])
+ setConfig(configData)
+ setLogs(logData.list)
+ } catch (e) {
+ console.error('加载失败', e)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleToggle = async (enabled: boolean) => {
+ try {
+ await toggleSSO(enabled)
+ Taro.showToast({ title: enabled ? '已启用' : '已禁用', icon: 'success' })
+ loadData()
+ } catch (e) {
+ Taro.showToast({ title: '操作失败', icon: 'none' })
+ }
+ }
+
+ const handleTest = async () => {
+ Taro.showLoading({ title: '测试中...' })
+ try {
+ const result = await testSSOConnection()
+ Taro.hideLoading()
+ if (result.success) {
+ Taro.showToast({ title: '连接成功', icon: 'success' })
+ } else {
+ Dialog.alert({
+ title: '连接失败',
+ content: result.error || '请检查配置是否正确'
+ })
+ }
+ } catch (e) {
+ Taro.hideLoading()
+ Taro.showToast({ title: '测试失败', icon: 'none' })
+ }
+ }
+
+ if (loading) {
+ return 加载中...
+ }
+
+ return (
+
+
+ {/* SSO 状态卡片 */}
+
+
+ SSO 单点登录
+ handleToggle(val)} />
+
+
+
+ 状态
+
+ {STATUS_CONFIG[config?.connectionStatus || 'disconnected']?.text}
+
+
+
+ 提供商
+
+ {PROVIDER_MAP[config?.provider || 'oidc']?.icon} {PROVIDER_MAP[config?.provider || 'oidc']?.text}
+
+
+
+
+
+ {/* 配置信息 */}
+ {config && (
+
+ 配置信息
+
+ Issuer
+ {config.issuer || '-'}
+
+
+ Client ID
+ {config.clientId || '-'}
+
+
+ Entity ID
+ {config.entityId || '-'}
+
+
+ ACS URL
+ {config.acsUrl || '-'}
+
+
+
+
+
+
+ )}
+
+ {/* 连接日志 */}
+
+ 最近日志
+ {logs.length === 0 ? (
+ 暂无日志记录
+ ) : (
+ logs.map(log => (
+
+
+
+ {log.action === 'login' ? '登录' : log.action === 'logout' ? '登出' : '错误'}
+
+ {log.createTime.slice(0, 16)}
+
+ {log.message}
+
+ ))
+ )}
+
+
+
+ )
+}
+
+export default SSOConfigPage
diff --git a/src/pages/index/index.scss b/src/pages/index/index.scss
index 1480b3d..f59ed9d 100644
--- a/src/pages/index/index.scss
+++ b/src/pages/index/index.scss
@@ -484,3 +484,102 @@ page {
grid-template-columns: 1fr 1fr;
gap: 12rpx;
}
+
+/* 角色入口卡片 */
+.roleEntry {
+ margin-top: 22rpx;
+ padding: 20rpx 18rpx;
+ border-radius: 24rpx;
+ background: #ffffff;
+ box-shadow: 0 16rpx 32rpx rgba(0, 0, 0, 0.06);
+}
+
+.roleEntry__title {
+ margin-bottom: 14rpx;
+}
+
+.roleEntry__titleText {
+ font-size: 34rpx;
+ font-weight: 900;
+ color: #111111;
+}
+
+.roleEntry__grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 14rpx;
+}
+
+.roleEntry__card {
+ display: flex;
+ align-items: center;
+ padding: 16rpx 14rpx;
+ border-radius: 16rpx;
+ gap: 12rpx;
+}
+
+.roleEntry__card--developer {
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%);
+ border: 2rpx solid rgba(59, 130, 246, 0.15);
+}
+
+.roleEntry__card--enterprise {
+ background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(249, 115, 22, 0.08) 100%);
+ border: 2rpx solid rgba(245, 158, 11, 0.15);
+}
+
+.roleEntry__icon {
+ width: 48rpx;
+ height: 48rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12rpx;
+}
+
+.roleEntry__card--developer .roleEntry__icon {
+ background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
+}
+
+.roleEntry__card--enterprise .roleEntry__icon {
+ background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);
+}
+
+.roleEntry__iconText {
+ font-size: 24rpx;
+}
+
+.roleEntry__info {
+ flex: 1;
+ min-width: 0;
+}
+
+.roleEntry__cardTitle {
+ display: block;
+ font-size: 28rpx;
+ font-weight: 800;
+ color: #111111;
+ line-height: 1.2;
+}
+
+.roleEntry__cardDesc {
+ display: block;
+ margin-top: 4rpx;
+ font-size: 22rpx;
+ color: #666666;
+ line-height: 1.2;
+}
+
+.roleEntry__arrow {
+ width: 32rpx;
+ height: 32rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.roleEntry__arrowText {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #999999;
+}
diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx
index 79cf83f..698b370 100644
--- a/src/pages/index/index.tsx
+++ b/src/pages/index/index.tsx
@@ -103,6 +103,14 @@ function Home() {
Taro.makePhoneCall({ phoneNumber: tel })
}
+ const goToDeveloperCenter = () => {
+ Taro.navigateTo({ url: '/developer/index' })
+ }
+
+ const goToEnterpriseConsole = () => {
+ Taro.navigateTo({ url: '/enterprise/index' })
+ }
+
const products = useMemo(
() => [
{
@@ -136,16 +144,18 @@ function Home() {
{
title: '开发者中心',
recommend: false,
- desc: '应用开发与交付入口:应用中心、源码仓库、Git 账号绑定、权限申请与教程文档。',
- tags: ['应用', '源码', 'Git', '教程'],
+ desc: '应用开发与交付入口:项目、应用、API Key、部署与教程文档。',
+ tags: ['项目', '应用', 'API Key', '部署'],
adminUrl: '/developer',
+ onClick: goToDeveloperCenter,
},
{
- title: '模板/插件市场',
+ title: '企业控制台',
recommend: false,
- desc: '支持模板与插件购买、授权与更新,形成生态与增值体系。',
- tags: ['市场', '授权', '更新', '变现'],
- adminUrl: '/market',
+ desc: '企业应用管理、成员权限、订单账单与运营监控。',
+ tags: ['应用', '成员', '账单', '监控'],
+ adminUrl: '/enterprise',
+ onClick: goToEnterpriseConsole,
},
],
[]
@@ -237,6 +247,39 @@ function Home() {
))}
+ {/* 角色入口卡片 */}
+
+
+ 快速入口
+
+
+
+
+ 🛠️
+
+
+ 开发者中心
+ 项目 · 应用 · API Key
+
+
+ ›
+
+
+
+
+ 🏢
+
+
+ 企业控制台
+ 应用 · 成员 · 账单
+
+
+ ›
+
+
+
+
+
{/* 产品矩阵 */}
@@ -266,9 +309,15 @@ function Home() {
))}
-
+ {p.onClick ? (
+
+ ) : (
+
+ )}
))}
diff --git a/src/types/analytics.ts b/src/types/analytics.ts
new file mode 100644
index 0000000..aab44cb
--- /dev/null
+++ b/src/types/analytics.ts
@@ -0,0 +1,115 @@
+/**
+ * 数据分析/运营监控相关类型定义
+ */
+
+// ==================== 统计数据 ====================
+
+/** 概览统计数据 */
+export interface OverviewStats {
+ todayUv?: number // 今日 UV
+ todayPv?: number // 今日 PV
+ todayApiCalls?: number // 今日 API 调用
+ todayErrors?: number // 今日错误数
+ activeUsers?: number // 活跃用户
+ newUsers?: number // 新增用户
+ retention?: number // 留存率
+ avgDuration?: number // 平均时长
+}
+
+/** API 调用统计 */
+export interface ApiCallStat {
+ time?: string
+ count?: number
+ successCount?: number
+ failCount?: number
+ avgLatency?: number
+}
+
+/** 错误统计 */
+export interface ErrorStat {
+ id?: number
+ type?: string
+ message?: string
+ count?: number
+ lastTime?: string
+ level?: 'warning' | 'error' | 'critical'
+}
+
+/** 性能指标 */
+export interface PerformanceMetric {
+ metric?: string
+ value?: number
+ unit?: string
+ trend?: 'up' | 'down' | 'stable'
+ change?: number
+}
+
+/** 区域分布 */
+export interface RegionStat {
+ region?: string
+ count?: number
+ percentage?: number
+}
+
+// ==================== 设备统计 ====================
+
+/** 设备类型统计 */
+export interface DeviceStat {
+ device?: string
+ count?: number
+ percentage?: number
+}
+
+/** 浏览器统计 */
+export interface BrowserStat {
+ browser?: string
+ count?: number
+ percentage?: number
+}
+
+/** 操作系统统计 */
+export interface OsStat {
+ os?: string
+ count?: number
+ percentage?: number
+}
+
+// ==================== 来源统计 ====================
+
+/** 流量来源 */
+export interface SourceStat {
+ source?: string
+ count?: number
+ percentage?: number
+}
+
+/** 页面访问 */
+export interface PageStat {
+ path?: string
+ title?: string
+ pv?: number
+ uv?: number
+ avgDuration?: number
+ bounceRate?: number
+}
+
+/** 事件追踪 */
+export interface EventTrack {
+ id?: number
+ name?: string
+ key?: string
+ count?: number
+ users?: number
+}
+
+// ==================== 搜索参数 ====================
+
+/** 统计搜索参数 */
+export interface AnalyticsParam {
+ page?: number
+ limit?: number
+ websiteId?: number
+ startDate?: string
+ endDate?: string
+ granularity?: 'hour' | 'day' | 'week' | 'month'
+}
diff --git a/src/types/cicd.ts b/src/types/cicd.ts
new file mode 100644
index 0000000..fe3fd7d
--- /dev/null
+++ b/src/types/cicd.ts
@@ -0,0 +1,145 @@
+/**
+ * CI/CD 流水线相关类型定义
+ */
+
+// ==================== 构建相关 ====================
+
+/** 构建状态 */
+export type BuildStatus = 'pending' | 'running' | 'success' | 'failed' | 'cancelled'
+
+/** 构建触发方式 */
+export type BuildTrigger = 'manual' | 'push' | 'schedule' | 'api'
+
+/** 构建任务 */
+export interface Build {
+ id?: number
+ websiteId?: number
+ buildNo?: string
+ branch?: string
+ commitId?: string
+ commitMessage?: string
+ author?: string
+ trigger?: BuildTrigger
+ status?: BuildStatus
+ duration?: number
+ logs?: string
+ artifacts?: BuildArtifact[]
+ startTime?: string
+ endTime?: string
+ createTime?: string
+}
+
+/** 构建产物 */
+export interface BuildArtifact {
+ name?: string
+ path?: string
+ size?: number
+ downloadUrl?: string
+}
+
+/** 构建搜索参数 */
+export interface BuildParam {
+ page?: number
+ limit?: number
+ websiteId?: number
+ branch?: string
+ status?: BuildStatus
+ trigger?: BuildTrigger
+ timeStart?: string
+ timeEnd?: string
+}
+
+// ==================== 部署相关 ====================
+
+/** 部署状态 */
+export type DeployStatus = 'pending' | 'deploying' | 'success' | 'failed' | 'rollback'
+
+/** 部署环境 */
+export type DeployEnv = 'development' | 'staging' | 'production'
+
+/** 部署记录 */
+export interface Deploy {
+ id?: number
+ websiteId?: number
+ buildId?: number
+ buildNo?: string
+ version?: string
+ env?: DeployEnv
+ status?: DeployStatus
+ previousVersion?: string
+ rollbackFrom?: string
+ deployer?: string
+ duration?: number
+ startTime?: string
+ endTime?: string
+ createTime?: string
+}
+
+/** 部署搜索参数 */
+export interface DeployParam {
+ page?: number
+ limit?: number
+ websiteId?: number
+ env?: DeployEnv
+ status?: DeployStatus
+ timeStart?: string
+ timeEnd?: string
+}
+
+// ==================== 流水线配置相关 ====================
+
+/** 流水线配置 */
+export interface PipelineConfig {
+ id?: number
+ websiteId?: number
+ name?: string
+ enabled?: boolean
+ autoDeploy?: boolean
+ branchRules?: BranchRule[]
+ environmentVars?: EnvVar[]
+ webhooks?: Webhook[]
+ createTime?: string
+ updateTime?: string
+}
+
+/** 分支规则 */
+export interface BranchRule {
+ branch?: string
+ autoBuild?: boolean
+ autoDeploy?: boolean
+ deployEnv?: DeployEnv
+ requireApproval?: boolean
+}
+
+/** 环境变量 */
+export interface EnvVar {
+ key?: string
+ value?: string
+ isSecret?: boolean
+}
+
+/** Webhook 配置 */
+export interface Webhook {
+ id?: number
+ url?: string
+ secret?: string
+ events?: string[]
+ enabled?: boolean
+}
+
+// ==================== 运行时相关 ====================
+
+/** 运行时实例 */
+export interface RuntimeInstance {
+ id?: number
+ websiteId?: number
+ env?: DeployEnv
+ status?: 'running' | 'stopped' | 'restarting' | 'error'
+ version?: string
+ instances?: number
+ cpu?: number
+ memory?: number
+ region?: string
+ startTime?: string
+ createTime?: string
+}
diff --git a/src/types/developer.ts b/src/types/developer.ts
new file mode 100644
index 0000000..e4f5580
--- /dev/null
+++ b/src/types/developer.ts
@@ -0,0 +1,86 @@
+/**
+ * 开发者相关类型定义
+ */
+
+// 开发者角色
+export type DeveloperRole = 'owner' | 'admin' | 'developer' | 'viewer';
+
+// 开发者申请状态
+export type ApplyStatus = 'pending' | 'approved' | 'rejected';
+
+// 申请类型
+export type ApplyType = 'developer' | 'permission' | 'resource';
+
+// 开发者信息
+export interface Developer {
+ id: string;
+ userId: string;
+ userName: string;
+ avatar?: string;
+ email?: string;
+ phone?: string;
+ company?: string;
+ position?: string;
+ experience?: string;
+ role: DeveloperRole;
+ status: ApplyStatus;
+ createTime: string;
+ updateTime?: string;
+}
+
+// 开发者申请
+export interface DeveloperApply {
+ id: string;
+ userId: string;
+ userName: string;
+ avatar?: string;
+ realName: string;
+ phone: string;
+ email: string;
+ company: string;
+ position: string;
+ experience: string;
+ status: ApplyStatus;
+ remark?: string;
+ reviewerId?: string;
+ reviewerName?: string;
+ reviewTime?: string;
+ createTime: string;
+}
+
+// 权限申请
+export interface PermissionApply {
+ id: string;
+ applicantId: string;
+ applicantName: string;
+ applicantAvatar?: string;
+ type: ApplyType;
+ targetId?: string;
+ targetName?: string;
+ reason?: string;
+ status: ApplyStatus;
+ reviewerId?: string;
+ reviewerName?: string;
+ reviewRemark?: string;
+ reviewTime?: string;
+ createTime: string;
+}
+
+// 项目开发者
+export interface ProjectDeveloper {
+ id: string;
+ projectId: string;
+ userId: string;
+ userName: string;
+ avatar?: string;
+ role: DeveloperRole;
+ createTime: string;
+}
+
+// 开发者统计
+export interface DeveloperStats {
+ total: number;
+ active: number;
+ pending: number;
+ approved: number;
+}
diff --git a/src/types/import.ts b/src/types/import.ts
new file mode 100644
index 0000000..41b29c8
--- /dev/null
+++ b/src/types/import.ts
@@ -0,0 +1,89 @@
+/**
+ * 数据导入类型定义
+ */
+
+export interface ImportTask {
+ id: string;
+ name: string; // 任务名称
+ type: ImportType; // 导入类型
+ status: ImportStatus; // 状态
+ total: number; // 总数
+ success: number; // 成功数
+ failed: number; // 失败数
+ progress: number; // 进度 0-100
+ fileName: string; // 文件名
+ fileSize: string; // 文件大小
+ errorFile?: string; // 错误文件下载地址
+ creatorId: string;
+ creatorName: string;
+ startTime: string;
+ endTime?: string;
+ createTime: string;
+}
+
+export type ImportType = 'user' | 'product' | 'order' | 'category' | 'banner' | 'config' | 'custom';
+
+export type ImportStatus = 'pending' | 'uploading' | 'parsing' | 'validating' | 'importing' | 'completed' | 'failed' | 'cancelled';
+
+export interface ImportTemplate {
+ id: string;
+ name: string; // 模板名称
+ type: ImportType; // 导入类型
+ description: string; // 说明
+ downloadUrl: string; // 模板下载
+ fields: ImportField[]; // 字段定义
+ rules: ImportRule[]; // 导入规则
+ exampleUrl?: string; // 示例下载
+ createTime: string;
+}
+
+export interface ImportField {
+ key: string; // 字段名
+ name: string; // 显示名
+ type: 'string' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select';
+ required: boolean; // 是否必填
+ defaultValue?: string; // 默认值
+ options?: string[]; // 选项(for select)
+ maxLength?: number; // 最大长度
+ pattern?: string; // 正则校验
+ example?: string; // 示例
+}
+
+export interface ImportRule {
+ field: string;
+ rule: 'unique' | 'required' | 'max-length' | 'pattern' | 'range' | 'custom';
+ message: string;
+ params?: any;
+}
+
+export interface ImportError {
+ row: number; // 行号
+ field: string; // 字段
+ value: string; // 值
+ error: string; // 错误信息
+ suggestion?: string; // 建议
+}
+
+export interface ExportTask {
+ id: string;
+ name: string; // 任务名称
+ type: ExportType; // 导出类型
+ status: ExportStatus; // 状态
+ format: ExportFormat; // 格式
+ filters: Record; // 筛选条件
+ total: number; // 总数
+ fileName?: string; // 文件名
+ fileSize?: string; // 文件大小
+ downloadUrl?: string; // 下载地址
+ expiresAt?: string; // 过期时间
+ creatorId: string;
+ creatorName: string;
+ createTime: string;
+ completeTime?: string;
+}
+
+export type ExportType = 'user' | 'order' | 'product' | 'bill' | 'log' | 'custom';
+
+export type ExportStatus = 'pending' | 'processing' | 'completed' | 'failed';
+
+export type ExportFormat = 'xlsx' | 'csv' | 'json' | 'pdf';
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..5c8be1e
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,18 @@
+/**
+ * 类型定义统一导出
+ */
+
+// 开发者模块
+export * from './developer'
+
+// CI/CD 流水线
+export * from './cicd'
+
+// 数据分析
+export * from './analytics'
+
+// 发票管理
+export * from './invoice'
+
+// SSO 单点登录
+export * from './sso'
diff --git a/src/types/invoice.ts b/src/types/invoice.ts
new file mode 100644
index 0000000..1485eec
--- /dev/null
+++ b/src/types/invoice.ts
@@ -0,0 +1,75 @@
+/**
+ * 发票相关类型定义
+ */
+
+/** 发票类型 */
+export type InvoiceType = 'normal' | 'special'
+
+/** 发票状态 */
+export type InvoiceStatus = 'pending' | 'approved' | 'rejected' | 'issued' | 'cancelled'
+
+/** 发票抬头类型 */
+export type InvoiceTitleType = 'personal' | 'enterprise'
+
+/**
+ * 发票抬头
+ */
+export interface InvoiceTitle {
+ id?: number
+ type?: InvoiceTitleType
+ name?: string
+ taxNo?: string
+ bank?: string
+ account?: string
+ phone?: string
+ address?: string
+ email?: string
+ isDefault?: boolean
+ createTime?: string
+}
+
+/**
+ * 发票申请
+ */
+export interface Invoice {
+ id?: number
+ invoiceNo?: string
+ enterpriseId?: number
+ enterpriseName?: string
+ titleId?: number
+ titleName?: string
+ titleType?: InvoiceTitleType
+ type?: InvoiceType
+ amount?: number
+ status?: InvoiceStatus
+ billingNo?: string
+ expressNo?: string
+ remark?: string
+ rejectReason?: string
+ invoiceDate?: string
+ createTime?: string
+ updateTime?: string
+}
+
+/**
+ * 发票申请参数
+ */
+export interface InvoiceApplyParam {
+ titleId: number
+ type: InvoiceType
+ amount: number
+ remark?: string
+}
+
+/**
+ * 发票搜索参数
+ */
+export interface InvoiceParam {
+ page?: number
+ limit?: number
+ enterpriseId?: number
+ status?: InvoiceStatus
+ type?: InvoiceType
+ timeStart?: string
+ timeEnd?: string
+}
diff --git a/src/types/sdk.ts b/src/types/sdk.ts
new file mode 100644
index 0000000..d8dc452
--- /dev/null
+++ b/src/types/sdk.ts
@@ -0,0 +1,51 @@
+/**
+ * SDK 管理类型定义
+ */
+
+export interface SDK {
+ id: string;
+ name: string; // SDK 名称
+ icon: string; // 图标
+ description: string; // 描述
+ version: string; // 最新版本
+ language: 'JavaScript' | 'TypeScript' | 'Python' | 'Java' | 'Go' | 'PHP' | 'Ruby' | '.NET' | 'Swift' | 'Kotlin' | 'Flutter' | 'React Native';
+ category: 'web' | 'mobile' | 'server' | 'mini-program';
+ downloadCount: number; // 下载量
+ stars: number; // Star 数
+ docsUrl: string; // 文档链接
+ npmPackage?: string; // NPM 包名
+ downloadUrl: string; // 下载地址
+ changelog: string; // 更新日志
+ dependencies: string[]; // 依赖
+ supportedVersions: string[]; // 支持的版本
+ lastUpdated: string; // 最后更新时间
+}
+
+export interface SDKVersion {
+ version: string;
+ releaseDate: string;
+ releaseNotes: string;
+ downloadUrl: string;
+ size: string;
+ sha256: string;
+}
+
+export interface SDKDownloadRecord {
+ id: string;
+ sdkId: string;
+ sdkName: string;
+ version: string;
+ language: string;
+ userId: string;
+ userName: string;
+ downloadTime: string;
+ ipAddress: string;
+}
+
+export interface SDKCategory {
+ id: string;
+ name: string;
+ icon: string;
+ count: number;
+ description: string;
+}
diff --git a/src/types/sso.ts b/src/types/sso.ts
new file mode 100644
index 0000000..8f6d7ae
--- /dev/null
+++ b/src/types/sso.ts
@@ -0,0 +1,77 @@
+/**
+ * SSO 单点登录相关类型定义
+ */
+
+/** SSO 提供商类型 */
+export type SSOProvider = 'oidc' | 'saml' | 'cas' | 'ldap' | 'oauth2'
+
+/** SSO 同步方向 */
+export type SyncDirection = 'oneway' | 'twoway'
+
+/** SSO 状态 */
+export type SSOStatus = 'inactive' | 'active' | 'error'
+
+/**
+ * SSO 配置
+ */
+export interface SSOConfig {
+ id?: number
+ enterpriseId?: number
+ provider?: SSOProvider
+ name?: string
+ enabled?: boolean
+ status?: SSOStatus
+ // OIDC 配置
+ issuer?: string
+ clientId?: string
+ clientSecret?: string
+ authorizationUrl?: string
+ tokenUrl?: string
+ userInfoUrl?: string
+ scopes?: string[]
+ // SAML 配置
+ idpEntityId?: string
+ idpSsoUrl?: string
+ idpCertificate?: string
+ spEntityId?: string
+ spAcsUrl?: string
+ // 通用配置
+ syncUsers?: boolean
+ syncDirection?: SyncDirection
+ autoCreateUsers?: boolean
+ defaultRole?: string
+ attributeMapping?: Record
+ logo?: string
+ lastSyncTime?: string
+ errorMessage?: string
+ createTime?: string
+ updateTime?: string
+}
+
+/**
+ * SSO 用户会话
+ */
+export interface SSOSession {
+ id?: number
+ userId?: number
+ username?: string
+ provider?: SSOProvider
+ loginTime?: string
+ ip?: string
+ userAgent?: string
+}
+
+/**
+ * SSO 日志
+ */
+export interface SSOLog {
+ id?: number
+ type?: 'login' | 'logout' | 'error' | 'sync'
+ provider?: SSOProvider
+ userId?: number
+ username?: string
+ ip?: string
+ status?: 'success' | 'failed'
+ message?: string
+ createTime?: string
+}
diff --git a/src/types/ticket.ts b/src/types/ticket.ts
new file mode 100644
index 0000000..eac22b9
--- /dev/null
+++ b/src/types/ticket.ts
@@ -0,0 +1,83 @@
+/**
+ * 工单/技术支持类型定义
+ */
+
+export interface Ticket {
+ id: string;
+ ticketNo: string; // 工单编号
+ title: string; // 标题
+ content: string; // 内容
+ type: TicketType; // 类型
+ priority: TicketPriority; // 优先级
+ status: TicketStatus; // 状态
+ category: TicketCategory; // 分类
+ attachments: string[]; // 附件
+ creatorId: string;
+ creatorName: string;
+ creatorAvatar: string;
+ assigneeId?: string;
+ assigneeName?: string;
+ assigneeAvatar?: string;
+ responseCount: number; // 回复数
+ solution?: string; // 解决方案
+ rating?: number; // 满意度评分
+ feedback?: string; // 反馈
+ resolveTime?: number; // 解决耗时(分钟)
+ createTime: string;
+ updateTime: string;
+ resolveTime2?: string; // 解决时间
+}
+
+export type TicketType = 'technical' | 'billing' | 'account' | 'feature' | 'bug' | 'other';
+
+export type TicketPriority = 'low' | 'medium' | 'high' | 'urgent';
+
+export type TicketStatus = 'pending' | 'processing' | 'waiting' | 'resolved' | 'closed' | 'rejected';
+
+export type TicketCategory = 'api' | 'sdk' | 'payment' | 'account' | 'security' | 'integration' | 'other';
+
+export interface TicketReply {
+ id: string;
+ ticketId: string;
+ content: string;
+ attachments: string[];
+ isInternal: boolean; // 是否为内部回复
+ senderId: string;
+ senderName: string;
+ senderAvatar: string;
+ senderRole: 'user' | 'support' | 'system';
+ createTime: string;
+}
+
+export interface TicketTemplate {
+ id: string;
+ title: string;
+ content: string;
+ type: TicketType;
+ category: TicketCategory;
+ priority: TicketPriority;
+}
+
+export interface TicketStats {
+ total: number;
+ pending: number;
+ processing: number;
+ resolved: number;
+ avgResponseTime: number; // 平均响应时间(分钟)
+ avgResolveTime: number; // 平均解决时间(小时)
+ satisfaction: number; // 满意度
+}
+
+export interface FAQ {
+ id: string;
+ question: string;
+ answer: string;
+ category: string;
+ viewCount: number;
+ helpful: number;
+ notHelpful: number;
+ tags: string[];
+ relatedQuestions: string[];
+ createTime: string;
+ updateTime: string;
+}
diff --git a/src/user/apps/index.tsx b/src/user/apps/index.tsx
index e12bbac..43e6053 100644
--- a/src/user/apps/index.tsx
+++ b/src/user/apps/index.tsx
@@ -83,7 +83,7 @@ const MyAppsPage = () => {
url: '/passport/webview/index?url=' + encodeURIComponent('https://websopy.websoft.top/products')
})}
>
- 去开通应用 >
+ 去开通应用 {'>'}
diff --git a/src/utils/request.ts b/src/utils/request.ts
index 58b5ac9..093413d 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -6,6 +6,7 @@ interface RequestConfig {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
data?: any;
+ params?: Record; // URL 查询参数
header?: Record;
timeout?: number;
retry?: number;