commit 6802624e59edc99d80bece53f9bcd65c49cbf704 Author: sjk <2513533895@qq.com> Date: Fri Dec 19 22:36:48 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e79a3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,158 @@ +# ============================================ +# IDE 和编辑器 +# ============================================ +.DS_Store +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# ============================================ +# Go 后端相关 +# ============================================ +# Go 编译产物 +go_backend/main +go_backend/main.exe +go_backend/*.exe +go_backend/*.dll +go_backend/*.so +go_backend/*.dylib +go_backend/ai_xhs +go_backend/ai_xhs.exe + +# Go 测试和覆盖率 +go_backend/*.test +go_backend/*.out +go_backend/coverage.txt +go_backend/coverage.html + +# Go 依赖 +go_backend/vendor/ + +# ============================================ +# Python 后端相关 +# ============================================ +# Python 虚拟环境 +backend/venv/ +backend/env/ +backend/.venv/ +backend/__pycache__/ +backend/**/__pycache__/ +backend/*.pyc +backend/*.pyo +backend/*.pyd +backend/.Python + +# Python 测试和覆盖率 +backend/.pytest_cache/ +backend/.coverage +backend/htmlcov/ + +# Playwright 浏览器缓存 +backend/.local-chromium/ +backend/.local-firefox/ +backend/.local-webkit/ +backend/ms-playwright/ + +# 敏感配置文件(保留示例文件) +backend/cookies.json +backend/test_cookies.json + +# ============================================ +# 微信小程序相关 +# ============================================ +# 依赖目录 +node_modules/ +miniprogram/miniprogram_npm/ +miniprogram_npm/ + +# 构建产物 +dist/ +build/ + +# 日志文件 +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# 临时文件 +*.tmp +*.temp +.cache/ + +# ============================================ +# 配置和敏感信息 +# ============================================ +# 环境变量文件 + + + +# 数据库备份 +*.sql.bak +*.sql.gz + +# 微信开发者工具相关 +project.config.json.bak +project.private.config.json + +# TypeScript 编译缓存 +*.tsbuildinfo + +# 测试覆盖率 +coverage/ +*.lcov +.nyc_output/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +Desktop.ini +$RECYCLE.BIN/ + +# 编辑器备份文件 +*~ +*.bak +*.orig + +# ============================================ +# 运行时文件和日志 +# ============================================ +# 进程ID文件 +*.pid +go_backend/*.pid +backend/*.pid + +# 运行时数据 +go_backend/data/ +backend/screenshots/ +backend/videos/ + +# ============================================ +# 压缩和打包文件 +# ============================================ +*.zip +*.tar.gz +*.rar +*.7z + +# ============================================ +# 其他项目特定文件 +# ============================================ +# 小红书MCP项目(如果不需要版本控制) +xiaohongshu-mcp-main/cookies/ +xiaohongshu-mcp-main/*.log + +# 临时测试文件 +test_*.html +test_*.json diff --git a/IMAGES_TAGS_FEATURE.md b/IMAGES_TAGS_FEATURE.md new file mode 100644 index 0000000..65db83e --- /dev/null +++ b/IMAGES_TAGS_FEATURE.md @@ -0,0 +1,446 @@ +# 文章图片和标签功能实现文档 + +## 📋 功能概述 + +为发布记录详情页面添加图片和标签展示功能,完整展示文章的所有关联信息。 + +--- + +## 🎯 实现内容 + +### 1. 后端实现 ✅ + +#### 数据库设计 + +**新增表:** +- `ai_article_tags` - 存储文章标签 + - `article_id` - 文章ID(唯一索引) + - `coze_tag` - 标签字符串(逗号分隔) + +**已有表:** +- `ai_article_images` - 存储文章图片 + - `article_id` - 文章ID + - `image_url` - 图片URL + - `image_thumb_url` - 缩略图URL + - `sort_order` - 排序 + - `keywords_name` - 图片关键词 + +#### 模型层(models/models.go) + +```go +// ArticleTag 文章标签表 +type ArticleTag struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + ArticleID int `gorm:"not null;default:0;uniqueIndex:uk_article_tag" json:"article_id"` + CozeTag string `gorm:"type:varchar(500)" json:"coze_tag"` + CreatedAt time.Time `json:"created_at"` +} +``` + +#### 服务层(service/employee_service.go) + +**GetPublishRecordDetail 方法增强:** + +```go +// 查询文章图片 +var articleImages []models.ArticleImage +if err := database.DB.Where("article_id = ?", article.ID).Order("sort_order ASC").Find(&articleImages).Error; err == nil { + for _, img := range articleImages { + images = append(images, map[string]interface{}{ + "id": img.ID, + "image_url": img.ImageURL, + "image_thumb_url": img.ImageThumbURL, + "sort_order": img.SortOrder, + "keywords_name": img.KeywordsName, + }) + } +} + +// 查询文章标签 +var articleTag models.ArticleTag +if err := database.DB.Where("article_id = ?", article.ID).First(&articleTag).Error; err == nil && articleTag.CozeTag != "" { + articleCozeTag = articleTag.CozeTag +} + +// 解析标签 +if articleCozeTag != "" { + for _, tag := range splitTags(articleCozeTag) { + if tag != "" { + tags = append(tags, tag) + } + } +} +``` + +**splitTags 辅助函数:** +- 支持英文逗号 `,` +- 支持中文逗号 `,` +- 支持竖线 `|` +- 自动去除空格 + +#### API 返回数据结构 + +```json +{ + "id": 1, + "article_id": 1, + "product_id": 1, + "topic": "夏日清爽", + "title": "💧夏日必备!2元一杯的柠檬水竟然这么好喝?", + "content": "姐妹们!今天必须给你们安利...", + "images": [ + { + "id": 1, + "image_url": "https://picsum.photos/800/600?random=1", + "image_thumb_url": "https://picsum.photos/300/200?random=1", + "sort_order": 1, + "keywords_name": "饮品,柠檬水" + } + ], + "tags": ["饮品", "柠檬水", "夏日", "清爽", "性价比", "蜜雪冰城"], + "coze_tag": "饮品,柠檬水,夏日,清爽,性价比,蜜雪冰城", + "publish_link": "", + "status": "published_review", + "publish_time": "2024-12-12 17:35:00" +} +``` + +--- + +### 2. 前端实现 ✅ + +#### TypeScript 类型定义(services/employee.ts) + +```typescript +static async getPublishRecordDetail(recordId: number) { + return get<{ + id: number; + article_id: number; + product_id: number; + product_name: string; + topic: string; + title: string; + content: string; + images: Array<{ + id: number; + image_url: string; + image_thumb_url: string; + sort_order: number; + keywords_name: string; + }>; + tags: string[]; + coze_tag: string; + publish_link: string; + status: string; + publish_time: string; + }>(`/api/employee/publish-record/${recordId}`); +} +``` + +#### 页面逻辑(pages/profile/article-detail/article-detail.ts) + +```typescript +data: { + article: { + title: '', + content: '', + topic: '', + publishLink: '', + images: [] as Array<{ + id: number; + image_url: string; + image_thumb_url: string; + sort_order: number; + keywords_name: string; + }>, + tags: [] as string[] + } +} + +// 加载数据 +this.setData({ + article: { + title: response.data.title, + content: response.data.content, + topic: response.data.topic, + publishLink: response.data.publish_link || '', + images: response.data.images || [], // 图片列表 + tags: response.data.tags || [] // 标签列表 + } +}); +``` + +#### WXML 模板(pages/profile/article-detail/article-detail.wxml) + +```xml + + + + + + + + + + + #{{item}} + + +``` + +#### WXSS 样式(pages/profile/article-detail/article-detail.wxss) + +```css +/* 文章图片 - 3列网格布局 */ +.article-images { + margin: 24rpx 0; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12rpx; + box-sizing: border-box; + width: 100%; +} + +.image-item { + width: 100%; + padding-bottom: 100%; /* 1:1 比例 */ + position: relative; + border-radius: 8rpx; + overflow: hidden; + background: #f5f5f5; +} + +.article-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +/* 标签样式 */ +.article-tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + padding-bottom: 20rpx; + margin-bottom: 24rpx; +} + +.tag-item { + padding: 8rpx 20rpx; + background: linear-gradient(135deg, #fff5f7 0%, #ffe8ec 100%); + border-radius: 20rpx; + font-size: 24rpx; + color: #ff2442; + font-weight: 500; +} +``` + +--- + +## 📦 测试数据 + +### 导入测试数据脚本 + +**方式1:批处理脚本** +```bash +cd go_backend +import_test_data.bat +``` + +**方式2:手动执行SQL** +```bash +mysql -u root -p ai_wht < go_backend/migrations/insert_test_images_tags.sql +``` + +### 测试数据内容 + +| 文章ID | 图片数量 | 标签内容 | +|--------|---------|----------| +| 1 | 3张 | 饮品,柠檬水,夏日,清爽,性价比,蜜雪冰城 | +| 2 | 2张 | 冰淇淋,甜品,夏日,平价美食,蜜雪冰城 | +| 3 | 4张 | 口红,彩妆,美妆,色号推荐,黄皮友好,完美日记 | +| 4 | 3张 | 手机,测评,小米,影像旗舰,拍照,数码 | + +**图片来源:** https://picsum.photos(免费随机图片服务) +- 缩略图:300x200 +- 原图:800x600 + +--- + +## 🔄 数据流程 + +``` +用户查看发布记录详情 + ↓ +前端调用 getPublishRecordDetail API + ↓ +后端 GetPublishRecordDetail 方法 + ├─ 查询 ai_article_published_records + ├─ 通过 article_id 关联 ai_articles + ├─ 查询 ai_article_images(按 sort_order 排序) + ├─ 查询 ai_article_tags + └─ 解析标签字符串为数组 + ↓ +返回完整数据(content + images + tags) + ↓ +前端渲染 + ├─ 文章内容 + ├─ 图片网格(3列布局) + └─ 标签列表 +``` + +--- + +## 📝 修改文件清单 + +### 后端文件 + +1. ✅ `models/models.go` + - 新增 ArticleTag 模型 + - 添加 TableName 方法 + +2. ✅ `service/employee_service.go` + - 增强 GetPublishRecordDetail 方法 + - 新增 splitTags 辅助函数 + - 添加 strings 包导入 + +3. ✅ `database/database.go` + - 添加 ArticleImage 和 ArticleTag 到 AutoMigrate + +4. ✅ `migrations/insert_test_images_tags.sql` + - 测试数据导入脚本 + +5. ✅ `migrations/test_article_relations.sql` + - 数据验证查询脚本 + +6. ✅ `import_test_data.bat` + - 快速导入脚本 + +### 前端文件 + +1. ✅ `services/employee.ts` + - 更新 getPublishRecordDetail 返回类型 + - 添加 images 和 tags 字段定义 + +2. ✅ `pages/profile/article-detail/article-detail.ts` + - 更新 data 定义 + - 添加 images 和 tags 字段 + +3. ✅ `pages/profile/article-detail/article-detail.wxml` + - 添加图片网格展示 + - 添加标签列表展示 + +4. ✅ `pages/profile/article-detail/article-detail.wxss` + - 添加图片网格样式 + - 优化标签样式 + +--- + +## 🎨 UI 设计亮点 + +### 图片展示 +- ✅ 3列网格布局,充分利用空间 +- ✅ 1:1 正方形比例,视觉统一 +- ✅ 圆角设计,柔和美观 +- ✅ 懒加载优化性能 +- ✅ 使用缩略图提升加载速度 + +### 标签展示 +- ✅ 渐变背景(#fff5f7 → #ffe8ec) +- ✅ 品牌色文字(#ff2442) +- ✅ 自动换行,适应多标签 +- ✅ # 号前缀,符合社交平台习惯 + +--- + +## 🚀 测试验证 + +### 后端验证 +```bash +cd go_backend +go build +./ai_xhs # 启动服务 +``` + +### 前端验证 +1. 打开小程序开发者工具 +2. 进入"我的" → "已发布" +3. 点击任意发布记录 +4. 验证显示: + - ✅ 文章内容 + - ✅ 图片网格(3列) + - ✅ 标签列表 + - ✅ 小红书链接 + +### API 测试 +```bash +# 获取发布记录详情 +curl http://localhost:8080/api/employee/publish-record/1 +``` + +--- + +## 📊 性能优化 + +1. **图片优化** + - 使用缩略图 (300x200) 而非原图 (800x600) + - 懒加载 (lazy-load) + - grid布局提升渲染性能 + +2. **数据库优化** + - article_id 索引加速关联查询 + - sort_order 字段优化排序 + - 单次查询获取所有数据 + +3. **前端优化** + - wx:if 条件渲染,减少无用节点 + - 图片按需加载 + - CSS grid 硬件加速 + +--- + +## ✨ 功能特性 + +1. ✅ 完整数据展示(内容+图片+标签) +2. ✅ 智能标签解析(支持多种分隔符) +3. ✅ 优雅的UI设计 +4. ✅ 响应式布局 +5. ✅ 性能优化 +6. ✅ 测试数据完整 +7. ✅ 向后兼容(支持无图片/标签的旧数据) + +--- + +## 📅 更新日期 + +2024-12-12 + +--- + +## 👥 相关文件 + +- 后端代码:`go_backend/` +- 前端代码:`miniprogram/` +- 测试脚本:`go_backend/migrations/` +- 文档:本文件 + +--- + +**🎉 功能已完整实现,可以正常使用!** diff --git a/ai_wht.sql b/ai_wht.sql new file mode 100644 index 0000000..069b5a2 --- /dev/null +++ b/ai_wht.sql @@ -0,0 +1,541 @@ +/* + Navicat Premium Dump SQL + + Source Server : mixue + Source Server Type : MySQL + Source Server Version : 90001 (9.0.1) + Source Host : localhost:3306 + Source Schema : ai_wht + + Target Server Type : MySQL + Target Server Version : 90001 (9.0.1) + File Encoding : 65001 + + Date: 12/12/2025 15:25:58 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for ai_article_images +-- ---------------------------- +DROP TABLE IF EXISTS `ai_article_images`; +CREATE TABLE `ai_article_images` ( + `id` int NOT NULL AUTO_INCREMENT, + `article_id` int NOT NULL DEFAULT 0 COMMENT '文章ID', + `image_id` int NOT NULL DEFAULT 0 COMMENT '图片ID', + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片URL', + `image_thumb_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '缩略图URL', + `image_tag_id` int NOT NULL DEFAULT 0 COMMENT '图片标签ID', + `sort_order` int NULL DEFAULT 0 COMMENT '排序', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `keywords_id` int NOT NULL DEFAULT 0 COMMENT '关键词ID', + `keywords_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '关键词名称', + `department_id` int NOT NULL DEFAULT 0 COMMENT '部门ID', + `department_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门名称', + `image_source` tinyint(1) NOT NULL DEFAULT 0 COMMENT '图片来源:1=tag|2=change', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_article_image`(`article_id` ASC, `image_id` ASC) USING BTREE, + INDEX `idx_article_id`(`article_id` ASC) USING BTREE, + INDEX `idx_image_id`(`image_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_article_published_records +-- ---------------------------- +DROP TABLE IF EXISTS `ai_article_published_records`; +CREATE TABLE `ai_article_published_records` ( + `id` int NOT NULL AUTO_INCREMENT, + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `product_id` int NOT NULL DEFAULT 0 COMMENT '关联产品ID', + `topic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'topic主题', + `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标题', + `created_user_id` int NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `review_user_id` int NULL DEFAULT NULL COMMENT '审核用户ID', + `publish_user_id` int NULL DEFAULT NULL COMMENT '发布用户ID', + `status` enum('topic','cover_image','generate','generate_failed','draft','pending_review','approved','rejected','published_review','published','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'draft' COMMENT '状态', + `channel` tinyint(1) NOT NULL DEFAULT 1 COMMENT '渠道:1=baidu|2=toutiao|3=weixin', + `review_comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '审核评论', + `publish_time` timestamp NULL DEFAULT NULL COMMENT '发布时间', + `word_count` int NULL DEFAULT 0 COMMENT '字数统计', + `image_count` int NULL DEFAULT 0 COMMENT '图片数量', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_product_id`(`product_id` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_created_user_id`(`created_user_id` ASC) USING BTREE, + INDEX `idx_enterprise_product`(`enterprise_id` ASC, `product_id` ASC) USING BTREE, + INDEX `idx_status_created`(`status` ASC, `created_at` DESC) USING BTREE, + INDEX `idx_created_at`(`created_at` DESC) USING BTREE, + INDEX `idx_updated_at`(`updated_at` DESC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_article_tags +-- ---------------------------- +DROP TABLE IF EXISTS `ai_article_tags`; +CREATE TABLE `ai_article_tags` ( + `id` int NOT NULL AUTO_INCREMENT, + `article_id` int NOT NULL DEFAULT 0 COMMENT '文章ID', + `coze_tag` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Coze生成的标签', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_article_tag`(`article_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_articles +-- ---------------------------- +DROP TABLE IF EXISTS `ai_articles`; +CREATE TABLE `ai_articles` ( + `id` int NOT NULL AUTO_INCREMENT, + `batch_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '批次ID', + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `product_id` int NOT NULL DEFAULT 0 COMMENT '关联产品ID', + `topic_type_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT 'topic类型ID', + `prompt_workflow_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '提示词工作流ID', + `topic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'topic主题', + `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标题', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章内容', + `department` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门', + `departmentids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门IDs', + `author_id` int NULL DEFAULT NULL COMMENT '作者ID', + `author_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '作者名称', + `department_id` int NULL DEFAULT NULL COMMENT '部门ID', + `department_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名称', + `created_user_id` int NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `review_user_id` int NULL DEFAULT NULL COMMENT '审核用户ID', + `publish_user_id` int NULL DEFAULT NULL COMMENT '发布用户ID', + `status` enum('topic','cover_image','generate','generate_failed','draft','pending_review','approved','rejected','published_review','published','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'draft' COMMENT '状态', + `channel` tinyint(1) NOT NULL DEFAULT 1 COMMENT '渠道:1=baidu|2=toutiao|3=weixin', + `review_comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '审核评论', + `publish_time` timestamp NULL DEFAULT NULL COMMENT '发布时间', + `baijiahao_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '百家号ID', + `baijiahao_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '百家号状态', + `word_count` int NULL DEFAULT 0 COMMENT '字数统计', + `image_count` int NULL DEFAULT 0 COMMENT '图片数量', + `coze_tag` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Coze生成的标签', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_product_id`(`product_id` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_created_user_id`(`created_user_id` ASC) USING BTREE, + INDEX `idx_enterprise_product`(`enterprise_id` ASC, `product_id` ASC) USING BTREE, + INDEX `idx_status_created`(`status` ASC, `created_at` DESC) USING BTREE, + INDEX `idx_created_at`(`created_at` DESC) USING BTREE, + INDEX `idx_updated_at`(`updated_at` DESC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_authors +-- ---------------------------- +DROP TABLE IF EXISTS `ai_authors`; +CREATE TABLE `ai_authors` ( + `id` int NOT NULL AUTO_INCREMENT, + `author_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '作者名称', + `app_id` varchar(127) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用ID', + `app_token` varchar(127) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用Token', + `department_id` int NOT NULL DEFAULT 0 COMMENT '部门ID', + `department_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门名称', + `department` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门', + `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '职称', + `hospital` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '医院', + `specialty` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '专业', + `toutiao_cookie` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '头条Cookie', + `toutiao_images_cookie` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '头条图片Cookie', + `introduction` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '介绍', + `avatar_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像URL', + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active' COMMENT '状态', + `channel` tinyint(1) NOT NULL DEFAULT 1 COMMENT '渠道:1=baidu|2=toutiao|3=weixin', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_department_id`(`department_id` ASC) USING BTREE, + INDEX `idx_channel`(`channel` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_data_statistics +-- ---------------------------- +DROP TABLE IF EXISTS `ai_data_statistics`; +CREATE TABLE `ai_data_statistics` ( + `id` int NOT NULL AUTO_INCREMENT, + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `product_id` int NOT NULL DEFAULT 0 COMMENT '关联产品ID', + `cumulative_releases_num` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '累计发布', + `published_today_num` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日发布', + `published_week_num` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '本周发布', + `participating_employees_num` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '参与员工', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_product_id`(`product_id` ASC) USING BTREE, + INDEX `idx_enterprise_product`(`enterprise_id` ASC, `product_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_departments +-- ---------------------------- +DROP TABLE IF EXISTS `ai_departments`; +CREATE TABLE `ai_departments` ( + `id` int NOT NULL AUTO_INCREMENT, + `department_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门名称', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_ai_departments_created_at`(`created_at` DESC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_enterprises +-- ---------------------------- +DROP TABLE IF EXISTS `ai_enterprises`; +CREATE TABLE `ai_enterprises` ( + `id` int NOT NULL AUTO_INCREMENT, + `enterprise_ID` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业ID', + `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业名称', + `short_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业简称', + `icon` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业图标URL', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '登录手机号', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '登录密码(加密存储)', + `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业邮箱', + `website` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业网站', + `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业地址', + `status` enum('active','disabled') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'active' COMMENT '状态', + `users_total` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '员工总数', + `products_total` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '产品总数', + `articles_total` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '文章总数', + `released_month_total` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '本月发布数量', + `linked_to_xhs_num` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '绑定小红书', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_phone`(`phone` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '企业信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ai_image_tags +-- ---------------------------- +DROP TABLE IF EXISTS `ai_image_tags`; +CREATE TABLE `ai_image_tags` ( + `id` int NOT NULL AUTO_INCREMENT, + `image_id` int NOT NULL DEFAULT 0 COMMENT '图片ID', + `image_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片名称', + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片URL', + `image_thumb_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '缩略图URL', + `tag_id` int NOT NULL DEFAULT 0 COMMENT '标签ID', + `tag_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标签名称', + `keywords_id` int NOT NULL DEFAULT 0 COMMENT '关键词ID', + `keywords_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '关键词名称', + `department_id` int NOT NULL DEFAULT 0 COMMENT '部门ID', + `department_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门名称', + `created_user_id` int NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_image_tag`(`image_id` ASC, `tag_id` ASC) USING BTREE, + INDEX `idx_tag_id`(`tag_id` ASC) USING BTREE, + INDEX `idx_department_id`(`department_id` ASC) USING BTREE, + INDEX `idx_keywords_id`(`keywords_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE, + INDEX `idx_dept_keywords`(`department_id` ASC, `keywords_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_image_tags_name +-- ---------------------------- +DROP TABLE IF EXISTS `ai_image_tags_name`; +CREATE TABLE `ai_image_tags_name` ( + `id` int NOT NULL AUTO_INCREMENT, + `tag_name` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标签名称', + `tag_category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签分类', + `department` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '描述', + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active' COMMENT '状态', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_tag_name`(`tag_name`(191) ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_image_tags_relation +-- ---------------------------- +DROP TABLE IF EXISTS `ai_image_tags_relation`; +CREATE TABLE `ai_image_tags_relation` ( + `id` int NOT NULL AUTO_INCREMENT, + `image_id` int NOT NULL DEFAULT 0 COMMENT '图片ID', + `image_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片名称', + `tag_id` int NOT NULL DEFAULT 0 COMMENT '标签ID', + `tag_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标签名称', + `created_user_id` int NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_image_tag`(`image_id` ASC, `tag_id` ASC) USING BTREE, + INDEX `idx_tag_id`(`tag_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_image_type +-- ---------------------------- +DROP TABLE IF EXISTS `ai_image_type`; +CREATE TABLE `ai_image_type` ( + `id` int NOT NULL AUTO_INCREMENT, + `type_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片类型,场景图', + `keywords_id` int NOT NULL DEFAULT 0 COMMENT '关键词ID', + `keywords_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '关键词名称', + `department_id` int NOT NULL DEFAULT 0 COMMENT '部门ID', + `department_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门名称', + `created_user_id` int NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_department_id`(`department_id` ASC) USING BTREE, + INDEX `idx_keywords_id`(`keywords_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE, + INDEX `idx_dept_keywords`(`department_id` ASC, `keywords_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_images +-- ---------------------------- +DROP TABLE IF EXISTS `ai_images`; +CREATE TABLE `ai_images` ( + `id` int NOT NULL AUTO_INCREMENT, + `image_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片名称', + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片URL', + `image_thumb_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '缩略图URL', + `thumbnail_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '缩略图URL', + `department` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门', + `keywords` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '关键词', + `size_type` enum('medical','lifestyle','instruction') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'medical' COMMENT '尺寸类型', + `image_type_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '图片类型ID', + `image_type_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片类型,场景图', + `file_size` bigint NULL DEFAULT NULL COMMENT '文件大小', + `width` int NULL DEFAULT NULL COMMENT '图片宽度', + `height` int NULL DEFAULT NULL COMMENT '图片高度', + `upload_user_id` int NOT NULL DEFAULT 0 COMMENT '上传用户ID', + `status` enum('active','inactive','deleted') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active' COMMENT '状态', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_upload_user_id`(`upload_user_id` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_image_type_id`(`image_type_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_logs +-- ---------------------------- +DROP TABLE IF EXISTS `ai_logs`; +CREATE TABLE `ai_logs` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NULL DEFAULT NULL COMMENT '用户ID', + `action` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作动作', + `target_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '目标类型', + `target_id` int NULL DEFAULT NULL COMMENT '目标ID', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '描述', + `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'IP地址', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '用户代理', + `request_data` json NULL COMMENT '请求数据', + `response_data` json NULL COMMENT '响应数据', + `status` enum('success','error','warning') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'success' COMMENT '状态', + `error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '错误消息', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_action`(`action` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_product_images +-- ---------------------------- +DROP TABLE IF EXISTS `ai_product_images`; +CREATE TABLE `ai_product_images` ( + `id` int NOT NULL AUTO_INCREMENT, + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `product_id` int NOT NULL DEFAULT 0 COMMENT '关联产品ID', + `image_id` int NOT NULL DEFAULT 0 COMMENT '图片ID', + `image_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片名称', + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片URL', + `thumbnail_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '缩略图URL', + `type_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片类型', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '图片描述', + `file_size` bigint NULL DEFAULT NULL COMMENT '文件大小', + `width` int NULL DEFAULT NULL COMMENT '图片宽度', + `height` int NULL DEFAULT NULL COMMENT '图片高度', + `upload_user_id` int NOT NULL DEFAULT 0 COMMENT '上传用户ID', + `status` enum('active','deleted') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active' COMMENT '状态', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_product_id`(`product_id` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_enterprise_product`(`enterprise_id` ASC, `product_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '产品图片库表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ai_product_tags +-- ---------------------------- +DROP TABLE IF EXISTS `ai_product_tags`; +CREATE TABLE `ai_product_tags` ( + `id` int NOT NULL AUTO_INCREMENT, + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `product_id` int NOT NULL DEFAULT 0 COMMENT '关联产品ID', + `tag_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '产品的标签', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_enterprise_product_tag`(`enterprise_id` ASC, `product_id` ASC, `tag_name`(100) ASC) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_product_id`(`product_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_product_types +-- ---------------------------- +DROP TABLE IF EXISTS `ai_product_types`; +CREATE TABLE `ai_product_types` ( + `id` int NOT NULL AUTO_INCREMENT, + `type_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '产品类型名称', + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `product_id` int NOT NULL DEFAULT 0 COMMENT '关联产品ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_enterprise_product_type`(`enterprise_id` ASC, `product_id` ASC, `type_name` ASC) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_product_id`(`product_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_products +-- ---------------------------- +DROP TABLE IF EXISTS `ai_products`; +CREATE TABLE `ai_products` ( + `id` int NOT NULL AUTO_INCREMENT, + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '产品名称', + `type_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '产品类型', + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '产品主图URL', + `image_thumbnail_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '缩略图URL', + `knowledge` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '产品知识库(纯文字)', + `articles_total` int NOT NULL DEFAULT 0 COMMENT '文章总数', + `published_total` int NOT NULL DEFAULT 0 COMMENT '发布总数', + `status` enum('draft','active','deleted') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'draft' COMMENT '状态:draft=草稿,active=正常,deleted=已删除', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_enterprise_status`(`enterprise_id` ASC, `status` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '产品信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ai_prompt_tags +-- ---------------------------- +DROP TABLE IF EXISTS `ai_prompt_tags`; +CREATE TABLE `ai_prompt_tags` ( + `id` int NOT NULL AUTO_INCREMENT, + `tag_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'tag名称', + `created_user_id` int NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_tag_name`(`tag_name` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '提示词模板表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ai_prompt_tags_relation +-- ---------------------------- +DROP TABLE IF EXISTS `ai_prompt_tags_relation`; +CREATE TABLE `ai_prompt_tags_relation` ( + `id` int NOT NULL AUTO_INCREMENT, + `prompt_workflow_id` int NOT NULL DEFAULT 0 COMMENT '提示词工作流ID', + `prompt_workflow_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '提示词工作流名称', + `tag_id` int NOT NULL DEFAULT 0 COMMENT '标签ID', + `tag_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标签名称', + `created_user_id` int NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_prompt_tag`(`prompt_workflow_id` ASC, `tag_id` ASC) USING BTREE, + INDEX `idx_tag_id`(`tag_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_prompt_workflow +-- ---------------------------- +DROP TABLE IF EXISTS `ai_prompt_workflow`; +CREATE TABLE `ai_prompt_workflow` ( + `id` int NOT NULL AUTO_INCREMENT, + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `prompt_workflow_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '提示词工作流名称', + `auth_token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '认证Token', + `workflow_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '工作流ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '提示词内容', + `usage_count` int NOT NULL DEFAULT 0 COMMENT '使用次数统计', + `created_user_id` int NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_workflow_id`(`workflow_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for ai_users +-- ---------------------------- +DROP TABLE IF EXISTS `ai_users`; +CREATE TABLE `ai_users` ( + `id` int NOT NULL AUTO_INCREMENT, + `enterprise_id` int NOT NULL DEFAULT 0 COMMENT '所属企业ID', + `enterprise_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '企业名称', + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码', + `real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实姓名', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `xhs_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '小红书绑定手机号', + `xhs_account` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '小红书账号名称', + `is_bound_xhs` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否绑定小红书:0=未绑定,1=已绑定', + `bound_at` timestamp NULL DEFAULT NULL COMMENT '绑定小红书的时间', + `department` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门', + `role` enum('admin','editor','reviewer','publisher','each_title_reviewer') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'editor' COMMENT '角色', + `status` enum('active','inactive','deleted') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active' COMMENT '状态', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_username`(`username` ASC) USING BTREE, + INDEX `idx_enterprise_id`(`enterprise_id` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_role`(`role` ASC) USING BTREE, + INDEX `idx_is_bound_xhs`(`is_bound_xhs` ASC) USING BTREE, + INDEX `idx_enterprise_status`(`enterprise_id` ASC, `status` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/backend/PUBLISH_FEATURE_SUMMARY.md b/backend/PUBLISH_FEATURE_SUMMARY.md new file mode 100644 index 0000000..3a9658f --- /dev/null +++ b/backend/PUBLISH_FEATURE_SUMMARY.md @@ -0,0 +1,247 @@ +# 小红书发布脚本 - 功能总结 + +## 🎉 最新更新(v1.1.0) + +已成功增强发布脚本,现在支持**网络图片 URL**! + +## ✨ 核心功能 + +### 1. Cookie 认证 +- 支持验证码登录 +- 支持 Cookie 注入 +- 自动验证登录状态 + +### 2. 内容发布 +- ✅ 标题和正文 +- ✅ 多图上传(最多 9 张) +- ✅ 标签自动添加 +- ✅ 发布状态追踪 + +### 3. 图片支持(新增) +- ✅ **本地文件路径**(绝对/相对路径) +- ✅ **网络图片 URL**(HTTP/HTTPS) +- ✅ **混合使用**(本地 + 网络) +- ✅ **自动下载**网络图片 +- ✅ **自动清理**临时文件 + +## 📝 使用示例 + +### 基础使用 + +```json +{ + "cookies": [...], + "title": "笔记标题", + "content": "笔记内容", + "images": [ + "https://picsum.photos/800/600?random=1", + "D:/local/image.jpg" + ], + "tags": ["标签1", "标签2"] +} +``` + +### 命令行 + +```bash +# 安装依赖 +pip install -r requirements.txt + +# 从配置文件发布 +python xhs_publish.py --config my_config.json + +# 测试网络图片功能 +python test_network_images.py +``` + +## 🔧 技术实现 + +### 图片处理流程 + +```python +1. 判断是否为网络 URL + ↓ 是 +2. 使用 aiohttp 下载图片 + ↓ +3. 保存到 temp_downloads/ + ↓ +4. 返回本地路径 + ↓ +5. 上传到小红书 + ↓ +6. 发布完成后清理临时文件 +``` + +### 关键代码 + +```python +class XHSPublishService: + async def download_image(self, url: str) -> str: + """下载网络图片""" + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=30) as response: + content = await response.read() + # 保存文件 + return local_path + + async def process_images(self, images: List[str]) -> List[str]: + """处理图片列表(下载网络图片)""" + local_images = [] + for img in images: + if self.is_url(img): + local_path = await self.download_image(img) + local_images.append(local_path) + else: + local_images.append(img) + return local_images +``` + +## 📦 文件结构 + +``` +backend/ +├── xhs_publish.py # 发布脚本(已增强) +├── xhs_login.py # 登录服务 +├── xhs_cli.py # 命令行工具 +├── test_publish.py # 基础测试 +├── test_network_images.py # 网络图片测试(新增) +├── publish_config_example.json # 配置示例(已更新) +├── requirements.txt # 依赖列表(已更新) +├── temp_downloads/ # 临时下载目录(自动创建) +└── 文档/ + ├── XHS_PUBLISH_README.md # 发布脚本文档 + ├── NETWORK_IMAGE_SUPPORT.md # 网络图片文档(新增) + └── PUBLISH_FEATURE_SUMMARY.md # 功能总结(本文档) +``` + +## 🚀 快速开始 + +### 1. 安装依赖 + +```bash +cd backend +pip install -r requirements.txt +playwright install chromium +``` + +### 2. 获取 Cookie + +```bash +python xhs_cli.py login 13800138000 123456 +``` + +### 3. 准备配置文件 + +```bash +cp publish_config_example.json my_publish.json +# 编辑 my_publish.json,填入实际数据 +``` + +### 4. 执行发布 + +```bash +python xhs_publish.py --config my_publish.json +``` + +## 📊 性能指标 + +| 指标 | 数值 | +|------|------| +| 图片下载超时 | 30 秒/张 | +| 最大图片数 | 9 张 | +| 建议图片大小 | < 5MB | +| 临时文件清理 | 自动 | + +## 🔍 实际应用 + +### 从数据库获取图片 URL 发布 + +```python +# 查询文章信息 +article = db.query("SELECT * FROM ai_articles WHERE id = ?", article_id) +images = db.query("SELECT image_url FROM ai_article_images WHERE article_id = ?", article_id) +tags = db.query("SELECT coze_tag FROM ai_article_tags WHERE article_id = ?", article_id) + +# 准备发布 +publisher = XHSPublishService(cookies) +result = await publisher.publish( + title=article['title'], + content=article['content'], + images=[img['image_url'] for img in images], # 使用数据库中的 URL + tags=tags[0]['coze_tag'].split(',') if tags else [] +) +``` + +### Go 后端调用示例 + +```go +func PublishArticle(articleID int) error { + // 1. 查询文章信息 + article := db.GetArticle(articleID) + images := db.GetArticleImages(articleID) + + // 2. 构造配置 + config := map[string]interface{}{ + "cookies": loadCookies(), + "title": article.Title, + "content": article.Content, + "images": images, // 直接使用 URL 数组 + "tags": splitTags(article.Tags), + } + + // 3. 保存配置文件 + configFile := fmt.Sprintf("temp/publish_%d.json", articleID) + saveJSON(configFile, config) + + // 4. 调用 Python 脚本 + cmd := exec.Command("python", "backend/xhs_publish.py", "--config", configFile) + output, err := cmd.CombinedOutput() + + // 5. 解析结果 + var result map[string]interface{} + json.Unmarshal(output, &result) + + return checkResult(result) +} +``` + +## ⚠️ 注意事项 + +### 1. 网络图片 +- 确保 URL 可公开访问 +- 避免使用需要认证的图片 +- 注意图片服务器的访问速度 + +### 2. 临时文件 +- 默认保存在 `temp_downloads/` +- 发布完成后自动清理 +- 可设置 `cleanup=False` 保留文件 + +### 3. 错误处理 +- 单张图片下载失败不影响其他图片 +- 会跳过失败的图片继续发布 +- 详细的错误日志输出 + +## 📚 相关文档 + +- [XHS_PUBLISH_README.md](XHS_PUBLISH_README.md) - 详细使用文档 +- [NETWORK_IMAGE_SUPPORT.md](NETWORK_IMAGE_SUPPORT.md) - 网络图片支持文档 +- [XHS_CLI_README.md](XHS_CLI_README.md) - 命令行工具文档 + +## 🐛 问题反馈 + +如遇到问题,请检查: +1. 是否安装了 `aiohttp` 库 +2. 网络连接是否正常 +3. Cookie 是否有效 +4. 图片 URL 是否可访问 +5. 磁盘空间是否充足 + +## 🎯 下一步计划 + +- [ ] 支持视频发布 +- [ ] 批量发布功能 +- [ ] 定时发布功能 +- [ ] 发布结果追踪 +- [ ] 图片压缩优化 +- [ ] 并发下载优化 diff --git a/backend/QUICK_START.md b/backend/QUICK_START.md new file mode 100644 index 0000000..a97a8ee --- /dev/null +++ b/backend/QUICK_START.md @@ -0,0 +1,313 @@ +# 快速开始指南 + +## 问题说明 + +如果您在使用 `xhs_publish.py` 时遇到 JSON 解析错误: + +```bash +python xhs_publish.py --cookies '[...]' --title "标题" --content "内容" +# 错误: JSON解析失败: Expecting property name enclosed in double quotes +``` + +这是因为命令行中的 JSON 字符串转义问题。 + +## 解决方案 + +我们提供了 **3 种简单的使用方式**: + +--- + +## 方式 1:快速发布脚本(推荐)⭐ + +使用 `quick_publish.py`,无需处理 JSON 转义。 + +### 用法 + +```bash +python quick_publish.py "标题" "内容" "图片1,图片2,图片3" "标签1,标签2" +``` + +### Windows PowerShell 用法 + +**在 Windows PowerShell 中,强烈推荐使用此方式,避免 JSON 转义问题:** + +```powershell +python quick_publish.py "测试笔记" "这是测试内容" "https://picsum.photos/800/600,https://picsum.photos/800/600" "测试,自动化" +``` + +### 参数说明 + +1. **标题**(必需) +2. **内容**(必需) +3. **图片**(可选)- 用逗号分隔,支持本地路径和网络 URL +4. **标签**(可选)- 用逗号分隔 + +### 示例 + +**1. 发布纯文字** +```bash +python quick_publish.py "测试笔记" "这是测试内容" "" "测试,自动化" +``` + +**2. 使用网络图片** +```bash +python quick_publish.py "测试笔记" "这是测试内容" "https://picsum.photos/800/600,https://picsum.photos/800/600" "测试,图片" +``` + +**3. 使用本地图片** +```bash +python quick_publish.py "测试笔记" "这是测试内容" "D:/image1.jpg,D:/image2.jpg" "测试" +``` + +**4. 混合使用** +```bash +python quick_publish.py "测试笔记" "这是测试内容" "https://picsum.photos/800/600,D:/local.jpg" "测试" +``` + +### Cookie 文件 + +脚本默认从 `cookies.json` 读取 Cookie。 + +如果 Cookie 在其他文件,可以指定: +```bash +python quick_publish.py "标题" "内容" "图片" "标签" "my_cookies.json" +``` + +--- + +## 方式 2:配置文件发布 + +### 步骤 1:准备 Cookie 文件 + +将您的 Cookie 保存为 `test_cookies.json`(已为您创建)。 + +### 步骤 2:创建配置文件 + +创建 `my_publish.json`: + +```json +{ + "cookies": "@test_cookies.json", + "title": "💧夏日必备!2元一杯的柠檬水竟然这么好喝?", + "content": "今天给大家分享一个超级实惠的夏日饮品!", + "images": [ + "https://picsum.photos/800/600?random=1", + "https://picsum.photos/800/600?random=2" + ], + "tags": ["夏日清爽", "饮品", "柠檬水"] +} +``` + +### 步骤 3:执行发布 + +```bash +python xhs_publish.py --config my_publish.json +``` + +--- + +## 方式 3:使用命令行参数(支持Cookie文件)⭐ + +### 用法 + +```bash +# Cookie 使用文件路径(推荐) +python xhs_publish.py --cookies test_cookies.json --title "标题" --content "内容" --images '["https://picsum.photos/800/600"]' --tags '["标签1"]' + +# Cookie 使用 JSON 字符串(需要转义,不推荐) +python xhs_publish.py --cookies '[{...}]' --title "标题" --content "内容" +``` + +### 参数说明 + +- `--cookies`: **Cookie JSON 字符串 或 Cookie 文件路径**(推荐使用文件路径) +- `--title`: 标题 +- `--content`: 内容 +- `--images`: 图片列表的 JSON 字符串(可选) +- `--tags`: 标签列表的 JSON 字符串(可选) + +### 示例 + +**1. 使用 Cookie 文件(推荐)** +```bash +python xhs_publish.py --cookies test_cookies.json --title "测试笔记" --content "这是测试内容" --images '["https://picsum.photos/800/600"]' --tags '["测试","自动化"]' +``` + +**2. 只发布文字** +```bash +python xhs_publish.py --cookies test_cookies.json --title "纯文字笔记" --content "这是一条纯文字笔记" +``` + +## 方式 4:Python 脚本调用 + +```python +import asyncio +import json +from xhs_publish import XHSPublishService + +async def publish_my_note(): + # 读取 Cookie + with open('test_cookies.json', 'r') as f: + cookies = json.load(f) + + # 创建发布服务 + publisher = XHSPublishService(cookies) + + # 发布笔记 + result = await publisher.publish( + title="测试笔记", + content="这是测试内容", + images=[ + "https://picsum.photos/800/600?random=1", + "https://picsum.photos/800/600?random=2" + ], + tags=["测试", "自动化"] + ) + + print(json.dumps(result, ensure_ascii=False, indent=2)) + +# 运行 +asyncio.run(publish_my_note()) +``` + +--- + +## 完整示例 + +### 使用您提供的 Cookie + +Cookie 已保存到 `test_cookies.json`,现在可以直接发布: + +```bash +# 方式 1:快速发布(推荐) +cd backend +python quick_publish.py "测试笔记" "这是一条测试笔记,使用网络图片自动发布" "https://picsum.photos/800/600,https://picsum.photos/800/600" "测试,自动化" test_cookies.json +``` + +### 输出示例 + +``` +================================================== +快速发布小红书笔记 +================================================== +标题: 测试笔记 +内容: 这是一条测试笔记,使用网络图片自动发布 +图片: 2 张 + 1. https://picsum.photos/800/600 + 2. https://picsum.photos/800/600 +标签: ['测试', '自动化'] +Cookie: test_cookies.json +================================================== + +正在处理 2 张图片... + 正在下载图片 [1]: https://picsum.photos/800/600 + ✅ 下载成功: image_0_xxx.jpg (45.2KB) + 正在下载图片 [2]: https://picsum.photos/800/600 + ✅ 下载成功: image_1_xxx.jpg (52.8KB) + +成功处理 2/2 张图片 + +1. 初始化浏览器... +浏览器初始化成功 + +2. 验证登录状态... +✅ 登录状态有效 + +3. 开始发布笔记... +[...] + +================================================== +发布结果: +{ + "success": true, + "message": "笔记发布成功", + "url": "https://www.xiaohongshu.com/explore/..." +} +================================================== + +✅ 发布成功! +📎 笔记链接: https://www.xiaohongshu.com/explore/... +``` + +--- + +## 常见问题 + +### Q: 为什么会出现 JSON 解析错误? + +A: 在命令行中直接使用 JSON 字符串时,Shell 会对引号进行特殊处理,导致解析失败。建议使用: +- **方式 1**:`quick_publish.py`(无需处理 JSON) +- **方式 2**:配置文件(JSON 在文件中) + +### Q: Cookie 从哪里获取? + +A: 三种方式: +1. 使用 `xhs_cli.py login` 登录获取 +2. 从浏览器开发者工具复制 +3. 使用现有的 Cookie JSON + +### Q: 图片支持哪些格式? + +A: +- **本地文件**:JPG、PNG、GIF、WEBP +- **网络 URL**:HTTP/HTTPS 链接 +- **混合使用**:可以同时使用本地和网络图片 + +### Q: 如何验证 Cookie 是否有效? + +A: 使用快速发布脚本,会自动验证: +```bash +python quick_publish.py "测试" "测试内容" "" "" +``` + +如果 Cookie 失效,会提示: +``` +❌ 发布失败: Cookie已失效或未登录 +``` + +--- + +## 进阶使用 + +### 批量发布 + +创建脚本 `batch_publish.py`: + +```python +import asyncio +from quick_publish import quick_publish + +async def main(): + articles = [ + { + "title": "文章1", + "content": "内容1", + "images": ["https://picsum.photos/800/600"], + "tags": ["标签1"] + }, + { + "title": "文章2", + "content": "内容2", + "images": ["https://picsum.photos/800/600"], + "tags": ["标签2"] + } + ] + + for article in articles: + result = await quick_publish(**article) + print(f"发布结果: {result['success']}") + await asyncio.sleep(60) # 间隔60秒 + +asyncio.run(main()) +``` + +--- + +## 总结 + +✅ **推荐使用 `quick_publish.py`** +- 无需处理 JSON 转义 +- 参数简单直观 +- 自动读取 Cookie 文件 + +现在您可以尝试使用快速发布脚本了!🚀 diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..3b786a3 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,170 @@ +# 小红书登录后端服务 + +基于 Playwright 的小红书登录服务,支持手机号+验证码登录。 + +## 功能特性 + +- ✅ 手机号+验证码登录 +- ✅ 自动化浏览器操作 +- ✅ 获取登录后的 Cookies 和用户信息 +- ✅ RESTful API 接口 + +## 技术栈 + +- Python 3.8+ +- FastAPI - Web 框架 +- Playwright - 浏览器自动化 +- Uvicorn - ASGI 服务器 + +## 安装步骤 + +### 1. 创建虚拟环境(如果还没有) + +```bash +cd backend +python -m venv venv +``` + +### 2. 激活虚拟环境 + +**Windows:** +```bash +venv\Scripts\activate +``` + +**Linux/Mac:** +```bash +source venv/bin/activate +``` + +### 3. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 4. 安装 Playwright 浏览器 + +```bash +playwright install chromium +``` + +## 使用方法 + +### 启动服务 + +```bash +python main.py +``` + +服务将在 `http://localhost:8000` 启动。 + +### API 接口 + +#### 1. 发送验证码 + +**POST** `/api/xhs/send-code` + +请求体: +```json +{ + "phone": "13800138000", + "country_code": "+86" +} +``` + +响应: +```json +{ + "code": 0, + "message": "验证码已发送,请在小红书APP中查看", + "data": { + "sent_at": "2025-12-10T10:00:00" + } +} +``` + +#### 2. 登录验证 + +**POST** `/api/xhs/login` + +请求体: +```json +{ + "phone": "13800138000", + "code": "123456", + "country_code": "+86" +} +``` + +响应: +```json +{ + "code": 0, + "message": "登录成功", + "data": { + "user_info": {...}, + "cookies": {...}, + "login_time": "2025-12-10T10:01:00" + } +} +``` + +#### 3. 健康检查 + +**GET** `/` + +响应: +```json +{ + "status": "ok", + "message": "小红书登录服务运行中" +} +``` + +## 注意事项 + +1. **滑块验证**: 小红书可能会要求滑块验证,需要手动完成 +2. **验证码**: 验证码会发送到小红书 APP,需要在 APP 中查看 +3. **浏览器模式**: + - 开发时使用 `headless=False` 可以看到浏览器操作 + - 生产环境可设置 `headless=True` 在后台运行 +4. **反爬虫**: 小红书有反爬虫机制,可能需要调整策略 + +## 开发调试 + +### 查看 API 文档 + +访问 `http://localhost:8000/docs` 可以看到自动生成的 Swagger 文档。 + +### 日志输出 + +服务会在控制台输出详细的操作日志,便于调试。 + +## 项目结构 + +``` +backend/ +├── main.py # FastAPI 主程序 +├── xhs_login.py # 小红书登录服务 +├── requirements.txt # Python 依赖 +├── venv/ # Python 虚拟环境 +└── README.md # 说明文档 +``` + +## 常见问题 + +### Q: 验证码发送失败? +A: 检查手机号格式是否正确,确保网络连接正常。 + +### Q: 登录失败? +A: 确认验证码是否正确,验证码有时效性,请及时输入。 + +### Q: 浏览器无法启动? +A: 确保已经运行 `playwright install chromium` 安装浏览器。 + +## 安全提示 + +- 不要在公网暴露此服务 +- 生产环境建议添加认证机制 +- 妥善保管获取到的 Cookies 和用户信息 diff --git a/backend/XHS_CLI_README.md b/backend/XHS_CLI_README.md new file mode 100644 index 0000000..766a84e --- /dev/null +++ b/backend/XHS_CLI_README.md @@ -0,0 +1,123 @@ +# 小红书登录 CLI 工具 + +## 概述 + +这是一个可以被 Go 服务直接调用的 Python CLI 工具,用于小红书登录功能。 +使用此工具后,不再需要单独启动 Python Web 服务。 + +## 使用方式 + +### 1. 发送验证码 + +```bash +python xhs_cli.py send_code <手机号> [国家区号] +``` + +示例: +```bash +python xhs_cli.py send_code 13800138000 +86 +``` + +返回 JSON 格式: +```json +{ + "success": true, + "message": "验证码发送成功" +} +``` + +### 2. 登录 + +```bash +python xhs_cli.py login <手机号> <验证码> [国家区号] +``` + +示例: +```bash +python xhs_cli.py login 13800138000 123456 +86 +``` + +返回 JSON 格式: +```json +{ + "success": true, + "user_info": {...}, + "cookies": {...}, + "url": "https://www.xiaohongshu.com/" +} +``` + +### 3. 注入 Cookie (验证登录状态) + +```bash +python xhs_cli.py inject_cookies '' +``` + +示例: +```bash +python xhs_cli.py inject_cookies '[{"name":"web_session","value":"xxx","domain":".xiaohongshu.com"}]' +``` + +返回 JSON 格式: +```json +{ + "success": true, + "logged_in": true, + "cookies": {...}, + "user_info": {...} +} +``` + +## Go 服务集成 + +Go 服务已经修改为直接调用 Python CLI 脚本,无需启动 Python Web 服务。 + +### 修改的文件 + +1. **backend/xhs_cli.py** (新增) + - 命令行接口工具 + +2. **go_backend/service/xhs_service.go** (修改) + - 使用 `exec.Command` 调用 Python 脚本 + - 不再通过 HTTP 调用 Python 服务 + +3. **go_backend/service/employee_service.go** (修改) + - 使用 `exec.Command` 调用 Python 脚本 + +### 优点 + +- ✅ 只需启动一个 Go 服务 +- ✅ 部署更简单,不需要管理多个服务进程 +- ✅ 减少网络开销 +- ✅ 更容易调试和维护 + +## 依赖要求 + +确保已安装 Python 依赖: +```bash +cd backend +pip install -r requirements.txt +``` + +主要依赖: +- playwright +- asyncio + +## 注意事项 + +1. Python 命令需要在系统 PATH 中可用 +2. 确保 `xhs_login.py` 和 `xhs_cli.py` 在同一目录 +3. Go 服务会在相对路径 `../backend` 下查找 Python 脚本 +4. 所有输出均为 JSON 格式,便于 Go 服务解析 + +## 错误处理 + +如果执行失败,会返回包含错误信息的 JSON: +```json +{ + "success": false, + "error": "错误描述信息" +} +``` + +Go 服务会捕获 stderr 输出并作为错误信息的一部分返回。 diff --git a/backend/XHS_PUBLISH_README.md b/backend/XHS_PUBLISH_README.md new file mode 100644 index 0000000..6c442fa --- /dev/null +++ b/backend/XHS_PUBLISH_README.md @@ -0,0 +1,313 @@ +# 小红书笔记发布脚本使用说明 + +## 功能介绍 + +`xhs_publish.py` 是一个用于自动发布小红书笔记的 Python 脚本,支持通过 Cookie 认证,自动完成图文笔记发布。 + +## 环境准备 + +### 1. 安装依赖 + +```bash +cd backend +pip install -r requirements.txt +``` + +主要依赖: +- playwright (浏览器自动化) +- asyncio (异步处理) + +### 2. 安装浏览器驱动 + +```bash +playwright install chromium +``` + +## 使用方式 + +### 方式一:使用配置文件(推荐) + +#### 1. 准备配置文件 + +复制 `publish_config_example.json` 并修改为实际参数: + +```json +{ + "cookies": [ + { + "name": "a1", + "value": "your_cookie_value_here", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": -1, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + } + ], + "title": "笔记标题", + "content": "笔记内容", + "images": [ + "D:/path/to/image1.jpg", + "D:/path/to/image2.jpg" + ], + "tags": [ + "标签1", + "标签2" + ] +} +``` + +#### 2. 执行发布 + +```bash +python xhs_publish.py --config publish_config.json +``` + +### 方式二:命令行参数 + +```bash +python xhs_publish.py \ + --cookies '[{"name":"a1","value":"xxx","domain":".xiaohongshu.com"}]' \ + --title "笔记标题" \ + --content "笔记内容" \ + --images '["D:/image1.jpg","D:/image2.jpg"]' \ + --tags '["标签1","标签2"]' +``` + +## 参数说明 + +### cookies (必需) + +Cookie 数组,每个 Cookie 对象包含以下字段: + +- `name`: Cookie 名称 +- `value`: Cookie 值 +- `domain`: 域名(通常为 `.xiaohongshu.com`) +- `path`: 路径(通常为 `/`) +- `expires`: 过期时间(-1 表示会话 Cookie) +- `httpOnly`: 是否仅 HTTP +- `secure`: 是否安全 +- `sameSite`: 同站策略(Lax/Strict/None) + +**重要 Cookie(必需):** +- `a1`: 用户身份认证 +- `webId`: 设备标识 +- `web_session`: 会话信息 + +### title (必需) + +笔记标题,字符串类型。 + +**示例:** +``` +"💧夏日必备!2元一杯的柠檬水竟然这么好喝?" +``` + +### content (必需) + +笔记正文内容,字符串类型,支持换行符 `\n`。 + +**示例:** +``` +"今天给大家分享一个超级实惠的夏日饮品!\n\n蜜雪冰城的柠檬水只要2元一杯,性价比真的太高了!" +``` + +### images (可选) + +图片文件路径数组,支持本地绝对路径。 + +**要求:** +- 图片必须是本地文件 +- 支持 jpg、png、gif 等格式 +- 最多上传 9 张图片 +- 建议尺寸:800x600 或更高 + +**示例:** +```json +[ + "D:/project/Work/ai_xhs/backend/temp_uploads/image1.jpg", + "D:/project/Work/ai_xhs/backend/temp_uploads/image2.jpg" +] +``` + +### tags (可选) + +标签数组,会自动添加 `#` 前缀。 + +**示例:** +```json +["夏日清爽", "饮品", "柠檬水"] +``` + +## 获取 Cookie 的方法 + +### 方法一:使用登录脚本 + +```bash +python xhs_cli.py login <手机号> <验证码> +``` + +登录成功后会自动保存 Cookie 到 `cookies.json` 文件。 + +### 方法二:浏览器手动获取 + +1. 在浏览器中登录小红书网页版 +2. 打开开发者工具(F12) +3. 切换到 Network(网络)标签 +4. 刷新页面 +5. 找到任意请求,查看 Request Headers +6. 复制 Cookie 字段内容 +7. 使用在线工具或脚本转换为 JSON 格式 + +### 方法三:使用 Cookie 注入验证 + +```bash +python xhs_cli.py inject_cookies '' +``` + +## 返回结果 + +### 成功示例 + +```json +{ + "success": true, + "message": "笔记发布成功", + "url": "https://www.xiaohongshu.com/explore/xxxx" +} +``` + +### 失败示例 + +```json +{ + "success": false, + "error": "Cookie已失效或未登录" +} +``` + +## 注意事项 + +### 1. Cookie 有效期 + +- Cookie 会在一段时间后失效 +- 需要定期重新登录获取新 Cookie +- 建议使用 Cookie 注入验证接口检查状态 + +### 2. 图片上传 + +- 确保图片文件存在且可访问 +- 图片路径使用绝对路径 +- Windows 系统路径使用 `/` 或 `\\` 分隔符 + +### 3. 发布限制 + +- 小红书可能有发布频率限制 +- 建议控制发布间隔,避免被限流 +- 内容需符合小红书社区规范 + +### 4. 错误处理 + +常见错误及解决方法: + +- **"Cookie已失效"**: 重新登录获取新 Cookie +- **"图片文件不存在"**: 检查图片路径是否正确 +- **"未找到发布按钮"**: 小红书页面结构可能变化,需要更新选择器 +- **"输入内容失败"**: 等待时间不足,增加延迟时间 + +## 与 Go 后端集成 + +在 Go 后端中调用此脚本: + +```go +import ( + "os/exec" + "encoding/json" +) + +// 发布笔记 +func PublishNote(cookies []Cookie, title, content string, images, tags []string) error { + // 构造配置文件 + config := map[string]interface{}{ + "cookies": cookies, + "title": title, + "content": content, + "images": images, + "tags": tags, + } + + // 保存到临时文件 + configFile := "temp_publish_config.json" + data, _ := json.Marshal(config) + ioutil.WriteFile(configFile, data, 0644) + + // 调用 Python 脚本 + cmd := exec.Command("python", "backend/xhs_publish.py", "--config", configFile) + output, err := cmd.CombinedOutput() + if err != nil { + return err + } + + // 解析结果 + var result map[string]interface{} + json.Unmarshal(output, &result) + + if !result["success"].(bool) { + return errors.New(result["error"].(string)) + } + + return nil +} +``` + +## 开发调试 + +### 启用浏览器可视模式 + +修改 `xhs_login.py` 中的 `headless` 参数: + +```python +self.browser = await self.playwright.chromium.launch( + headless=False, # 改为 False 可以看到浏览器操作过程 + args=['--disable-blink-features=AutomationControlled'] +) +``` + +### 查看详细日志 + +脚本会在控制台输出详细的执行日志,包括: +- 浏览器初始化 +- 登录状态验证 +- 图片上传进度 +- 内容输入状态 +- 发布结果 + +## 常见问题 + +### Q: 为什么上传图片后没有显示? + +A: 可能是图片上传时间较长,脚本已经增加了等待时间。如果仍有问题,可以调整 `xhs_login.py` 中的等待时间。 + +### Q: 如何批量发布多条笔记? + +A: 准备多个配置文件,使用循环调用脚本: + +```bash +for config in publish_config_*.json; do + python xhs_publish.py --config "$config" + sleep 60 # 间隔60秒 +done +``` + +### Q: Cookie 多久失效? + +A: 小红书 Cookie 通常在 7-30 天后失效,具体取决于 Cookie 的过期时间设置。 + +## 技术支持 + +如有问题,请查看: +1. 脚本执行日志 +2. 小红书页面结构是否变化 +3. Cookie 是否有效 +4. 图片文件是否存在 diff --git a/backend/fix_print.py b/backend/fix_print.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..e2c0a75 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,282 @@ +from fastapi import FastAPI, HTTPException, File, UploadFile, Form +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import Optional, Dict, Any, List +import asyncio +from datetime import datetime +import os +import shutil +from pathlib import Path + +from xhs_login import XHSLoginService + +app = FastAPI(title="小红书登录API") + +# CORS配置 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 生产环境应该限制具体域名 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 全局登录服务实例 +login_service = XHSLoginService() + +# 临时文件存储目录 +TEMP_DIR = Path("temp_uploads") +TEMP_DIR.mkdir(exist_ok=True) + +# 请求模型 +class SendCodeRequest(BaseModel): + phone: str + country_code: str = "+86" + +class LoginRequest(BaseModel): + phone: str + code: str + country_code: str = "+86" + +class PublishNoteRequest(BaseModel): + title: str + content: str + images: Optional[list] = None + topics: Optional[list] = None + +class InjectCookiesRequest(BaseModel): + cookies: list + +# 响应模型 +class BaseResponse(BaseModel): + code: int + message: str + data: Optional[Dict[str, Any]] = None + +@app.on_event("startup") +async def startup_event(): + """启动时不初始化浏览器,等待第一次请求时再初始化""" + pass + +@app.on_event("shutdown") +async def shutdown_event(): + """关闭时清理浏览器""" + await login_service.close_browser() + +@app.post("/api/xhs/send-code", response_model=BaseResponse) +async def send_code(request: SendCodeRequest): + """ + 发送验证码 + 通过playwright访问小红书官网,输入手机号并触发验证码发送 + """ + try: + # 调用登录服务发送验证码 + result = await login_service.send_verification_code( + phone=request.phone, + country_code=request.country_code + ) + + if result["success"]: + return BaseResponse( + code=0, + message="验证码已发送,请在小红书APP中查看", + data={"sent_at": datetime.now().isoformat()} + ) + else: + return BaseResponse( + code=1, + message=result.get("error", "发送验证码失败"), + data=None + ) + + except Exception as e: + print(f"发送验证码异常: {str(e)}") + return BaseResponse( + code=1, + message=f"发送验证码失败: {str(e)}", + data=None + ) + +@app.post("/api/xhs/login", response_model=BaseResponse) +async def login(request: LoginRequest): + """ + 登录验证 + 用户填写验证码后,完成登录并获取小红书返回的数据 + """ + try: + # 调用登录服务进行登录 + result = await login_service.login( + phone=request.phone, + code=request.code, + country_code=request.country_code + ) + + if result["success"]: + return BaseResponse( + code=0, + message="登录成功", + data={ + "user_info": result.get("user_info"), + "cookies": result.get("cookies"), # 键值对格式(前端展示) + "cookies_full": result.get("cookies_full"), # Playwright完整格式(数据库存储/脚本使用) + "login_time": datetime.now().isoformat() + } + ) + else: + return BaseResponse( + code=1, + message=result.get("error", "登录失败"), + data=None + ) + + except Exception as e: + print(f"登录异常: {str(e)}") + return BaseResponse( + code=1, + message=f"登录失败: {str(e)}", + data=None + ) + +@app.get("/") +async def root(): + """健康检查""" + return {"status": "ok", "message": "小红书登录服务运行中"} + +@app.post("/api/xhs/inject-cookies", response_model=BaseResponse) +async def inject_cookies(request: InjectCookiesRequest): + """ + 注入Cookies并验证登录状态 + 允许使用之前保存的Cookies跳过登录 + """ + try: + # 关闭旧的浏览器(如果有) + if login_service.browser: + await login_service.close_browser() + + # 使用Cookies初始化浏览器 + await login_service.init_browser(cookies=request.cookies) + + # 验证登录状态 + result = await login_service.verify_login_status() + + if result.get("logged_in"): + return BaseResponse( + code=0, + message="Cookie注入成功,已登录", + data={ + "logged_in": True, + "user_info": result.get("user_info"), + "cookies": result.get("cookies"), # 键值对格式 + "cookies_full": result.get("cookies_full"), # Playwright完整格式 + "url": result.get("url") + } + ) + else: + return BaseResponse( + code=1, + message=result.get("message", "Cookie已失效,请重新登录"), + data={ + "logged_in": False + } + ) + + except Exception as e: + print(f"注入Cookies异常: {str(e)}") + return BaseResponse( + code=1, + message=f"注入失败: {str(e)}", + data=None + ) + +@app.post("/api/xhs/publish", response_model=BaseResponse) +async def publish_note(request: PublishNoteRequest): + """ + 发布笔记 + 登录后可以发布图文笔记到小红书 + """ + try: + # 调用登录服务发布笔记 + result = await login_service.publish_note( + title=request.title, + content=request.content, + images=request.images, + topics=request.topics + ) + + if result["success"]: + return BaseResponse( + code=0, + message="笔记发布成功", + data={ + "url": result.get("url"), + "publish_time": datetime.now().isoformat() + } + ) + else: + return BaseResponse( + code=1, + message=result.get("error", "发布失败"), + data=None + ) + + except Exception as e: + print(f"发布笔记异常: {str(e)}") + return BaseResponse( + code=1, + message=f"发布失败: {str(e)}", + data=None + ) + +@app.post("/api/xhs/upload-images") +async def upload_images(files: List[UploadFile] = File(...)): + """ + 上传图片到服务器临时目录 + 返回图片的服务器路径 + """ + try: + uploaded_paths = [] + + for file in files: + # 检查文件类型 + if not file.content_type.startswith('image/'): + return { + "code": 1, + "message": f"文件 {file.filename} 不是图片类型", + "data": None + } + + # 生成唯一文件名 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") + file_ext = os.path.splitext(file.filename)[1] + safe_filename = f"{timestamp}{file_ext}" + file_path = TEMP_DIR / safe_filename + + # 保存文件 + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # 使用绝对路径 + abs_path = str(file_path.absolute()) + uploaded_paths.append(abs_path) + print(f"已上传图片: {abs_path}") + + return { + "code": 0, + "message": f"成功上传 {len(uploaded_paths)} 张图片", + "data": { + "paths": uploaded_paths, + "count": len(uploaded_paths) + } + } + + except Exception as e: + print(f"上传图片异常: {str(e)}") + return { + "code": 1, + "message": f"上传失败: {str(e)}", + "data": None + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/publish_config_example.json b/backend/publish_config_example.json new file mode 100644 index 0000000..9a109c3 --- /dev/null +++ b/backend/publish_config_example.json @@ -0,0 +1,38 @@ +{ + "cookies": [ + { + "name": "a1", + "value": "your_cookie_value_here", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": -1, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "webId", + "value": "your_webid_here", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": -1, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + } + ], + "title": "💧夏日必备!2元一杯的柠檬水竟然这么好喝?", + "content": "今天给大家分享一个超级实惠的夏日饮品!\n\n蜜雪冰城的柠檬水只要2元一杯,性价比真的太高了!\n酸酸甜甜的口感,冰冰凉凉超解渴~\n\n夏天必备!强烈推荐给大家!", + "images": [ + "https://picsum.photos/800/600?random=1", + "https://picsum.photos/800/600?random=2", + "D:/project/Work/ai_xhs/backend/temp_uploads/image3.jpg" + ], + "tags": [ + "夏日清爽", + "饮品", + "柠檬水", + "性价比", + "蜜雪冰城" + ] +} diff --git a/backend/publish_example.bat b/backend/publish_example.bat new file mode 100644 index 0000000..eb72b8a --- /dev/null +++ b/backend/publish_example.bat @@ -0,0 +1,15 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo 小红书快速发布示例 +echo ======================================== +echo. + +REM 使用 Cookie 文件路径发布 +python xhs_publish.py --cookies "./cookies.json" --title "【测试】批处理发布" --content "这是通过批处理文件发布的测试笔记" --images "[\"https://picsum.photos/800/600\",\"https://picsum.photos/800/600\"]" --tags "[\"测试\",\"批处理\",\"自动化\"]" + +echo. +echo ======================================== +echo 发布完成 +echo ======================================== +pause diff --git a/backend/quick_publish.py b/backend/quick_publish.py new file mode 100644 index 0000000..2eae7aa --- /dev/null +++ b/backend/quick_publish.py @@ -0,0 +1,136 @@ +""" +快速发布脚本 +简化命令行调用,避免 JSON 转义问题 +""" +import sys +import json +import asyncio +from xhs_publish import XHSPublishService + + +def load_cookies_from_file(filepath='cookies.json'): + """从文件加载 Cookie""" + try: + with open(filepath, 'r', encoding='utf-8') as f: + return json.load(f) + except FileNotFoundError: + print(f"❌ Cookie 文件不存在: {filepath}") + return None + except json.JSONDecodeError as e: + print(f"❌ Cookie 文件格式错误: {e}") + return None + + +async def quick_publish( + title: str, + content: str, + images: list = None, + tags: list = None, + cookies_file: str = 'cookies.json' +): + """ + 快速发布 + + Args: + title: 标题 + content: 内容 + images: 图片列表(支持本地路径和网络 URL) + tags: 标签列表 + cookies_file: Cookie 文件路径 + """ + # 加载 Cookie + cookies = load_cookies_from_file(cookies_file) + if not cookies: + return { + "success": False, + "error": "无法加载 Cookie" + } + + # 创建发布服务 + publisher = XHSPublishService(cookies) + + # 执行发布 + result = await publisher.publish( + title=title, + content=content, + images=images, + tags=tags + ) + + return result + + +def main(): + """ + 命令行入口 + + 使用方式: + python quick_publish.py "标题" "内容" "图片1,图片2,图片3" "标签1,标签2" + python quick_publish.py "标题" "内容" "" "标签1,标签2" # 不使用图片 + """ + if len(sys.argv) < 3: + print("使用方式:") + print(' python quick_publish.py "标题" "内容" ["图片1,图片2"] ["标签1,标签2"]') + print() + print("示例:") + print(' python quick_publish.py "测试笔记" "这是内容" "https://picsum.photos/800/600,D:/test.jpg" "测试,自动化"') + sys.exit(1) + + # 解析参数 + title = sys.argv[1] + content = sys.argv[2] + + # 解析图片(逗号分隔) + images = [] + if len(sys.argv) > 3 and sys.argv[3].strip(): + images = [img.strip() for img in sys.argv[3].split(',') if img.strip()] + + # 解析标签(逗号分隔) + tags = [] + if len(sys.argv) > 4 and sys.argv[4].strip(): + tags = [tag.strip() for tag in sys.argv[4].split(',') if tag.strip()] + + # Cookie 文件路径(可选) + cookies_file = sys.argv[5] if len(sys.argv) > 5 else 'cookies.json' + + print("="*50) + print("快速发布小红书笔记") + print("="*50) + print(f"标题: {title}") + print(f"内容: {content[:100]}{'...' if len(content) > 100 else ''}") + print(f"图片: {len(images)} 张") + if images: + for i, img in enumerate(images, 1): + print(f" {i}. {img}") + print(f"标签: {tags}") + print(f"Cookie: {cookies_file}") + print("="*50) + print() + + # 执行发布 + result = asyncio.run(quick_publish( + title=title, + content=content, + images=images if images else None, + tags=tags if tags else None, + cookies_file=cookies_file + )) + + # 输出结果 + print() + print("="*50) + print("发布结果:") + print(json.dumps(result, ensure_ascii=False, indent=2)) + print("="*50) + + if result.get('success'): + print("\n✅ 发布成功!") + if 'url' in result: + print(f"📎 笔记链接: {result['url']}") + else: + print(f"\n❌ 发布失败: {result.get('error')}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..5d6b484 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +playwright==1.40.0 +pydantic==2.5.0 +python-multipart==0.0.6 +aiohttp==3.9.1 diff --git a/backend/start.bat b/backend/start.bat new file mode 100644 index 0000000..63c77cd --- /dev/null +++ b/backend/start.bat @@ -0,0 +1,8 @@ +@echo off +echo 正在激活虚拟环境... +venv\Scripts\activate + +echo 正在启动小红书登录服务... +python main.py + +pause diff --git a/backend/start.sh b/backend/start.sh new file mode 100644 index 0000000..c2047c4 --- /dev/null +++ b/backend/start.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo "正在激活虚拟环境..." +source venv/bin/activate + +echo "正在启动小红书登录服务..." +python main.py diff --git a/backend/temp_uploads/20251210_155442_144626.jpg b/backend/temp_uploads/20251210_155442_144626.jpg new file mode 100644 index 0000000..e4cb90a Binary files /dev/null and b/backend/temp_uploads/20251210_155442_144626.jpg differ diff --git a/backend/temp_uploads/20251216_114303_912126.jpg b/backend/temp_uploads/20251216_114303_912126.jpg new file mode 100644 index 0000000..e4cb90a Binary files /dev/null and b/backend/temp_uploads/20251216_114303_912126.jpg differ diff --git a/backend/temp_uploads/20251216_114321_739110.jpg b/backend/temp_uploads/20251216_114321_739110.jpg new file mode 100644 index 0000000..e4cb90a Binary files /dev/null and b/backend/temp_uploads/20251216_114321_739110.jpg differ diff --git a/backend/temp_uploads/20251216_193730_208100.jpg b/backend/temp_uploads/20251216_193730_208100.jpg new file mode 100644 index 0000000..e4cb90a Binary files /dev/null and b/backend/temp_uploads/20251216_193730_208100.jpg differ diff --git a/backend/temp_uploads/20251216_202154_985776.jpg b/backend/temp_uploads/20251216_202154_985776.jpg new file mode 100644 index 0000000..4fd81ba Binary files /dev/null and b/backend/temp_uploads/20251216_202154_985776.jpg differ diff --git a/backend/temp_uploads/20251216_202823_315658.jpg b/backend/temp_uploads/20251216_202823_315658.jpg new file mode 100644 index 0000000..4fd81ba Binary files /dev/null and b/backend/temp_uploads/20251216_202823_315658.jpg differ diff --git a/backend/temp_uploads/20251216_202828_835648.jpg b/backend/temp_uploads/20251216_202828_835648.jpg new file mode 100644 index 0000000..4fd81ba Binary files /dev/null and b/backend/temp_uploads/20251216_202828_835648.jpg differ diff --git a/backend/temp_uploads/20251216_202837_337689.jpg b/backend/temp_uploads/20251216_202837_337689.jpg new file mode 100644 index 0000000..4fd81ba Binary files /dev/null and b/backend/temp_uploads/20251216_202837_337689.jpg differ diff --git a/backend/temp_uploads/20251216_203949_751793.jpg b/backend/temp_uploads/20251216_203949_751793.jpg new file mode 100644 index 0000000..4fd81ba Binary files /dev/null and b/backend/temp_uploads/20251216_203949_751793.jpg differ diff --git a/backend/test_api_response.py b/backend/test_api_response.py new file mode 100644 index 0000000..54e1964 --- /dev/null +++ b/backend/test_api_response.py @@ -0,0 +1,188 @@ +""" +测试 API 返回格式 +验证登录 API 是否正确返回 cookies 和 cookies_full +""" +import json + + +def test_api_response_format(): + """测试 API 响应格式""" + + # 模拟 API 返回的数据 + mock_response = { + "code": 0, + "message": "登录成功", + "data": { + "user_info": {}, + "cookies": { + "a1": "xxx", + "webId": "yyy", + "web_session": "zzz" + }, + "cookies_full": [ + { + "name": "a1", + "value": "xxx", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1797066496, + "httpOnly": False, + "secure": False, + "sameSite": "Lax" + }, + { + "name": "webId", + "value": "yyy", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1797066496, + "httpOnly": False, + "secure": False, + "sameSite": "Lax" + }, + { + "name": "web_session", + "value": "zzz", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1797066497, + "httpOnly": True, + "secure": True, + "sameSite": "Lax" + } + ], + "login_time": "2025-12-12T23:30:00" + } + } + + print("="*60) + print("API 响应格式测试") + print("="*60) + print() + + # 检查响应结构 + assert "code" in mock_response, "缺少 code 字段" + assert "message" in mock_response, "缺少 message 字段" + assert "data" in mock_response, "缺少 data 字段" + + data = mock_response["data"] + + # 检查 cookies 字段(键值对格式) + print("✅ 检查 cookies 字段(键值对格式):") + assert "cookies" in data, "缺少 cookies 字段" + assert isinstance(data["cookies"], dict), "cookies 应该是字典类型" + print(f" 类型: {type(data['cookies']).__name__}") + print(f" 示例: {json.dumps(data['cookies'], ensure_ascii=False, indent=2)}") + print() + + # 检查 cookies_full 字段(Playwright 完整格式) + print("✅ 检查 cookies_full 字段(Playwright 完整格式):") + assert "cookies_full" in data, "缺少 cookies_full 字段" + assert isinstance(data["cookies_full"], list), "cookies_full 应该是列表类型" + print(f" 类型: {type(data['cookies_full']).__name__}") + print(f" 数量: {len(data['cookies_full'])} 个 Cookie") + print(f" 示例(第一个):") + print(f"{json.dumps(data['cookies_full'][0], ensure_ascii=False, indent=6)}") + print() + + # 检查 cookies_full 的每个元素 + print("✅ 检查 cookies_full 的结构:") + for i, cookie in enumerate(data["cookies_full"]): + assert "name" in cookie, f"Cookie[{i}] 缺少 name 字段" + assert "value" in cookie, f"Cookie[{i}] 缺少 value 字段" + assert "domain" in cookie, f"Cookie[{i}] 缺少 domain 字段" + assert "path" in cookie, f"Cookie[{i}] 缺少 path 字段" + assert "expires" in cookie, f"Cookie[{i}] 缺少 expires 字段" + assert "httpOnly" in cookie, f"Cookie[{i}] 缺少 httpOnly 字段" + assert "secure" in cookie, f"Cookie[{i}] 缺少 secure 字段" + assert "sameSite" in cookie, f"Cookie[{i}] 缺少 sameSite 字段" + print(f" Cookie[{i}] ({cookie['name']}): ✅ 所有字段完整") + + print() + print("="*60) + print("🎉 所有检查通过!API 返回格式正确") + print("="*60) + print() + + # 使用场景说明 + print("📝 使用场景:") + print() + print("1. 前端展示 - 使用 cookies(键值对格式):") + print(" const cookies = response.data.cookies;") + print(" console.log(cookies.a1, cookies.webId);") + print() + + print("2. 数据库存储 - 使用 cookies_full(完整格式):") + print(" const cookiesFull = response.data.cookies_full;") + print(" await db.saveCookies(userId, JSON.stringify(cookiesFull));") + print() + + print("3. Python 脚本使用 - 使用 cookies_full:") + print(" cookies_full = response['data']['cookies_full']") + print(" publisher = XHSPublishService(cookies_full)") + print() + + +def compare_formats(): + """对比两种格式""" + + print("="*60) + print("格式对比分析") + print("="*60) + print() + + # 键值对格式 + cookies_dict = { + "a1": "xxx", + "webId": "yyy", + "web_session": "zzz" + } + + # Playwright 完整格式 + cookies_full = [ + { + "name": "a1", + "value": "xxx", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1797066496, + "httpOnly": False, + "secure": False, + "sameSite": "Lax" + } + ] + + print("📊 键值对格式:") + dict_str = json.dumps(cookies_dict, ensure_ascii=False, indent=2) + print(dict_str) + print(f" 大小: {len(dict_str)} 字符") + print() + + print("📊 Playwright 完整格式:") + full_str = json.dumps(cookies_full, ensure_ascii=False, indent=2) + print(full_str) + print(f" 大小: {len(full_str)} 字符") + print() + + print("📊 对比结果:") + print(f" 完整格式 vs 键值对格式: {len(full_str)} / {len(dict_str)} = {len(full_str)/len(dict_str):.1f}x") + print(f" 每个 Cookie 完整格式约增加: {(len(full_str) - len(dict_str)) // len(cookies_dict)} 字符") + print() + + print("✅ 结论:") + print(" - 完整格式虽然较大,但包含所有必要属性") + print(" - 对于数据库存储,建议使用完整格式") + print(" - 对于前端展示,可以使用键值对格式") + print() + + +if __name__ == "__main__": + # 测试 API 响应格式 + test_api_response_format() + + # 对比两种格式 + compare_formats() + + print("="*60) + print("✅ 测试完成!") + print("="*60) diff --git a/backend/test_cookie_file.py b/backend/test_cookie_file.py new file mode 100644 index 0000000..ef349b0 --- /dev/null +++ b/backend/test_cookie_file.py @@ -0,0 +1,162 @@ +""" +测试 Cookie 文件路径支持 +""" +import subprocess +import sys +import json + + +def test_cookie_file_param(): + """测试 --cookies 参数支持文件路径""" + + print("="*60) + print("测试 Cookie 文件路径参数支持") + print("="*60) + print() + + # 测试命令 + cmd = [ + sys.executable, + "xhs_publish.py", + "--cookies", "test_cookies.json", # 使用文件路径 + "--title", "【测试】Cookie文件路径参数", + "--content", "测试使用 --cookies 参数传递文件路径,而不是 JSON 字符串", + "--images", '["https://picsum.photos/800/600","https://picsum.photos/800/600"]', + "--tags", '["测试","Cookie文件","自动化"]' + ] + + print("执行命令:") + print(" ".join(cmd)) + print() + print("-"*60) + print() + + # 执行命令 + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8' + ) + + # 输出结果 + print("标准输出:") + print(result.stdout) + + if result.stderr: + print("\n标准错误:") + print(result.stderr) + + print() + print("-"*60) + + # 解析结果 + try: + # 尝试从输出中提取 JSON 结果 + lines = result.stdout.strip().split('\n') + for i, line in enumerate(lines): + if line.strip().startswith('{'): + json_str = '\n'.join(lines[i:]) + response = json.loads(json_str) + + print("\n解析结果:") + print(json.dumps(response, ensure_ascii=False, indent=2)) + + if response.get('success'): + print("\n✅ 测试成功!Cookie 文件路径参数工作正常") + if 'url' in response: + print(f"📎 笔记链接: {response['url']}") + else: + print(f"\n❌ 测试失败: {response.get('error')}") + break + except json.JSONDecodeError: + print("⚠️ 无法解析 JSON 输出") + + return result.returncode == 0 + + except Exception as e: + print(f"❌ 执行失败: {str(e)}") + return False + + +def test_quick_publish(): + """测试 quick_publish.py 脚本""" + + print("\n") + print("="*60) + print("测试 quick_publish.py 脚本") + print("="*60) + print() + + cmd = [ + sys.executable, + "quick_publish.py", + "【测试】快速发布脚本", + "测试 quick_publish.py 的简化调用方式", + "https://picsum.photos/800/600,https://picsum.photos/800/600", + "测试,快速发布,自动化", + "test_cookies.json" + ] + + print("执行命令:") + print(" ".join(cmd)) + print() + print("-"*60) + print() + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding='utf-8' + ) + + print(result.stdout) + + if result.stderr: + print("\n标准错误:") + print(result.stderr) + + return result.returncode == 0 + + except Exception as e: + print(f"❌ 执行失败: {str(e)}") + return False + + +if __name__ == "__main__": + print() + print("🧪 Cookie 文件路径支持测试") + print() + + # 检查 Cookie 文件是否存在 + import os + if not os.path.exists('test_cookies.json'): + print("❌ 错误: test_cookies.json 文件不存在") + print("请先创建 Cookie 文件") + sys.exit(1) + + print("✅ 找到 Cookie 文件: test_cookies.json") + print() + + # 测试1: xhs_publish.py 使用文件路径 + success1 = test_cookie_file_param() + + # 测试2: quick_publish.py + success2 = test_quick_publish() + + # 总结 + print() + print("="*60) + print("测试总结") + print("="*60) + print(f"xhs_publish.py (Cookie文件): {'✅ 通过' if success1 else '❌ 失败'}") + print(f"quick_publish.py: {'✅ 通过' if success2 else '❌ 失败'}") + print() + + if success1 and success2: + print("🎉 所有测试通过!") + else: + print("⚠️ 部分测试失败,请检查错误信息") diff --git a/backend/test_cookie_formats.py b/backend/test_cookie_formats.py new file mode 100644 index 0000000..f59d851 --- /dev/null +++ b/backend/test_cookie_formats.py @@ -0,0 +1,143 @@ +""" +测试两种 Cookie 格式支持 +""" +import asyncio +import json +from xhs_publish import XHSPublishService + + +# 格式1: Playwright 完整格式(从文件读取) +playwright_cookies = [ + { + "name": "a1", + "value": "19b11d16e24t3h3xmlvojbrw1cr55xwamiacluw3c50000231766", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1797066496, + "httpOnly": False, + "secure": False, + "sameSite": "Lax" + }, + { + "name": "web_session", + "value": "030037ae088f0acf2c81329d432e4a12fcb0ca", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1797066497.112584, + "httpOnly": True, + "secure": True, + "sameSite": "Lax" + } +] + +# 格式2: 键值对格式(从数据库读取) +keyvalue_cookies = { + "a1": "19b11d16e24t3h3xmlvojbrw1cr55xwamiacluw3c50000231766", + "abRequestId": "b273b4d0-3ef7-5b8f-bba4-2d19e63ad883", + "acw_tc": "0a4ae09717655304937202738e4b75c08d6eb78f2c8d30d7dc5a465429e1e6", + "gid": "yjDyyfyKiD6DyjDyyfyKd37EJ49qxqC61hlV0qSDFEySFS2822CE01888JqyWKK8Djdi8d2j", + "loadts": "1765530496548", + "sec_poison_id": "a589e333-c364-477c-9d14-53af8a1e7f1c", + "unread": "{%22ub%22:%22648455690000000014025d90%22%2C%22ue%22:%2264b34737000000002f0262f9%22%2C%22uc%22:22}", + "webBuild": "5.0.6", + "webId": "fdf2dccee4bec7534aff5581310c0e26", + "web_session": "030037ae088f0acf2c81329d432e4a12fcb0ca", + "websectiga": "984412fef754c018e472127b8effd174be8a5d51061c991aadd200c69a2801d6", + "xsecappid": "xhs-pc-web" +} + + +async def test_playwright_format(): + """测试 Playwright 格式""" + print("="*60) + print("测试 1: Playwright 格式(完整格式)") + print("="*60) + + try: + publisher = XHSPublishService(playwright_cookies) + print("✅ 初始化成功") + print(f" 转换后的 Cookie 数量: {len(publisher.cookies)}") + return True + except Exception as e: + print(f"❌ 初始化失败: {e}") + return False + + +async def test_keyvalue_format(): + """测试键值对格式""" + print("\n" + "="*60) + print("测试 2: 键值对格式(数据库格式)") + print("="*60) + + try: + publisher = XHSPublishService(keyvalue_cookies) + print("✅ 初始化成功") + print(f" 转换后的 Cookie 数量: {len(publisher.cookies)}") + + # 显示转换后的一个示例 + print("\n转换示例(第一个 Cookie):") + print(json.dumps(publisher.cookies[0], ensure_ascii=False, indent=2)) + + return True + except Exception as e: + print(f"❌ 初始化失败: {e}") + return False + + +async def test_from_file(): + """从文件读取测试""" + print("\n" + "="*60) + print("测试 3: 从 cookies.json 文件读取") + print("="*60) + + try: + with open('cookies.json', 'r', encoding='utf-8') as f: + cookies = json.load(f) + + publisher = XHSPublishService(cookies) + print("✅ 初始化成功") + print(f" Cookie 数量: {len(publisher.cookies)}") + return True + except FileNotFoundError: + print("⚠️ cookies.json 文件不存在,跳过此测试") + return None + except Exception as e: + print(f"❌ 初始化失败: {e}") + return False + + +async def main(): + print("\n🧪 Cookie 格式兼容性测试\n") + + # 测试1: Playwright格式 + result1 = await test_playwright_format() + + # 测试2: 键值对格式 + result2 = await test_keyvalue_format() + + # 测试3: 从文件读取 + result3 = await test_from_file() + + # 总结 + print("\n" + "="*60) + print("测试总结") + print("="*60) + print(f"Playwright 格式: {'✅ 通过' if result1 else '❌ 失败'}") + print(f"键值对格式: {'✅ 通过' if result2 else '❌ 失败'}") + if result3 is not None: + print(f"文件读取: {'✅ 通过' if result3 else '❌ 失败'}") + else: + print(f"文件读取: ⚠️ 跳过") + + if result1 and result2: + print("\n🎉 所有格式测试通过!") + print("\n💡 使用说明:") + print(" - 从 Python 脚本保存的 cookies.json → Playwright 格式") + print(" - 从数据库读取的 Cookie → 键值对格式") + print(" - 两种格式都可以正常使用!") + else: + print("\n⚠️ 部分测试失败") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/backend/test_network_images.py b/backend/test_network_images.py new file mode 100644 index 0000000..d3a5ae3 --- /dev/null +++ b/backend/test_network_images.py @@ -0,0 +1,80 @@ +""" +测试网络图片下载功能 +""" +import asyncio +import json +from xhs_publish import XHSPublishService + + +async def test_network_images(): + """测试网络图片功能""" + + print("="*50) + print("网络图片下载功能测试") + print("="*50) + print() + + # 1. 准备测试 Cookie(从 cookies.json 读取) + try: + with open('cookies.json', 'r', encoding='utf-8') as f: + cookies = json.load(f) + print(f"✅ 成功读取 {len(cookies)} 个 Cookie") + except FileNotFoundError: + print("❌ cookies.json 文件不存在") + print("请先运行登录获取 Cookie:") + print(" python xhs_cli.py login <手机号> <验证码>") + return + + # 2. 准备测试数据 + title = "【测试】网络图片发布测试" + content = """测试使用网络 URL 图片发布笔记 📸 + +本次测试使用了: +✅ 网络 URL 图片(picsum.photos) +✅ 自动下载功能 +✅ 临时文件管理 + +如果你看到这条笔记,说明网络图片功能正常!""" + + # 3. 使用网络图片 URL + images = [ + "https://picsum.photos/800/600?random=test1", + "https://picsum.photos/800/600?random=test2", + "https://picsum.photos/800/600?random=test3" + ] + + print(f"\n测试图片 URL:") + for i, url in enumerate(images, 1): + print(f" {i}. {url}") + + tags = ["测试", "网络图片", "自动发布"] + + # 4. 创建发布服务 + print("\n开始测试发布...") + publisher = XHSPublishService(cookies) + + # 5. 执行发布 + result = await publisher.publish( + title=title, + content=content, + images=images, + tags=tags, + cleanup=True # 自动清理临时文件 + ) + + # 6. 显示结果 + print("\n" + "="*50) + print("测试结果:") + print(json.dumps(result, ensure_ascii=False, indent=2)) + print("="*50) + + if result.get('success'): + print("\n✅ 测试成功!网络图片功能正常") + if 'url' in result: + print(f"📎 笔记链接: {result['url']}") + else: + print(f"\n❌ 测试失败: {result.get('error')}") + + +if __name__ == "__main__": + asyncio.run(test_network_images()) diff --git a/backend/test_publish.py b/backend/test_publish.py new file mode 100644 index 0000000..1186a0b --- /dev/null +++ b/backend/test_publish.py @@ -0,0 +1,90 @@ +""" +小红书发布功能测试脚本 +快速测试发布功能是否正常工作 +""" +import asyncio +import json +import os +from xhs_publish import XHSPublishService + + +async def test_publish(): + """测试发布功能""" + + # 1. 从 cookies.json 读取 Cookie + try: + with open('cookies.json', 'r', encoding='utf-8') as f: + cookies = json.load(f) + print(f"✅ 成功读取 {len(cookies)} 个 Cookie") + except FileNotFoundError: + print("❌ cookies.json 文件不存在") + print("请先运行登录获取 Cookie:") + print(" python xhs_cli.py login <手机号> <验证码>") + return + except Exception as e: + print(f"❌ 读取 cookies.json 失败: {e}") + return + + # 2. 准备测试数据 + title = "【测试】小红书发布功能测试" + content = """这是一条测试笔记 📝 + +今天测试一下自动发布功能是否正常~ + +如果你看到这条笔记,说明发布成功了! + +#测试 #自动化""" + + # 3. 准备测试图片(可选) + images = [] + test_image_dir = "temp_uploads" + if os.path.exists(test_image_dir): + for file in os.listdir(test_image_dir): + if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): + img_path = os.path.abspath(os.path.join(test_image_dir, file)) + images.append(img_path) + if len(images) >= 3: # 最多3张测试图片 + break + + if images: + print(f"✅ 找到 {len(images)} 张测试图片") + else: + print("⚠️ 未找到测试图片,将只发布文字") + + # 4. 准备标签 + tags = ["测试", "自动化发布"] + + # 5. 创建发布服务 + print("\n开始发布测试笔记...") + publisher = XHSPublishService(cookies) + + # 6. 执行发布 + result = await publisher.publish( + title=title, + content=content, + images=images if images else None, + tags=tags + ) + + # 7. 显示结果 + print("\n" + "="*50) + print("发布结果:") + print(json.dumps(result, ensure_ascii=False, indent=2)) + print("="*50) + + if result.get('success'): + print("\n✅ 测试成功!笔记已发布") + if 'url' in result: + print(f"📎 笔记链接: {result['url']}") + else: + print(f"\n❌ 测试失败: {result.get('error')}") + + +if __name__ == "__main__": + print("="*50) + print("小红书发布功能测试") + print("="*50) + print() + + # 运行测试 + asyncio.run(test_publish()) diff --git a/backend/xhs_cli.py b/backend/xhs_cli.py new file mode 100644 index 0000000..2297bee --- /dev/null +++ b/backend/xhs_cli.py @@ -0,0 +1,138 @@ +""" +小红书登录CLI工具 +供Go服务调用的命令行脚本 +""" +import sys +import json +import asyncio +import io +import os +from xhs_login import XHSLoginService + +# 设置标准输出为UTF-8编码 +if sys.platform == 'win32': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# 将xhs_login.py中的print输出重定向到stderr +original_print = print +def silent_print(*args, **kwargs): + """ 将print输出重定向到stderr """ + kwargs['file'] = sys.stderr + original_print(*args, **kwargs) + +# 替换全局print函数 +import builtins +builtins.print = silent_print + + +async def send_code(phone: str, country_code: str = "+86"): + """发送验证码""" + service = XHSLoginService() + try: + result = await service.send_verification_code(phone, country_code) + return result + finally: + await service.close_browser() + + +async def login(phone: str, code: str, country_code: str = "+86"): + """登录""" + service = XHSLoginService() + try: + # 先发送验证码(页面已打开) + await service.send_verification_code(phone, country_code) + # 等待一下确保验证码发送完成 + await asyncio.sleep(1) + # 执行登录 + result = await service.login(phone, code, country_code) + return result + finally: + await service.close_browser() + + +async def inject_cookies(cookies_json: str): + """注入Cookie并验证登录状态""" + service = XHSLoginService() + try: + cookies = json.loads(cookies_json) + await service.init_browser(cookies=cookies) + result = await service.verify_login_status() + return result + finally: + await service.close_browser() + + +def main(): + """主函数""" + if len(sys.argv) < 2: + print(json.dumps({ + "success": False, + "error": "缺少命令参数" + })) + sys.exit(1) + + command = sys.argv[1] + + try: + if command == "send_code": + # python xhs_cli.py send_code [country_code] + if len(sys.argv) < 3: + print(json.dumps({ + "success": False, + "error": "缺少手机号参数" + })) + sys.exit(1) + + phone = sys.argv[2] + country_code = sys.argv[3] if len(sys.argv) > 3 else "+86" + + result = asyncio.run(send_code(phone, country_code)) + sys.stdout.write(json.dumps(result, ensure_ascii=False)) + + elif command == "login": + # python xhs_cli.py login [country_code] + if len(sys.argv) < 4: + print(json.dumps({ + "success": False, + "error": "缺少手机号或验证码参数" + })) + sys.exit(1) + + phone = sys.argv[2] + code = sys.argv[3] + country_code = sys.argv[4] if len(sys.argv) > 4 else "+86" + + result = asyncio.run(login(phone, code, country_code)) + sys.stdout.write(json.dumps(result, ensure_ascii=False)) + + elif command == "inject_cookies": + # python xhs_cli.py inject_cookies + if len(sys.argv) < 3: + print(json.dumps({ + "success": False, + "error": "缺少Cookie参数" + })) + sys.exit(1) + + cookies_json = sys.argv[2] + result = asyncio.run(inject_cookies(cookies_json)) + sys.stdout.write(json.dumps(result, ensure_ascii=False)) + + else: + print(json.dumps({ + "success": False, + "error": f"未知命令: {command}" + })) + sys.exit(1) + + except Exception as e: + print(json.dumps({ + "success": False, + "error": str(e) + }, ensure_ascii=False)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/backend/xhs_login.py b/backend/xhs_login.py new file mode 100644 index 0000000..7b14bb1 --- /dev/null +++ b/backend/xhs_login.py @@ -0,0 +1,1414 @@ +""" +小红书登录服务 +使用 Playwright 模拟浏览器登录小红书 +""" +from playwright.async_api import async_playwright, Browser, Page, BrowserContext +from typing import Dict, Any, Optional +import asyncio +import json +import random +import unicodedata +import sys + + +class XHSLoginService: + """小红书登录服务""" + + def __init__(self): + self.playwright = None + self.browser: Optional[Browser] = None + self.context: Optional[BrowserContext] = None + self.page: Optional[Page] = None + self.current_phone = None + + async def init_browser(self, cookies: Optional[list] = None, proxy: Optional[str] = None, user_agent: Optional[str] = None): + """ + 初始化浏览器 + + Args: + cookies: 可选的Cookie列表,用于恢复登录状态 + proxy: 可选的代理地址,例如 http://user:pass@ip:port + user_agent: 可选的自定义User-Agent + """ + try: + self.playwright = await async_playwright().start() + + # 启动浏览器(使用chromium) + # headless=True 在服务器环境下运行,不显示浏览器界面 + launch_kwargs = { + "headless": True, # 服务器环境使用无头模式,本地调试可改为False + "args": ['--disable-blink-features=AutomationControlled'], + } + if proxy: + launch_kwargs["proxy"] = {"server": proxy} + + self.browser = await self.playwright.chromium.launch(**launch_kwargs) + + # 创建浏览器上下文,模拟真实用户 + context_kwargs = { + "viewport": {'width': 1280, 'height': 720}, + "user_agent": user_agent or 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + } + self.context = await self.browser.new_context(**context_kwargs) + + # 如果提供了Cookies,注入到浏览器上下文 + if cookies: + await self.context.add_cookies(cookies) + print(f"已注入 {len(cookies)} 个Cookie", file=sys.stderr) + + # 创建新页面 + self.page = await self.context.new_page() + + print("浏览器初始化成功", file=sys.stderr) + + except Exception as e: + print(f"浏览器初始化失败: {str(e)}", file=sys.stderr) + raise + + async def close_browser(self): + """关闭浏览器""" + try: + if self.page: + await self.page.close() + if self.context: + await self.context.close() + if self.browser: + await self.browser.close() + if self.playwright: + await self.playwright.stop() + print("浏览器已关闭", file=sys.stderr) + except Exception as e: + print(f"关闭浏览器异常: {str(e)}", file=sys.stderr) + + async def send_verification_code(self, phone: str, country_code: str = "+86") -> Dict[str, Any]: + """ + 发送验证码 + + Args: + phone: 手机号 + country_code: 国家区号 + + Returns: + Dict containing success status and error message if any + """ + try: + if not self.page: + await self.init_browser() + + self.current_phone = phone + + # 访问小红书创作者平台登录页(专门的登录页面) + print(f"正在访问小红书创作者平台登录页...", file=sys.stderr) + # 直接访问创作者平台登录页面,超时时间延长到60秒 + try: + await self.page.goto('https://creator.xiaohongshu.com/login', wait_until='domcontentloaded', timeout=60000) + print("✅ 页面加载完成", file=sys.stderr) + except Exception as e: + print(f"访问页面超时,但继续尝试: {str(e)}", file=sys.stderr) + + # 等待登录表单加载 + await asyncio.sleep(2) + print("✅ 已进入创作者平台登录页面", file=sys.stderr) + + # 根据记忆:小红书登录跳过协议复选框,无需处理 + # 但保留协议弹窗处理逻辑,以防页面变化 + try: + await asyncio.sleep(0.5) + agreement_selectors = [ + 'text="同意并继续"', + 'text="已阅读并同意"', + 'button:has-text("同意")', + 'button:has-text("继续")', + ] + + for selector in agreement_selectors: + try: + agreement_btn = await self.page.wait_for_selector(selector, timeout=1000) + if agreement_btn: + await agreement_btn.click() + print(f"✅ 已点击协议按钮: {selector}", file=sys.stderr) + await asyncio.sleep(0.5) + break + except Exception: + continue + except Exception as e: + print(f"无协议弹窗(正常情况)", file=sys.stderr) + + # 输入手机号 + try: + # 创作者平台登录页面的手机号输入框选择器 + print("查找手机号输入框...", file=sys.stderr) + phone_input_selectors = [ + 'input[placeholder="手机号"]', # 根据HTML精确匹配 + 'input.css-nt440g', # 根据HTML中的class + 'input[placeholder*="手机号"]', + 'input[autocomplete="on"][autofocus]', + 'input[type="tel"]', + 'input[type="text"]', + 'input[name*="phone"]', + 'input[name*="mobile"]', + ] + + phone_input = None + for selector in phone_input_selectors: + try: + phone_input = await self.page.wait_for_selector(selector, timeout=2000) + if phone_input: + print(f"✅ 找到手机号输入框: {selector}", file=sys.stderr) + break + except Exception: + continue + + if not phone_input: + # 打印页面信息用于调试 + print("⚠️ 未找到手机号输入框,打印页面信息...", file=sys.stderr) + print(f"页面URL: {self.page.url}", file=sys.stderr) + # 查找所有input元素 + inputs = await self.page.query_selector_all('input') + print(f"页面上找到 {len(inputs)} 个input元素", file=sys.stderr) + for i, inp in enumerate(inputs[:5]): + try: + placeholder = await inp.get_attribute('placeholder') + input_type = await inp.get_attribute('type') + name = await inp.get_attribute('name') + class_name = await inp.get_attribute('class') + print(f"Input {i+1}: type={input_type}, placeholder={placeholder}, name={name}, class={class_name}", file=sys.stderr) + except Exception: + pass + + return { + "success": False, + "error": "未找到手机号输入框,请检查页面是否正确加载" + } + + # 清空并输入手机号 + await phone_input.click() + await asyncio.sleep(0.2) + # 使用 Ctrl+A 全选后输入,更快速地清空 + await phone_input.press('Control+A') + await phone_input.type(phone, delay=50) # 模拟真实输入,每个字符50ms延迟 + print(f"✅ 已输入手机号: {phone}", file=sys.stderr) + await asyncio.sleep(0.3) + except Exception as e: + return { + "success": False, + "error": f"输入手机号失败: {str(e)}" + } + + # 点击发送验证码按钮 + try: + print("查找发送验证码按钮...", file=sys.stderr) + # 创作者平台登录页面的验证码按钮选择器 + send_code_btn = None + selectors = [ + 'text="发送验证码"', # 根据截图 + 'text="重新发送"', # 根据HTML + 'div.css-uyobdj', # 根据HTML中的class + 'button:has-text("发送验证码")', + 'button:has-text("重新发送")', + 'div:has-text("重新发送")', + 'text="获取验证码"', + 'button:has-text("获取验证码")', + ] + + for selector in selectors: + try: + send_code_btn = await self.page.wait_for_selector(selector, timeout=1500) + if send_code_btn: + print(f"✅ 找到发送验证码按钮: {selector}", file=sys.stderr) + break + except Exception: + continue + + if not send_code_btn: + # 尝试查找所有按钮和div元素 + print("⚠️ 未找到预定选择器,查找所有可点击元素...", file=sys.stderr) + buttons = await self.page.query_selector_all('button, div[class*="css-"]') + print(f"页面上找到 {len(buttons)} 个可能的元素", file=sys.stderr) + for i, btn in enumerate(buttons[:20]): # 查看前20个 + try: + text = await btn.inner_text() + if text and len(text.strip()) > 0: # 只打印有文本的 + classes = await btn.get_attribute('class') + print(f"元素 {i+1}: 文本=[{text.strip()}] class=[{classes}]", file=sys.stderr) + except Exception: + pass + + # 尝试根据文本内容查找 + print("尝试根据文本内容查找验证码按钮...", file=sys.stderr) + for btn in buttons: + try: + text = await btn.inner_text() + if text and ('验证码' in text or '发送' in text or '获取' in text or '重新' in text): + send_code_btn = btn + print(f"✅ 通过文本找到按钮: {text.strip()}", file=sys.stderr) + break + except Exception: + continue + + if send_code_btn: + await send_code_btn.click() + print("✅ 已点击发送验证码", file=sys.stderr) + await asyncio.sleep(2) # 等待验证码发送 + + # 点击后可能再次出现协议弹窗,再次处理 + try: + await asyncio.sleep(0.5) + agreement_selectors = [ + 'text="同意并继续"', + 'text="已阅读并同意"', + 'button:has-text("同意")', + ] + + for selector in agreement_selectors: + try: + agreement_btn = await self.page.wait_for_selector(selector, timeout=1000) + if agreement_btn: + await agreement_btn.click() + print(f"✅ 再次点击协议按钮: {selector}", file=sys.stderr) + await asyncio.sleep(0.5) + break + except Exception: + continue + except Exception as e: + print(f"无二次协议弹窗(正常)", file=sys.stderr) + else: + return { + "success": False, + "error": "未找到发送验证码按钮,请检查页面结构" + } + except Exception as e: + return { + "success": False, + "error": f"点击发送验证码失败: {str(e)}" + } + + # 检查是否需要滑块验证 + try: + await asyncio.sleep(1) + # 如果出现滑块,需要手动处理或使用自动化工具 + slider_selectors = [ + '.slider', + '.captcha', + '[class*="captcha"]', + '[class*="slider"]', + '[id*="captcha"]', + ] + + slider_found = False + for selector in slider_selectors: + try: + slider = await self.page.query_selector(selector) + if slider: + slider_found = True + print("⚠️ 检测到滑块验证,请手动完成...", file=sys.stderr) + # 等待用户手动完成滑块 + await asyncio.sleep(15) + break + except Exception: + pass + + if not slider_found: + print("✅ 未检测到滑块验证", file=sys.stderr) + except Exception as e: + print(f"滑块检测异常: {str(e)}", file=sys.stderr) + + print("\n✅ 验证码发送流程完成,请查看手机短信", file=sys.stderr) + print("请在小程序中输入收到的验证码并点击登录\n", file=sys.stderr) + + return { + "success": True, + "message": "验证码发送成功,请查看手机短信" + } + + except Exception as e: + error_msg = str(e) + print(f"\n\u274c 发送验证码异常: {error_msg}", file=sys.stderr) + print(f"当前页面URL: {self.page.url if self.page else 'N/A'}", file=sys.stderr) + + # 打印调试信息 + if self.page: + try: + print("尝试截图保存错误状态...", file=sys.stderr) + await self.page.screenshot(path='error_screenshot.png') + print("✅ 错误状态已截图保存到 error_screenshot.png", file=sys.stderr) + except Exception: + pass + + return { + "success": False, + "error": error_msg + } + + async def login(self, phone: str, code: str, country_code: str = "+86") -> Dict[str, Any]: + """ + 使用验证码登录 + + Args: + phone: 手机号 + code: 验证码 + country_code: 国家区号 + + Returns: + Dict containing login result, user info and cookies + """ + try: + if not self.page: + return { + "success": False, + "error": "页面未初始化,请先发送验证码" + } + + # 输入验证码 + try: + print("查找验证码输入框...", file=sys.stderr) + code_input_selectors = [ + 'input[placeholder="验证码"]', # 根据HTML精确匹配 + 'input.css-1ge5flv', # 根据HTML中的class + 'input[placeholder*="验证码"]', + 'input[type="text"]:not([placeholder*="手机"])', + ] + + code_input = None + for selector in code_input_selectors: + try: + code_input = await self.page.wait_for_selector(selector, timeout=2000) + if code_input: + print(f"✅ 找到验证码输入框: {selector}", file=sys.stderr) + break + except Exception: + continue + + if not code_input: + return { + "success": False, + "error": "未找到验证码输入框" + } + + await code_input.click() + await asyncio.sleep(0.2) + await code_input.press('Control+A') + await code_input.type(code, delay=50) + print(f"✅ 已输入验证码: {code}", file=sys.stderr) + await asyncio.sleep(0.5) + except Exception as e: + return { + "success": False, + "error": f"输入验证码失败: {str(e)}" + } + + # 点击登录按钮 + try: + print("查找登录按钮...", file=sys.stderr) + login_btn_selectors = [ + 'button.beer-login-btn', # 根据HTML中的class + 'button.css-y4h4ay', # 根据HTML + 'button:has-text("登 录")', # 注意有空格 + 'button:has-text("登录")', + 'text="登 录"', + 'text="登录"', + '.login-button', + ] + + login_btn = None + for selector in login_btn_selectors: + try: + login_btn = await self.page.wait_for_selector(selector, timeout=2000) + if login_btn: + print(f"✅ 找到登录按钮: {selector}", file=sys.stderr) + break + except Exception: + continue + + if not login_btn: + # 打印所有按钮用于调试 + print("⚠️ 未找到登录按钮,打印所有按钮...", file=sys.stderr) + buttons = await self.page.query_selector_all('button') + print(f"页面上找到 {len(buttons)} 个按钮", file=sys.stderr) + for i, btn in enumerate(buttons[:10]): + try: + text = await btn.inner_text() + classes = await btn.get_attribute('class') + print(f"按钮 {i+1}: 文本=[{text.strip()}] class=[{classes}]", file=sys.stderr) + except Exception: + pass + + return { + "success": False, + "error": "未找到登录按钮" + } + + await login_btn.click() + print("✅ 已点击登录按钮", file=sys.stderr) + + # 等待一下,检查是否出现协议弹窗 + await asyncio.sleep(1) + + # 处理登录后可能出现的协议弹窗 + try: + agreement_popup_selectors = [ + 'text="同意并继续"', + 'button:has-text("同意并继续")', + 'text="已阅读并同意"', + ] + + for selector in agreement_popup_selectors: + try: + popup_btn = await self.page.wait_for_selector(selector, timeout=2000) + if popup_btn: + await popup_btn.click() + print(f"✅ 已点击登录后的协议弹窗: {selector}", file=sys.stderr) + await asyncio.sleep(1) + break + except Exception: + continue + except Exception as e: + print(f"无登录后协议弹窗(正常)", file=sys.stderr) + + # 等待登录完成 + await asyncio.sleep(3) + except Exception as e: + return { + "success": False, + "error": f"点击登录按钮失败: {str(e)}" + } + + # 检查是否登录成功 + try: + # 等待页面跳转或出现用户信息 + await self.page.wait_for_selector('.user-info, .avatar, [class*="user"]', timeout=10000) + print("登录成功", file=sys.stderr) + except Exception as e: + return { + "success": False, + "error": f"登录验证失败,可能验证码错误: {str(e)}" + } + + # 获取Cookies + cookies = await self.context.cookies() + + # 注意:这里返回两种格式 + # 1. cookies_dict: 键值对格式(用于 API 返回,方便前端展示) + # 2. cookies: Playwright 完整格式(用于保存文件和后续使用) + cookies_dict = {cookie['name']: cookie['value'] for cookie in cookies} + + # 打印重要的Cookies + print(f"\n========== Cookies 信息 ==========", file=sys.stderr) + print(f"共获取到 {len(cookies)} 个Cookie", file=sys.stderr) + + # 打印所有Cookie名称 + print(f"\nCookie名称列表: {list(cookies_dict.keys())}", file=sys.stderr) + + # 完整打印所有Cookies(键值对格式) + print(f"\n完整Cookies内容(键值对格式):", file=sys.stderr) + for name, value in cookies_dict.items(): + print(f" {name}: {value}", file=sys.stderr) + + print(f"\n================================\n", file=sys.stderr) + + # 获取用户信息(从页面或API) + user_info = {} + try: + # 等待页面完全加载 + await asyncio.sleep(2) + + # 尝试从localStorage获取用户信息 + storage = await self.page.evaluate('() => JSON.stringify(localStorage)') + storage_dict = json.loads(storage) + + print(f"LocalStorage内容: {list(storage_dict.keys())}", file=sys.stderr) + + # 提取有用的localStorage数据 + useful_keys = ['b1', 'b1b1', 'p1', 'xhs_context_networkQuality'] + for key in useful_keys: + if key in storage_dict: + try: + # 尝试解析JSON + value = storage_dict[key] + if value and value.strip(): + user_info[key] = json.loads(value) if value.startswith('{') or value.startswith('[') else value + except: + user_info[key] = storage_dict[key] + + # 小红书可能将用户信息存储在特定键中 + for key, value in storage_dict.items(): + if 'user' in key.lower(): + try: + user_data = json.loads(value) + user_info['user_data'] = user_data + print(f"从localStorage获取用户信息 - key: {key}", file=sys.stderr) + break + except: + pass + + # 尝试从window对象获取用户信息(更完整的方式) + try: + # 获取window.__INITIAL_STATE__或其他可能的用户信息对象 + window_data = await self.page.evaluate(''' + () => { + const result = {}; + + // 尝试获取常见的用户信息存储位置 + if (window.__INITIAL_STATE__) result.initial_state = window.__INITIAL_STATE__; + if (window.user) result.user = window.user; + if (window.userInfo) result.userInfo = window.userInfo; + if (window.__APOLLO_STATE__) result.apollo_state = window.__APOLLO_STATE__; + + // 尝试从Redux store获取 + if (window.__REDUX_DEVTOOLS_EXTENSION__) { + try { + const state = window.__REDUX_DEVTOOLS_EXTENSION__.extractState(); + if (state) result.redux_state = state; + } catch(e) {} + } + + return result; + } + ''') + + if window_data: + user_info['window_data'] = window_data + print(f"从window对象获取数据,包含键: {list(window_data.keys())}", file=sys.stderr) + except Exception as e: + print(f"从window对象获取失败: {str(e)}", file=sys.stderr) + + # 尝试从页面元素获取用户信息 + if not user_info.get('username'): + try: + # 尝试获取用户昵称 + username_el = await self.page.query_selector('.user-name, .username, [class*="user"][class*="name"]') + if username_el: + username = await username_el.inner_text() + user_info['username'] = username + print(f"从页面获取用户名: {username}", file=sys.stderr) + except: + pass + + print(f"最终获取到用户信息字段: {list(user_info.keys())}", file=sys.stderr) + + except Exception as e: + print(f"获取用户信息失败: {str(e)}", file=sys.stderr) + + # 获取当前URL(可能包含token等信息) + current_url = self.page.url + print(f"当前URL: {current_url}", file=sys.stderr) + + # 将Cookies保存到文件(Playwright 完整格式) + try: + with open('cookies.json', 'w', encoding='utf-8') as f: + json.dump(cookies, f, ensure_ascii=False, indent=2) + print("✅ 已保存 Cookies 到 cookies.json 文件(Playwright 格式)", file=sys.stderr) + print(f" 文件包含 {len(cookies)} 个完整的 Cookie 对象", file=sys.stderr) + except Exception as e: + print(f"保存Cookies文件失败: {str(e)}", file=sys.stderr) + + return { + "success": True, + "user_info": user_info, + "cookies": cookies_dict, # API 返回:键值对格式(方便前端展示) + "cookies_full": cookies, # API 返回:完整格式(可选,供需要者使用) + "url": current_url + } + + except Exception as e: + print(f"登录异常: {str(e)}", file=sys.stderr) + return { + "success": False, + "error": str(e) + } + + async def get_user_profile(self) -> Dict[str, Any]: + """ + 获取用户详细信息 + 登录成功后可以调用此方法获取更多用户信息 + """ + try: + if not self.page: + return { + "success": False, + "error": "页面未初始化" + } + + # 访问用户主页 + await self.page.goto('https://www.xiaohongshu.com/user/profile', wait_until='networkidle') + await asyncio.sleep(2) + + # 这里可以根据实际需求抓取用户信息 + # 示例:获取用户昵称、头像等 + + return { + "success": True, + "profile": {} + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + + async def verify_login_status(self) -> Dict[str, Any]: + """ + 验证当前登录状态 + 访问小红书创作者平台检查是否已登录 + + Returns: + Dict containing login status and user info if logged in + """ + try: + if not self.page: + return { + "success": False, + "logged_in": False, + "error": "页面未初始化" + } + + print("正在验证登录状态...", file=sys.stderr) + + # 访问小红书创作者平台(而不是首页) + print("访问创作者平台...", file=sys.stderr) + try: + await self.page.goto('https://creator.xiaohongshu.com/', wait_until='domcontentloaded', timeout=60000) + await asyncio.sleep(2) # 等待页面加载 + print(f"✅ 已访问创作者平台,当前URL: {self.page.url}", file=sys.stderr) + except Exception as e: + print(f"访问创作者平台失败: {str(e)}", file=sys.stderr) + return { + "success": False, + "logged_in": False, + "error": f"访问创作者平台失败: {str(e)}" + } + + # 检查是否被重定向到登录页(未登录状态) + current_url = self.page.url + if 'login' in current_url.lower(): + print("❌ 未登录状态(被重定向到登录页)", file=sys.stderr) + return { + "success": True, + "logged_in": False, + "cookie_expired": True, # 标识Cookie已失效 + "message": "Cookie已失效或未登录", + "url": current_url + } + + # 如果在创作者平台主页,说明已登录 + if 'creator.xiaohongshu.com' in current_url and 'login' not in current_url.lower(): + print("✅ 已登录状态(成功访问创作者平台)", file=sys.stderr) + + # 获取当前的Cookies + cookies = await self.context.cookies() + + # 转换为键值对格式(用于 API 返回) + cookies_dict = {cookie['name']: cookie['value'] for cookie in cookies} + + # 尝试获取用户信息 + user_info = {} + try: + storage = await self.page.evaluate('() => JSON.stringify(localStorage)') + storage_dict = json.loads(storage) + + # 提取有用的localStorage数据 + for key, value in storage_dict.items(): + if 'user' in key.lower(): + try: + user_data = json.loads(value) + user_info['user_data'] = user_data + break + except: + pass + except Exception as e: + print(f"获取用户信息失败: {str(e)}", file=sys.stderr) + + return { + "success": True, + "logged_in": True, + "message": "Cookie有效,已登录", + "cookies": cookies_dict, # 键值对格式(前端展示) + "cookies_full": cookies, # Playwright完整格式(数据库存储/脚本使用) + "user_info": user_info, + "url": current_url + } + else: + print("❌ 未登录状态(URL异常)", file=sys.stderr) + return { + "success": True, + "logged_in": False, + "cookie_expired": True, # 标识Cookie已失效 + "message": "Cookie已失效或未登录", + "url": current_url + } + + except Exception as e: + print(f"验证登录状态异常: {str(e)}", file=sys.stderr) + return { + "success": False, + "logged_in": False, + "error": str(e) + } + + def _calculate_title_width(self, title: str) -> int: + width = 0 + for ch in title: + if unicodedata.east_asian_width(ch) in ("F", "W"): + width += 2 + else: + width += 1 + return width + + async def publish_note(self, title: str, content: str, images: list = None, topics: list = None, cookies: list = None) -> Dict[str, Any]: + """ + 发布笔记(支持Cookie注入) + + Args: + title: 笔记标题 + content: 笔记内容 + images: 图片路径列表(本地文件路径) + topics: 话题标签列表 + cookies: 可选的Cookie列表(Playwright完整格式),用于注入登录态 + + Returns: + Dict containing publish result + """ + try: + # ========== 内容验证 ========== + print("\n========== 开始验证发布内容 ==========", file=sys.stderr) + + # 1. 验证标题长度 + if not title or len(title.strip()) == 0: + return { + "success": False, + "error": "标题不能为空", + "error_type": "validation_error" + } + + title = title.strip() + title_width = self._calculate_title_width(title) + if title_width > 40: + return { + "success": False, + "error": f"标题超出限制:当前宽度 {title_width},平台限制 40", + "error_type": "validation_error" + } + print(f"✅ 标题验证通过: 宽度 {title_width}/40", file=sys.stderr) + + # 2. 验证内容长度 + if not content or len(content.strip()) == 0: + return { + "success": False, + "error": "内容不能为空", + "error_type": "validation_error" + } + + content_length = len(content) + if content_length > 1000: + return { + "success": False, + "error": f"内容超出限制:当前 {content_length} 个字,最多 1000 个字", + "error_type": "validation_error" + } + print(f"✅ 内容验证通过: {content_length}/1000 个字", file=sys.stderr) + + # 3. 验证图片数量 + images_count = len(images) if images else 0 + if images_count == 0: + return { + "success": False, + "error": "至少需要 1 张图片", + "error_type": "validation_error" + } + if images_count > 18: + return { + "success": False, + "error": f"图片超出限制:当前 {images_count} 张,最多 18 张", + "error_type": "validation_error" + } + print(f"✅ 图片数量验证通过: {images_count}/18 张", file=sys.stderr) + + print("✅ 所有验证通过,开始发布\n", file=sys.stderr) + + # ========== 开始发布流程 ========== + # 如果提供了Cookie,初始化浏览器并注入Cookie + if cookies: + print("✅ 检测到Cookie,将注入到浏览器", file=sys.stderr) + if not self.page: + await self.init_browser(cookies) + else: + # 如果浏览器已存在,添加Cookie + await self.context.add_cookies(cookies) + print(f"✅ 已注入 {len(cookies)} 个Cookie", file=sys.stderr) + + if not self.page: + return { + "success": False, + "error": "页面未初始化,请先登录或提供Cookie" + } + + print("\n========== 开始发布笔记 ==========", file=sys.stderr) + print(f"标题: {title}", file=sys.stderr) + print(f"内容: {content[:50]}..." if len(content) > 50 else f"内容: {content}", file=sys.stderr) + print(f"图片数量: {len(images) if images else 0}", file=sys.stderr) + print(f"话题: {topics if topics else []}", file=sys.stderr) + + # 访问官方创作者平台发布页面(带有Cookie的状态下直接访问) + print("访问创作者平台图文发布页面...", file=sys.stderr) + try: + await self.page.goto('https://creator.xiaohongshu.com/publish/publish?source=official', + wait_until='networkidle', timeout=60000) + # 等待页面核心元素加载完成,而不是固定时间 + await asyncio.sleep(3) # 增加等待时间确保JavaScript完全执行 + + # 点击「上传图文」tab,符合平台规范 + try: + print("查找“上传图文”tab...", file=sys.stderr) + tab_selectors = [ + 'button:has-text("上传图文")', + 'div:has-text("上传图文")', + 'text="上传图文"', + ] + tab_clicked = False + for selector in tab_selectors: + try: + tab = await self.page.wait_for_selector(selector, timeout=3000) + if tab: + await tab.click() + tab_clicked = True + print(f"✅ 已点击“上传图文”tab: {selector}", file=sys.stderr) + await asyncio.sleep(1) + break + except Exception: + continue + if not tab_clicked: + print("⚠️ 未找到“上传图文”tab,将继续使用当前页面进行发布", file=sys.stderr) + except Exception as e: + print(f"点击“上传图文”tab时异常: {str(e)}", file=sys.stderr) + + print("✅ 已进入图文发布页面", file=sys.stderr) + except Exception as e: + return { + "success": False, + "error": f"访问发布页面失败: {str(e)}" + } + + # 上传图片(如果有) + if images and len(images) > 0: + try: + print(f"开始上传 {len(images)} 张图片...", file=sys.stderr) + await asyncio.sleep(1) # 等待图文上传页面完全加载 + + # 直接查找图片上传控件(已经在图文上传页面了) + print("查找图片上传控件...", file=sys.stderr) + upload_selectors = [ + 'input[type="file"][accept*="image"]', + 'input[type="file"]', + 'input[accept*="image"]', + '.upload-input', + '[class*="upload"] input[type="file"]', + ] + + file_input = None + for selector in upload_selectors: + try: + file_input = await self.page.wait_for_selector(selector, timeout=3000) + if file_input: + print(f"找到文件上传控件: {selector}", file=sys.stderr) + break + except Exception: + continue + + if file_input: + # 批量上传图片 + images_count = len(images) + print(f"正在上传 {images_count} 张图片: {images}", file=sys.stderr) + await file_input.set_input_files(images) + print(f"已设置文件路径,等待上传...", file=sys.stderr) + + # 等待所有图片上传完成(检测多张图片) + upload_success = False + uploaded_count = 0 + + for i in range(20): # 最多等待20秒(多图需要更长时间) + await asyncio.sleep(1) + try: + # 查找所有已上传的图片缩略图 + uploaded_images = await self.page.query_selector_all('img[src*="blob:"]') + if not uploaded_images: + # 尝试其他选择器 + uploaded_images = await self.page.query_selector_all('[class*="image"][class*="item"] img') + + uploaded_count = len(uploaded_images) + + if uploaded_count > 0: + print(f"✅ 已上传 {uploaded_count}/{images_count} 张图片", file=sys.stderr) + + # 检查是否所有图片都已上传 + if uploaded_count >= images_count: + print(f"✅ 所有图片上传完成!共 {uploaded_count} 张", file=sys.stderr) + upload_success = True + break + + print(f"等待图片上传... {uploaded_count}/{images_count} ({i+1}/20秒)", file=sys.stderr) + except Exception as e: + print(f"检测上传状态异常: {e}", file=sys.stderr) + pass + + if upload_success: + print(f"✅ 图片上传成功!共 {uploaded_count} 张", file=sys.stderr) + await asyncio.sleep(2) # 额外等待2秒确保完全上传 + else: + print(f"⚠️ 仅检测到 {uploaded_count}/{images_count} 张图片,但继续执行...", file=sys.stderr) + else: + print("未找到隐藏的file input,尝试查找可点击的上传区域...", file=sys.stderr) + + # 调试: 打印页面上所有包含upload的元素 + try: + all_elements = await self.page.query_selector_all('[class*="upload"], [id*="upload"]') + print(f"\u627e到 {len(all_elements)} 个包含upload的元素", file=sys.stderr) + for i, el in enumerate(all_elements[:10]): # 只看前10个 + try: + tag_name = await el.evaluate('el => el.tagName') + class_name = await el.evaluate('el => el.className') + print(f" [{i+1}] {tag_name} class='{class_name}'", file=sys.stderr) + except Exception: + pass + except Exception: + pass + + # 尝试点击上传区域或按钮 + upload_area_selectors = [ + '[class*="upload"][class*="box"]', + '[class*="upload"][class*="area"]', + '[class*="upload"][class*="wrapper"]', + '.upload-zone', + 'div:has-text("上传图片")', + 'div:has-text("点击上传")', + 'button:has-text("上传图片")', + ] + + clicked = False + for selector in upload_area_selectors: + try: + area = await self.page.wait_for_selector(selector, timeout=2000) + if area: + print(f"找到上传区域: {selector}", file=sys.stderr) + await area.click() + await asyncio.sleep(0.5) + # 点击后再次查找file input + file_input = await self.page.wait_for_selector('input[type="file"]', timeout=2000) + if file_input: + images_count = len(images) + print(f"正在上传 {images_count} 张图片: {images}", file=sys.stderr) + await file_input.set_input_files(images) + print(f"已设置文件路径,等待上传...", file=sys.stderr) + + # 等待所有图片上传完成 + upload_success = False + uploaded_count = 0 + + for i in range(20): + await asyncio.sleep(1) + try: + uploaded_images = await self.page.query_selector_all('img[src*="blob:"]') + if not uploaded_images: + uploaded_images = await self.page.query_selector_all('[class*="image"][class*="item"] img') + + uploaded_count = len(uploaded_images) + + if uploaded_count > 0: + print(f"✅ 已上传 {uploaded_count}/{images_count} 张图片", file=sys.stderr) + + if uploaded_count >= images_count: + print(f"✅ 所有图片上传完成!共 {uploaded_count} 张", file=sys.stderr) + upload_success = True + break + + print(f"等待图片上传... {uploaded_count}/{images_count} ({i+1}/20秒)", file=sys.stderr) + except Exception as e: + print(f"检测上传状态异常: {e}", file=sys.stderr) + pass + + if upload_success: + print(f"✅ 图片上传成功!共 {uploaded_count} 张", file=sys.stderr) + await asyncio.sleep(2) + else: + print(f"⚠️ 仅检测到 {uploaded_count}/{images_count} 张图片,但继续执行...", file=sys.stderr) + + clicked = True + break + except Exception: + continue + + if not clicked: + print("⚠️ 未找到任何上传控件,跳过图片上传", file=sys.stderr) + + except Exception as e: + print(f"上传图片失败: {str(e)}", file=sys.stderr) + # 不中断流程,继续发布文字 + + # 输入标题和内容 + try: + print("开始输入文字内容...", file=sys.stderr) + + # 查找标题输入框(使用显式等待确保元素可交互) + title_selectors = [ + 'input[placeholder*="标题"]', + 'input[placeholder*="填写标题"]', + 'input[placeholder*="曝光"]', + '.title-input', + '[class*="title"] input', + ] + + title_input = None + for selector in title_selectors: + try: + # 等待元素可见且可编辑 + title_input = await self.page.wait_for_selector( + selector, + state='visible', # 确保元素可见 + timeout=5000 # 增加超时时间 + ) + if title_input: + # 确保元素可交互(等待一小段时间让JS初始化完成) + await asyncio.sleep(0.5) + print(f"找到标题输入框: {selector}", file=sys.stderr) + break + except Exception as e: + print(f"选择器 {selector} 未找到: {str(e)}", file=sys.stderr) + continue + + if title_input: + await title_input.click() + await asyncio.sleep(0.3) + await title_input.fill(title) + print(f"已输入标题: {title}", file=sys.stderr) + else: + print("未找到标题输入框,可能不需要单独标题", file=sys.stderr) + + # 查找内容输入框(正文)(使用显式等待确保元素可交互) + content_selectors = [ + 'div[contenteditable="true"]', + 'div[placeholder*="正文"]', + 'div[placeholder*="输入正文"]', + 'textarea[placeholder*="输入正文"]', + 'textarea[placeholder*="填写笔记内容"]', + 'textarea[placeholder*="笔记内容"]', + '[class*="content"] div[contenteditable="true"]', + '[class*="editor"] div[contenteditable="true"]', + 'textarea', + ] + + content_input = None + for selector in content_selectors: + try: + # 等待元素可见且可编辑 + content_input = await self.page.wait_for_selector( + selector, + state='visible', # 确保元素可见 + timeout=5000 # 增加超时时间 + ) + if content_input: + # 确保元素可交互 + await asyncio.sleep(0.5) + print(f"找到内容输入框: {selector}", file=sys.stderr) + break + except Exception as e: + print(f"选择器 {selector} 未找到: {str(e)}", file=sys.stderr) + continue + + if content_input: + # 清空并输入内容 + await content_input.click() + await asyncio.sleep(0.5) + + # 检查是否是contenteditable元素 + try: + is_contenteditable = await content_input.evaluate('el => el.getAttribute("contenteditable") === "true"') + if is_contenteditable: + # 使用innerText设置内容 + await content_input.evaluate(f'el => el.innerText = {json.dumps(content)}') + else: + # 普通textarea + await content_input.fill(content) + except Exception: + # 如果判断失败,尝试直接fill + await content_input.fill(content) + + print("已输入笔记内容", file=sys.stderr) + await asyncio.sleep(0.5) + + # 添加话题标签 + if topics: + print(f"添加话题标签: {topics}", file=sys.stderr) + for topic in topics: + # 在内容末尾添加话题 + topic_text = f" #{topic}" + try: + is_contenteditable = await content_input.evaluate('el => el.getAttribute("contenteditable") === "true"') + if is_contenteditable: + await content_input.evaluate(f'el => el.innerText += {json.dumps(topic_text)}') + else: + current_value = await content_input.evaluate('el => el.value') + await content_input.fill(current_value + topic_text) + except Exception: + # 如果添加失败,继续下一个 + pass + print(f"已添加 {len(topics)} 个话题标签", file=sys.stderr) + + await asyncio.sleep(1) + + # 单独在话题输入框中模拟人类方式输入标签 + if topics: + print("尝试在话题输入框中逐个输入标签...", file=sys.stderr) + tag_input_selectors = [ + 'input[placeholder*="话题"]', + 'input[placeholder*="#"]', + 'input[placeholder*="添加标签"]', + '[class*="tag"] input', + '[class*="topic"] input', + ] + tag_input = None + for selector in tag_input_selectors: + try: + tag_input = await self.page.wait_for_selector(selector, timeout=3000) + if tag_input: + print(f"找到话题输入框: {selector}", file=sys.stderr) + break + except Exception: + continue + if tag_input: + for topic in topics: + try: + await tag_input.click() + await asyncio.sleep(0.3) + # 清空已有内容 + try: + await tag_input.fill("") + except Exception: + pass + await tag_input.type("#" + topic, delay=50) + await asyncio.sleep(0.8) + # 等待联想列表并选择第一项 + suggestion = None + suggestion_selectors = [ + '[class*="suggest"] li', + '[role="listbox"] li', + '[class*="dropdown"] li', + ] + for s_selector in suggestion_selectors: + try: + suggestion = await self.page.query_selector(s_selector) + if suggestion: + break + except Exception: + continue + if suggestion: + await suggestion.click() + print(f"✅ 已选择联想话题: {topic}", file=sys.stderr) + else: + # 没有联想列表时,通过回车确认 + await tag_input.press("Enter") + print(f"✅ 未找到联想列表,使用回车确认话题: {topic}", file=sys.stderr) + await asyncio.sleep(0.5) + except Exception as e: + print(f"添加话题 {topic} 到输入框失败: {str(e)}", file=sys.stderr) + else: + print("⚠️ 未找到话题输入框,已退回到在正文中追加 #话题 的方式", file=sys.stderr) + else: + return { + "success": False, + "error": "未找到内容输入框" + } + + except Exception as e: + return { + "success": False, + "error": f"输入内容失败: {str(e)}" + } + + # 模拟简单的人类滚动行为 + try: + for _ in range(3): + await self.page.mouse.wheel(0, random.randint(200, 500)) + await asyncio.sleep(random.uniform(0.3, 0.8)) + except Exception: + pass + + # 点击发布按钮 + try: + print("查找发布按钮...", file=sys.stderr) + submit_selectors = [ + 'button:has-text("发布笔记")', + 'button:has-text("发布")', + 'text="发布笔记"', + 'text="发布"', + '.publish-btn', + '.submit-btn', + ] + + submit_btn = None + for selector in submit_selectors: + try: + submit_btn = await self.page.wait_for_selector(selector, timeout=3000) + if submit_btn: + # 检查按钮是否可点击 + is_disabled = await submit_btn.evaluate('el => el.disabled') + if not is_disabled: + print(f"找到发布按钮: {selector}", file=sys.stderr) + break + else: + submit_btn = None + except Exception: + continue + + if submit_btn: + # 设置网络监听,捕获发布接口响应 + note_id = None + share_link = None + + async def handle_response(response): + nonlocal note_id, share_link + try: + # 监听发布笔记的API响应 + if '/web_api/sns/v2/note' in response.url: + print(f"✅ 捕获到发布API响应: {response.url}", file=sys.stderr) + if response.status == 200: + try: + data = await response.json() + print(f"API响应数据: {json.dumps(data, ensure_ascii=False)}", file=sys.stderr) + + if data.get('success') and data.get('data'): + note_id = data['data'].get('id') + # 优先使用share_link,如果没有则使用note_id拼接 + if 'share_link' in data: + share_link = data['share_link'] + print(f"✅ 获取到笔记链接: {share_link}", file=sys.stderr) + elif note_id: + share_link = f"https://www.xiaohongshu.com/discovery/item/{note_id}" + print(f"✅ 根据ID生成笔记链接: {share_link}", file=sys.stderr) + except Exception as e: + print(f"解析API响应失败: {str(e)}", file=sys.stderr) + except Exception as e: + print(f"处理响应失败: {str(e)}", file=sys.stderr) + + # 添加响应监听器 + self.page.on('response', handle_response) + + await submit_btn.click() + print("✅ 已点击发布按钮", file=sys.stderr) + await asyncio.sleep(3) # 等待更长时间以捕获API响应 + + # 检查是否出现社区规范限制提示 + print("检查是否有社区规范限制...", file=sys.stderr) + try: + # 尝试查找各种可能的错误提示 + error_selectors = [ + 'text="因违反社区规范禁止发笔记"', + 'text*="违反社区规范"', + 'text*="禁止发布"', + 'text*="账号被限制"', + 'text*="账号异常"', + '.error-tip', + '.warning-tip', + '[class*="error"]', + '[class*="warning"]', + ] + + for selector in error_selectors: + try: + error_el = await self.page.wait_for_selector(selector, timeout=2000) + if error_el: + error_text = await error_el.inner_text() + print(f"❌ 检测到错误提示: {error_text}", file=sys.stderr) + return { + "success": False, + "error": f"发布失败: {error_text}", + "error_type": "community_violation", # 标记错误类型 + "message": error_text + } + except Exception: + continue + except Exception as e: + print(f"检查错误提示异常: {str(e)}", file=sys.stderr) + + # 检查是否发布成功 + print("检查发布结果...", file=sys.stderr) + try: + await asyncio.sleep(2) # 等待发布完成 + + # 如果捕获到了真实的笔记链接,直接返回 + if share_link: + print(f"✅ 发布成功,获取到笔记链接: {share_link}", file=sys.stderr) + return { + "success": True, + "message": "笔记发布成功", + "data": { + "note_id": note_id, + "note_url": share_link + }, + "url": share_link # 保持兼容性 + } + + # 如果没有捕获到,使用原来的逻辑 + # 等待发布成功的提示或页面跳转 + success_selectors = [ + 'text="发布成功"', + 'text="发布完成"', + 'text*="成功"', + '.success-tip', + '.success-message', + ] + + publish_success = False + for selector in success_selectors: + try: + success_el = await self.page.wait_for_selector(selector, timeout=3000) + if success_el: + success_text = await success_el.inner_text() + print(f"✅ 检测到发布成功提示: {success_text}", file=sys.stderr) + publish_success = True + break + except Exception: + continue + + # 如果没有明确的成功提示,检查URL是否变化 + current_url = self.page.url + if not publish_success: + # 如果还在发布页面,可能是发布失败 + if 'publish' in current_url.lower(): + print("⚠️ 未检测到成功提示,但继续执行", file=sys.stderr) + else: + print("✅ URL已变化,似乎发布成功", file=sys.stderr) + publish_success = True + + print(f"发布后URL: {current_url}", file=sys.stderr) + + return { + "success": True, + "message": "笔记发布成功", + "url": current_url + } + except Exception as e: + print(f"检查发布结果异常: {str(e)}", file=sys.stderr) + # 即使检查异常,也返回成功(因为按钮已点击) + return { + "success": True, + "message": "笔记已提交发布,但未能确认结果", + "url": self.page.url + } + else: + return { + "success": False, + "error": "未找到可用的发布按钮,可能内容不完整" + } + + except Exception as e: + return { + "success": False, + "error": f"点击发布按钮失败: {str(e)}" + } + + except Exception as e: + print(f"发布笔记异常: {str(e)}", file=sys.stderr) + return { + "success": False, + "error": str(e) + } diff --git a/backend/xhs_publish.py b/backend/xhs_publish.py new file mode 100644 index 0000000..95ca6ce --- /dev/null +++ b/backend/xhs_publish.py @@ -0,0 +1,571 @@ +""" +小红书笔记发布脚本 +提供Cookie、文案(标题、内容、标签、图片)完成发布操作 +支持本地图片路径和网络URL图片 +""" +import sys +import json +import asyncio +import io +import os +import re +import aiohttp +import hashlib +import unicodedata +from typing import List, Dict, Any, Union +from pathlib import Path +from xhs_login import XHSLoginService + + +class XHSPublishService: + """小红书笔记发布服务""" + + def __init__(self, cookies: Union[List[Dict[str, Any]], Dict[str, str]], proxy: str | None = None, user_agent: str | None = None): + """ + 初始化发布服务 + + Args: + cookies: Cookie数据,支持两种格式: + 1. Playwright格式(列表): [{"name": "a1", "value": "xxx", "domain": "...", ...}] + 2. 键值对格式(字典): {"a1": "xxx", "webId": "yyy", ...} + proxy: 可选的代理地址(例如 http://user:pass@ip:port) + user_agent: 可选的自定义User-Agent + """ + # 转换Cookie格式 + self.cookies = self._normalize_cookies(cookies) + self.proxy = proxy + self.user_agent = user_agent + self.service = XHSLoginService() + self.temp_dir = "temp_downloads" # 临时下载目录 + self.downloaded_files = [] # 记录下载的文件,用于清理 + + def _normalize_cookies(self, cookies: Union[List[Dict[str, Any]], Dict[str, str]]) -> List[Dict[str, Any]]: + """ + 将Cookie标准化为Playwright格式 + + Args: + cookies: 输入的Cookie(支持两种格式) + + Returns: + Playwright格式的Cookie列表 + """ + # 如果已经是列表格式(Playwright格式) + if isinstance(cookies, list): + # 检查是否包含必要字段 + if cookies and 'name' in cookies[0] and 'value' in cookies[0]: + print("✅ 使用 Playwright 格式的 Cookie", file=sys.stderr) + return cookies + + # 如果是字典格式(键值对格式),转换为Playwright格式 + if isinstance(cookies, dict): + print("✅ 检测到键值对格式的 Cookie,转换为 Playwright 格式", file=sys.stderr) + playwright_cookies = [] + for name, value in cookies.items(): + cookie = { + "name": name, + "value": str(value), + "domain": ".xiaohongshu.com", + "path": "/", + "expires": -1, # 会话Cookie + "httpOnly": False, + "secure": False, + "sameSite": "Lax" + } + + # 特殊处理某些Cookie的属性 + if name == "web_session": + cookie["httpOnly"] = True + cookie["secure"] = True + elif name in ["acw_tc"]: + cookie["httpOnly"] = True + + playwright_cookies.append(cookie) + + print(f" 转换了 {len(playwright_cookies)} 个 Cookie", file=sys.stderr) + return playwright_cookies + + # 如果格式不支持,抛出异常 + raise ValueError(f"不支持的Cookie格式: {type(cookies)}。请使用列表或字典格式。") + + def _calculate_title_width(self, title: str) -> int: + width = 0 + for ch in title: + if unicodedata.east_asian_width(ch) in ("F", "W"): + width += 2 + else: + width += 1 + return width + + def is_url(self, path: str) -> bool: + """ + 判断是否为网络URL + + Args: + path: 图片路径或URL + + Returns: + 是否为URL + """ + url_pattern = re.compile(r'^https?://', re.IGNORECASE) + return bool(url_pattern.match(path)) + + async def download_image(self, url: str, index: int = 0) -> str: + """ + 下载网络图片到本地临时目录 + + Args: + url: 图片URL + index: 图片索引(用于命名) + + Returns: + 本地文件路径 + """ + try: + print(f" 正在下载图片 [{index + 1}]: {url}", file=sys.stderr) + + # 创建临时目录 + Path(self.temp_dir).mkdir(exist_ok=True) + + # 生成文件名(使用URL的hash值) + url_hash = hashlib.md5(url.encode()).hexdigest()[:10] + + # 从URL提取文件扩展名 + ext = '.jpg' # 默认扩展名 + url_path = url.split('?')[0] # 去除URL参数 + if '.' in url_path: + ext = '.' + url_path.split('.')[-1].lower() + if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: + ext = '.jpg' + + filename = f"image_{index}_{url_hash}{ext}" + filepath = os.path.join(self.temp_dir, filename) + + # 下载图片 + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response: + if response.status == 200: + content = await response.read() + + # 保存文件 + with open(filepath, 'wb') as f: + f.write(content) + + # 记录已下载文件 + self.downloaded_files.append(filepath) + + # 获取文件大小 + file_size = len(content) / 1024 # KB + print(f" ✅ 下载成功: {filename} ({file_size:.1f}KB)", file=sys.stderr) + + return os.path.abspath(filepath) + else: + raise Exception(f"下载失败,HTTP状态码: {response.status}") + + except asyncio.TimeoutError: + raise Exception(f"下载超时: {url}") + except Exception as e: + raise Exception(f"下载图片失败 ({url}): {str(e)}") + + async def process_images(self, images: List[str]) -> List[str]: + """ + 处理图片列表,将网络URL下载到本地 + + Args: + images: 图片路径列表(可以是本地路径或网络URL) + + Returns: + 本地图片路径列表 + """ + if not images: + return [] + + local_images = [] + + print(f"\n正在处理 {len(images)} 张图片...", file=sys.stderr) + + for i, img in enumerate(images): + if self.is_url(img): + # 网络URL,需要下载 + try: + local_path = await self.download_image(img, i) + local_images.append(local_path) + except Exception as e: + print(f" ⚠️ 图片下载失败: {str(e)}", file=sys.stderr) + # 继续处理其他图片 + continue + else: + # 本地路径 + if os.path.exists(img): + local_images.append(os.path.abspath(img)) + print(f" ✅ 本地图片 [{i + 1}]: {os.path.basename(img)}", file=sys.stderr) + else: + print(f" ⚠️ 本地图片不存在: {img}", file=sys.stderr) + + print(f"\n成功处理 {len(local_images)}/{len(images)} 张图片", file=sys.stderr) + return local_images + + def cleanup_temp_files(self): + """ + 清理临时下载的文件 + """ + if not self.downloaded_files: + return + + print(f"\n清理 {len(self.downloaded_files)} 个临时文件...", file=sys.stderr) + for filepath in self.downloaded_files: + try: + if os.path.exists(filepath): + os.remove(filepath) + print(f" 已删除: {os.path.basename(filepath)}", file=sys.stderr) + except Exception as e: + print(f" 删除失败 {filepath}: {e}", file=sys.stderr) + + # 清空记录 + self.downloaded_files = [] + + async def publish( + self, + title: str, + content: str, + images: List[str] = None, + tags: List[str] = None, + cleanup: bool = True + ) -> Dict[str, Any]: + """ + 发布笔记 + + Args: + title: 笔记标题 + content: 笔记内容 + images: 图片路径列表(支持本地文件路径或网络URL) + tags: 标签列表(例如:["美食", "探店"]) + cleanup: 是否清理临时下载的图片文件(默认True) + + Returns: + Dict containing success status, message, and publish result + """ + try: + print("\n========== 开始发布小红书笔记 ==========", file=sys.stderr) + print(f"标题: {title}", file=sys.stderr) + print(f"内容: {content[:100]}{'...' if len(content) > 100 else ''}", file=sys.stderr) + print(f"图片: {len(images) if images else 0} 张", file=sys.stderr) + print(f"标签: {tags if tags else []}", file=sys.stderr) + + width = self._calculate_title_width(title) + if width > 40: + return { + "success": False, + "error": f"标题长度超过限制(当前宽度 {width},平台限制 40)" + } + + if tags: + if len(tags) > 10: + tags = tags[:10] + print("⚠️ 标签数量超过10,已截取前10个标签", file=sys.stderr) + + local_images = None + if images: + local_images = await self.process_images(images) + if not local_images: + print("⚠️ 警告:没有可用的图片", file=sys.stderr) + return { + "success": False, + "error": "没有可用的图片,无法发布笔记" + } + + # 初始化浏览器并注入Cookie + print("\n1. 初始化浏览器...", file=sys.stderr) + await self.service.init_browser(cookies=self.cookies, proxy=self.proxy, user_agent=self.user_agent) + + # 验证登录状态 + print("\n2. 验证登录状态...", file=sys.stderr) + verify_result = await self.service.verify_login_status() + + if not verify_result.get('logged_in'): + return { + "success": False, + "error": "Cookie已失效或未登录", + "details": verify_result + } + + print("✅ 登录状态有效", file=sys.stderr) + + # 发布笔记 + print("\n3. 开始发布笔记...", file=sys.stderr) + result = await self.service.publish_note( + title=title, + content=content, + images=local_images, + topics=tags + ) + + print("\n========== 发布完成 ==========", file=sys.stderr) + return result + + except Exception as e: + print(f"\n发布异常: {str(e)}", file=sys.stderr) + return { + "success": False, + "error": str(e) + } + + finally: + # 关闭浏览器 + await self.service.close_browser() + + # 清理临时文件 + if cleanup: + self.cleanup_temp_files() + + +async def publish_from_config(config_file: str) -> Dict[str, Any]: + """ + 从配置文件读取参数并发布 + + Args: + config_file: JSON配置文件路径 + + Returns: + 发布结果 + """ + try: + # 读取配置文件 + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + + # 提取参数 + cookies = config.get('cookies', []) + title = config.get('title', '') + content = config.get('content', '') + images = config.get('images', []) + tags = config.get('tags', []) + proxy = config.get('proxy') + user_agent = config.get('user_agent') + + # 验证必需参数 + if not cookies: + return { + "success": False, + "error": "缺少Cookie参数" + } + + if not title or not content: + return { + "success": False, + "error": "标题和内容不能为空" + } + + # 注意:不再验证图片文件是否存在,因为可能是网络URL + # 图片验证交给 process_images 方法处理 + + # 创建发布服务并执行 + publisher = XHSPublishService(cookies, proxy=proxy, user_agent=user_agent) + result = await publisher.publish( + title=title, + content=content, + images=images, + tags=tags + ) + + return result + + except Exception as e: + return { + "success": False, + "error": f"读取配置文件失败: {str(e)}" + } + + +async def publish_from_params( + cookies_json: str, + title: str, + content: str, + images_json: str = None, + tags_json: str = None +) -> Dict[str, Any]: + """ + 从命令行参数发布 + + Args: + cookies_json: Cookie JSON字符串 或 Cookie文件路径 + title: 标题 + content: 内容 + images_json: 图片路径数组的JSON字符串 (可选) + tags_json: 标签数组的JSON字符串 (可选) + + Returns: + 发布结果 + """ + try: + # 解析Cookie - 支持JSON字符串或文件路径 + cookies = None + + # 检查是否为文件路径 + if os.path.isfile(cookies_json): + # 从文件读取 + try: + with open(cookies_json, 'r', encoding='utf-8') as f: + cookies = json.load(f) + print(f"✅ 从文件加载 Cookie: {cookies_json}") + except Exception as e: + return { + "success": False, + "error": f"读取 Cookie 文件失败: {str(e)}" + } + else: + # 解析JSON字符串 + try: + cookies = json.loads(cookies_json) + print("✅ 从 JSON 字符串解析 Cookie") + except json.JSONDecodeError as e: + return { + "success": False, + "error": f"Cookie 参数既不是有效文件路径,也不是有效 JSON 字符串: {str(e)}" + } + + if not cookies: + return { + "success": False, + "error": "Cookie 为空" + } + + # 解析图片列表 + images = [] + if images_json: + images = json.loads(images_json) + + # 解析标签列表 + tags = [] + if tags_json: + tags = json.loads(tags_json) + + # 创建发布服务并执行(命令行模式暂不支持传入代理和自定义UA) + publisher = XHSPublishService(cookies) + result = await publisher.publish( + title=title, + content=content, + images=images, + tags=tags + ) + + return result + + except json.JSONDecodeError as e: + return { + "success": False, + "error": f"JSON解析失败: {str(e)}" + } + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def main(): + """ + 命令行主函数 + + 使用方式: + 1. 从配置文件发布: + python xhs_publish.py --config publish_config.json + + 2. 从命令行参数发布: + python xhs_publish.py --cookies '' --title '标题' --content '内容' [--images ''] [--tags ''] + """ + # 设置标准输出为UTF-8编码 + if sys.platform == 'win32': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + if len(sys.argv) < 2: + print(json.dumps({ + "success": False, + "error": "缺少参数,请使用 --config 或 --cookies" + }, ensure_ascii=False)) + sys.exit(1) + + try: + # 解析命令行参数 + args = sys.argv[1:] + + # 方式1: 从配置文件读取 + if args[0] == '--config': + if len(args) < 2: + print(json.dumps({ + "success": False, + "error": "缺少配置文件路径" + }, ensure_ascii=False)) + sys.exit(1) + + config_file = args[1] + result = asyncio.run(publish_from_config(config_file)) + print(json.dumps(result, ensure_ascii=False, indent=2)) + + # 方式2: 从命令行参数 + elif args[0] == '--cookies': + # 解析参数 + params = {} + i = 0 + while i < len(args): + if args[i] == '--cookies' and i + 1 < len(args): + params['cookies'] = args[i + 1] + i += 2 + elif args[i] == '--title' and i + 1 < len(args): + params['title'] = args[i + 1] + i += 2 + elif args[i] == '--content' and i + 1 < len(args): + params['content'] = args[i + 1] + i += 2 + elif args[i] == '--images' and i + 1 < len(args): + params['images'] = args[i + 1] + i += 2 + elif args[i] == '--tags' and i + 1 < len(args): + params['tags'] = args[i + 1] + i += 2 + else: + i += 1 + + # 验证必需参数 + if 'cookies' not in params: + print(json.dumps({ + "success": False, + "error": "缺少 --cookies 参数" + }, ensure_ascii=False)) + sys.exit(1) + + if 'title' not in params or 'content' not in params: + print(json.dumps({ + "success": False, + "error": "缺少 --title 或 --content 参数" + }, ensure_ascii=False)) + sys.exit(1) + + result = asyncio.run(publish_from_params( + cookies_json=params['cookies'], + title=params['title'], + content=params['content'], + images_json=params.get('images'), + tags_json=params.get('tags') + )) + print(json.dumps(result, ensure_ascii=False, indent=2)) + + else: + print(json.dumps({ + "success": False, + "error": f"未知参数: {args[0]},请使用 --config 或 --cookies" + }, ensure_ascii=False)) + sys.exit(1) + + except Exception as e: + print(json.dumps({ + "success": False, + "error": str(e) + }, ensure_ascii=False)) + sys.exit(1) + + +if __name__ == "__main__": + main() + + diff --git a/go_backend/.gitignore b/go_backend/.gitignore new file mode 100644 index 0000000..2e904c2 --- /dev/null +++ b/go_backend/.gitignore @@ -0,0 +1,39 @@ +# .gitignore for Go Backend + +# Binaries +*.exe +*.exe~ +*.dll +*.so +*.dylib +ai_xhs +ai_xhs.exe + +# Test binary +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db + +# Config (如果包含敏感信息,取消注释下面这行) +# config/*.yaml diff --git a/go_backend/ENV_CONFIG_GUIDE.md b/go_backend/ENV_CONFIG_GUIDE.md new file mode 100644 index 0000000..39b592f --- /dev/null +++ b/go_backend/ENV_CONFIG_GUIDE.md @@ -0,0 +1,264 @@ +# 环境变量配置指南 + +## 概述 + +Go 服务支持通过**环境变量**来确定配置,优先级为: + +``` +环境变量 > 配置文件 > 默认值 +``` + +## 1. 环境选择 + +### 方式一:环境变量 `APP_ENV`(推荐) +```bash +# Windows PowerShell +$env:APP_ENV="prod" +.\start.bat + +# Linux/Mac +export APP_ENV=prod +./start.sh + +# Docker +docker run -e APP_ENV=prod ... +``` + +### 方式二:命令行参数 +```bash +go run main.go -env=prod +``` + +### 方式三:默认值 +不设置任何参数时,默认使用 `dev` 环境 + +--- + +## 2. 支持的环境变量 + +### 服务器配置 +| 环境变量 | 配置项 | 说明 | 示例 | +|---------|--------|------|------| +| `SERVER_PORT` | server.port | 服务端口 | 8080 | +| `SERVER_MODE` | server.mode | 运行模式 | release | + +### 数据库配置 +| 环境变量 | 配置项 | 说明 | 示例 | +|---------|--------|------|------| +| `DB_HOST` | database.host | 数据库地址 | localhost | +| `DB_PORT` | database.port | 数据库端口 | 3306 | +| `DB_USERNAME` | database.username | 用户名 | root | +| `DB_PASSWORD` | database.password | 密码 | your_password | +| `DB_NAME` | database.dbname | 数据库名 | ai_wht | +| `DB_CHARSET` | database.charset | 字符集 | utf8mb4 | + +### JWT 配置 +| 环境变量 | 配置项 | 说明 | 示例 | +|---------|--------|------|------| +| `JWT_SECRET` | jwt.secret | JWT 密钥 | your_secret_key | +| `JWT_EXPIRE_HOURS` | jwt.expire_hours | 过期时间(小时) | 168 | + +### 微信配置 +| 环境变量 | 配置项 | 说明 | 示例 | +|---------|--------|------|------| +| `WECHAT_APP_ID` | wechat.app_id | 微信 AppID | wx1234567890 | +| `WECHAT_APP_SECRET` | wechat.app_secret | 微信 AppSecret | your_secret | + +### 小红书配置 +| 环境变量 | 配置项 | 说明 | 示例 | +|---------|--------|------|------| +| `XHS_PYTHON_SERVICE_URL` | xhs.python_service_url | Python服务地址 | http://localhost:8000 | + +--- + +## 3. 使用场景 + +### 场景一:本地开发(覆盖数据库密码) +```bash +# Windows +$env:DB_PASSWORD="local_password" +go run main.go + +# Linux/Mac +export DB_PASSWORD=local_password +go run main.go +``` + +### 场景二:生产部署 +```bash +# 设置生产环境 +$env:APP_ENV="prod" +$env:DB_HOST="prod-db.example.com" +$env:DB_PASSWORD="prod_secure_password" +$env:JWT_SECRET="prod_jwt_secret_key" +$env:WECHAT_APP_ID="wx_prod_appid" +$env:WECHAT_APP_SECRET="wx_prod_secret" + +.\start.bat +``` + +### 场景三:Docker 部署 +```dockerfile +# Dockerfile +FROM golang:1.21-alpine + +WORKDIR /app +COPY . . + +RUN go build -o main . + +# 设置环境变量 +ENV APP_ENV=prod +ENV SERVER_PORT=8080 +ENV DB_HOST=mysql-server + +CMD ["./main"] +``` + +```bash +# docker-compose.yml +version: '3.8' +services: + go-backend: + build: . + environment: + - APP_ENV=prod + - DB_HOST=mysql + - DB_PASSWORD=${DB_PASSWORD} + - WECHAT_APP_ID=${WECHAT_APP_ID} + - WECHAT_APP_SECRET=${WECHAT_APP_SECRET} + ports: + - "8080:8080" +``` + +### 场景四:CI/CD 流水线 +```yaml +# GitHub Actions +env: + APP_ENV: prod + DB_HOST: ${{ secrets.DB_HOST }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + WECHAT_APP_ID: ${{ secrets.WECHAT_APP_ID }} + WECHAT_APP_SECRET: ${{ secrets.WECHAT_APP_SECRET }} +``` + +--- + +## 4. 配置优先级示例 + +假设 `config.prod.yaml` 中配置: +```yaml +database: + host: localhost + port: 3306 + password: file_password +``` + +运行时设置环境变量: +```bash +$env:DB_HOST="prod-server.com" +$env:DB_PASSWORD="env_password" +``` + +**最终生效的配置:** +``` +host: prod-server.com # 来自环境变量 DB_HOST +port: 3306 # 来自配置文件 +password: env_password # 来自环境变量 DB_PASSWORD +``` + +--- + +## 5. 查看当前配置 + +启动服务时,会在日志中输出: +``` +2024/12/15 14:45:00 从环境变量 APP_ENV 读取环境: prod +2024/12/15 14:45:00 配置加载成功: prod 环境 +2024/12/15 14:45:00 数据库配置: root@prod-server.com:3306/ai_wht +``` + +--- + +## 6. 安全建议 + +1. **敏感信息不要写入配置文件**,使用环境变量: + - `DB_PASSWORD` + - `JWT_SECRET` + - `WECHAT_APP_SECRET` + +2. **生产环境必须覆盖的环境变量**: + ```bash + APP_ENV=prod + DB_PASSWORD= + JWT_SECRET= + WECHAT_APP_SECRET= + ``` + +3. **使用密钥管理工具**(可选): + - Azure Key Vault + - AWS Secrets Manager + - HashiCorp Vault + +--- + +## 7. 快速启动脚本 + +### Windows (start_with_env.bat) +```bat +@echo off +set APP_ENV=prod +set DB_HOST=localhost +set DB_PASSWORD=your_password +set JWT_SECRET=your_jwt_secret +set WECHAT_APP_ID=your_appid +set WECHAT_APP_SECRET=your_secret + +go run main.go +``` + +### Linux/Mac (start_with_env.sh) +```bash +#!/bin/bash +export APP_ENV=prod +export DB_HOST=localhost +export DB_PASSWORD=your_password +export JWT_SECRET=your_jwt_secret +export WECHAT_APP_ID=your_appid +export WECHAT_APP_SECRET=your_secret + +go run main.go +``` + +--- + +## 8. 常见问题 + +### Q: 环境变量没生效? +**A:** 检查环境变量名称是否正确,区分大小写。 + +### Q: 如何查看所有环境变量? +**A:** +```bash +# Windows +Get-ChildItem Env: + +# Linux/Mac +printenv +``` + +### Q: 如何临时设置环境变量? +**A:** +```bash +# Windows - 当前会话有效 +$env:DB_PASSWORD="temp_password" + +# Linux/Mac - 当前会话有效 +export DB_PASSWORD=temp_password +``` + +### Q: 如何永久设置环境变量? +**A:** +- Windows: 系统设置 → 高级系统设置 → 环境变量 +- Linux/Mac: 添加到 `~/.bashrc` 或 `~/.zshrc` diff --git a/go_backend/PYTHON_CROSS_PLATFORM.md b/go_backend/PYTHON_CROSS_PLATFORM.md new file mode 100644 index 0000000..07ca8b5 --- /dev/null +++ b/go_backend/PYTHON_CROSS_PLATFORM.md @@ -0,0 +1,166 @@ +# Python虚拟环境跨平台配置说明 + +## 📋 概述 + +Go服务已支持跨平台调用Python脚本,可以在Windows和Ubuntu/Linux环境下正常运行。 + +## 🔄 Windows vs Linux 路径对比 + +### Windows环境 +``` +backend/ +├── venv/ +│ ├── Scripts/ ← Windows使用Scripts目录 +│ │ ├── python.exe +│ │ ├── activate.bat +│ │ └── ... +│ └── Lib/ +``` + +**Python解释器**: `backend/venv/Scripts/python.exe` + +### Ubuntu/Linux环境 +``` +backend/ +├── venv/ +│ ├── bin/ ← Linux使用bin目录 +│ │ ├── python +│ │ ├── activate +│ │ └── ... +│ └── lib/ +``` + +**Python解释器**: `backend/venv/bin/python` + +## 🚀 部署步骤 + +### 在Ubuntu服务器上部署 + +1. **创建Python虚拟环境**: +```bash +cd /path/to/backend +python3 -m venv venv +``` + +2. **激活虚拟环境**: +```bash +source venv/bin/activate +``` + +3. **安装依赖**: +```bash +pip install -r requirements.txt +playwright install chromium +``` + +4. **启动Go服务**: +```bash +cd /path/to/go_backend +go run main.go +``` + +### 在Windows上开发 + +1. **创建Python虚拟环境**: +```cmd +cd backend +python -m venv venv +``` + +2. **激活虚拟环境**: +```cmd +venv\Scripts\activate +``` + +3. **安装依赖**: +```cmd +pip install -r requirements.txt +playwright install chromium +``` + +4. **启动Go服务**: +```cmd +cd go_backend +go run main.go +``` + +## 🔧 技术实现 + +### 跨平台路径检测 + +Go代码中使用`runtime.GOOS`自动检测操作系统: + +```go +func getPythonPath(backendDir string) string { + if runtime.GOOS == "windows" { + // Windows: venv\Scripts\python.exe + return filepath.Join(backendDir, "venv", "Scripts", "python.exe") + } + // Linux/Mac: venv/bin/python + return filepath.Join(backendDir, "venv", "bin", "python") +} +``` + +### 使用位置 + +该函数在以下服务中被调用: +- `service/xhs_service.go` - 小红书登录服务 +- `service/employee_service.go` - 员工服务(绑定小红书账号) + +## ✅ 验证部署 + +### 测试Python环境 +```bash +# Ubuntu +cd backend +source venv/bin/activate +python xhs_cli.py --help + +# Windows +cd backend +venv\Scripts\activate +python xhs_cli.py --help +``` + +### 测试Go调用 +```bash +# 启动Go服务后,测试发送验证码接口 +curl -X POST http://localhost:8080/api/employee/xhs/send-code \ + -H "Content-Type: application/json" \ + -d '{"phone":"13800138000"}' +``` + +## ⚠️ 注意事项 + +1. **不要提交venv目录到Git**:已在`.gitignore`中配置忽略 +2. **环境隔离**:Windows和Ubuntu各自维护独立的venv环境 +3. **依赖一致性**:确保requirements.txt在两个平台上一致 +4. **Playwright浏览器**:在Ubuntu上需要安装chromium依赖库 + +## 🐛 常见问题 + +### Q: Ubuntu上提示找不到Python +**A**: 确保已安装Python3: +```bash +sudo apt update +sudo apt install python3 python3-venv python3-pip +``` + +### Q: Playwright启动失败 +**A**: 安装系统依赖: +```bash +playwright install-deps chromium +``` + +### Q: Go服务找不到Python脚本 +**A**: 检查`backend`目录与`go_backend`目录的相对位置,确保为: +``` +project/ +├── backend/ # Python脚本 +└── go_backend/ # Go服务 +``` + +## 📚 相关文档 + +- [Go服务环境变量配置](ENV_CONFIG_GUIDE.md) +- [Python CLI工具文档](../backend/XHS_CLI_README.md) diff --git a/go_backend/README.md b/go_backend/README.md new file mode 100644 index 0000000..d652d42 --- /dev/null +++ b/go_backend/README.md @@ -0,0 +1,240 @@ +# AI小红书 - Go后端服务 + +基于Go + Gin + GORM + MySQL开发的小红书营销助手后端服务。 + +## 技术栈 + +- **Web框架**: Gin +- **ORM**: GORM +- **数据库**: MySQL 8.0+ +- **配置管理**: Viper +- **认证**: JWT + +## 项目结构 + +``` +go_backend/ +├── config/ # 配置文件 +│ ├── config.go # 配置加载 +│ ├── config.dev.yaml # 开发环境配置 +│ └── config.prod.yaml# 生产环境配置 +├── models/ # 数据模型 +│ └── models.go +├── database/ # 数据库 +│ └── database.go +├── service/ # 业务逻辑层 +│ └── employee_service.go +├── controller/ # 控制器层 +│ └── employee_controller.go +├── router/ # 路由 +│ └── router.go +├── middleware/ # 中间件 +│ └── auth.go +├── common/ # 公共模块 +│ └── response.go +├── utils/ # 工具函数 +│ └── jwt.go +├── main.go # 入口文件 +├── go.mod # 依赖管理 +├── start.sh # Linux/Mac启动脚本 +├── start.bat # Windows开发环境启动脚本 +└── start_prod.bat # Windows生产环境启动脚本 +``` + +## 快速开始 + +### 1. 环境要求 + +- Go 1.21+ +- MySQL 8.0+ + +### 2. 数据库准备 + +创建数据库: + +```sql +-- 开发环境 +CREATE DATABASE ai_xhs_dev DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 生产环境 +CREATE DATABASE ai_xhs_prod DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +### 3. 配置文件 + +配置文件位于 `config/` 目录: + +- `config.dev.yaml` - 开发环境配置 +- `config.prod.yaml` - 生产环境配置 + +根据需要修改数据库连接信息和JWT密钥。 + +### 4. 安装依赖 + +```bash +go mod tidy +``` + +### 5. 启动服务 + +#### Windows开发环境 + +双击运行 `start.bat` 或命令行执行: + +```bash +start.bat +``` + +#### Windows生产环境 + +双击运行 `start_prod.bat` 或命令行执行: + +```bash +start_prod.bat +``` + +#### Linux/Mac + +```bash +chmod +x start.sh +./start.sh +``` + +或手动启动: + +```bash +# 开发环境 +go run main.go -env=dev + +# 生产环境 +go run main.go -env=prod +``` + +### 6. 验证服务 + +访问健康检查接口: + +```bash +curl http://localhost:8080/health +``` + +返回: + +```json +{ + "status": "ok" +} +``` + +## API接口 + +所有接口都需要在请求头中携带JWT Token(除登录接口外): + +``` +Authorization: Bearer +``` + +### 员工端接口 + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 获取个人信息 | GET | `/api/employee/profile` | 获取当前员工信息 | +| 绑定小红书 | POST | `/api/employee/bind-xhs` | 绑定小红书账号 | +| 解绑小红书 | POST | `/api/employee/unbind-xhs` | 解绑小红书账号 | +| 可领取文案 | GET | `/api/employee/available-copies` | 获取可领取的文案列表 | +| 领取文案 | POST | `/api/employee/claim-copy` | 领取指定文案 | +| 随机领取 | POST | `/api/employee/claim-random-copy` | 随机领取文案 | +| 发布内容 | POST | `/api/employee/publish` | 发布内容到小红书 | +| 发布记录 | GET | `/api/employee/my-publish-records` | 获取我的发布记录 | +| 产品列表 | GET | `/api/employee/products` | 获取产品列表 | + +详细接口文档请参考项目需求文档。 + +## 数据库表结构 + +系统会在启动时自动创建以下数据表: + +- `enterprises` - 企业表 +- `employees` - 员工表 +- `products` - 产品表 +- `copies` - 文案表 +- `copy_claims` - 文案领取记录表 +- `publish_records` - 发布记录表 + +## 开发说明 + +### 添加新接口 + +1. 在 `models/` 中定义数据模型 +2. 在 `service/` 中实现业务逻辑 +3. 在 `controller/` 中创建控制器方法 +4. 在 `router/router.go` 中注册路由 + +### 环境切换 + +通过 `-env` 参数切换环境: + +```bash +go run main.go -env=dev # 开发环境 +go run main.go -env=prod # 生产环境 +``` + +### 日志 + +GORM日志级别在 `database/database.go` 中配置,默认为 `Info` 级别。 + +## 部署 + +### 编译 + +```bash +# Windows +go build -o ai_xhs.exe main.go + +# Linux/Mac +go build -o ai_xhs main.go +``` + +### 运行 + +```bash +# Windows +ai_xhs.exe -env=prod + +# Linux/Mac +./ai_xhs -env=prod +``` + +### 生产环境注意事项 + +1. 修改 `config.prod.yaml` 中的JWT密钥 +2. 配置正确的数据库连接信息 +3. 设置服务器防火墙规则 +4. 建议使用进程管理工具(如systemd、supervisor) +5. 配置反向代理(如Nginx) +6. 启用HTTPS + +## 常见问题 + +### 1. 数据库连接失败 + +检查: +- MySQL服务是否启动 +- 数据库名称是否正确 +- 用户名密码是否正确 +- 数据库字符集是否为 utf8mb4 + +### 2. 端口已被占用 + +修改配置文件中的 `server.port` 配置项。 + +### 3. JWT认证失败 + +检查: +- Token是否正确携带 +- Token格式是否为 `Bearer ` +- Token是否过期 + +## 许可证 + +MIT diff --git a/go_backend/UBUNTU_SCRIPTS_GUIDE.md b/go_backend/UBUNTU_SCRIPTS_GUIDE.md new file mode 100644 index 0000000..3158816 --- /dev/null +++ b/go_backend/UBUNTU_SCRIPTS_GUIDE.md @@ -0,0 +1,412 @@ +# Ubuntu 启动脚本使用指南 + +## 📁 脚本文件列表 + +| 脚本文件 | 用途 | 推荐场景 | +|---------|------|----------| +| `restart.sh` | 智能重启脚本(支持 dev/prod) | **推荐使用** - 开发和生产环境通用 | +| `start_prod.sh` | 生产环境快速启动 | 仅生产环境快速部署 | +| `stop.sh` | 停止服务脚本 | 停止所有运行的服务 | +| `start.sh` | 开发环境启动(原有) | 开发环境直接运行 | + +--- + +## 🚀 快速开始 + +### 1. 赋予执行权限 +```bash +cd go_backend + +# 一次性赋予所有脚本执行权限 +chmod +x restart.sh start_prod.sh stop.sh start.sh +``` + +### 2. 启动开发环境 +```bash +# 方式1: 使用 restart.sh (推荐) +./restart.sh dev + +# 方式2: 默认启动开发环境 +./restart.sh + +# 方式3: 使用原有脚本 +./start.sh +``` + +### 3. 启动生产环境 +```bash +# 方式1: 使用 restart.sh (推荐) +./restart.sh prod + +# 方式2: 使用专用脚本 +./start_prod.sh +``` + +### 4. 停止服务 +```bash +./stop.sh +``` + +--- + +## 📖 详细说明 + +### restart.sh - 智能重启脚本 ⭐推荐 + +**功能特点:** +- ✅ 自动停止旧服务 +- ✅ 支持开发/生产环境切换 +- ✅ 完整的环境检查 +- ✅ 多重端口清理机制 +- ✅ 启动验证和错误检测 +- ✅ 彩色输出,易于阅读 + +**使用方法:** +```bash +# 启动开发环境 (端口 8080) +./restart.sh +./restart.sh dev + +# 启动生产环境 (端口 8070) +./restart.sh prod + +# 查看帮助 +./restart.sh help +``` + +**输出示例:** +``` +======================================== + AI小红书 Go 后端服务重启脚本 +======================================== +环境: dev +端口: 8080 +日志: ai_xhs.log + +=== [1/4] 停止现有服务 === +✅ 端口 8080 已释放 + +=== [2/4] 环境检查 === +✅ Go 环境: go version go1.21.0 linux/amd64 +✅ 主文件: main.go +✅ 配置文件: config/config.dev.yaml + +=== [3/4] 下载依赖 === +✅ 依赖下载完成 + +=== [4/4] 启动服务 === +✅ 服务已启动,进程 PID: 12345 + +======================================== + 🎉 服务启动成功! +======================================== + +服务信息: + 环境: dev + 端口: 8080 + 进程PID: 12345 + 日志文件: ai_xhs.log + +快捷命令: + 查看日志: tail -f ai_xhs.log + 停止服务: kill -9 12345 + +访问地址: + 本地: http://localhost:8080 +``` + +--- + +### start_prod.sh - 生产环境快速启动 + +**功能特点:** +- ✅ 专为生产环境优化 +- ✅ 简洁快速 +- ✅ 固定端口 8070 +- ✅ 独立日志文件 `ai_xhs_prod.log` + +**使用方法:** +```bash +./start_prod.sh +``` + +--- + +### stop.sh - 停止服务脚本 + +**功能特点:** +- ✅ 同时停止开发和生产环境 +- ✅ 多种停止方法确保彻底清理 +- ✅ 自动验证停止结果 +- ✅ 清理所有相关进程 + +**使用方法:** +```bash +./stop.sh +``` + +**清理范围:** +- 所有 `go run main.go` 进程 +- 占用 8080 端口的进程(开发环境) +- 占用 8070 端口的进程(生产环境) +- 其他相关 main.go 进程 + +--- + +## 🔧 环境变量配置 + +### 通过环境变量覆盖配置 + +脚本支持通过环境变量覆盖配置文件: + +```bash +# 设置环境 +export APP_ENV=prod + +# 覆盖数据库配置 +export DB_HOST=prod-server.com +export DB_PASSWORD=secure_password + +# 覆盖微信配置 +export WECHAT_APP_ID=wx_prod_id +export WECHAT_APP_SECRET=wx_prod_secret + +# 启动服务 +./restart.sh +``` + +**支持的环境变量:** 详见 [ENV_CONFIG_GUIDE.md](ENV_CONFIG_GUIDE.md) + +--- + +## 📋 日志管理 + +### 查看实时日志 +```bash +# 开发环境 +tail -f ai_xhs.log + +# 生产环境 +tail -f ai_xhs_prod.log + +# 查看最后 50 行 +tail -n 50 ai_xhs.log + +# 搜索错误日志 +grep -i "error\|fatal\|panic" ai_xhs.log +``` + +### 日志文件说明 + +| 文件 | 用途 | 环境 | +|------|------|------| +| `ai_xhs.log` | 开发环境日志 | dev | +| `ai_xhs_prod.log` | 生产环境日志 | prod | + +--- + +## 🛠 常用命令 + +### 检查服务状态 +```bash +# 查看 Go 进程 +ps aux | grep "go run main.go" + +# 查看端口占用 +lsof -i:8080 # 开发环境 +lsof -i:8070 # 生产环境 + +# 查看所有监听端口 +netstat -tunlp | grep go +``` + +### 手动停止服务 +```bash +# 方法1: 使用 PID (推荐) +kill -9 + +# 方法2: 停止所有 go run 进程 +pkill -f "go run main.go" + +# 方法3: 通过端口停止 +sudo fuser -k 8080/tcp +``` + +### 测试服务 +```bash +# 测试服务是否启动 +curl http://localhost:8080/api/health + +# 查看返回内容 +curl -i http://localhost:8080/api/health +``` + +--- + +## ⚠️ 注意事项 + +### 1. 权限问题 +某些清理操作需要 sudo 权限: +```bash +# 如果遇到权限问题,使用 sudo +sudo ./stop.sh +``` + +### 2. 端口冲突 +如果端口被其他程序占用: +```bash +# 查看占用端口的程序 +lsof -i:8080 + +# 修改配置文件中的端口 +vim config/config.dev.yaml +``` + +### 3. 日志文件过大 +定期清理日志: +```bash +# 清空日志文件 +> ai_xhs.log + +# 或删除旧日志 +rm ai_xhs.log +``` + +### 4. 后台运行说明 +- 脚本使用 `nohup` 在后台运行服务 +- 关闭终端不会停止服务 +- 必须使用 `kill` 或 `stop.sh` 停止服务 + +--- + +## 🔄 系统服务化 (可选) + +### 创建 systemd 服务 + +如果需要开机自启动,可以创建系统服务: + +```bash +# 创建服务文件 +sudo vim /etc/systemd/system/ai_xhs.service +``` + +添加内容: +```ini +[Unit] +Description=AI XHS Go Backend +After=network.target mysql.service + +[Service] +Type=simple +User=your_user +WorkingDirectory=/path/to/go_backend +Environment="APP_ENV=prod" +ExecStart=/usr/local/go/bin/go run main.go +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +启用服务: +```bash +# 重载配置 +sudo systemctl daemon-reload + +# 启动服务 +sudo systemctl start ai_xhs + +# 设置开机自启 +sudo systemctl enable ai_xhs + +# 查看状态 +sudo systemctl status ai_xhs + +# 查看日志 +sudo journalctl -u ai_xhs -f +``` + +--- + +## 📞 故障排查 + +### 问题1: 服务启动失败 +```bash +# 检查日志 +tail -f ai_xhs.log + +# 检查配置文件 +cat config/config.dev.yaml + +# 检查 Go 环境 +go version +``` + +### 问题2: 端口无法释放 +```bash +# 强制停止 +sudo ./stop.sh + +# 检查是否还有进程 +lsof -i:8080 + +# 手动清理 +sudo fuser -k 8080/tcp +``` + +### 问题3: 找不到依赖 +```bash +# 重新下载依赖 +go mod tidy +go mod download + +# 清理缓存 +go clean -modcache +``` + +--- + +## 📝 快速参考 + +### 一键部署生产环境 +```bash +# 1. 进入目录 +cd go_backend + +# 2. 赋予权限 +chmod +x restart.sh + +# 3. 设置环境变量(可选) +export DB_PASSWORD=your_password + +# 4. 启动服务 +./restart.sh prod + +# 5. 查看日志 +tail -f ai_xhs.log +``` + +### 一键停止所有服务 +```bash +chmod +x stop.sh +./stop.sh +``` + +--- + +## 🎯 最佳实践 + +1. **使用 restart.sh** - 功能最完善,错误检查最全面 +2. **配置环境变量** - 敏感信息不要写入配置文件 +3. **定期查看日志** - 及时发现问题 +4. **使用 systemd** - 生产环境推荐系统服务化 +5. **备份配置文件** - 修改前先备份 + +--- + +## 📚 相关文档 + +- [环境变量配置指南](ENV_CONFIG_GUIDE.md) +- [数据库迁移指南](DATABASE_MIGRATION_GUIDE.md) +- [微信登录集成指南](WECHAT_LOGIN_GUIDE.md) diff --git a/go_backend/common/response.go b/go_backend/common/response.go new file mode 100644 index 0000000..ebee1c6 --- /dev/null +++ b/go_backend/common/response.go @@ -0,0 +1,62 @@ +package common + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// Success 成功响应 +func Success(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: 200, + Message: "success", + Data: data, + }) +} + +// SuccessWithMessage 成功响应(自定义消息) +func SuccessWithMessage(c *gin.Context, message string, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: 200, + Message: message, + Data: data, + }) +} + +// Error 错误响应 +func Error(c *gin.Context, code int, message string) { + c.JSON(http.StatusOK, Response{ + Code: code, + Message: message, + }) +} + +// ErrorWithData 错误响应(带数据) +func ErrorWithData(c *gin.Context, code int, message string, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: code, + Message: message, + Data: data, + }) +} + +// 常见错误码 +const ( + CodeSuccess = 200 + CodeInvalidParams = 400 + CodeUnauthorized = 401 + CodeForbidden = 403 + CodeNotFound = 404 + CodeInternalError = 500 + CodeServerError = 500 // 服务器错误(别名) + CodeBindXHSFailed = 1001 + CodeCopyNotAvailable = 1002 + CodeAlreadyClaimed = 1003 +) diff --git a/go_backend/config/config.dev.yaml b/go_backend/config/config.dev.yaml new file mode 100644 index 0000000..3a1a2eb --- /dev/null +++ b/go_backend/config/config.dev.yaml @@ -0,0 +1,42 @@ +server: + port: 8080 + mode: debug # debug, release, test + +database: + host: localhost + port: 3306 + username: root + password: JKjk20011115 + dbname: ai_wht + charset: utf8mb4 + parse_time: true + loc: Local + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600 + +jwt: + secret: dev_secret_key_change_in_production + expire_hours: 168 # 7天 + +wechat: + app_id: "wxa5bf062342ef754d" # 微信小程序AppID,留空则使用默认登录 + app_secret: "69d2a3ddc902b26f82f4b56a6e277f7a" # 微信小程序AppSecret + +xhs: + python_service_url: "http://localhost:8000" # Python服务地址 + +scheduler: + enabled: true # 是否启用定时任务 + publish_cron: "* * * * * *" # 每1小时执行一次(开发环境测试用) + max_concurrent: 2 # 最大并发发布数 + publish_timeout: 300 # 发布超时时间(秒) + max_articles_per_user_per_run: 2 # 每轮每个用户最大发文数 + max_failures_per_user_per_run: 3 # 每轮每个用户最大失败次数 + max_daily_articles_per_user: 6 # 每个用户每日最大发文数(自动发布) + max_hourly_articles_per_user: 2 # 每个用户每小时最大发文数(自动发布) + proxy: "" # 可选:静态全局代理地址,例如 http://user:pass@ip:port + user_agent: "" # 可选:自定义User-Agent,不填则使用默认 + proxy_pool: + enabled: true # 开发环境启用代理池 + api_url: "http://api.tianqiip.com/getip?secret=lu29e593&num=1&type=txt&port=1&mr=1&sign=4b81a62eaed89ba802a8f34053e2c964" diff --git a/go_backend/config/config.go b/go_backend/config/config.go new file mode 100644 index 0000000..d9a37a4 --- /dev/null +++ b/go_backend/config/config.go @@ -0,0 +1,159 @@ +package config + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/viper" +) + +type Config struct { + Server ServerConfig `mapstructure:"server"` + Database DatabaseConfig `mapstructure:"database"` + JWT JWTConfig `mapstructure:"jwt"` + Wechat WechatConfig `mapstructure:"wechat"` + XHS XHSConfig `mapstructure:"xhs"` + Scheduler SchedulerConfig `mapstructure:"scheduler"` +} + +type ServerConfig struct { + Port int `mapstructure:"port"` + Mode string `mapstructure:"mode"` +} + +type DatabaseConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + DBName string `mapstructure:"dbname"` + Charset string `mapstructure:"charset"` + ParseTime bool `mapstructure:"parse_time"` + Loc string `mapstructure:"loc"` + MaxIdleConns int `mapstructure:"max_idle_conns"` + MaxOpenConns int `mapstructure:"max_open_conns"` + ConnMaxLifetime int `mapstructure:"conn_max_lifetime"` +} + +type JWTConfig struct { + Secret string `mapstructure:"secret"` + ExpireHours int `mapstructure:"expire_hours"` +} + +type WechatConfig struct { + AppID string `mapstructure:"app_id"` + AppSecret string `mapstructure:"app_secret"` +} + +type XHSConfig struct { + PythonServiceURL string `mapstructure:"python_service_url"` +} + +type SchedulerConfig struct { + Enabled bool `mapstructure:"enabled"` // 是否启用定时任务 + PublishCron string `mapstructure:"publish_cron"` // 发布任务的Cron表达式 + MaxConcurrent int `mapstructure:"max_concurrent"` // 最大并发发布数 + PublishTimeout int `mapstructure:"publish_timeout"` // 发布超时时间(秒) + MaxArticlesPerUserPerRun int `mapstructure:"max_articles_per_user_per_run"` // 每轮每个用户最大发文数 + MaxFailuresPerUserPerRun int `mapstructure:"max_failures_per_user_per_run"` // 每轮每个用户最大失败次数 + MaxDailyArticlesPerUser int `mapstructure:"max_daily_articles_per_user"` // 每个用户每日最大发文数 + MaxHourlyArticlesPerUser int `mapstructure:"max_hourly_articles_per_user"` // 每个用户每小时最大发文数 + Proxy string `mapstructure:"proxy"` // 全局代理地址(可选) + UserAgent string `mapstructure:"user_agent"` // 全局User-Agent(可选) + ProxyFetchURL string `mapstructure:"proxy_fetch_url"` // 动态获取代理的接口地址(可选) +} + +var AppConfig *Config + +// LoadConfig 加载配置文件 +// 优先级: 环境变量 > 配置文件 +func LoadConfig(env string) error { + // 1. 优先从环境变量 APP_ENV 获取环境配置 + if envFromEnv := os.Getenv("APP_ENV"); envFromEnv != "" { + env = envFromEnv + log.Printf("从环境变量 APP_ENV 读取环境: %s", env) + } + + if env == "" { + env = "dev" + } + + // 2. 加载配置文件 + viper.SetConfigName(fmt.Sprintf("config.%s", env)) + viper.SetConfigType("yaml") + viper.AddConfigPath("./config") + viper.AddConfigPath("../config") + + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("读取配置文件失败: %w", err) + } + + // 3. 绑定环境变量(自动读取环境变量覆盖配置) + viper.AutomaticEnv() + + // 绑定特定的环境变量到配置项 + bindEnvVariables() + + AppConfig = &Config{} + if err := viper.Unmarshal(AppConfig); err != nil { + return fmt.Errorf("解析配置文件失败: %w", err) + } + + log.Printf("配置加载成功: %s 环境", env) + log.Printf("数据库配置: %s@%s:%d/%s", AppConfig.Database.Username, AppConfig.Database.Host, AppConfig.Database.Port, AppConfig.Database.DBName) + return nil +} + +// bindEnvVariables 绑定环境变量到配置项 +func bindEnvVariables() { + // Server 配置 + viper.BindEnv("server.port", "SERVER_PORT") + viper.BindEnv("server.mode", "SERVER_MODE") + + // Database 配置 + viper.BindEnv("database.host", "DB_HOST") + viper.BindEnv("database.port", "DB_PORT") + viper.BindEnv("database.username", "DB_USERNAME") + viper.BindEnv("database.password", "DB_PASSWORD") + viper.BindEnv("database.dbname", "DB_NAME") + viper.BindEnv("database.charset", "DB_CHARSET") + + // JWT 配置 + viper.BindEnv("jwt.secret", "JWT_SECRET") + viper.BindEnv("jwt.expire_hours", "JWT_EXPIRE_HOURS") + + // Wechat 配置 + viper.BindEnv("wechat.app_id", "WECHAT_APP_ID") + viper.BindEnv("wechat.app_secret", "WECHAT_APP_SECRET") + + // XHS 配置 + viper.BindEnv("xhs.python_service_url", "XHS_PYTHON_SERVICE_URL") + + // Scheduler 配置 + viper.BindEnv("scheduler.enabled", "SCHEDULER_ENABLED") + viper.BindEnv("scheduler.publish_cron", "SCHEDULER_PUBLISH_CRON") + viper.BindEnv("scheduler.max_concurrent", "SCHEDULER_MAX_CONCURRENT") + viper.BindEnv("scheduler.publish_timeout", "SCHEDULER_PUBLISH_TIMEOUT") + viper.BindEnv("scheduler.max_articles_per_user_per_run", "SCHEDULER_MAX_ARTICLES_PER_USER_PER_RUN") + viper.BindEnv("scheduler.max_failures_per_user_per_run", "SCHEDULER_MAX_FAILURES_PER_USER_PER_RUN") + viper.BindEnv("scheduler.max_daily_articles_per_user", "SCHEDULER_MAX_DAILY_ARTICLES_PER_USER") + viper.BindEnv("scheduler.max_hourly_articles_per_user", "SCHEDULER_MAX_HOURLY_ARTICLES_PER_USER") + viper.BindEnv("scheduler.proxy", "SCHEDULER_PROXY") + viper.BindEnv("scheduler.user_agent", "SCHEDULER_USER_AGENT") + viper.BindEnv("scheduler.proxy_fetch_url", "SCHEDULER_PROXY_FETCH_URL") +} + +// GetDSN 获取数据库连接字符串 +func (c *DatabaseConfig) GetDSN() string { + return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=%t&loc=%s", + c.Username, + c.Password, + c.Host, + c.Port, + c.DBName, + c.Charset, + c.ParseTime, + c.Loc, + ) +} diff --git a/go_backend/config/config.prod.yaml b/go_backend/config/config.prod.yaml new file mode 100644 index 0000000..77c6e39 --- /dev/null +++ b/go_backend/config/config.prod.yaml @@ -0,0 +1,42 @@ +server: + port: 8070 + mode: release + +database: + host: 8.149.233.36 + port: 3306 + username: ai_wht_write + password: 7aK_H2yvokVumr84lLNDt8fDBp6P + dbname: ai_wht + charset: utf8mb4 + parse_time: true + loc: Local + max_idle_conns: 20 + max_open_conns: 200 + conn_max_lifetime: 3600 + +jwt: + secret: prod_secret_key_please_change_this + expire_hours: 168 + +wechat: + app_id: "wxa5bf062342ef754d" # 微信小程序AppID,留空则使用默认登录 + app_secret: "69d2a3ddc902b26f82f4b56a6e277f7a" # 微信小程序AppSecret + +xhs: + python_service_url: "http://localhost:8000" # Python服务地址,生产环境请修改为实际地址 + +scheduler: + enabled: true # 是否启用定时任务 + publish_cron: "0 0 */2 * * *" # 每2小时执行一次(防封号策略) + max_concurrent: 2 # 最大并发发布数 + publish_timeout: 300 # 发布超时时间(秒) + max_articles_per_user_per_run: 2 # 每轮每个用户最大发文数 + max_failures_per_user_per_run: 3 # 每轮每个用户最大失败次数 + max_daily_articles_per_user: 5 # 每个用户每日最大发文数(自动发布) + max_hourly_articles_per_user: 1 # 每个用户每小时最大发文数(自动发布) + proxy: "" # 可选:静态全局代理地址,例如 http://user:pass@ip:port + user_agent: "" # 可选:自定义User-Agent,不填则使用默认 + proxy_pool: + enabled: true # 生产环境启用代理池 + api_url: "http://api.tianqiip.com/getip?secret=lu29e593&num=1&type=txt&port=1&mr=1&sign=4b81a62eaed89ba802a8f34053e2c964" diff --git a/go_backend/controller/auth_controller.go b/go_backend/controller/auth_controller.go new file mode 100644 index 0000000..89b5e97 --- /dev/null +++ b/go_backend/controller/auth_controller.go @@ -0,0 +1,100 @@ +package controller + +import ( + "ai_xhs/common" + "ai_xhs/service" + + "github.com/gin-gonic/gin" +) + +type AuthController struct { + authService *service.AuthService +} + +func NewAuthController() *AuthController { + return &AuthController{ + authService: service.NewAuthService(), + } +} + +// WechatLogin 微信小程序登录 +func (ctrl *AuthController) WechatLogin(c *gin.Context) { + var req struct { + Code string `json:"code" binding:"required"` + Phone string `json:"phone"` // 可选,员工手机号(直接传明文) + PhoneCode string `json:"phone_code"` // 可选,微信手机号加密code + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error()) + return + } + + // 调用登录服务 + token, employee, err := ctrl.authService.WechatLogin(req.Code, req.Phone, req.PhoneCode) + if err != nil { + common.Error(c, common.CodeServerError, err.Error()) + return + } + + // 获取用户显示名称(优先使用真实姓名,其次用户名) + displayName := employee.RealName + if displayName == "" { + displayName = employee.Username + } + + common.SuccessWithMessage(c, "登录成功", gin.H{ + "token": token, + "employee": gin.H{ + "id": employee.ID, + "name": displayName, + "username": employee.Username, + "real_name": employee.RealName, + "phone": employee.Phone, + "role": employee.Role, + "enterprise_id": employee.EnterpriseID, + "enterprise_name": employee.EnterpriseName, + "is_bound_xhs": employee.IsBoundXHS, + }, + }) +} + +// PhoneLogin 手机号登录(用于测试) +func (ctrl *AuthController) PhoneLogin(c *gin.Context) { + var req struct { + Phone string `json:"phone" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error()) + return + } + + // 调用手机号登录服务 + token, employee, err := ctrl.authService.PhoneLogin(req.Phone) + if err != nil { + common.Error(c, common.CodeServerError, err.Error()) + return + } + + // 获取用户显示名称(优先使用真实姓名,其次用户名) + displayName := employee.RealName + if displayName == "" { + displayName = employee.Username + } + + common.SuccessWithMessage(c, "登录成功", gin.H{ + "token": token, + "employee": gin.H{ + "id": employee.ID, + "name": displayName, + "username": employee.Username, + "real_name": employee.RealName, + "phone": employee.Phone, + "role": employee.Role, + "enterprise_id": employee.EnterpriseID, + "enterprise_name": employee.EnterpriseName, + "is_bound_xhs": employee.IsBoundXHS, + }, + }) +} diff --git a/go_backend/controller/employee_controller.go b/go_backend/controller/employee_controller.go new file mode 100644 index 0000000..5cb75c4 --- /dev/null +++ b/go_backend/controller/employee_controller.go @@ -0,0 +1,257 @@ +package controller + +import ( + "ai_xhs/common" + "ai_xhs/service" + "strconv" + + "github.com/gin-gonic/gin" +) +type EmployeeController struct { + service *service.EmployeeService +} + +func NewEmployeeController() *EmployeeController { + return &EmployeeController{ + service: &service.EmployeeService{}, + } +} + +// SendXHSCode 发送小红书验证码 +func (ctrl *EmployeeController) SendXHSCode(c *gin.Context) { + var req struct { + XHSPhone string `json:"xhs_phone" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误:手机号不能为空") + return + } + + err := ctrl.service.SendXHSCode(req.XHSPhone) + if err != nil { + common.Error(c, common.CodeInternalError, err.Error()) + return + } + + common.SuccessWithMessage(c, "验证码已发送,请在小红书APP中查看", nil) +} + +// GetProfile 获取员工个人信息 +func (ctrl *EmployeeController) GetProfile(c *gin.Context) { + employeeID := c.GetInt("employee_id") + + employee, err := ctrl.service.GetProfile(employeeID) + if err != nil { + common.Error(c, common.CodeNotFound, "员工不存在") + return + } + + // 获取用户显示名称(优先使用真实姓名,其次用户名) + displayName := employee.RealName + if displayName == "" { + displayName = employee.Username + } + + data := map[string]interface{}{ + "id": employee.ID, + "name": displayName, + "username": employee.Username, + "real_name": employee.RealName, + "phone": employee.Phone, + "role": employee.Role, + "enterprise_id": employee.EnterpriseID, + "enterprise_name": employee.Enterprise.Name, + "is_bound_xhs": employee.IsBoundXHS, + "xhs_account": employee.XHSAccount, + "xhs_phone": employee.XHSPhone, + "has_xhs_cookie": employee.XHSCookie != "", // 标识是否有Cookie,不返回完整Cookie + } + + if employee.BoundAt != nil { + data["bound_at"] = employee.BoundAt.Format("2006-01-02 15:04:05") + } + + common.Success(c, data) +} + +// BindXHS 绑定小红书账号 +func (ctrl *EmployeeController) BindXHS(c *gin.Context) { + employeeID := c.GetInt("employee_id") + + var req struct { + XHSPhone string `json:"xhs_phone" binding:"required"` + Code string `json:"code" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误") + return + } + + xhsAccount, err := ctrl.service.BindXHS(employeeID, req.XHSPhone, req.Code) + if err != nil { + common.Error(c, common.CodeBindXHSFailed, err.Error()) + return + } + + common.SuccessWithMessage(c, "绑定成功", map[string]interface{}{ + "xhs_account": xhsAccount, + }) +} + +// UnbindXHS 解绑小红书账号 +func (ctrl *EmployeeController) UnbindXHS(c *gin.Context) { + employeeID := c.GetInt("employee_id") + + if err := ctrl.service.UnbindXHS(employeeID); err != nil { + common.Error(c, common.CodeInternalError, err.Error()) + return + } + + common.SuccessWithMessage(c, "解绑成功", nil) +} + +// GetAvailableCopies 获取可领取文案列表 +func (ctrl *EmployeeController) GetAvailableCopies(c *gin.Context) { + employeeID := c.GetInt("employee_id") + productID, err := strconv.Atoi(c.Query("product_id")) + if err != nil { + common.Error(c, common.CodeInvalidParams, "产品ID参数错误") + return + } + + data, err := ctrl.service.GetAvailableCopies(employeeID, productID) + if err != nil { + common.Error(c, common.CodeInternalError, err.Error()) + return + } + + common.Success(c, data) +} + +// ClaimCopy 领取文案 +func (ctrl *EmployeeController) ClaimCopy(c *gin.Context) { + employeeID := c.GetInt("employee_id") + + var req struct { + CopyID int `json:"copy_id" binding:"required"` + ProductID int `json:"product_id" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误") + return + } + + data, err := ctrl.service.ClaimCopy(employeeID, req.CopyID, req.ProductID) + if err != nil { + common.Error(c, common.CodeAlreadyClaimed, err.Error()) + return + } + + common.SuccessWithMessage(c, "领取成功", data) +} + +// ClaimRandomCopy 随机领取文案 +func (ctrl *EmployeeController) ClaimRandomCopy(c *gin.Context) { + employeeID := c.GetInt("employee_id") + + var req struct { + ProductID int `json:"product_id" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误") + return + } + + data, err := ctrl.service.ClaimRandomCopy(employeeID, req.ProductID) + if err != nil { + common.Error(c, common.CodeCopyNotAvailable, err.Error()) + return + } + + common.SuccessWithMessage(c, "领取成功", data) +} + +// Publish 发布内容 +func (ctrl *EmployeeController) Publish(c *gin.Context) { + employeeID := c.GetInt("employee_id") + + var req service.PublishRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误") + return + } + + recordID, err := ctrl.service.Publish(employeeID, req) + if err != nil { + common.Error(c, common.CodeInternalError, err.Error()) + return + } + + common.SuccessWithMessage(c, "发布成功", map[string]interface{}{ + "record_id": recordID, + }) +} + +// GetMyPublishRecords 获取我的发布记录 +func (ctrl *EmployeeController) GetMyPublishRecords(c *gin.Context) { + employeeID := c.GetInt("employee_id") + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + + data, err := ctrl.service.GetMyPublishRecords(employeeID, page, pageSize) + if err != nil { + common.Error(c, common.CodeInternalError, err.Error()) + return + } + + common.Success(c, data) +} + +// GetPublishRecordDetail 获取发布记录详情 +func (ctrl *EmployeeController) GetPublishRecordDetail(c *gin.Context) { + employeeID := c.GetInt("employee_id") + recordID, err := strconv.Atoi(c.Param("id")) + if err != nil { + common.Error(c, common.CodeInvalidParams, "记录ID参数错误") + return + } + + data, err := ctrl.service.GetPublishRecordDetail(employeeID, recordID) + if err != nil { + common.Error(c, common.CodeNotFound, err.Error()) + return + } + + common.Success(c, data) +} + +// CheckXHSStatus 检查小红书绑定与Cookie状态 +func (ctrl *EmployeeController) CheckXHSStatus(c *gin.Context) { + employeeID := c.GetInt("employee_id") + + status, err := ctrl.service.CheckXHSStatus(employeeID) + if err != nil { + common.Error(c, common.CodeInternalError, err.Error()) + return + } + + common.Success(c, status) +} + +// GetProducts 获取产品列表 +func (ctrl *EmployeeController) GetProducts(c *gin.Context) { + data, err := ctrl.service.GetProducts() + if err != nil { + common.Error(c, common.CodeInternalError, err.Error()) + return + } + + common.Success(c, map[string]interface{}{ + "list": data, + }) +} diff --git a/go_backend/controller/xhs_controller.go b/go_backend/controller/xhs_controller.go new file mode 100644 index 0000000..e7d9ea3 --- /dev/null +++ b/go_backend/controller/xhs_controller.go @@ -0,0 +1,75 @@ +package controller + +import ( + "ai_xhs/common" + "ai_xhs/service" + + "github.com/gin-gonic/gin" +) + +type XHSController struct { + service *service.XHSService +} + +func NewXHSController() *XHSController { + return &XHSController{ + service: &service.XHSService{}, + } +} + +// SendCode 发送小红书验证码 +func (ctrl *XHSController) SendCode(c *gin.Context) { + var req struct { + Phone string `json:"phone" binding:"required"` + CountryCode string `json:"country_code"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误:手机号不能为空") + return + } + + // 调用Service层发送验证码 + result, err := ctrl.service.SendVerificationCode(req.Phone, req.CountryCode) + if err != nil { + common.Error(c, common.CodeInternalError, "调用Python服务失败: "+err.Error()) + return + } + + // 判断Python服务返回的结果 + if result.Code != 0 { + common.Error(c, result.Code, result.Message) + return + } + + common.SuccessWithMessage(c, result.Message, result.Data) +} + +// VerifyCode 验证小红书验证码并登录 +func (ctrl *XHSController) VerifyCode(c *gin.Context) { + var req struct { + Phone string `json:"phone" binding:"required"` + Code string `json:"code" binding:"required"` + CountryCode string `json:"country_code"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, common.CodeInvalidParams, "参数错误:手机号和验证码不能为空") + return + } + + // 调用Service层验证登录 + result, err := ctrl.service.VerifyLogin(req.Phone, req.Code, req.CountryCode) + if err != nil { + common.Error(c, common.CodeInternalError, "调用Python服务失败: "+err.Error()) + return + } + + // 判断Python服务返回的结果 + if result.Code != 0 { + common.Error(c, result.Code, result.Message) + return + } + + common.SuccessWithMessage(c, result.Message, result.Data) +} diff --git a/go_backend/database/database.go b/go_backend/database/database.go new file mode 100644 index 0000000..992aa15 --- /dev/null +++ b/go_backend/database/database.go @@ -0,0 +1,74 @@ +package database + +import ( + "fmt" + "log" + "time" + + "ai_xhs/config" + "ai_xhs/models" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var DB *gorm.DB + +// InitDB 初始化数据库连接 +func InitDB() error { + cfg := config.AppConfig.Database + dsn := cfg.GetDSN() + + var err error + DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + return fmt.Errorf("数据库连接失败: %w", err) + } + + sqlDB, err := DB.DB() + if err != nil { + return fmt.Errorf("获取数据库实例失败: %w", err) + } + + // 设置连接池 + sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) + sqlDB.SetMaxOpenConns(cfg.MaxOpenConns) + sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second) + + log.Println("数据库连接成功") + return nil +} + +// AutoMigrate 自动迁移数据库表 +func AutoMigrate() error { + // 注意:由于使用现有数据库,这里只做模型注册,不执行实际迁移 + // 如果需要同步表结构,请手动执行 AutoMigrate + err := DB.AutoMigrate( + &models.Enterprise{}, + &models.User{}, + &models.Product{}, + &models.Article{}, + &models.PublishRecord{}, + &models.ArticleImage{}, + &models.ArticleTag{}, + &models.Log{}, + // &models.XHSAccount{}, // 不再使用,小红书信息直接存储在 ai_users 表中 + ) + if err != nil { + return fmt.Errorf("数据库表迁移失败: %w", err) + } + log.Println("数据库表迁移成功") + return nil +} + +// Close 关闭数据库连接 +func Close() error { + sqlDB, err := DB.DB() + if err != nil { + return err + } + return sqlDB.Close() +} diff --git a/go_backend/go.mod b/go_backend/go.mod new file mode 100644 index 0000000..a8c9e1b --- /dev/null +++ b/go_backend/go.mod @@ -0,0 +1,57 @@ +module ai_xhs + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/spf13/viper v1.18.2 + gorm.io/driver/mysql v1.5.2 + gorm.io/gorm v1.25.5 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go_backend/go.sum b/go_backend/go.sum new file mode 100644 index 0000000..69155ba --- /dev/null +++ b/go_backend/go.sum @@ -0,0 +1,144 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/go_backend/main.go b/go_backend/main.go new file mode 100644 index 0000000..5b6595e --- /dev/null +++ b/go_backend/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "ai_xhs/config" + "ai_xhs/database" + "ai_xhs/middleware" + "ai_xhs/router" + "ai_xhs/service" + "flag" + "fmt" + "log" + + "github.com/gin-gonic/gin" +) + +func main() { + // 解析命令行参数 + env := flag.String("env", "dev", "运行环境: dev, prod") + flag.Parse() + + // 加载配置 + if err := config.LoadConfig(*env); err != nil { + log.Fatalf("配置加载失败: %v", err) + } + + // 初始化数据库 + if err := database.InitDB(); err != nil { + log.Fatalf("数据库初始化失败: %v", err) + } + + // 自动迁移数据库表 + //if err := database.AutoMigrate(); err != nil { + // log.Fatalf("数据库迁移失败: %v", err) + //} + + // 初始化定时任务 + if config.AppConfig.Scheduler.Enabled { + scheduler := service.NewSchedulerService( + config.AppConfig.Scheduler.MaxConcurrent, + config.AppConfig.Scheduler.PublishTimeout, + ) + if err := scheduler.Start(config.AppConfig.Scheduler.PublishCron); err != nil { + log.Fatalf("定时任务启动失败: %v", err) + } + log.Println("定时任务服务已启动") + defer scheduler.Stop() + } else { + log.Println("定时任务服务已禁用") + } + + // 设置运行模式 + gin.SetMode(config.AppConfig.Server.Mode) + + // 创建路由 + r := gin.New() + + // 添加中间件 + r.Use(gin.Recovery()) // 崩溃恢复 + r.Use(middleware.RequestLogger()) // API请求日志 + + // 设置路由 + router.SetupRouter(r) + + // 启动服务 + addr := fmt.Sprintf(":%d", config.AppConfig.Server.Port) + log.Printf("服务启动在端口 %s, 环境: %s", addr, *env) + if err := r.Run(addr); err != nil { + log.Fatalf("服务启动失败: %v", err) + } +} diff --git a/go_backend/middleware/auth.go b/go_backend/middleware/auth.go new file mode 100644 index 0000000..ee001cf --- /dev/null +++ b/go_backend/middleware/auth.go @@ -0,0 +1,59 @@ +package middleware + +import ( + "ai_xhs/common" + "ai_xhs/utils" + "strings" + + "github.com/gin-gonic/gin" +) + +// AuthMiddleware JWT认证中间件 +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 从请求头获取token + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + common.Error(c, common.CodeUnauthorized, "未登录或token为空") + c.Abort() + return + } + + // 检查token格式 + parts := strings.SplitN(authHeader, " ", 2) + if !(len(parts) == 2 && parts[0] == "Bearer") { + common.Error(c, common.CodeUnauthorized, "token格式错误") + c.Abort() + return + } + + // 解析token + claims, err := utils.ParseToken(parts[1]) + if err != nil { + common.Error(c, common.CodeUnauthorized, "无效的token") + c.Abort() + return + } + + // 将员工ID存入上下文 + c.Set("employee_id", claims.EmployeeID) + c.Next() + } +} + +// CORS 跨域中间件 +func CORS() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} diff --git a/go_backend/middleware/logger.go b/go_backend/middleware/logger.go new file mode 100644 index 0000000..8177c43 --- /dev/null +++ b/go_backend/middleware/logger.go @@ -0,0 +1,171 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +// responseWriter 包装 gin.ResponseWriter 以捕获响应体 +type responseWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (w responseWriter) Write(b []byte) (int, error) { + w.body.Write(b) + return w.ResponseWriter.Write(b) +} + +// RequestLogger API请求和响应日志中间件 +func RequestLogger() gin.HandlerFunc { + return func(c *gin.Context) { + startTime := time.Now() + + // 读取请求体 + var requestBody []byte + if c.Request.Body != nil { + requestBody, _ = io.ReadAll(c.Request.Body) + // 恢复请求体供后续处理使用 + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + } + + // 包装 ResponseWriter 以捕获响应 + blw := &responseWriter{ + ResponseWriter: c.Writer, + body: bytes.NewBufferString(""), + } + c.Writer = blw + + // 打印请求信息 + printRequest(c, requestBody) + + // 处理请求 + c.Next() + + // 计算请求耗时 + duration := time.Since(startTime) + + // 打印响应信息 + printResponse(c, blw.body.Bytes(), duration) + } +} + +// printRequest 打印请求详情 +func printRequest(c *gin.Context, body []byte) { + fmt.Println("\n" + strings.Repeat("=", 100)) + fmt.Printf("📥 [REQUEST] %s\n", time.Now().Format("2006-01-02 15:04:05")) + fmt.Println(strings.Repeat("=", 100)) + + // 请求基本信息 + fmt.Printf("Method: %s\n", c.Request.Method) + fmt.Printf("Path: %s\n", c.Request.URL.Path) + fmt.Printf("Full URL: %s\n", c.Request.URL.String()) + fmt.Printf("Client IP: %s\n", c.ClientIP()) + fmt.Printf("User-Agent: %s\n", c.Request.UserAgent()) + + // 请求头 + if len(c.Request.Header) > 0 { + fmt.Println("\n--- Headers ---") + for key, values := range c.Request.Header { + // 过滤敏感信息 + if strings.ToLower(key) == "authorization" || strings.ToLower(key) == "cookie" { + fmt.Printf("%s: [HIDDEN]\n", key) + } else { + fmt.Printf("%s: %s\n", key, strings.Join(values, ", ")) + } + } + } + + // 查询参数 + if len(c.Request.URL.Query()) > 0 { + fmt.Println("\n--- Query Parameters ---") + for key, values := range c.Request.URL.Query() { + fmt.Printf("%s: %s\n", key, strings.Join(values, ", ")) + } + } + + // 请求体 + if len(body) > 0 { + fmt.Println("\n--- Request Body ---") + // 尝试格式化 JSON + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, body, "", " "); err == nil { + fmt.Println(prettyJSON.String()) + } else { + fmt.Println(string(body)) + } + } + + fmt.Println(strings.Repeat("-", 100)) +} + +// printResponse 打印响应详情 +func printResponse(c *gin.Context, body []byte, duration time.Duration) { + fmt.Println("\n" + strings.Repeat("=", 100)) + fmt.Printf("📤 [RESPONSE] %s | Duration: %v\n", time.Now().Format("2006-01-02 15:04:05"), duration) + fmt.Println(strings.Repeat("=", 100)) + + // 响应基本信息 + fmt.Printf("Status Code: %d %s\n", c.Writer.Status(), getStatusText(c.Writer.Status())) + fmt.Printf("Size: %d bytes\n", c.Writer.Size()) + + // 响应头 + if len(c.Writer.Header()) > 0 { + fmt.Println("\n--- Response Headers ---") + for key, values := range c.Writer.Header() { + fmt.Printf("%s: %s\n", key, strings.Join(values, ", ")) + } + } + + // 响应体 + if len(body) > 0 { + fmt.Println("\n--- Response Body ---") + // 尝试格式化 JSON + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, body, "", " "); err == nil { + fmt.Println(prettyJSON.String()) + } else { + fmt.Println(string(body)) + } + } + + // 性能提示 + if duration > 1*time.Second { + fmt.Printf("\n⚠️ WARNING: Request took %.2f seconds (>1s)\n", duration.Seconds()) + } else if duration > 500*time.Millisecond { + fmt.Printf("\n⚡ NOTICE: Request took %.0f milliseconds (>500ms)\n", duration.Milliseconds()) + } + + fmt.Println(strings.Repeat("=", 100)) + fmt.Println() +} + +// getStatusText 获取状态码文本 +func getStatusText(code int) string { + switch code { + case 200: + return "OK" + case 201: + return "Created" + case 204: + return "No Content" + case 400: + return "Bad Request" + case 401: + return "Unauthorized" + case 403: + return "Forbidden" + case 404: + return "Not Found" + case 500: + return "Internal Server Error" + default: + return "" + } +} diff --git a/go_backend/models/models.go b/go_backend/models/models.go new file mode 100644 index 0000000..9db1159 --- /dev/null +++ b/go_backend/models/models.go @@ -0,0 +1,288 @@ +package models + +import ( + "time" +) + +// Enterprise 企业表 +type Enterprise struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + EnterpriseID string `gorm:"type:varchar(255);not null;default:''" json:"enterprise_id" comment:"企业ID"` + Name string `gorm:"type:varchar(200);not null;default:''" json:"name" comment:"企业名称"` + ShortName string `gorm:"type:varchar(100);not null;default:''" json:"short_name" comment:"企业简称"` + Icon string `gorm:"type:varchar(500);not null;default:''" json:"icon" comment:"企业图标URL"` + Phone string `gorm:"type:varchar(20);not null;default:'';uniqueIndex:uk_phone" json:"phone" comment:"登录手机号"` + Password string `gorm:"type:varchar(255);not null;default:''" json:"-" comment:"登录密码(加密存储)"` + Email string `gorm:"type:varchar(128);not null;default:''" json:"email" comment:"企业邮箱"` + Website string `gorm:"type:varchar(255);not null;default:''" json:"website" comment:"企业网站"` + Address string `gorm:"type:varchar(255);not null;default:''" json:"address" comment:"企业地址"` + Status string `gorm:"type:enum('active','disabled');not null;default:'active';index:idx_status" json:"status" comment:"状态"` + UsersTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"users_total" comment:"员工总数"` + ProductsTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"products_total" comment:"产品总数"` + ArticlesTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"articles_total" comment:"文章总数"` + ReleasedMonthTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"released_month_total" comment:"本月发布数量"` + LinkedToXHSNum int `gorm:"type:int(10) unsigned;not null;default:0" json:"linked_to_xhs_num" comment:"绑定小红书"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// User 用户账号表(原Employee,对应ai_users) +type User struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"` + Enterprise Enterprise `gorm:"foreignKey:EnterpriseID" json:"enterprise,omitempty"` + EnterpriseName string `gorm:"type:varchar(255);not null;default:''" json:"enterprise_name" comment:"企业名称"` + Username string `gorm:"type:varchar(50);not null;default:'';uniqueIndex:uk_username" json:"username" comment:"用户名"` + Password string `gorm:"type:varchar(255);not null;default:''" json:"-" comment:"密码"` + RealName string `gorm:"type:varchar(50)" json:"real_name" comment:"真实姓名"` + Email string `gorm:"type:varchar(100)" json:"email" comment:"邮箱"` + Phone string `gorm:"type:varchar(20)" json:"phone" comment:"手机号"` + WechatOpenID *string `gorm:"type:varchar(100);uniqueIndex:uk_wechat_openid" json:"wechat_openid,omitempty" comment:"微信OpenID"` + WechatUnionID *string `gorm:"type:varchar(100)" json:"wechat_unionid,omitempty" comment:"微信UnionID"` + XHSPhone string `gorm:"type:varchar(20);not null;default:''" json:"xhs_phone" comment:"小红书绑定手机号"` + XHSAccount string `gorm:"type:varchar(255);not null;default:''" json:"xhs_account" comment:"小红书账号名称"` + XHSCookie string `gorm:"type:text" json:"xhs_cookie" comment:"小红书Cookie"` + IsBoundXHS int `gorm:"type:tinyint(1);not null;default:0;index:idx_is_bound_xhs" json:"is_bound_xhs" comment:"是否绑定小红书:0=未绑定,1=已绑定"` + BoundAt *time.Time `json:"bound_at" comment:"绑定小红书的时间"` + Department string `gorm:"type:varchar(50)" json:"department" comment:"部门"` + Role string `gorm:"type:enum('admin','editor','reviewer','publisher','each_title_reviewer');default:'editor'" json:"role" comment:"角色"` + Status string `gorm:"type:enum('active','inactive','deleted');default:'active';index:idx_status" json:"status" comment:"状态"` + CreatedAt time.Time `json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// Employee 员工表别名,兼容旧代码 +type Employee = User + +// Product 产品表 +type Product struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"` + Name string `gorm:"type:varchar(200);not null;default:''" json:"name" comment:"产品名称"` + TypeName string `gorm:"type:varchar(128);not null;default:''" json:"type_name" comment:"产品类型"` + ImageURL string `gorm:"type:varchar(500);not null;default:''" json:"image_url" comment:"产品主图URL"` + ImageThumbnailURL string `gorm:"type:varchar(500);not null;default:''" json:"image_thumbnail_url" comment:"缩略图URL"` + Knowledge string `gorm:"type:text" json:"knowledge" comment:"产品知识库(纯文字)"` + ArticlesTotal int `gorm:"not null;default:0" json:"articles_total" comment:"文章总数"` + PublishedTotal int `gorm:"not null;default:0" json:"published_total" comment:"发布总数"` + Status string `gorm:"type:enum('draft','active','deleted');not null;default:'draft';index:idx_status" json:"status" comment:"状态:draft=草稿,active=正常,deleted=已删除"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// Article 文章表(原Copy,对应ai_articles) +type Article struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + BatchID uint64 `gorm:"type:bigint unsigned;not null;default:0" json:"batch_id" comment:"批次ID"` + EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"` + ProductID int `gorm:"not null;default:0;index:idx_product_id" json:"product_id" comment:"关联产品ID"` + TopicTypeID int `gorm:"type:int unsigned;not null;default:0" json:"topic_type_id" comment:"topic类型ID"` + PromptWorkflowID int `gorm:"type:int unsigned;not null;default:0" json:"prompt_workflow_id" comment:"提示词工作流ID"` + Topic string `gorm:"type:varchar(255);not null;default:''" json:"topic" comment:"topic主题"` + Title string `gorm:"type:varchar(200);not null;default:''" json:"title" comment:"标题"` + Content string `gorm:"type:text" json:"content" comment:"文章内容"` + Department string `gorm:"type:varchar(255);not null;default:''" json:"department" comment:"部门"` + DepartmentIDs string `gorm:"column:departmentids;type:varchar(255);not null;default:''" json:"department_ids" comment:"部门IDs"` + AuthorID *int `json:"author_id" comment:"作者ID"` + AuthorName string `gorm:"type:varchar(100)" json:"author_name" comment:"作者名称"` + DepartmentID *int `json:"department_id" comment:"部门ID"` + DepartmentName string `gorm:"type:varchar(255)" json:"department_name" comment:"部门名称"` + CreatedUserID int `gorm:"not null;default:0" json:"created_user_id" comment:"创建用户ID"` + ReviewUserID *int `json:"review_user_id" comment:"审核用户ID"` + PublishUserID *int `json:"publish_user_id" comment:"发布用户ID"` + Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"` + Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=baidu|2=toutiao|3=weixin"` + ReviewComment string `gorm:"type:text" json:"review_comment" comment:"审核评论"` + PublishTime *time.Time `json:"publish_time" comment:"发布时间"` + BaijiahaoID string `gorm:"type:varchar(100)" json:"baijiahao_id" comment:"百家号ID"` + BaijiahaoStatus string `gorm:"type:varchar(50)" json:"baijiahao_status" comment:"百家号状态"` + WordCount int `gorm:"default:0" json:"word_count" comment:"字数统计"` + ImageCount int `gorm:"default:0" json:"image_count" comment:"图片数量"` + CozeTag string `gorm:"type:varchar(500)" json:"coze_tag" comment:"Coze生成的标签"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `gorm:"index:idx_updated_at" json:"updated_at" comment:"更新时间"` +} + +// Copy 文案表别名,兼容旧代码 +type Copy = Article + +// PublishRecord 发布记录表(对应ai_article_published_records) +type PublishRecord struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + ArticleID *int `gorm:"index:idx_article_id" json:"article_id" comment:"文章ID"` + EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"` + ProductID int `gorm:"not null;default:0;index:idx_product_id" json:"product_id" comment:"关联产品ID"` + Topic string `gorm:"type:varchar(255);not null;default:''" json:"topic" comment:"topic主题"` + Title string `gorm:"type:varchar(200);not null;default:''" json:"title" comment:"标题"` + CreatedUserID int `gorm:"not null;default:0;index:idx_created_user_id" json:"created_user_id" comment:"创建用户ID"` + ReviewUserID *int `json:"review_user_id" comment:"审核用户ID"` + PublishUserID *int `json:"publish_user_id" comment:"发布用户ID"` + Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"` + Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=baidu|2=toutiao|3=weixin"` + ReviewComment string `gorm:"type:text" json:"review_comment" comment:"审核评论"` + PublishTime *time.Time `json:"publish_time" comment:"发布时间"` + PublishLink string `gorm:"type:varchar(128);not null;default:''" json:"publish_link" comment:"发布访问链接"` + WordCount int `gorm:"default:0" json:"word_count" comment:"字数统计"` + ImageCount int `gorm:"default:0" json:"image_count" comment:"图片数量"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `gorm:"index:idx_updated_at" json:"updated_at" comment:"更新时间"` +} + +// XHSAccount 小红书账号表(保持兼容) +type XHSAccount struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + EmployeeID int `gorm:"not null;default:0;uniqueIndex:uk_employee_id" json:"employee_id"` + Employee User `gorm:"foreignKey:EmployeeID" json:"employee,omitempty"` + XHSUserID string `gorm:"type:varchar(100);not null;default:'';index:idx_xhs_user_id" json:"xhs_user_id"` + XHSNickname string `gorm:"type:varchar(100);not null;default:''" json:"xhs_nickname"` + XHSPhone string `gorm:"type:varchar(20);not null;default:''" json:"xhs_phone"` + XHSAvatar string `gorm:"type:varchar(500);not null;default:''" json:"xhs_avatar"` + FansCount int `gorm:"not null;default:0" json:"fans_count"` + NotesCount int `gorm:"not null;default:0" json:"notes_count"` + Cookies string `gorm:"type:text" json:"cookies"` + AccessToken string `gorm:"type:varchar(500);not null;default:''" json:"access_token"` + RefreshToken string `gorm:"type:varchar(500);not null;default:''" json:"refresh_token"` + TokenExpireAt *time.Time `json:"token_expire_at"` + Status string `gorm:"type:enum('active','expired','banned');default:'active';index:idx_status" json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// PromptWorkflow 提示词工作流表 +type PromptWorkflow struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"` + PromptWorkflowName string `gorm:"type:varchar(100);not null;default:''" json:"prompt_workflow_name" comment:"提示词工作流名称"` + AuthToken string `gorm:"type:varchar(100);not null;default:''" json:"auth_token" comment:"认证Token"` + WorkflowID string `gorm:"type:varchar(100);not null;default:'';index:idx_workflow_id" json:"workflow_id" comment:"工作流ID"` + Content string `gorm:"type:text" json:"content" comment:"提示词内容"` + UsageCount int `gorm:"not null;default:0" json:"usage_count" comment:"使用次数统计"` + CreatedUserID int `gorm:"not null;default:0" json:"created_user_id" comment:"创建用户ID"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// ProductImage 产品图片库表 +type ProductImage struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"` + ProductID int `gorm:"not null;default:0;index:idx_product_id" json:"product_id" comment:"关联产品ID"` + ImageID int `gorm:"not null;default:0" json:"image_id" comment:"图片ID"` + ImageName string `gorm:"type:varchar(255);not null;default:''" json:"image_name" comment:"图片名称"` + ImageURL string `gorm:"type:varchar(500);not null;default:''" json:"image_url" comment:"图片URL"` + ThumbnailURL string `gorm:"type:varchar(500);not null;default:''" json:"thumbnail_url" comment:"缩略图URL"` + TypeName string `gorm:"type:varchar(50);not null;default:''" json:"type_name" comment:"图片类型"` + Description string `gorm:"type:varchar(500);not null;default:''" json:"description" comment:"图片描述"` + FileSize *int64 `json:"file_size" comment:"文件大小"` + Width *int `json:"width" comment:"图片宽度"` + Height *int `json:"height" comment:"图片高度"` + UploadUserID int `gorm:"not null;default:0" json:"upload_user_id" comment:"上传用户ID"` + Status string `gorm:"type:enum('active','deleted');default:'active';index:idx_status" json:"status" comment:"状态"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"上传时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// ArticleImage 文章图片表 +type ArticleImage struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + ArticleID int `gorm:"not null;default:0;index:idx_article_id" json:"article_id" comment:"文章ID"` + ImageID int `gorm:"not null;default:0;index:idx_image_id" json:"image_id" comment:"图片ID"` + ImageURL string `gorm:"type:varchar(500);not null;default:''" json:"image_url" comment:"图片URL"` + ImageThumbURL string `gorm:"type:varchar(255);not null;default:''" json:"image_thumb_url" comment:"缩略图URL"` + ImageTagID int `gorm:"not null;default:0" json:"image_tag_id" comment:"图片标签ID"` + SortOrder int `gorm:"default:0" json:"sort_order" comment:"排序"` + KeywordsID int `gorm:"not null;default:0" json:"keywords_id" comment:"关键词ID"` + KeywordsName string `gorm:"type:varchar(255);not null;default:''" json:"keywords_name" comment:"关键词名称"` + DepartmentID int `gorm:"not null;default:0" json:"department_id" comment:"部门ID"` + DepartmentName string `gorm:"type:varchar(255);not null;default:''" json:"department_name" comment:"部门名称"` + ImageSource int `gorm:"type:tinyint(1);not null;default:0" json:"image_source" comment:"图片来源:1=tag|2=change"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// ArticleTag 文章标签表 +type ArticleTag struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + ArticleID int `gorm:"not null;default:0;uniqueIndex:uk_article_tag" json:"article_id" comment:"文章ID"` + CozeTag string `gorm:"type:varchar(500)" json:"coze_tag" comment:"Coze生成的标签"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` +} + +// DataStatistics 数据统计表 +type DataStatistics struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"` + ProductID int `gorm:"not null;default:0;index:idx_product_id" json:"product_id" comment:"关联产品ID"` + CumulativeReleasesNum int `gorm:"type:int(10) unsigned;not null;default:0" json:"cumulative_releases_num" comment:"累计发布"` + PublishedTodayNum int `gorm:"type:int(10) unsigned;not null;default:0" json:"published_today_num" comment:"今日发布"` + PublishedWeekNum int `gorm:"type:int(10) unsigned;not null;default:0" json:"published_week_num" comment:"本周发布"` + ParticipatingEmployees int `gorm:"type:int(10) unsigned;not null;default:0" json:"participating_employees_num" comment:"参与员工"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` + UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` +} + +// Log 操作日志表(对应ai_logs) +type Log struct { + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + UserID *int `gorm:"index:idx_user_id" json:"user_id" comment:"用户ID"` + Action string `gorm:"type:varchar(100);not null;default:'';index:idx_action" json:"action" comment:"操作动作"` + TargetType string `gorm:"type:varchar(50)" json:"target_type" comment:"目标类型"` + TargetID *int `json:"target_id" comment:"目标ID"` + Description string `gorm:"type:text" json:"description" comment:"描述"` + IPAddress string `gorm:"type:varchar(45)" json:"ip_address" comment:"IP地址"` + UserAgent string `gorm:"type:text" json:"user_agent" comment:"用户代理"` + RequestData string `gorm:"type:json" json:"request_data" comment:"请求数据"` + ResponseData string `gorm:"type:json" json:"response_data" comment:"响应数据"` + Status string `gorm:"type:enum('success','error','warning');default:'success';index:idx_status" json:"status" comment:"状态"` + ErrorMessage string `gorm:"type:text" json:"error_message" comment:"错误消息"` + CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"` +} + +// TableName 指定表名(带ai_前缀) +func (Enterprise) TableName() string { + return "ai_enterprises" +} + +func (User) TableName() string { + return "ai_users" +} + +func (Product) TableName() string { + return "ai_products" +} + +func (Article) TableName() string { + return "ai_articles" +} + +func (XHSAccount) TableName() string { + return "wht_xhs_accounts" // 保持兼容旧表 +} + +func (PublishRecord) TableName() string { + return "ai_article_published_records" +} + +func (PromptWorkflow) TableName() string { + return "ai_prompt_workflow" +} + +func (ProductImage) TableName() string { + return "ai_product_images" +} + +func (ArticleImage) TableName() string { + return "ai_article_images" +} + +func (ArticleTag) TableName() string { + return "ai_article_tags" +} + +func (DataStatistics) TableName() string { + return "ai_data_statistics" +} + +func (Log) TableName() string { + return "ai_logs" +} diff --git a/go_backend/restart.sh b/go_backend/restart.sh new file mode 100644 index 0000000..c95601b --- /dev/null +++ b/go_backend/restart.sh @@ -0,0 +1,246 @@ +#!/bin/bash + +######################################### +# AI小红书 Go 后端服务重启脚本 +# 用途: Ubuntu/Linux 环境下重启 Go 服务 +# 支持: 开发环境(dev) 和 生产环境(prod) +######################################### + +# 默认配置 +DEFAULT_ENV="prod" +DEFAULT_PORT=8080 +PROD_PORT=8070 +LOG_FILE="ai_xhs.log" +MAIN_FILE="main.go" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 帮助信息 +show_help() { + echo -e "${BLUE}用法:${NC}" + echo " ./restart.sh [环境]" + echo "" + echo -e "${BLUE}环境参数:${NC}" + echo " dev - 开发环境 (默认, 端口 8080)" + echo " prod - 生产环境 (端口 8070)" + echo "" + echo -e "${BLUE}示例:${NC}" + echo " ./restart.sh # 启动开发环境" + echo " ./restart.sh dev # 启动开发环境" + echo " ./restart.sh prod # 启动生产环境" + exit 0 +} + +# 解析参数 +ENV="${1:-$DEFAULT_ENV}" + +if [ "$ENV" = "help" ] || [ "$ENV" = "-h" ] || [ "$ENV" = "--help" ]; then + show_help +fi + +# 确定端口 +if [ "$ENV" = "prod" ]; then + PORT=$PROD_PORT +else + PORT=$DEFAULT_PORT + ENV="dev" +fi + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} AI小红书 Go 后端服务重启脚本${NC}" +echo -e "${BLUE}========================================${NC}" +echo -e "${YELLOW}环境: $ENV${NC}" +echo -e "${YELLOW}端口: $PORT${NC}" +echo -e "${YELLOW}日志: $LOG_FILE${NC}" +echo "" + +######################################### +# 1. 停止现有服务 +######################################### +echo -e "${BLUE}=== [1/4] 停止现有服务 ===${NC}" + +# 方法1: 查找 go run main.go 进程 +GO_PIDS=$(ps aux | grep "go run $MAIN_FILE" | grep -v grep | awk '{print $2}') + +if [ -n "$GO_PIDS" ]; then + echo -e "${YELLOW}找到 Go 服务进程: $GO_PIDS${NC}" + for PID in $GO_PIDS; do + kill -9 $PID 2>/dev/null && echo " 已终止进程: $PID" + done + sleep 1 +fi + +# 方法2: 查找编译后的可执行文件进程 +COMPILED_PIDS=$(ps aux | grep "/tmp/go-build.*$MAIN_FILE" | grep -v grep | awk '{print $2}') + +if [ -n "$COMPILED_PIDS" ]; then + echo -e "${YELLOW}找到编译后的进程: $COMPILED_PIDS${NC}" + for PID in $COMPILED_PIDS; do + kill -9 $PID 2>/dev/null && echo " 已终止进程: $PID" + done + sleep 1 +fi + +# 方法3: 强制清理占用端口的进程 +echo -e "${YELLOW}清理端口 $PORT...${NC}" + +# 使用 lsof 查找占用端口的进程 +PORT_PID=$(lsof -ti:$PORT 2>/dev/null) +if [ -n "$PORT_PID" ]; then + echo " 端口 $PORT 被进程 $PORT_PID 占用" + kill -9 $PORT_PID 2>/dev/null && echo " 已终止进程: $PORT_PID" +fi + +# 使用 fuser 强制清理 (需要 sudo) +if command -v fuser &> /dev/null; then + sudo fuser -k $PORT/tcp 2>/dev/null || true +fi + +# 额外的清理方法 +sudo pkill -f ":$PORT" 2>/dev/null || true +sudo pkill -f "main.go" 2>/dev/null || true + +# 等待端口完全释放 +sleep 2 + +# 验证端口是否释放 +if lsof -ti:$PORT &> /dev/null; then + echo -e "${RED}⚠️ 警告: 端口 $PORT 仍被占用${NC}" + lsof -i:$PORT +else + echo -e "${GREEN}✅ 端口 $PORT 已释放${NC}" +fi + +echo "" + +######################################### +# 2. 环境检查 +######################################### +echo -e "${BLUE}=== [2/4] 环境检查 ===${NC}" + +# 检查 Go 环境 +if ! command -v go &> /dev/null; then + echo -e "${RED}❌ 错误: 未检测到 Go 环境,请先安装 Go${NC}" + exit 1 +fi + +GO_VERSION=$(go version) +echo -e "${GREEN}✅ Go 环境: $GO_VERSION${NC}" + +# 检查 main.go 是否存在 +if [ ! -f "$MAIN_FILE" ]; then + echo -e "${RED}❌ 错误: 未找到 $MAIN_FILE 文件${NC}" + exit 1 +fi + +echo -e "${GREEN}✅ 主文件: $MAIN_FILE${NC}" + +# 检查配置文件 +CONFIG_FILE="config/config.${ENV}.yaml" +if [ ! -f "$CONFIG_FILE" ]; then + echo -e "${RED}❌ 错误: 未找到配置文件 $CONFIG_FILE${NC}" + exit 1 +fi + +echo -e "${GREEN}✅ 配置文件: $CONFIG_FILE${NC}" +echo "" + +######################################### +# 3. 下载依赖 +######################################### +echo -e "${BLUE}=== [3/4] 下载依赖 ===${NC}" + +if [ -f "go.mod" ]; then + go mod tidy + echo -e "${GREEN}✅ 依赖下载完成${NC}" +else + echo -e "${YELLOW}⚠️ 未找到 go.mod 文件,跳过依赖下载${NC}" +fi + +echo "" + +######################################### +# 4. 启动服务 +######################################### +echo -e "${BLUE}=== [4/4] 启动服务 ===${NC}" + +# 设置环境变量 +export APP_ENV=$ENV + +# 清空旧日志 +> $LOG_FILE + +# 启动服务 (后台运行) +echo -e "${YELLOW}启动命令: nohup go run $MAIN_FILE > $LOG_FILE 2>&1 &${NC}" +nohup go run $MAIN_FILE > $LOG_FILE 2>&1 & + +# 记录进程 PID +NEW_PID=$! +echo -e "${GREEN}✅ 服务已启动,进程 PID: $NEW_PID${NC}" + +echo "" + +######################################### +# 5. 验证启动 +######################################### +echo -e "${BLUE}=== 启动验证 ===${NC}" +echo -e "${YELLOW}等待服务启动 (5秒)...${NC}" +sleep 5 + +# 检查进程是否存在 +if ps -p $NEW_PID > /dev/null 2>&1; then + echo -e "${GREEN}✅ 服务进程运行正常 (PID: $NEW_PID)${NC}" +else + echo -e "${RED}❌ 服务进程未找到,可能启动失败${NC}" + echo -e "${YELLOW}最近日志:${NC}" + tail -n 20 $LOG_FILE + exit 1 +fi + +# 检查端口是否监听 +if lsof -ti:$PORT &> /dev/null; then + echo -e "${GREEN}✅ 端口 $PORT 监听正常${NC}" +else + echo -e "${RED}❌ 端口 $PORT 未监听,服务可能启动失败${NC}" + echo -e "${YELLOW}最近日志:${NC}" + tail -n 20 $LOG_FILE + exit 1 +fi + +# 检查日志中是否有错误 +if grep -i "fatal\|panic\|error" $LOG_FILE > /dev/null 2>&1; then + echo -e "${YELLOW}⚠️ 日志中发现错误信息:${NC}" + grep -i "fatal\|panic\|error" $LOG_FILE | head -n 5 +fi + +echo "" + +######################################### +# 6. 完成信息 +######################################### +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} 🎉 服务启动成功!${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo -e "${BLUE}服务信息:${NC}" +echo -e " 环境: ${YELLOW}$ENV${NC}" +echo -e " 端口: ${YELLOW}$PORT${NC}" +echo -e " 进程PID: ${YELLOW}$NEW_PID${NC}" +echo -e " 日志文件: ${YELLOW}$LOG_FILE${NC}" +echo "" +echo -e "${BLUE}快捷命令:${NC}" +echo -e " 查看日志: ${YELLOW}tail -f $LOG_FILE${NC}" +echo -e " 查看进程: ${YELLOW}ps aux | grep 'go run'${NC}" +echo -e " 停止服务: ${YELLOW}kill -9 $NEW_PID${NC}" +echo -e " 检查端口: ${YELLOW}lsof -i:$PORT${NC}" +echo "" +echo -e "${BLUE}访问地址:${NC}" +echo -e " 本地: ${GREEN}http://localhost:$PORT${NC}" +echo -e " API测试: ${GREEN}http://localhost:$PORT/api/health${NC}" +echo "" +echo -e "${YELLOW}提示: 使用 Ctrl+C 不会停止后台服务,请使用 kill 命令停止${NC}" diff --git a/go_backend/router/router.go b/go_backend/router/router.go new file mode 100644 index 0000000..af9e8db --- /dev/null +++ b/go_backend/router/router.go @@ -0,0 +1,71 @@ +package router + +import ( + "ai_xhs/controller" + "ai_xhs/middleware" + + "github.com/gin-gonic/gin" +) + +func SetupRouter(r *gin.Engine) { + // 跨域中间件 + r.Use(middleware.CORS()) + + // 健康检查 + r.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "ok", + }) + }) + + // API路由组 + api := r.Group("/api") + { + // 公开接口(不需要认证) + authCtrl := controller.NewAuthController() + api.POST("/login/wechat", authCtrl.WechatLogin) // 微信登录 + api.POST("/login/phone", authCtrl.PhoneLogin) // 手机号登录(测试用) + + // 小红书相关公开接口 + employeeCtrlPublic := controller.NewEmployeeController() + api.POST("/xhs/send-code", employeeCtrlPublic.SendXHSCode) // 发送小红书验证码 + api.GET("/products", employeeCtrlPublic.GetProducts) // 获取产品列表(公开) + + // 员工路由(需要认证) + employee := api.Group("/employee") + employee.Use(middleware.AuthMiddleware()) + { + employeeCtrl := controller.NewEmployeeController() + + // 10.1 获取员工个人信息 + employee.GET("/profile", employeeCtrl.GetProfile) + + // 10.2 绑定小红书账号 + employee.POST("/bind-xhs", employeeCtrl.BindXHS) + + // 10.3 解绑小红书账号 + employee.POST("/unbind-xhs", employeeCtrl.UnbindXHS) + + // 10.4 获取可领取文案列表 + employee.GET("/available-copies", employeeCtrl.GetAvailableCopies) + + // 10.5 领取文案 + employee.POST("/claim-copy", employeeCtrl.ClaimCopy) + + // 10.6 随机领取文案 + employee.POST("/claim-random-copy", employeeCtrl.ClaimRandomCopy) + + // 10.7 发布内容 + employee.POST("/publish", employeeCtrl.Publish) + + // 10.8 获取我的发布记录 + employee.GET("/my-publish-records", employeeCtrl.GetMyPublishRecords) + + // 10.8.1 获取发布记录详情 + employee.GET("/publish-record/:id", employeeCtrl.GetPublishRecordDetail) + + // 10.9 检查小红书绑定与Cookie状态 + employee.GET("/xhs/status", employeeCtrl.CheckXHSStatus) + } + } +} diff --git a/go_backend/service/auth_service.go b/go_backend/service/auth_service.go new file mode 100644 index 0000000..e98bc43 --- /dev/null +++ b/go_backend/service/auth_service.go @@ -0,0 +1,214 @@ +package service + +import ( + "ai_xhs/config" + "ai_xhs/database" + "ai_xhs/models" + "ai_xhs/utils" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" +) + +type AuthService struct{} + +func NewAuthService() *AuthService { + return &AuthService{} +} + +// 微信手机号响应 +type WxPhoneResponse struct { + PhoneInfo struct { + PhoneNumber string `json:"phoneNumber"` + PurePhoneNumber string `json:"purePhoneNumber"` + CountryCode string `json:"countryCode"` + } `json:"phone_info"` + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} + +// 微信登录响应 +type WxLoginResponse struct { + OpenID string `json:"openid"` + SessionKey string `json:"session_key"` + UnionID string `json:"unionid"` + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} + +// WechatLogin 微信小程序登录 +func (s *AuthService) WechatLogin(code string, phone string, phoneCode string) (string, *models.User, error) { + // 1. 调用微信API验证code + // 注意:需要在配置文件中添加小程序的AppID和AppSecret + appID := config.AppConfig.Wechat.AppID + appSecret := config.AppConfig.Wechat.AppSecret + + // 调试日志:打印配置信息 + log.Printf("[微信登录] AppID: %s, AppSecret: %s (长度:%d)", appID, appSecret, len(appSecret)) + + // 如果没有配置微信AppID,使用手机号登录逻辑 + if appID == "" || appSecret == "" { + if phone == "" { + // 没有配置微信且没有手机号,使用默认员工ID=1 + return s.loginByEmployeeID(1) + } + // 使用手机号登录 + return s.PhoneLogin(phone) + } + + // 调用微信API + url := fmt.Sprintf( + "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", + appID, appSecret, code, + ) + + // 调试日志:打印请求URL(隐藏密钥) + log.Printf("[微信登录] 请求URL: https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=***&js_code=%s&grant_type=authorization_code", appID, code) + + resp, err := http.Get(url) + if err != nil { + return "", nil, fmt.Errorf("调用微信API失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", nil, fmt.Errorf("读取响应失败: %v", err) + } + + // 调试日志:打印微信返回的原始响应 + log.Printf("[微信登录] 微信API响应: %s", string(body)) + + var wxResp WxLoginResponse + if err := json.Unmarshal(body, &wxResp); err != nil { + return "", nil, fmt.Errorf("解析响应失败: %v", err) + } + + if wxResp.ErrCode != 0 { + return "", nil, fmt.Errorf("微信登录失败: %s", wxResp.ErrMsg) + } + + // 1.5 如果有 phoneCode,调用微信API获取手机号 + if phoneCode != "" { + accessTokenURL := fmt.Sprintf( + "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", + appID, appSecret, + ) + + // 获取 access_token + accessTokenResp, err := http.Get(accessTokenURL) + if err != nil { + log.Printf("获取access_token失败: %v", err) + } else { + defer accessTokenResp.Body.Close() + accessTokenBody, _ := io.ReadAll(accessTokenResp.Body) + + var tokenResult struct { + AccessToken string `json:"access_token"` + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + } + + if err := json.Unmarshal(accessTokenBody, &tokenResult); err == nil && tokenResult.AccessToken != "" { + // 获取手机号 + phoneURL := fmt.Sprintf( + "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s", + tokenResult.AccessToken, + ) + + phoneReqBody := map[string]string{"code": phoneCode} + phoneReqJSON, _ := json.Marshal(phoneReqBody) + + phoneResp, err := http.Post(phoneURL, "application/json", bytes.NewBuffer(phoneReqJSON)) + if err == nil { + defer phoneResp.Body.Close() + phoneBody, _ := io.ReadAll(phoneResp.Body) + + var phoneResult WxPhoneResponse + if err := json.Unmarshal(phoneBody, &phoneResult); err == nil && phoneResult.ErrCode == 0 { + // 获取手机号成功,覆盖 phone 参数 + phone = phoneResult.PhoneInfo.PurePhoneNumber + log.Printf("[微信登录] 获取手机号成功: %s", phone) + } else { + log.Printf("[微信登录] 获取手机号失败: %s", string(phoneBody)) + } + } + } + } + } + + // 2. 根据OpenID查找或创建员工 + var employee models.User + + // 优先通过OpenID查找(注意:使用IS NOT NULL过滤空值) + result := database.DB.Where("wechat_openid = ? AND wechat_openid IS NOT NULL", wxResp.OpenID).First(&employee) + + if result.Error != nil { + // OpenID不存在,需要绑定OpenID + if phone == "" { + return "", nil, errors.New("首次登录请提供手机号") + } + + // 通过手机号查找员工 + result = database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee) + if result.Error != nil { + return "", nil, errors.New("员工不存在,请联系管理员添加") + } + + // 绑定OpenID和UnionID(使用指针) + employee.WechatOpenID = &wxResp.OpenID + if wxResp.UnionID != "" { + employee.WechatUnionID = &wxResp.UnionID + } + database.DB.Save(&employee) + } + + // 3. 生成JWT token + token, err := utils.GenerateToken(employee.ID) + if err != nil { + return "", nil, fmt.Errorf("生成token失败: %v", err) + } + + return token, &employee, nil +} + +// PhoneLogin 手机号登录(用于测试或无微信配置时) +func (s *AuthService) PhoneLogin(phone string) (string, *models.User, error) { + var employee models.User + + // 查找员工 + result := database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee) + if result.Error != nil { + return "", nil, errors.New("员工不存在或已被禁用") + } + + // 生成token + token, err := utils.GenerateToken(employee.ID) + if err != nil { + return "", nil, fmt.Errorf("生成token失败: %v", err) + } + + return token, &employee, nil +} + +// loginByEmployeeID 通过员工ID登录(内部方法) +func (s *AuthService) loginByEmployeeID(employeeID int) (string, *models.User, error) { + var employee models.User + + result := database.DB.Where("id = ? AND status = ?", employeeID, "active").First(&employee) + if result.Error != nil { + return "", nil, errors.New("员工不存在或已被禁用") + } + + // 生成token + token, err := utils.GenerateToken(employee.ID) + if err != nil { + return "", nil, fmt.Errorf("生成token失败: %v", err) + } + + return token, &employee, nil +} diff --git a/go_backend/service/employee_service.go b/go_backend/service/employee_service.go new file mode 100644 index 0000000..de55704 --- /dev/null +++ b/go_backend/service/employee_service.go @@ -0,0 +1,954 @@ +package service + +import ( + "ai_xhs/database" + "ai_xhs/models" + "bytes" + "encoding/json" + "errors" + "fmt" + "log" + "os/exec" + "path/filepath" + "strings" + "time" + + "gorm.io/gorm" +) + +type EmployeeService struct{} + +type XHSCookieVerifyResult struct { + LoggedIn bool + CookieExpired bool +} + +// SendXHSCode 发送小红书验证码 +func (s *EmployeeService) SendXHSCode(phone string) error { + // 获取Python脚本路径和venv中的Python解释器 + backendDir := filepath.Join("..", "backend") + pythonScript := filepath.Join(backendDir, "xhs_cli.py") + + // 使用venv中的Python解释器 (跨平台) + pythonCmd := getPythonPath(backendDir) + + // 执行Python脚本 + cmd := exec.Command(pythonCmd, pythonScript, "send_code", phone, "+86") + cmd.Dir = backendDir + + // 捕获输出 + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // 执行命令 + err := cmd.Run() + + // 打印Python脚本的日志输出(stderr) + if stderr.Len() > 0 { + log.Printf("[Python日志-发送验证码] %s", stderr.String()) + } + + if err != nil { + return fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String()) + } + + // 获取UTF-8编码的输出 + outputStr := stdout.String() + + // 解析JSON输出 + var result map[string]interface{} + if err := json.Unmarshal([]byte(outputStr), &result); err != nil { + return fmt.Errorf("解析Python输出失败: %w, output: %s", err, outputStr) + } + + // 检查success字段 + if success, ok := result["success"].(bool); !ok || !success { + if errMsg, ok := result["error"].(string); ok { + return fmt.Errorf("%s", errMsg) + } + return errors.New("发送验证码失败") + } + + return nil +} + +// GetProfile 获取员工个人信息 +func (s *EmployeeService) GetProfile(employeeID int) (*models.User, error) { + var employee models.User + err := database.DB.Preload("Enterprise").First(&employee, employeeID).Error + if err != nil { + return nil, err + } + + // 如果已绑定小红书且有Cookie,验证Cookie是否有效 + if employee.IsBoundXHS == 1 && employee.XHSCookie != "" { + // 检查绑定时间,刚绑定的30秒内不验证(避免与绑定操作冲突) + if employee.BoundAt != nil { + timeSinceBound := time.Since(*employee.BoundAt) + if timeSinceBound < 30*time.Second { + log.Printf("GetProfile - 用户%d刚绑定%.0f秒,跳过Cookie验证", employeeID, timeSinceBound.Seconds()) + return &employee, nil + } + } + // 异步验证Cookie(不阻塞返回) - 暂时禁用自动验证,避免频繁清空Cookie + // TODO: 改为定时任务验证,而不是每次GetProfile都验证 + log.Printf("GetProfile - 用户%d有Cookie,长度: %d(已跳过自动验证)", employeeID, len(employee.XHSCookie)) + // go s.VerifyCookieAndClear(employeeID) + } + + return &employee, nil +} + +// BindXHS 绑定小红书账号 +func (s *EmployeeService) BindXHS(employeeID int, xhsPhone, code string) (string, error) { + if code == "" { + return "", errors.New("验证码不能为空") + } + + // 获取员工信息 + var employee models.User + if err := database.DB.First(&employee, employeeID).Error; err != nil { + return "", err + } + + // 检查是否已绑定(如果Cookie已失效,允许重新绑定) + if employee.IsBoundXHS == 1 && employee.XHSCookie != "" { + return "", errors.New("已绑定小红书账号,请先解绑") + } + + // 调用Python服务进行验证码验证和登录 + loginResult, err := s.callPythonLogin(xhsPhone, code) + if err != nil { + return "", fmt.Errorf("小红书登录失败: %w", err) + } + + // 检查Python服务返回结果 + if loginResult.Code != 0 { + return "", fmt.Errorf("小红书登录失败: %s", loginResult.Message) + } + + // 从返回结果中提取用户信息和cookies + userInfo, _ := loginResult.Data["user_info"].(map[string]interface{}) + + // 优先使用 cookies_full(Playwright完整格式),如果没有则使用 cookies(键值对格式) + var cookiesData interface{} + if cookiesFull, ok := loginResult.Data["cookies_full"].([]interface{}); ok && len(cookiesFull) > 0 { + // 使用完整格式(推荐) + cookiesData = cookiesFull + } else if cookiesMap, ok := loginResult.Data["cookies"].(map[string]interface{}); ok && len(cookiesMap) > 0 { + // 降级使用键值对格式(不推荐,但兼容旧版本) + cookiesData = cookiesMap + } + + // 提取小红书账号昵称 + xhsNickname := "小红书用户" + if userInfo != nil { + if nickname, ok := userInfo["nickname"].(string); ok && nickname != "" { + xhsNickname = nickname + } else if username, ok := userInfo["username"].(string); ok && username != "" { + xhsNickname = username + } + } + + // 序列化cookies为JSON字符串(使用完整格式) + cookiesJSON := "" + if cookiesData != nil { + cookiesBytes, err := json.Marshal(cookiesData) + if err == nil { + cookiesJSON = string(cookiesBytes) + log.Printf("绑定小红书 - 用户%d - Cookie长度: %d", employeeID, len(cookiesJSON)) + } else { + log.Printf("绑定小红书 - 用户%d - 序列化Cookie失败: %v", employeeID, err) + } + } else { + log.Printf("绑定小红书 - 用户%d - 警告: cookiesData为nil", employeeID) + } + + if cookiesJSON == "" { + log.Printf("绑定小红书 - 用户%d - 错误: 未能获取到Cookie数据", employeeID) + return "", errors.New("登录成功但未能获取到Cookie数据,请重试") + } + + now := time.Now() + + // 开启事务 + tx := database.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // 更新 ai_users 表的绑定状态和cookie信息 + log.Printf("绑定小红书 - 用户%d - 开始更新数据库", employeeID) + err = tx.Model(&employee).Updates(map[string]interface{}{ + "is_bound_xhs": 1, + "xhs_account": xhsNickname, + "xhs_phone": xhsPhone, + "xhs_cookie": cookiesJSON, + "bound_at": &now, + }).Error + + if err != nil { + tx.Rollback() + log.Printf("绑定小红书 - 用户%d - 数据库更新失败: %v", employeeID, err) + return "", fmt.Errorf("更新员工绑定状态失败: %w", err) + } + + log.Printf("绑定小红书 - 用户%d - 数据库更新成功", employeeID) + + // 提交事务 + if err := tx.Commit().Error; err != nil { + log.Printf("绑定小红书 - 用户%d - 事务提交失败: %v", employeeID, err) + return "", fmt.Errorf("提交事务失败: %w", err) + } + + log.Printf("绑定小红书 - 用户%d - 绑定成功 - 账号: %s", employeeID, xhsNickname) + return xhsNickname, nil +} + +// callPythonLogin 调用Python脚本完成小红书登录 +func (s *EmployeeService) callPythonLogin(phone, code string) (*PythonLoginResponse, error) { + // 获取Python脚本路径和venv中的Python解释器 + backendDir := filepath.Join("..", "backend") + pythonScript := filepath.Join(backendDir, "xhs_cli.py") + + // 使用venv中的Python解释器 (跨平台) + pythonCmd := getPythonPath(backendDir) + + // 执行Python脚本 + cmd := exec.Command(pythonCmd, pythonScript, "login", phone, code, "+86") + cmd.Dir = backendDir + + // 捕获输出 + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // 执行命令 + err := cmd.Run() + + // 打印Python脚本的日志输出(stderr) + if stderr.Len() > 0 { + log.Printf("[Python日志] %s", stderr.String()) + } + + if err != nil { + return nil, fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String()) + } + + // 获取UTF-8编码的输出 + outputStr := stdout.String() + + // 解析JSON输出 + var result map[string]interface{} + if err := json.Unmarshal([]byte(outputStr), &result); err != nil { + return nil, fmt.Errorf("解析Python输出失败: %w, output: %s", err, outputStr) + } + + // 检查success字段 + if success, ok := result["success"].(bool); !ok || !success { + errorMsg := "登录失败" + if errStr, ok := result["error"].(string); ok { + errorMsg = errStr + } + return &PythonLoginResponse{ + Code: 1, + Message: errorMsg, + }, nil + } + + return &PythonLoginResponse{ + Code: 0, + Message: "登录成功", + Data: result, + }, nil +} + +// PythonLoginResponse Python服务登录响应 +type PythonLoginResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data map[string]interface{} `json:"data"` +} + +// UnbindXHS 解绑小红书账号 +func (s *EmployeeService) UnbindXHS(employeeID int) error { + var employee models.User + if err := database.DB.First(&employee, employeeID).Error; err != nil { + return err + } + + if employee.IsBoundXHS == 0 { + return errors.New("未绑定小红书账号") + } + + // 开启事务 + tx := database.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // 清空 ai_users 表的绑定信息和cookie + err := tx.Model(&employee).Updates(map[string]interface{}{ + "is_bound_xhs": 0, + "xhs_account": "", + "xhs_phone": "", + "xhs_cookie": "", + "bound_at": nil, + }).Error + + if err != nil { + tx.Rollback() + return fmt.Errorf("更新员工绑定状态失败: %w", err) + } + + // 提交事务 + if err := tx.Commit().Error; err != nil { + return fmt.Errorf("提交事务失败: %w", err) + } + + return nil +} + +// verifyCookieWithPython 使用Python脚本验证Cookie,并返回登录与过期状态 +func (s *EmployeeService) verifyCookieWithPython(rawCookie string) (*XHSCookieVerifyResult, error) { + // 解析Cookie + var cookies []interface{} + if err := json.Unmarshal([]byte(rawCookie), &cookies); err != nil { + return nil, fmt.Errorf("解析Cookie失败: %w", err) + } + + // 调用Python脚本验证Cookie + backendDir := filepath.Join("..", "backend") + pythonScript := filepath.Join(backendDir, "xhs_cli.py") + pythonCmd := getPythonPath(backendDir) + + // 将cookies序列化为JSON字符串 + cookiesJSON, err := json.Marshal(cookies) + if err != nil { + return nil, fmt.Errorf("序列化Cookie失败: %w", err) + } + + // 执行Python脚本: inject_cookies + cmd := exec.Command(pythonCmd, pythonScript, "inject_cookies", string(cookiesJSON)) + cmd.Dir = backendDir + + // 捕获输出 + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // 执行命令 + err = cmd.Run() + + // 打印Python脚本的日志输出(stderr) + if stderr.Len() > 0 { + log.Printf("[Python日志-验证Cookie] %s", stderr.String()) + } + + if err != nil { + return nil, fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String()) + } + + // 解析返回结果 + outputStr := stdout.String() + var result map[string]interface{} + if err := json.Unmarshal([]byte(outputStr), &result); err != nil { + return nil, fmt.Errorf("解析Python输出失败: %w, output: %s", err, outputStr) + } + + loggedIn, _ := result["logged_in"].(bool) + cookieExpired, _ := result["cookie_expired"].(bool) + + return &XHSCookieVerifyResult{ + LoggedIn: loggedIn, + CookieExpired: cookieExpired, + }, nil +} + +// VerifyCookieAndClear 验证Cookie并在失效时清空 +func (s *EmployeeService) VerifyCookieAndClear(employeeID int) error { + var employee models.User + if err := database.DB.First(&employee, employeeID).Error; err != nil { + return err + } + + // 检查是否已绑定 + if employee.IsBoundXHS == 0 || employee.XHSCookie == "" { + return nil // 没有绑定或已无Cookie,直接返回 + } + + // 检查绑定时间,刚绑定的30秒内不验证(避免与绑定操作冲突) + if employee.BoundAt != nil { + timeSinceBound := time.Since(*employee.BoundAt) + if timeSinceBound < 30*time.Second { + log.Printf("验证Cookie - 用户%d刚绑定%.0f秒,跳过验证", employeeID, timeSinceBound.Seconds()) + return nil + } + } + + // 调用Python脚本验证Cookie + verifyResult, err := s.verifyCookieWithPython(employee.XHSCookie) + if err != nil { + log.Printf("执行Python脚本失败: %v", err) + // 执行失败,不清空Cookie + return err + } + + if !verifyResult.LoggedIn || verifyResult.CookieExpired { + // Cookie已失效,清空数据库 + log.Printf("检测到Cookie已失效,清空用户%d的Cookie", employeeID) + return s.clearXHSCookie(employeeID) + } + + log.Printf("用户%d的Cookie有效", employeeID) + return nil +} + +// XHSStatus 小红书绑定及Cookie状态 +type XHSStatus struct { + IsBound bool `json:"is_bound"` + HasCookie bool `json:"has_cookie"` + CookieValid bool `json:"cookie_valid"` + CookieExpired bool `json:"cookie_expired"` + Message string `json:"message"` +} + +// CheckXHSStatus 检查小红书绑定与Cookie健康状态 +func (s *EmployeeService) CheckXHSStatus(employeeID int) (*XHSStatus, error) { + var employee models.User + if err := database.DB.First(&employee, employeeID).Error; err != nil { + return nil, err + } + + status := &XHSStatus{ + IsBound: employee.IsBoundXHS == 1, + HasCookie: employee.XHSCookie != "", + CookieValid: false, + CookieExpired: false, + } + + if employee.IsBoundXHS == 0 { + status.Message = "未绑定小红书账号" + return status, nil + } + + if employee.XHSCookie == "" { + status.CookieExpired = true + status.Message = "已绑定但无有效Cookie,可直接重新绑定" + return status, nil + } + + // 刚绑定30秒内视为有效,避免频繁触发验证 + if employee.BoundAt != nil { + timeSinceBound := time.Since(*employee.BoundAt) + if timeSinceBound < 30*time.Second { + status.CookieValid = true + status.Message = "刚绑定,小于30秒,暂不检测,视为有效" + return status, nil + } + } + + verifyResult, err := s.verifyCookieWithPython(employee.XHSCookie) + if err != nil { + status.Message = fmt.Sprintf("验证Cookie失败: %v", err) + return status, err + } + + if !verifyResult.LoggedIn || verifyResult.CookieExpired { + // Cookie已失效,清空后允许直接重新绑定 + if err := s.clearXHSCookie(employeeID); err != nil { + return nil, err + } + status.HasCookie = false + status.CookieExpired = true + status.CookieValid = false + status.Message = "Cookie已失效,已清空,可直接重新绑定" + return status, nil + } + + status.CookieValid = true + status.CookieExpired = false + status.Message = "Cookie有效,已登录" + return status, nil +} + +// clearXHSCookie 清空小红书Cookie(保留绑定状态) +func (s *EmployeeService) clearXHSCookie(employeeID int) error { + // 只清空Cookie,保留is_bound_xhs、xhs_account和xhs_phone + err := database.DB.Model(&models.User{}).Where("id = ?", employeeID).Updates(map[string]interface{}{ + "xhs_cookie": "", + }).Error + + if err != nil { + return fmt.Errorf("清空Cookie失败: %w", err) + } + + log.Printf("已清空用户%d的XHS Cookie", employeeID) + return nil +} + +// GetAvailableCopies 获取可领取的文案列表 +func (s *EmployeeService) GetAvailableCopies(employeeID int, productID int) (map[string]interface{}, error) { + // 获取产品信息 + var product models.Product + if err := database.DB.First(&product, productID).Error; err != nil { + return nil, err + } + + // 获取该产品下所有可用文案(注意:新数据库中status有更多状态) + var copies []models.Article + if err := database.DB.Where("product_id = ? AND status IN ?", productID, []string{"draft", "approved"}).Order("created_at DESC").Find(&copies).Error; err != nil { + return nil, err + } + + return map[string]interface{}{ + "product": map[string]interface{}{ + "id": product.ID, + "name": product.Name, + "image": product.ImageURL, + }, + "copies": copies, + }, nil +} + +// ClaimCopy 领取文案(新版本:直接返回文案信息,不再创建领取记录) +func (s *EmployeeService) ClaimCopy(employeeID int, copyID int, productID int) (map[string]interface{}, error) { + // 检查文案是否存在且可用(注意:新数据库中status有更多状态) + var copy models.Article + if err := database.DB.Where("id = ? AND status IN ?", copyID, []string{"draft", "approved"}).First(©).Error; err != nil { + return nil, errors.New("文案不存在或不可用") + } + + // 获取关联的图片(如果有ai_article_images表) + var images []string + // TODO: 从 ai_article_images 表获取图片 + + return map[string]interface{}{ + "copy": map[string]interface{}{ + "id": copy.ID, + "title": copy.Title, + "content": copy.Content, + "images": images, + }, + }, nil +} + +// ClaimRandomCopy 随机领取文案 +func (s *EmployeeService) ClaimRandomCopy(employeeID int, productID int) (map[string]interface{}, error) { + // 查询未领取的可用文案(注意:新数据库中status有更多状态) + var copy models.Article + query := database.DB.Where("product_id = ? AND status IN ?", productID, []string{"draft", "approved"}) + + if err := query.Order("RAND()").First(©).Error; err != nil { + return nil, errors.New("暂无可领取的文案") + } + + // 领取该文案 + return s.ClaimCopy(employeeID, copy.ID, productID) +} + +// Publish 发布内容 +func (s *EmployeeService) Publish(employeeID int, req PublishRequest) (int, error) { + // 检查文案是否存在 + var copy models.Article + if err := database.DB.First(©, req.CopyID).Error; err != nil { + return 0, errors.New("文案不存在") + } + + // 检查文案是否已被发布 + if copy.Status == "published" || copy.Status == "published_review" { + return 0, errors.New("文案已被发布或处于发布审核中") + } + + // 获取员工信息 + var employee models.User + if err := database.DB.First(&employee, employeeID).Error; err != nil { + return 0, err + } + + // 开启事务 + tx := database.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + now := time.Now() + var recordID int + var publishStatus string = "published_review" // 默认为发布审核中 + var errMessage string + + // 1. 更新文案状态为 published_review + if err := tx.Model(&models.Article{}).Where("id = ?", req.CopyID).Updates(map[string]interface{}{ + "status": publishStatus, + "publish_user_id": employeeID, + "publish_time": now, + }).Error; err != nil { + publishStatus = "failed" + errMessage = "更新文案状态失败: " + err.Error() + + // 记录失败日志 + s.createLog(tx, employeeID, "article_publish_update_failed", "article", ©.ID, + "发布文案-更新状态失败", errMessage, "error") + + tx.Rollback() + return 0, errors.New(errMessage) + } + + // 记录更新文案状态日志 + s.createLog(tx, employeeID, "article_status_update", "article", ©.ID, + fmt.Sprintf("文案ID:%d 状态更新为 %s", copy.ID, publishStatus), "", "success") + + // 2. 创建发布记录 + record := models.PublishRecord{ + ArticleID: ©.ID, + EnterpriseID: employee.EnterpriseID, + ProductID: copy.ProductID, + Topic: copy.Topic, + Title: req.Title, + CreatedUserID: employeeID, + PublishUserID: &employeeID, + Status: publishStatus, + PublishTime: &now, + PublishLink: req.PublishLink, + WordCount: copy.WordCount, + ImageCount: copy.ImageCount, + Channel: copy.Channel, + } + + if err := tx.Create(&record).Error; err != nil { + publishStatus = "failed" + errMessage = "创建发布记录失败: " + err.Error() + + // 记录失败日志 + s.createLog(tx, employeeID, "publish_record_create_failed", "publish_record", nil, + "创建发布记录失败", errMessage, "error") + + // 回滚文案状态为failed + tx.Model(&models.Article{}).Where("id = ?", req.CopyID).Update("status", "failed") + s.createLog(tx, employeeID, "article_status_rollback", "article", ©.ID, + fmt.Sprintf("文案ID:%d 状态回滚为 failed", copy.ID), errMessage, "warning") + + tx.Rollback() + return 0, errors.New(errMessage) + } + + recordID = record.ID + + // 记录创建发布记录日志 + s.createLog(tx, employeeID, "publish_record_create", "publish_record", &recordID, + fmt.Sprintf("创建发布记录ID:%d, 文案ID:%d, 状态:%s", recordID, copy.ID, publishStatus), "", "success") + + // 提交事务 + if err := tx.Commit().Error; err != nil { + publishStatus = "failed" + errMessage = "提交事务失败: " + err.Error() + + // 事务提交失败,需要在新事务中更新状态为failed + database.DB.Model(&models.Article{}).Where("id = ?", req.CopyID).Update("status", "failed") + s.createLog(nil, employeeID, "publish_transaction_failed", "article", ©.ID, + "发布事务提交失败,状态更新为failed", errMessage, "error") + + return 0, errors.New(errMessage) + } + + // 成功日志 + s.createLog(nil, employeeID, "article_publish_success", "article", ©.ID, + fmt.Sprintf("文案ID:%d 发布成功,记录ID:%d", copy.ID, recordID), "", "success") + + return recordID, nil +} + +// createLog 创建日志记录 +func (s *EmployeeService) createLog(tx *gorm.DB, userID int, action, targetType string, targetID *int, description, errMsg, status string) { + log := models.Log{ + UserID: &userID, + Action: action, + TargetType: targetType, + TargetID: targetID, + Description: description, + Status: status, + ErrorMessage: errMsg, + } + + db := database.DB + if tx != nil { + db = tx + } + + if err := db.Create(&log).Error; err != nil { + // 日志创建失败不影响主流程,只输出错误 + fmt.Printf("创建日志失败: %v\n", err) + } +} + +// GetMyPublishRecords 获取我的发布记录 +func (s *EmployeeService) GetMyPublishRecords(employeeID int, page, pageSize int) (map[string]interface{}, error) { + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + + var total int64 + var records []models.PublishRecord + + // 查询总数(使用publish_user_id字段) + database.DB.Model(&models.PublishRecord{}).Where("publish_user_id = ?", employeeID).Count(&total) + + // 查询列表(不使用Preload,直接使用冗余字段) + offset := (page - 1) * pageSize + err := database.DB.Where("publish_user_id = ?", employeeID). + Order("publish_time DESC"). + Limit(pageSize). + Offset(offset). + Find(&records).Error + + if err != nil { + return nil, err + } + + // 构造返回数据 + list := make([]map[string]interface{}, 0) + for _, record := range records { + publishTimeStr := "" + if record.PublishTime != nil { + publishTimeStr = record.PublishTime.Format("2006-01-02 15:04:05") + } + + // 查询产品名称 + var product models.Product + productName := "" + if err := database.DB.Where("id = ?", record.ProductID).First(&product).Error; err == nil { + productName = product.Name + } + + // 查询文章图片和标签 + var images []map[string]interface{} + var tags []string + + if record.ArticleID != nil && *record.ArticleID > 0 { + // 查询文章图片 + var articleImages []models.ArticleImage + if err := database.DB.Where("article_id = ?", *record.ArticleID).Order("sort_order ASC").Find(&articleImages).Error; err == nil { + for _, img := range articleImages { + images = append(images, map[string]interface{}{ + "id": img.ID, + "image_url": img.ImageURL, + "image_thumb_url": img.ImageThumbURL, + "sort_order": img.SortOrder, + "keywords_name": img.KeywordsName, + }) + } + } + + // 查询文章标签 + var articleTag models.ArticleTag + if err := database.DB.Where("article_id = ?", *record.ArticleID).First(&articleTag).Error; err == nil && articleTag.CozeTag != "" { + // 解析标签 + for _, tag := range splitTags(articleTag.CozeTag) { + if tag != "" { + tags = append(tags, tag) + } + } + } + } + + list = append(list, map[string]interface{}{ + "id": record.ID, + "product_id": record.ProductID, + "product_name": productName, + "topic": record.Topic, + "title": record.Title, + "publish_link": record.PublishLink, + "publish_time": publishTimeStr, + "images": images, + "tags": tags, + }) + } + + return map[string]interface{}{ + "total": total, + "list": list, + }, nil +} + +// GetPublishRecordDetail 获取发布记录详情 +func (s *EmployeeService) GetPublishRecordDetail(employeeID int, recordID int) (map[string]interface{}, error) { + var record models.PublishRecord + err := database.DB.Where("id = ?", recordID).First(&record).Error + if err != nil { + return nil, errors.New("发布记录不存在") + } + + publishTimeStr := "" + if record.PublishTime != nil { + publishTimeStr = record.PublishTime.Format("2006-01-02 15:04:05") + } + + // 通过ArticleID关联查询文章内容 + var article models.Article + content := "" + var images []map[string]interface{} + var tags []string + articleCozeTag := "" + + // 查询产品名称 + var product models.Product + productName := "" + if err := database.DB.Where("id = ?", record.ProductID).First(&product).Error; err == nil { + productName = product.Name + } + + if record.ArticleID != nil && *record.ArticleID > 0 { + // 优先使用ArticleID关联 + if err := database.DB.Where("id = ?", *record.ArticleID).First(&article).Error; err == nil { + content = article.Content + articleCozeTag = article.CozeTag + + // 查询文章图片 + var articleImages []models.ArticleImage + if err := database.DB.Where("article_id = ?", article.ID).Order("sort_order ASC").Find(&articleImages).Error; err == nil { + for _, img := range articleImages { + images = append(images, map[string]interface{}{ + "id": img.ID, + "image_url": img.ImageURL, + "image_thumb_url": img.ImageThumbURL, + "sort_order": img.SortOrder, + "keywords_name": img.KeywordsName, + }) + } + } + + // 查询文章标签(ai_article_tags表) + var articleTag models.ArticleTag + if err := database.DB.Where("article_id = ?", article.ID).First(&articleTag).Error; err == nil && articleTag.CozeTag != "" { + // 使用ai_article_tags表的标签 + articleCozeTag = articleTag.CozeTag + } + + // 解析标签(假设标签是逗号分隔的字符串) + if articleCozeTag != "" { + // 尝试按逗号分割 + for _, tag := range splitTags(articleCozeTag) { + if tag != "" { + tags = append(tags, tag) + } + } + } + } + } else { + // 备用方案:通过title和product_id关联(向后兼容) + if err := database.DB.Where("title = ? AND product_id = ?", record.Title, record.ProductID).First(&article).Error; err == nil { + content = article.Content + articleCozeTag = article.CozeTag + + // 解析标签 + if articleCozeTag != "" { + for _, tag := range splitTags(articleCozeTag) { + if tag != "" { + tags = append(tags, tag) + } + } + } + } + } + + return map[string]interface{}{ + "id": record.ID, + "article_id": record.ArticleID, + "product_id": record.ProductID, + "product_name": productName, + "topic": record.Topic, + "title": record.Title, + "content": content, + "images": images, + "tags": tags, + "coze_tag": articleCozeTag, + "publish_link": record.PublishLink, + "status": record.Status, + "publish_time": publishTimeStr, + }, nil +} + +// splitTags 分割标签字符串 +func splitTags(tagStr string) []string { + if tagStr == "" { + return []string{} + } + + // 尝试多种分隔符 + var tags []string + + // 先尝试逗号分割 + if strings.Contains(tagStr, ",") { + for _, tag := range strings.Split(tagStr, ",") { + tag = strings.TrimSpace(tag) + if tag != "" { + tags = append(tags, tag) + } + } + } else if strings.Contains(tagStr, ",") { + // 中文逗号 + for _, tag := range strings.Split(tagStr, ",") { + tag = strings.TrimSpace(tag) + if tag != "" { + tags = append(tags, tag) + } + } + } else if strings.Contains(tagStr, "|") { + // 竪线分隔 + for _, tag := range strings.Split(tagStr, "|") { + tag = strings.TrimSpace(tag) + if tag != "" { + tags = append(tags, tag) + } + } + } else { + // 单个标签 + tags = append(tags, strings.TrimSpace(tagStr)) + } + + return tags +} + +// GetProducts 获取产品列表 +func (s *EmployeeService) GetProducts() ([]map[string]interface{}, error) { + var products []models.Product + if err := database.DB.Find(&products).Error; err != nil { + return nil, err + } + + result := make([]map[string]interface{}, 0) + for _, product := range products { + // 统计该产品下可用文案数量(注意:新数据库中status有更多状态) + var totalCopies int64 + database.DB.Model(&models.Article{}).Where("product_id = ? AND status IN ?", product.ID, []string{"draft", "approved"}).Count(&totalCopies) + + result = append(result, map[string]interface{}{ + "id": product.ID, + "name": product.Name, + "image": product.ImageURL, + "knowledge": product.Knowledge, + "available_copies": totalCopies, + }) + } + + return result, nil +} + +// PublishRequest 发布请求参数 +type PublishRequest struct { + CopyID int `json:"copy_id" binding:"required"` + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` + PublishLink string `json:"publish_link"` + XHSNoteID string `json:"xhs_note_id"` +} diff --git a/go_backend/service/python_utils.go b/go_backend/service/python_utils.go new file mode 100644 index 0000000..c485983 --- /dev/null +++ b/go_backend/service/python_utils.go @@ -0,0 +1,16 @@ +package service + +import ( + "path/filepath" + "runtime" +) + +// getPythonPath 获取虚拟环境中的Python解释器路径(跨平台) +func getPythonPath(backendDir string) string { + if runtime.GOOS == "windows" { + // Windows: venv\Scripts\python.exe + return filepath.Join(backendDir, "venv", "Scripts", "python.exe") + } + // Linux/Mac: venv/bin/python + return filepath.Join(backendDir, "venv", "bin", "python") +} diff --git a/go_backend/service/scheduler_service.go b/go_backend/service/scheduler_service.go new file mode 100644 index 0000000..531b31a --- /dev/null +++ b/go_backend/service/scheduler_service.go @@ -0,0 +1,561 @@ +package service + +import ( + "ai_xhs/config" + "ai_xhs/database" + "ai_xhs/models" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/robfig/cron/v3" +) + +// SchedulerService 定时任务服务 +type SchedulerService struct { + cron *cron.Cron + maxConcurrent int + publishTimeout int + publishSem chan struct{} // 用于控制并发数的信号量 +} + +// NewSchedulerService 创建定时任务服务 +func NewSchedulerService(maxConcurrent, publishTimeout int) *SchedulerService { + // 使用WithSeconds选项支持6位Cron表达式(秒 分 时 日 月 周) + return &SchedulerService{ + cron: cron.New(cron.WithSeconds()), + maxConcurrent: maxConcurrent, + publishTimeout: publishTimeout, + publishSem: make(chan struct{}, maxConcurrent), + } +} + +// Start 启动定时任务 +func (s *SchedulerService) Start(cronExpr string) error { + // 添加定时任务 + _, err := s.cron.AddFunc(cronExpr, s.AutoPublishArticles) + if err != nil { + return fmt.Errorf("添加定时任务失败: %w", err) + } + + // 启动cron + s.cron.Start() + log.Printf("定时发布任务已启动,Cron表达式: %s", cronExpr) + return nil +} + +// Stop 停止定时任务 +func (s *SchedulerService) Stop() { + s.cron.Stop() + log.Println("定时发布任务已停止") +} + +const ( + defaultMaxArticlesPerUserPerRun = 5 + defaultMaxFailuresPerUserPerRun = 3 +) + +// fetchProxyFromPool 从代理池接口获取一个代理地址(http://ip:port) +func fetchProxyFromPool() (string, error) { + proxyURL := config.AppConfig.Scheduler.ProxyFetchURL + if proxyURL == "" { + return "", nil + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(proxyURL) + if err != nil { + return "", fmt.Errorf("请求代理池接口失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("代理池接口返回非200状态码: %d", resp.StatusCode) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取代理池响应失败: %w", err) + } + + content := strings.TrimSpace(string(bodyBytes)) + if content == "" { + return "", fmt.Errorf("代理池返回内容为空") + } + + // 支持多行情况,取第一行 ip:port + line := strings.Split(content, "\n")[0] + line = strings.TrimSpace(line) + if line == "" { + return "", fmt.Errorf("代理池首行内容为空") + } + + // 如果已经包含协议前缀,则直接返回 + if strings.HasPrefix(line, "http://") || strings.HasPrefix(line, "https://") { + return line, nil + } + + // 默认补上 http:// 前缀 + return "http://" + line, nil +} + +func limitArticlesPerUserPerRun(articles []models.Article, perUserLimit int) []models.Article { + if perUserLimit <= 0 { + return articles + } + + grouped := make(map[int][]models.Article) + for _, art := range articles { + userID := art.CreatedUserID + if art.PublishUserID != nil { + userID = *art.PublishUserID + } + grouped[userID] = append(grouped[userID], art) + } + + limited := make([]models.Article, 0, len(articles)) + for _, group := range grouped { + if len(group) > perUserLimit { + limited = append(limited, group[:perUserLimit]...) + } else { + limited = append(limited, group...) + } + } + + return limited +} + +// filterByDailyAndHourlyLimit 按每日和每小时上限过滤文章 +func (s *SchedulerService) filterByDailyAndHourlyLimit(articles []models.Article, maxDaily, maxHourly int) []models.Article { + if maxDaily <= 0 && maxHourly <= 0 { + return articles + } + + // 提取所有涉及的用户ID + userIDs := make(map[int]bool) + for _, art := range articles { + userID := art.CreatedUserID + if art.PublishUserID != nil { + userID = *art.PublishUserID + } + userIDs[userID] = true + } + + // 批量查询每个用户的当日和当前小时已发布数量 + userDailyPublished := make(map[int]int) + userHourlyPublished := make(map[int]int) + + now := time.Now() + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + currentHourStart := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) + + for userID := range userIDs { + // 查询当日已发布数量 + if maxDaily > 0 { + var dailyCount int64 + if err := database.DB.Model(&models.Article{}). + Where("status = ? AND publish_time >= ? AND (publish_user_id = ? OR (publish_user_id IS NULL AND created_user_id = ?))", + "published", todayStart, userID, userID). + Count(&dailyCount).Error; err != nil { + log.Printf("[警告] 查询用户 %d 当日已发布数量失败: %v", userID, err) + } else { + userDailyPublished[userID] = int(dailyCount) + } + } + + // 查询当前小时已发布数量 + if maxHourly > 0 { + var hourlyCount int64 + if err := database.DB.Model(&models.Article{}). + Where("status = ? AND publish_time >= ? AND (publish_user_id = ? OR (publish_user_id IS NULL AND created_user_id = ?))", + "published", currentHourStart, userID, userID). + Count(&hourlyCount).Error; err != nil { + log.Printf("[警告] 查询用户 %d 当前小时已发布数量失败: %v", userID, err) + } else { + userHourlyPublished[userID] = int(hourlyCount) + } + } + } + + // 过滤超限文章 + filtered := make([]models.Article, 0, len(articles)) + skippedUsersDailyMap := make(map[int]bool) + skippedUsersHourlyMap := make(map[int]bool) + + for _, art := range articles { + userID := art.CreatedUserID + if art.PublishUserID != nil { + userID = *art.PublishUserID + } + + // 检查每日上限 + if maxDaily > 0 && userDailyPublished[userID] >= maxDaily { + if !skippedUsersDailyMap[userID] { + log.Printf("[频控] 用户 %d 今日已发布 %d 篇,达到每日上限 %d,跳过后续文案", userID, userDailyPublished[userID], maxDaily) + skippedUsersDailyMap[userID] = true + } + continue + } + + // 检查每小时上限 + if maxHourly > 0 && userHourlyPublished[userID] >= maxHourly { + if !skippedUsersHourlyMap[userID] { + log.Printf("[频控] 用户 %d 当前小时已发布 %d 篇,达到每小时上限 %d,跳过后续文案", userID, userHourlyPublished[userID], maxHourly) + skippedUsersHourlyMap[userID] = true + } + continue + } + + filtered = append(filtered, art) + } + + return filtered +} + +// AutoPublishArticles 自动发布文案 +func (s *SchedulerService) AutoPublishArticles() { + log.Println("========== 开始执行定时发布任务 ==========") + startTime := time.Now() + + // 查询所有待发布的文案(状态为published_review) + var articles []models.Article + if err := database.DB.Where("status = ?", "published_review").Find(&articles).Error; err != nil { + log.Printf("查询待发布文案失败: %v", err) + return + } + + if len(articles) == 0 { + log.Println("没有待发布的文案") + return + } + + originalTotal := len(articles) + + perUserLimit := config.AppConfig.Scheduler.MaxArticlesPerUserPerRun + if perUserLimit <= 0 { + perUserLimit = defaultMaxArticlesPerUserPerRun + } + + articles = limitArticlesPerUserPerRun(articles, perUserLimit) + + log.Printf("找到 %d 篇待发布文案,按照每个用户每轮最多 %d 篇,本次计划发布 %d 篇", originalTotal, perUserLimit, len(articles)) + + // 查询每用户每日/每小时已发布数量,过滤超限用户 + maxDaily := config.AppConfig.Scheduler.MaxDailyArticlesPerUser + maxHourly := config.AppConfig.Scheduler.MaxHourlyArticlesPerUser + + if maxDaily > 0 || maxHourly > 0 { + beforeFilterCount := len(articles) + articles = s.filterByDailyAndHourlyLimit(articles, maxDaily, maxHourly) + log.Printf("应用每日/每小时上限过滤:过滤前 %d 篇,过滤后 %d 篇", beforeFilterCount, len(articles)) + } + + if len(articles) == 0 { + log.Println("所有文案均因频率限制被过滤,本轮无任务") + return + } + + // 并发发布 + var wg sync.WaitGroup + successCount := 0 + failCount := 0 + var mu sync.Mutex + userFailCount := make(map[int]int) + pausedUsers := make(map[int]bool) + + failLimit := config.AppConfig.Scheduler.MaxFailuresPerUserPerRun + if failLimit <= 0 { + failLimit = defaultMaxFailuresPerUserPerRun + } + + for _, article := range articles { + userID := article.CreatedUserID + if article.PublishUserID != nil { + userID = *article.PublishUserID + } + + mu.Lock() + if pausedUsers[userID] { + mu.Unlock() + log.Printf("用户 %d 在本轮已暂停,跳过文案 ID: %d", userID, article.ID) + continue + } + mu.Unlock() + + // 获取信号量 + s.publishSem <- struct{}{} + wg.Add(1) + + go func(art models.Article, uid int) { + defer wg.Done() + defer func() { <-s.publishSem }() + + sleepSeconds := 3 + rand.Intn(8) + time.Sleep(time.Duration(sleepSeconds) * time.Second) + + // 发布文案 + err := s.publishArticle(art) + mu.Lock() + if err != nil { + failCount++ + userFailCount[uid]++ + if userFailCount[uid] >= failLimit && !pausedUsers[uid] { + pausedUsers[uid] = true + log.Printf("用户 %d 在本轮定时任务中失败次数达到 %d 次,暂停本轮后续发布", uid, userFailCount[uid]) + } + log.Printf("发布失败 [文案ID: %d, 标题: %s]: %v", art.ID, art.Title, err) + } else { + successCount++ + log.Printf("发布成功 [文案ID: %d, 标题: %s]", art.ID, art.Title) + } + mu.Unlock() + }(article, userID) + } + + // 等待所有发布完成 + wg.Wait() + + duration := time.Since(startTime) + log.Printf("========== 定时发布任务完成 ==========") + log.Printf("总计: %d 篇, 成功: %d 篇, 失败: %d 篇, 耗时: %v", + len(articles), successCount, failCount, duration) +} + +// publishArticle 发布单篇文案 +func (s *SchedulerService) publishArticle(article models.Article) error { + // 1. 获取用户信息(发布用户) + var user models.User + if article.PublishUserID != nil { + if err := database.DB.First(&user, *article.PublishUserID).Error; err != nil { + return fmt.Errorf("获取发布用户信息失败: %w", err) + } + } else { + // 如果没有发布用户,使用创建用户 + if err := database.DB.First(&user, article.CreatedUserID).Error; err != nil { + return fmt.Errorf("获取创建用户信息失败: %w", err) + } + } + + // 2. 检查用户是否绑定了小红书 + if user.IsBoundXHS != 1 || user.XHSCookie == "" { + return errors.New("用户未绑定小红书账号或Cookie已失效") + } + + // 3. 获取文章图片 + var articleImages []models.ArticleImage + if err := database.DB.Where("article_id = ?", article.ID). + Order("sort_order ASC"). + Find(&articleImages).Error; err != nil { + return fmt.Errorf("获取文章图片失败: %w", err) + } + + // 4. 提取图片URL列表 + var imageURLs []string + for _, img := range articleImages { + if img.ImageURL != "" { + imageURLs = append(imageURLs, img.ImageURL) + } + } + + // 5. 获取标签 + var tags []string + var articleTag models.ArticleTag + if err := database.DB.Where("article_id = ?", article.ID).First(&articleTag).Error; err == nil { + if articleTag.CozeTag != "" { + // 解析标签(支持逗号、分号、空格分隔) + tags = parseTags(articleTag.CozeTag) + } + } + + // 6. 解析Cookie(数据库中存储的是JSON字符串) + var cookies interface{} + if err := json.Unmarshal([]byte(user.XHSCookie), &cookies); err != nil { + return fmt.Errorf("解析Cookie失败: %w,Cookie内容: %s", err, user.XHSCookie) + } + + // 7. 构造发布配置 + publishConfig := map[string]interface{}{ + "cookies": cookies, // 解析后的Cookie对象或数组 + "title": article.Title, + "content": article.Content, + "images": imageURLs, + "tags": tags, + } + + // 决定本次发布使用的代理 + proxyToUse := config.AppConfig.Scheduler.Proxy + if proxyToUse == "" && config.AppConfig.Scheduler.ProxyFetchURL != "" { + if dynamicProxy, err := fetchProxyFromPool(); err != nil { + log.Printf("[代理池] 获取代理失败: %v", err) + } else if dynamicProxy != "" { + proxyToUse = dynamicProxy + log.Printf("[代理池] 使用动态代理: %s", proxyToUse) + } + } + + // 注入代理和User-Agent(如果有配置) + if proxyToUse != "" { + publishConfig["proxy"] = proxyToUse + } + if ua := config.AppConfig.Scheduler.UserAgent; ua != "" { + publishConfig["user_agent"] = ua + } + + // 8. 保存临时配置文件 + tempDir := filepath.Join("..", "backend", "temp") + os.MkdirAll(tempDir, 0755) + + configFile := filepath.Join(tempDir, fmt.Sprintf("publish_%d_%d.json", article.ID, time.Now().Unix())) + configData, err := json.MarshalIndent(publishConfig, "", " ") + if err != nil { + return fmt.Errorf("生成配置文件失败: %w", err) + } + + if err := os.WriteFile(configFile, configData, 0644); err != nil { + return fmt.Errorf("保存配置文件失败: %w", err) + } + defer os.Remove(configFile) // 发布完成后删除临时文件 + + // 9. 调用Python发布脚本 + backendDir := filepath.Join("..", "backend") + pythonScript := filepath.Join(backendDir, "xhs_publish.py") + pythonCmd := getPythonPath(backendDir) + + cmd := exec.Command(pythonCmd, pythonScript, "--config", configFile) + cmd.Dir = backendDir + + // 设置超时 + if s.publishTimeout > 0 { + timer := time.AfterFunc(time.Duration(s.publishTimeout)*time.Second, func() { + cmd.Process.Kill() + }) + defer timer.Stop() + } + + // 捕获输出 + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // 执行命令 + err = cmd.Run() + + // 打印Python脚本日志 + if stderr.Len() > 0 { + log.Printf("[Python日志-发布文案%d] %s", article.ID, stderr.String()) + } + + if err != nil { + // 更新文章状态为failed + s.updateArticleStatus(article.ID, "failed", fmt.Sprintf("发布失败: %v", err)) + return fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String()) + } + + // 10. 解析发布结果 + // 注意:Python脚本可能输出日志到stdout,需要提取最后一行JSON + outputStr := stdout.String() + + // 查找最后一个完整的JSON对象 + var result map[string]interface{} + found := false + + // 尝试从最后一行开始解析JSON + lines := strings.Split(strings.TrimSpace(outputStr), "\n") + + // 从后往前找第一个有效的JSON + for i := len(lines) - 1; i >= 0; i-- { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + + // 尝试解析为JSON(必须以{开头) + if strings.HasPrefix(line, "{") { + if err := json.Unmarshal([]byte(line), &result); err == nil { + found = true + log.Printf("成功解析JSON结果(第%d行): %s", i+1, line) + break + } + } + } + + if !found { + errMsg := "Python脚本未返回有效JSON结果" + s.updateArticleStatus(article.ID, "failed", errMsg) + log.Printf("完整输出内容:\n%s", outputStr) + if stderr.Len() > 0 { + log.Printf("错误输出:\n%s", stderr.String()) + } + return fmt.Errorf("%s, output: %s", errMsg, outputStr) + } + + // 11. 检查发布是否成功 + success, ok := result["success"].(bool) + if !ok || !success { + errMsg := "未知错误" + if errStr, ok := result["error"].(string); ok { + errMsg = errStr + } + s.updateArticleStatus(article.ID, "failed", errMsg) + return fmt.Errorf("发布失败: %s", errMsg) + } + + // 12. 更新文章状态为published + s.updateArticleStatus(article.ID, "published", "发布成功") + + return nil +} + +// updateArticleStatus 更新文章状态 +func (s *SchedulerService) updateArticleStatus(articleID int, status, message string) { + updates := map[string]interface{}{ + "status": status, + } + + if status == "published" { + now := time.Now() + updates["publish_time"] = now + } + + if message != "" { + updates["review_comment"] = message + } + + if err := database.DB.Model(&models.Article{}).Where("id = ?", articleID).Updates(updates).Error; err != nil { + log.Printf("更新文章%d状态失败: %v", articleID, err) + } +} + +// parseTags 解析标签字符串(支持逗号、分号、空格分隔) +func parseTags(tagStr string) []string { + if tagStr == "" { + return nil + } + + // 统一使用逗号分隔符 + tagStr = strings.ReplaceAll(tagStr, ";", ",") + tagStr = strings.ReplaceAll(tagStr, " ", ",") + tagStr = strings.ReplaceAll(tagStr, "、", ",") + + tagsRaw := strings.Split(tagStr, ",") + var tags []string + for _, tag := range tagsRaw { + tag = strings.TrimSpace(tag) + if tag != "" { + tags = append(tags, tag) + } + } + + return tags +} diff --git a/go_backend/service/xhs_service.go b/go_backend/service/xhs_service.go new file mode 100644 index 0000000..89bb6a6 --- /dev/null +++ b/go_backend/service/xhs_service.go @@ -0,0 +1,165 @@ +package service + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "os/exec" + "path/filepath" +) + +type XHSService struct{} + +// SendCodeRequest 发送验证码请求 +type SendCodeRequest struct { + Phone string `json:"phone" binding:"required"` + CountryCode string `json:"country_code"` +} + +// SendCodeResponse 发送验证码响应 +type SendCodeResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data map[string]interface{} `json:"data"` +} + +// LoginRequest 登录请求 +type LoginRequest struct { + Phone string `json:"phone" binding:"required"` + Code string `json:"code" binding:"required"` + CountryCode string `json:"country_code"` +} + +// LoginResponse 登录响应 +type LoginResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data map[string]interface{} `json:"data"` +} + +// SendVerificationCode 调用Python脚本发送验证码 +func (s *XHSService) SendVerificationCode(phone, countryCode string) (*SendCodeResponse, error) { + // 如果没有传国家码,默认使用+86 + if countryCode == "" { + countryCode = "+86" + } + + // 获取Python脚本路径和venv中的Python解释器 + backendDir := filepath.Join("..", "backend") + pythonScript := filepath.Join(backendDir, "xhs_cli.py") + + // 使用venv中的Python解释器 (跨平台) + pythonCmd := getPythonPath(backendDir) + + // 执行Python脚本 + cmd := exec.Command(pythonCmd, pythonScript, "send_code", phone, countryCode) + + // 设置工作目录为Python脚本所在目录 + cmd.Dir = backendDir + + // 捕获输出 + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // 执行命令 + err := cmd.Run() + + // 打印Python脚本的日志输出(stderr) + if stderr.Len() > 0 { + log.Printf("[Python日志-发送验证码] %s", stderr.String()) + } + + if err != nil { + return nil, fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String()) + } + + // 获取UTF-8编码的输出 + outputStr := stdout.String() + + // 解析JSON输出 + var result SendCodeResponse + if err := json.Unmarshal([]byte(outputStr), &result); err != nil { + return nil, fmt.Errorf("解析Python输出失败: %w, output: %s", err, outputStr) + } + + // 检查Python脚本返回的success字段 + if !result.Data["success"].(bool) { + return &SendCodeResponse{ + Code: 1, + Message: result.Data["error"].(string), + }, nil + } + + return &SendCodeResponse{ + Code: 0, + Message: "验证码已发送", + Data: result.Data, + }, nil +} + +// VerifyLogin 调用Python脚本验证登录 +func (s *XHSService) VerifyLogin(phone, code, countryCode string) (*LoginResponse, error) { + // 如果没有传国家码,默认使用+86 + if countryCode == "" { + countryCode = "+86" + } + + // 获取Python脚本路径和venv中的Python解释器 + backendDir := filepath.Join("..", "backend") + pythonScript := filepath.Join(backendDir, "xhs_cli.py") + + // 使用venv中的Python解释器 (跨平台) + pythonCmd := getPythonPath(backendDir) + + // 执行Python脚本 + cmd := exec.Command(pythonCmd, pythonScript, "login", phone, code, countryCode) + + // 设置工作目录 + cmd.Dir = backendDir + + // 捕获输出 + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // 执行命令 + err := cmd.Run() + + // 打印Python脚本的日志输出(stderr) + if stderr.Len() > 0 { + log.Printf("[Python日志-登录] %s", stderr.String()) + } + + if err != nil { + return nil, fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String()) + } + + // 获取UTF-8编码的输出 + outputStr := stdout.String() + + // 解析JSON输出 + var result LoginResponse + if err := json.Unmarshal([]byte(outputStr), &result); err != nil { + return nil, fmt.Errorf("解析Python输出失败: %w, output: %s", err, outputStr) + } + + // 检查Python脚本返回的success字段 + if !result.Data["success"].(bool) { + errorMsg := "登录失败" + if errStr, ok := result.Data["error"].(string); ok { + errorMsg = errStr + } + return &LoginResponse{ + Code: 1, + Message: errorMsg, + }, nil + } + + return &LoginResponse{ + Code: 0, + Message: "登录成功", + Data: result.Data, + }, nil +} diff --git a/go_backend/start.bat b/go_backend/start.bat new file mode 100644 index 0000000..5c3d4af --- /dev/null +++ b/go_backend/start.bat @@ -0,0 +1,21 @@ +@echo off +chcp 65001 >nul +echo 启动AI小红书后端服务(开发环境)... + +:: 检查go环境 +where go >nul 2>nul +if %ERRORLEVEL% neq 0 ( + echo 错误: 未检测到Go环境,请先安装Go + pause + exit /b 1 +) + +:: 下载依赖 +echo 下载依赖... +go mod tidy + +:: 启动服务 +echo 启动开发环境服务... +go run main.go -env=dev + +pause diff --git a/go_backend/start.sh b/go_backend/start.sh new file mode 100644 index 0000000..d3cd984 --- /dev/null +++ b/go_backend/start.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +echo "启动AI小红书后端服务(开发环境)..." + +# 检查go环境 +if ! command -v go &> /dev/null +then + echo "错误: 未检测到Go环境,请先安装Go" + exit 1 +fi + +# 下载依赖 +echo "下载依赖..." +go mod tidy + +# 启动服务 +echo "启动开发环境服务..." +go run main.go -env=dev diff --git a/go_backend/start_prod.bat b/go_backend/start_prod.bat new file mode 100644 index 0000000..510362d --- /dev/null +++ b/go_backend/start_prod.bat @@ -0,0 +1,27 @@ +@echo off +chcp 65001 >nul +echo 启动AI小红书后端服务(生产环境)... + +:: 检查go环境 +where go >nul 2>nul +if %ERRORLEVEL% neq 0 ( + echo 错误: 未检测到Go环境,请先安装Go + pause + exit /b 1 +) + +:: 编译项目 +echo 编译项目... +go build -o ai_xhs.exe main.go + +if %ERRORLEVEL% neq 0 ( + echo 编译失败 + pause + exit /b 1 +) + +:: 启动服务 +echo 启动生产环境服务... +ai_xhs.exe -env=prod + +pause diff --git a/go_backend/start_prod.sh b/go_backend/start_prod.sh new file mode 100644 index 0000000..1b4999c --- /dev/null +++ b/go_backend/start_prod.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +######################################### +# AI小红书 Go 后端 - 生产环境启动脚本 +# 专用于生产环境快速部署 +######################################### + +PORT=8070 +ENV="prod" +LOG_FILE="ai_xhs_prod.log" + +echo "=== 停止端口 $PORT 上的 Go 服务 ===" + +# 方法1: 查找 go run 进程 +GO_PID=$(ps aux | grep "go run main.go" | grep -v grep | awk '{print $2}') + +if [ -n "$GO_PID" ]; then + echo "找到 Go 服务进程: $GO_PID" + kill -9 $GO_PID 2>/dev/null + echo "Go 服务进程已终止" + sleep 2 +fi + +# 方法2: 强制清理端口 +echo "强制清理端口 $PORT..." +PORT_PID=$(lsof -ti:$PORT 2>/dev/null) +if [ -n "$PORT_PID" ]; then + echo "端口被进程 $PORT_PID 占用,正在终止..." + kill -9 $PORT_PID 2>/dev/null +fi + +sudo fuser -k $PORT/tcp 2>/dev/null || true +sudo pkill -f ":$PORT" 2>/dev/null || true + +# 等待端口释放 +sleep 3 + +echo "" +echo "=== 环境检查 ===" + +# 检查 Go 环境 +if ! command -v go &> /dev/null; then + echo "❌ 错误: 未检测到 Go 环境" + exit 1 +fi +echo "✅ Go 环境: $(go version)" + +# 检查配置文件 +if [ ! -f "config/config.prod.yaml" ]; then + echo "❌ 错误: 未找到生产环境配置文件" + exit 1 +fi +echo "✅ 配置文件: config/config.prod.yaml" + +echo "" +echo "=== 下载依赖 ===" +go mod tidy + +echo "" +echo "=== 启动生产环境服务 ===" + +# 设置环境变量 +export APP_ENV=prod + +# 清空旧日志 +> $LOG_FILE + +# 启动服务 +nohup go run main.go > $LOG_FILE 2>&1 & +NEW_PID=$! + +echo "✅ 服务已启动 (PID: $NEW_PID)" + +# 验证启动 +echo "" +echo "=== 启动验证 (等待 5 秒) ===" +sleep 5 + +if ps -p $NEW_PID > /dev/null 2>&1; then + echo "✅ Go 服务启动成功" + echo "📋 日志文件: $LOG_FILE" + echo "👀 查看日志: tail -f $LOG_FILE" + echo "🌐 服务地址: http://localhost:$PORT" + echo "🔍 进程PID: $NEW_PID" + + # 检查端口监听 + if lsof -ti:$PORT > /dev/null 2>&1; then + echo "✅ 端口 $PORT 监听正常" + else + echo "⚠️ 端口 $PORT 未监听,请检查日志" + fi + + # 显示最近日志 + echo "" + echo "=== 最近日志 ===" + tail -n 10 $LOG_FILE +else + echo "❌ Go 服务启动失败,请检查日志" + echo "" + tail -n 20 $LOG_FILE + exit 1 +fi diff --git a/go_backend/stop.sh b/go_backend/stop.sh new file mode 100644 index 0000000..d46cce2 --- /dev/null +++ b/go_backend/stop.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +######################################### +# AI小红书 Go 后端 - 停止服务脚本 +######################################### + +# 默认端口 +DEV_PORT=8080 +PROD_PORT=8070 + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} 停止 AI小红书 Go 后端服务${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# 停止指定端口的服务 +stop_port() { + local PORT=$1 + echo -e "${YELLOW}正在停止端口 $PORT 上的服务...${NC}" + + # 查找占用端口的进程 + PORT_PID=$(lsof -ti:$PORT 2>/dev/null) + + if [ -n "$PORT_PID" ]; then + echo " 找到进程: $PORT_PID" + kill -9 $PORT_PID 2>/dev/null && echo -e " ${GREEN}✅ 已终止进程 $PORT_PID${NC}" + else + echo -e " ${YELLOW}未找到占用端口 $PORT 的进程${NC}" + fi + + # 使用 fuser 强制清理 + sudo fuser -k $PORT/tcp 2>/dev/null || true +} + +# 停止所有 go run main.go 进程 +echo -e "${BLUE}=== 方法1: 停止 go run 进程 ===${NC}" +GO_PIDS=$(ps aux | grep "go run main.go" | grep -v grep | awk '{print $2}') + +if [ -n "$GO_PIDS" ]; then + echo -e "${YELLOW}找到 Go 服务进程:${NC}" + ps aux | grep "go run main.go" | grep -v grep + echo "" + for PID in $GO_PIDS; do + kill -9 $PID 2>/dev/null && echo -e "${GREEN}✅ 已终止进程: $PID${NC}" + done +else + echo -e "${YELLOW}未找到 go run main.go 进程${NC}" +fi + +echo "" + +# 停止开发环境端口 +echo -e "${BLUE}=== 方法2: 清理开发环境端口 ($DEV_PORT) ===${NC}" +stop_port $DEV_PORT + +echo "" + +# 停止生产环境端口 +echo -e "${BLUE}=== 方法3: 清理生产环境端口 ($PROD_PORT) ===${NC}" +stop_port $PROD_PORT + +echo "" + +# 清理所有相关进程 +echo -e "${BLUE}=== 方法4: 清理所有相关进程 ===${NC}" +sudo pkill -f "main.go" 2>/dev/null && echo -e "${GREEN}✅ 已清理所有 main.go 进程${NC}" || echo -e "${YELLOW}未找到其他相关进程${NC}" + +# 等待进程完全退出 +sleep 2 + +echo "" + +# 验证 +echo -e "${BLUE}=== 验证结果 ===${NC}" + +# 检查端口 +for PORT in $DEV_PORT $PROD_PORT; do + if lsof -ti:$PORT > /dev/null 2>&1; then + echo -e "${RED}⚠️ 端口 $PORT 仍被占用:${NC}" + lsof -i:$PORT + else + echo -e "${GREEN}✅ 端口 $PORT 已释放${NC}" + fi +done + +# 检查进程 +if ps aux | grep "go run main.go" | grep -v grep > /dev/null; then + echo -e "${RED}⚠️ 仍有 go run 进程在运行:${NC}" + ps aux | grep "go run main.go" | grep -v grep +else + echo -e "${GREEN}✅ 所有 go run 进程已停止${NC}" +fi + +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} ✅ 服务已停止${NC}" +echo -e "${GREEN}========================================${NC}" diff --git a/go_backend/tools/generate_password.go b/go_backend/tools/generate_password.go new file mode 100644 index 0000000..d53635f --- /dev/null +++ b/go_backend/tools/generate_password.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "golang.org/x/crypto/bcrypt" +) + +// GeneratePassword 生成bcrypt加密密码 +func GeneratePassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashedPassword), nil +} + +func main() { + // 为测试数据生成加密密码 + passwords := []string{ + "admin123", // 企业管理员密码 + "user123", // 普通用户密码 + } + + fmt.Println("生成加密密码:") + fmt.Println("=====================================") + + for i, pwd := range passwords { + hashed, err := GeneratePassword(pwd) + if err != nil { + fmt.Printf("生成密码失败: %v\n", err) + continue + } + fmt.Printf("%d. 原始密码: %s\n", i+1, pwd) + fmt.Printf(" 加密后: %s\n\n", hashed) + } + + fmt.Println("=====================================") + fmt.Println("使用说明:") + fmt.Println("1. 复制上面的加密密码") + fmt.Println("2. 在 test_data_ai_wht.sql 中替换对应的密码占位符") + fmt.Println("3. 执行 SQL 文件导入测试数据") +} diff --git a/go_backend/tools/generate_token.go b/go_backend/tools/generate_token.go new file mode 100644 index 0000000..dbe278d --- /dev/null +++ b/go_backend/tools/generate_token.go @@ -0,0 +1,39 @@ +package main + +import ( + "ai_xhs/config" + "ai_xhs/utils" + "flag" + "fmt" + "log" +) + +func main() { + // 解析命令行参数 + env := flag.String("env", "dev", "运行环境: dev, prod") + employeeID := flag.Int("id", 1, "员工ID") + flag.Parse() + + // 加载配置 + if err := config.LoadConfig(*env); err != nil { + log.Fatalf("配置加载失败: %v", err) + } + + // 生成Token + token, err := utils.GenerateToken(*employeeID) + if err != nil { + log.Fatalf("生成Token失败: %v", err) + } + + fmt.Println("========================================") + fmt.Printf("环境: %s\n", *env) + fmt.Printf("员工ID: %d\n", *employeeID) + fmt.Println("========================================") + fmt.Printf("JWT Token:\n%s\n", token) + fmt.Println("========================================") + fmt.Println("\n使用方式:") + fmt.Println("在请求头中添加: Authorization: Bearer " + token) + fmt.Println("\ncURL示例:") + fmt.Printf("curl -H \"Authorization: Bearer %s\" http://localhost:8080/api/employee/profile\n", token) + fmt.Println("========================================") +} diff --git a/go_backend/utils/jwt.go b/go_backend/utils/jwt.go new file mode 100644 index 0000000..597e979 --- /dev/null +++ b/go_backend/utils/jwt.go @@ -0,0 +1,46 @@ +package utils + +import ( + "ai_xhs/config" + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + EmployeeID int `json:"employee_id"` + jwt.RegisteredClaims +} + +// GenerateToken 生成JWT token +func GenerateToken(employeeID int) (string, error) { + claims := Claims{ + EmployeeID: employeeID, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(config.AppConfig.JWT.ExpireHours) * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(config.AppConfig.JWT.Secret)) +} + +// ParseToken 解析JWT token +func ParseToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(config.AppConfig.JWT.Secret), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} diff --git a/miniprogram/MINIPROGRAM_ENV_CONFIG.md b/miniprogram/MINIPROGRAM_ENV_CONFIG.md new file mode 100644 index 0000000..9809c63 --- /dev/null +++ b/miniprogram/MINIPROGRAM_ENV_CONFIG.md @@ -0,0 +1,141 @@ +# 小程序多环境配置说明 + +## 📝 配置文件 + +文件位置: `miniprogram/miniprogram/config/api.ts` + +## 🎯 环境说明 + +小程序支持三种环境,会根据运行版本**自动切换**: + +| 小程序版本 | 环境 | 说明 | +|-----------|------|------| +| 开发版 (develop) | `dev` | 微信开发者工具、真机调试 | +| 体验版 (trial) | `test` | 上传体验版后自动使用 | +| 正式版 (release) | `prod` | 发布正式版后自动使用 | + +## ⚙️ 配置方法 + +### 修改后端地址 + +编辑 `api.ts` 文件中的 `API_CONFIG` 配置: + +```typescript +const API_CONFIG: Record = { + // 开发环境 - 本地开发 + dev: { + baseURL: 'http://localhost:8080', // 本地Go服务 + pythonURL: 'http://localhost:8000', // 本地Python服务 + timeout: 30000 + }, + + // 测试环境 - 服务器测试 + test: { + baseURL: 'http://8.149.233.36:8070', // 测试服务器Go服务 + pythonURL: 'http://8.149.233.36:8000', // 测试服务器Python服务 + timeout: 30000 + }, + + // 生产环境 + prod: { + baseURL: 'https://api.yourdomain.com', // 生产环境Go服务 (需修改) + pythonURL: 'https://python.yourdomain.com', // 生产环境Python服务 (需修改) + timeout: 30000 + } +}; +``` + +### 配置项说明 + +- **baseURL**: 主服务地址 (Go Backend) +- **pythonURL**: Python 服务地址 (可选,用于小红书发布等功能) +- **timeout**: 请求超时时间 (毫秒) + +## 🚀 使用示例 + +### 1. 本地开发 +- 在微信开发者工具中打开项目 +- 自动使用 `dev` 环境配置 +- 后端地址: `http://localhost:8080` + +### 2. 服务器测试 +- 点击"上传"按钮上传代码 +- 在微信公众平台设置为体验版 +- 自动使用 `test` 环境配置 +- 后端地址: `http://8.149.233.36:8070` + +### 3. 正式发布 +- 提交审核并发布 +- 自动使用 `prod` 环境配置 +- 后端地址: `https://api.yourdomain.com` (**需先修改配置**) + +## 🔍 调试信息 + +启动小程序时,会在控制台输出当前环境信息: + +``` +[API Config] 当前环境: dev +[API Config] 主服务: http://localhost:8080 +[API Config] Python服务: http://localhost:8000 +``` + +可在微信开发者工具的控制台查看。 + +## ⚠️ 注意事项 + +1. **生产环境必须修改**: 发布前务必将 `prod` 配置中的域名修改为实际的生产服务器地址 + +2. **域名配置**: 小程序只能访问已在微信公众平台配置的合法域名 + - 登录 [微信公众平台](https://mp.weixin.qq.com/) + - 进入"开发" → "开发管理" → "开发设置" + - 在"服务器域名"中添加你的域名 + +3. **HTTPS要求**: 正式版必须使用 HTTPS 协议 (开发工具可以关闭校验) + +4. **环境隔离**: 不同环境使用不同的数据库,避免测试数据污染生产数据 + +## 📋 快速检查清单 + +发布前检查: + +- [ ] 已修改 `prod.baseURL` 为实际生产地址 +- [ ] 已修改 `prod.pythonURL` 为实际生产地址 (如果使用) +- [ ] 生产域名已在微信公众平台配置 +- [ ] 生产服务器使用 HTTPS 协议 +- [ ] Go 后端服务已部署并启动 +- [ ] Python 服务已部署并启动 (如果使用) + +## 🛠 技术说明 + +### 环境自动检测 + +通过微信 API 自动检测: + +```typescript +function detectEnvironment(): EnvType { + const accountInfo = wx.getAccountInfoSync(); + const envVersion = accountInfo.miniProgram.envVersion; + + switch (envVersion) { + case 'develop': return 'dev'; + case 'trial': return 'test'; + case 'release': return 'prod'; + default: return 'dev'; + } +} +``` + +### 动态配置加载 + +所有 API 调用都会动态获取当前环境的配置: + +```typescript +export const API = { + get baseURL(): string { + return currentConfig().baseURL; + }, + // ... +}; +``` + +这样可以确保始终使用正确环境的后端地址。 diff --git a/miniprogram/README.md b/miniprogram/README.md new file mode 100644 index 0000000..f993405 --- /dev/null +++ b/miniprogram/README.md @@ -0,0 +1,110 @@ +# AI文章审核小程序 + +根据 index.html 页面原型完成的微信小程序界面。 + +## 功能页面 + +### 1. 登录页面 (pages/login) +- 微信一键登录 +- 登录成功后自动跳转到文章列表页 + +### 2. 文章列表页 (pages/articles) +- 状态筛选:全部、待审核、已通过、已驳回、已发布 +- 显示文章卡片信息: + - 封面(根据话题类型自动生成颜色和图标) + - 标题 + - 作者信息 + - 渠道标签(百度/头条/微信) + - 状态标签 + - 统计信息(字数、图片数、创建时间) +- 点击文章卡片进入详情页 + +### 3. 文章详情页 (pages/article-detail) +- 显示完整文章信息: + - 封面 + - 标题 + - 元信息(批次ID、话题、部门、渠道、标签等) + - 作者信息和状态 + - 文章内容 + - 统计数据 +- 审核功能: + - 待审核状态:可通过或驳回 + - 已通过状态:可发布文章 + - 已发布状态:显示已发布 + - 审核意见输入 + +### 4. 我的页面 (pages/index) +- 保留原有功能(可后续扩展) + +## 技术特点 + +1. **TypeScript 开发**:完整的类型支持 +2. **模拟数据**:内置 6 篇文章数据,包含所有状态 +3. **工具函数**: + - formatDate:智能日期格式化 + - getStatusInfo:状态信息映射 + - getChannelInfo:渠道信息映射 + - getCoverColor:封面颜色生成 + - getCoverIcon:封面图标生成 + +4. **UI 设计**: + - 品牌色:#ff2442(小红书红) + - 响应式布局 + - 丰富的状态样式 + - 优雅的交互动画 + +5. **状态管理**: + - 11 种文章状态 + - 3 种发布渠道 + - 完整的审核流程 + +## 使用说明 + +1. 首次打开小程序,显示登录页 +2. 点击"微信一键登录"进行授权 +3. 登录成功后自动跳转到文章列表 +4. 在文章列表页: + - 点击顶部标签切换筛选条件 + - 点击文章卡片查看详情 +5. 在文章详情页: + - 待审核文章可以通过或驳回 + - 已通过文章可以发布 + - 驳回时必须填写原因 + +## 数据说明 + +当前使用模拟数据,包含: +- 2 篇待审核文章 +- 1 篇已通过文章 +- 1 篇已驳回文章 +- 1 篇已发布文章 +- 1 篇草稿文章 + +实际使用时需要: +1. 将模拟数据替换为 API 调用 +2. 实现真实的登录认证 +3. 添加网络请求处理 +4. 实现数据持久化 + +## 目录结构 + +``` +miniprogram/ +├── pages/ +│ ├── login/ # 登录页 +│ ├── articles/ # 文章列表页 +│ ├── article-detail/ # 文章详情页 +│ └── index/ # 我的页面 +├── utils/ +│ └── util.ts # 工具函数 +├── app.ts # 小程序入口 +├── app.json # 小程序配置 +└── app.wxss # 全局样式 +``` + +## 注意事项 + +1. TabBar 图标需要自行准备(images 目录) +2. 登录功能需要配置小程序 AppID +3. 审核操作目前只是模拟,未真实保存数据 +4. 建议在真实环境中添加加载状态和错误处理 diff --git a/miniprogram/SHARE_FEATURE.md b/miniprogram/SHARE_FEATURE.md new file mode 100644 index 0000000..06fe8ed --- /dev/null +++ b/miniprogram/SHARE_FEATURE.md @@ -0,0 +1,219 @@ +# 小程序分享功能说明 + +## 📋 功能概述 + +万花筒AI助手小程序已支持**分享给好友**和**分享到朋友圈**功能,用户可以将精彩内容分享给微信好友或发布到朋友圈。 + +## ✨ 已实现页面 + +### 1. **首页** (`pages/home/home`) +- **分享标题**: 万花筒AI助手 - 智能生成种草文案 +- **分享路径**: `/pages/home/home` +- **应用场景**: 用户浏览产品列表时可分享小程序首页 + +### 2. **文章详情页** (`pages/article-detail/article-detail`) +- **分享标题**: 动态显示文章标题 +- **分享路径**: 带文章ID和产品ID参数 +- **应用场景**: 用户查看文案详情时可分享特定文案 + +### 3. **文章列表页** (`pages/articles/articles`) +- **分享标题**: 动态显示当前文案标题 +- **分享路径**: 带产品ID参数 +- **分享封面**: 使用产品图片 +- **应用场景**: 用户浏览文案列表时分享 + +### 4. **我的发布** (`pages/profile/published/published`) +- **分享标题**: 我的发布记录 - 万花筒AI助手 +- **分享路径**: `/pages/home/home` +- **应用场景**: 用户查看自己的发布记录时分享 + +## 🎯 分享方式 + +### 方式一: 右上角菜单分享 +用户点击小程序右上角的 `...` 按钮,可以看到: +- **转发** - 分享给微信好友/群 +- **分享到朋友圈** - 发布到微信朋友圈 + +### 方式二: 页面内分享按钮(可选) +可以在页面中添加自定义分享按钮: + +```html + + +``` + +## 💡 技术实现 + +### 1. 分享给好友 (onShareAppMessage) + +```typescript +onShareAppMessage() { + return { + title: '分享标题', // 分享卡片标题 + path: '/pages/home/home', // 分享路径 + imageUrl: '' // 分享封面图(可选) + }; +} +``` + +### 2. 分享到朋友圈 (onShareTimeline) + +```typescript +onShareTimeline() { + return { + title: '分享标题', // 朋友圈显示标题 + imageUrl: '' // 分享封面图(可选) + }; +} +``` + +## 📝 配置要求 + +### app.json 配置 +确保启用朋友圈分享: + +```json +{ + "permission": { + "scope.userLocation": { + "desc": "你的位置信息将用于小程序位置接口的效果展示" + } + } +} +``` + +### 页面配置 +在需要分享的页面.json中可配置: + +```json +{ + "navigationBarTitleText": "页面标题", + "enableShareAppMessage": true, // 允许分享给好友(默认true) + "enableShareTimeline": true // 允许分享到朋友圈(默认true) +} +``` + +## 🎨 优化建议 + +### 1. 自定义分享封面 +为提升分享效果,建议添加分享封面图: + +```typescript +onShareAppMessage() { + return { + title: '精彩种草文案', + path: '/pages/home/home', + imageUrl: '/images/share-cover.jpg' // 建议尺寸: 5:4 + }; +} +``` + +**推荐尺寸**: +- 分享给好友: 5:4 (例如: 500x400) +- 分享到朋友圈: 1:1 (例如: 500x500) + +### 2. 动态分享内容 +根据页面内容动态生成分享信息: + +```typescript +onShareAppMessage() { + const currentArticle = this.data.currentArticle; + return { + title: currentArticle.title, + path: `/pages/article-detail/article-detail?id=${currentArticle.id}`, + imageUrl: currentArticle.coverImage + }; +} +``` + +### 3. 分享数据统计 +可以在分享时记录统计数据: + +```typescript +onShareAppMessage() { + // 记录分享事件 + wx.reportAnalytics('share_action', { + page: 'home', + content_type: 'product_list' + }); + + return { + title: '万花筒AI助手', + path: '/pages/home/home' + }; +} +``` + +## 🔍 分享参数传递 + +### 场景还原 +用户通过分享卡片进入小程序时,可以获取分享参数: + +```typescript +onLoad(options: any) { + // 获取分享参数 + const productId = options.productId; + const articleId = options.articleId; + + if (productId) { + // 直接加载对应产品 + this.loadProduct(productId); + } +} +``` + +### 分享路径示例 + +```typescript +// 带参数的分享路径 +path: `/pages/articles/articles?productId=${this.data.productId}&from=share` + +// 多个参数 +path: `/pages/detail/detail?id=123&type=article&source=timeline` +``` + +## ⚠️ 注意事项 + +1. **路径限制**: 分享路径必须是已在 `app.json` 中注册的页面路径 +2. **参数长度**: 分享路径总长度不能超过 128 字节 +3. **图片要求**: + - 分享图片支持本地路径和网络图片 + - 网络图片需要先下载到本地 +4. **朋友圈限制**: 分享到朋友圈时,`title` 不能超过 32 个字符 +5. **用户取消**: 用户可能取消分享,无法强制分享 + +## 📊 分享效果监控 + +### 分享回调 +小程序没有直接的分享成功回调,但可以通过以下方式间接监控: + +1. **分享参数标记**: 在分享路径中添加 `from=share` 参数 +2. **数据统计**: 统计带分享参数的访问量 +3. **微信数据助手**: 在微信公众平台查看分享数据 + +## 🚀 未来优化方向 + +1. **个性化分享**: 根据用户行为推荐分享内容 +2. **分享激励**: 添加分享奖励机制 +3. **社交裂变**: 设计分享裂变活动 +4. **A/B测试**: 测试不同分享文案的效果 + +## 📱 测试方法 + +### 开发环境测试 +1. 打开微信开发者工具 +2. 点击右上角 `...` 菜单 +3. 选择 "转发" 或 "分享到朋友圈" +4. 查看分享卡片预览效果 + +### 真机测试 +1. 使用体验版或开发版小程序 +2. 在真实微信环境中测试分享 +3. 验证分享卡片样式和跳转 + +## 📚 相关文档 + +- [微信官方文档 - 转发](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onShareAppMessage-Object-object) +- [微信官方文档 - 分享到朋友圈](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onShareTimeline) diff --git a/miniprogram/download-icons.js b/miniprogram/download-icons.js new file mode 100644 index 0000000..2758f0e --- /dev/null +++ b/miniprogram/download-icons.js @@ -0,0 +1,96 @@ +/** + * TabBar 图标下载脚本 + * 从 Iconfont 下载图标并自动配置 + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +// 图标配置 +const icons = [ + { + name: 'home', + url: 'https://img.icons8.com/ios/100/999999/home--v1.png', + desc: '首页-未选中' + }, + { + name: 'home-active', + url: 'https://img.icons8.com/ios-filled/100/07c160/home--v1.png', + desc: '首页-选中' + }, + { + name: 'article', + url: 'https://img.icons8.com/ios/100/999999/document--v1.png', + desc: '我的发布-未选中' + }, + { + name: 'article-active', + url: 'https://img.icons8.com/ios-filled/100/07c160/document--v1.png', + desc: '我的发布-选中' + }, + { + name: 'user', + url: 'https://img.icons8.com/ios/100/999999/user--v1.png', + desc: '我的-未选中' + }, + { + name: 'user-active', + url: 'https://img.icons8.com/ios-filled/100/07c160/user--v1.png', + desc: '我的-选中' + } +]; + +// 创建目标目录 +const targetDir = path.join(__dirname, 'miniprogram', 'images', 'tabbar'); +if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + console.log('✅ 创建目录:', targetDir); +} + +// 下载单个图标 +function downloadIcon(icon) { + return new Promise((resolve, reject) => { + const filePath = path.join(targetDir, `${icon.name}.png`); + const file = fs.createWriteStream(filePath); + + https.get(icon.url, (response) => { + response.pipe(file); + file.on('finish', () => { + file.close(); + console.log(`✅ 下载成功: ${icon.desc} (${icon.name}.png)`); + resolve(); + }); + }).on('error', (err) => { + fs.unlink(filePath, () => {}); + console.error(`❌ 下载失败: ${icon.desc}`, err.message); + reject(err); + }); + }); +} + +// 批量下载 +async function downloadAll() { + console.log('\n🚀 开始下载 TabBar 图标...\n'); + + try { + for (const icon of icons) { + await downloadIcon(icon); + // 延迟避免请求过快 + await new Promise(resolve => setTimeout(resolve, 500)); + } + + console.log('\n✨ 所有图标下载完成!\n'); + console.log('📁 图标位置:', targetDir); + console.log('\n📝 下一步:'); + console.log('1. 打开 miniprogram/app.json'); + console.log('2. 在 tabBar.list 中添加图标路径'); + console.log('3. 重新编译小程序\n'); + + } catch (error) { + console.error('\n❌ 下载过程出错:', error); + } +} + +// 执行下载 +downloadAll(); diff --git a/miniprogram/generate-icons.html b/miniprogram/generate-icons.html new file mode 100644 index 0000000..a8b6eb7 --- /dev/null +++ b/miniprogram/generate-icons.html @@ -0,0 +1,216 @@ + + + + + + TabBar 图标生成器 + + + +

🎨 TabBar 图标生成器

+ +
+ 使用说明:
+ 1. 点击每个图标下方的"下载"按钮
+ 2. 将下载的 PNG 文件重命名并保存到 miniprogram/images/tabbar/ 目录
+ 3. 在 app.json 中配置图标路径 +
+ +
+
+

🏠 首页 (未选中)

+ +
+ +
+ +
+

🏠 首页 (选中)

+ +
+ +
+ +
+

📄 我的发布 (未选中)

+ +
+ +
+ +
+

📄 我的发布 (选中)

+ +
+ +
+ +
+

👤 我的 (未选中)

+ +
+ +
+ +
+

👤 我的 (选中)

+ +
+ +
+
+ + + + diff --git a/miniprogram/miniprogram/app.json b/miniprogram/miniprogram/app.json new file mode 100644 index 0000000..f285c61 --- /dev/null +++ b/miniprogram/miniprogram/app.json @@ -0,0 +1,56 @@ +{ + "pages": [ + "pages/home/home", + "pages/article-generate/article-generate", + "pages/login/login", + "pages/articles/articles", + "pages/article-detail/article-detail", + "pages/profile/profile", + "pages/profile/user-info/user-info", + "pages/profile/social-binding/social-binding", + "pages/profile/platform-bind/platform-bind", + "pages/profile/xhs-login/xhs-login", + "pages/profile/published/published", + "pages/profile/article-detail/article-detail", + "pages/profile/about/about", + "pages/profile/feedback/feedback", + "pages/agreement/user-agreement/user-agreement", + "pages/agreement/privacy-policy/privacy-policy", + "pages/index/index", + "pages/logs/logs" + ], + "window": { + "navigationBarTextStyle": "white", + "navigationBarTitleText": "万花筒AI助手", + "navigationBarBackgroundColor": "#07c160", + "backgroundColor": "#f5f5f5" + }, + "tabBar": { + "color": "#999999", + "selectedColor": "#07c160", + "backgroundColor": "#ffffff", + "borderStyle": "black", + "list": [ + { + "pagePath": "pages/home/home", + "text": "首页", + "iconPath": "images/tabbar/home.png", + "selectedIconPath": "images/tabbar/home-active.png" + }, + { + "pagePath": "pages/profile/published/published", + "text": "我的发布", + "iconPath": "images/tabbar/article.png", + "selectedIconPath": "images/tabbar/article-active.png" + }, + { + "pagePath": "pages/profile/profile", + "text": "我的", + "iconPath": "images/tabbar/user.png", + "selectedIconPath": "images/tabbar/user-active.png" + } + ] + }, + "style": "v2", + "lazyCodeLoading": "requiredComponents" +} \ No newline at end of file diff --git a/miniprogram/miniprogram/app.ts b/miniprogram/miniprogram/app.ts new file mode 100644 index 0000000..739cc11 --- /dev/null +++ b/miniprogram/miniprogram/app.ts @@ -0,0 +1,73 @@ +// app.ts +import { API } from './config/api'; + +interface IAppInstance { + globalData: Record; + showEnvironmentTip: () => void; +} + +App({ + globalData: {}, + onLaunch() { + // 展示本地存储能力 + const logs = wx.getStorageSync('logs') || [] + logs.unshift(Date.now()) + wx.setStorageSync('logs', logs) + + // 显示当前环境提示 + this.showEnvironmentTip(); + + // 登录 + wx.login({ + success: res => { + console.log(res.code) + // 发送 res.code 到后台换取 openId, sessionKey, unionId + }, + }) + }, + + // 显示环境提示 + showEnvironmentTip() { + try { + const accountInfo = wx.getAccountInfoSync(); + const envVersion = accountInfo.miniProgram.envVersion; + + let envName = ''; + let envColor = '#07c160'; + + switch (envVersion) { + case 'develop': + envName = '开发环境'; + envColor = '#52c41a'; + break; + case 'trial': + envName = '体验环境'; + envColor = '#faad14'; + break; + case 'release': + envName = '生产环境'; + envColor = '#1677ff'; + break; + default: + envName = '未知环境'; + } + + // 输出到控制台 + console.log(`[App] 当前环境: ${envName}`); + console.log(`[App] 后端地址: ${API.baseURL}`); + + // 显示弹窗提示 (仅开发和体验环境) + if (envVersion === 'develop' || envVersion === 'trial') { + wx.showModal({ + title: '环境提示', + content: `当前运行在${envName}\n后端地址: ${API.baseURL}`, + showCancel: false, + confirmText: '知道了', + confirmColor: envColor + }); + } + } catch (error) { + console.error('[App] 获取环境信息失败:', error); + } + } +}) \ No newline at end of file diff --git a/miniprogram/miniprogram/app.wxss b/miniprogram/miniprogram/app.wxss new file mode 100644 index 0000000..66b2203 --- /dev/null +++ b/miniprogram/miniprogram/app.wxss @@ -0,0 +1,14 @@ +/**app.wxss**/ +/* 全局样式 */ +page { + width: 100%; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* 按钮样式重置 */ +button::after { + border: none; +} diff --git a/miniprogram/miniprogram/config/api.ts b/miniprogram/miniprogram/config/api.ts new file mode 100644 index 0000000..1711c60 --- /dev/null +++ b/miniprogram/miniprogram/config/api.ts @@ -0,0 +1,142 @@ +// API配置文件 +// 统一管理后端接口地址 + +/** + * 环境类型 + * dev: 开发环境(本地) + * test: 测试环境(服务器测试) + * prod: 生产环境 + */ +type EnvType = 'dev' | 'test' | 'prod'; + +/** + * 环境配置接口 + */ +interface EnvConfig { + baseURL: string; // 主服务地址 + pythonURL?: string; // Python服务地址(可选) + timeout: number; // 请求超时时间 +} + +/** + * 多环境配置 + * 修改这里的配置即可切换不同环境的后端地址 + */ +const API_CONFIG: Record = { + // 开发环境 - 本地开发 + dev: { + baseURL: 'http://localhost:8080', // 本地Go服务 + pythonURL: 'http://localhost:8000', // 本地Python服务 + timeout: 90000 + }, + + // 测试环境 - 服务器测试 + test: { + baseURL: 'https://lehang.tech', // 测试服务器Go服务 + pythonURL: 'https://lehang.tech', // 测试服务器Python服务 + timeout: 90000 + }, + + // 生产环境 + prod: { + baseURL: 'https://lehang.tech', // 生产环境Go服务 + pythonURL: 'https://lehang.tech', // 生产环境Python服务 + timeout: 90000 + } +}; + +/** + * 自动检测环境 + * 根据小程序的运行环境自动判断: + * - 开发版(develop) → dev + * - 体验版(trial) → test + * - 正式版(release) → prod + */ +function detectEnvironment(): EnvType { + try { + const accountInfo = wx.getAccountInfoSync(); + const envVersion = accountInfo.miniProgram.envVersion; + + switch (envVersion) { + case 'develop': + return 'dev'; // 开发版 + case 'trial': + return 'test'; // 体验版 + case 'release': + return 'prod'; // 正式版 + default: + return 'dev'; + } + } catch (error) { + console.warn('无法检测环境,使用默认开发环境', error); + return 'dev'; + } +} + +// 当前环境(自动检测) +const currentEnv: EnvType = detectEnvironment(); +const currentConfig = (): EnvConfig => API_CONFIG[currentEnv]; + +// 输出环境信息 +console.log(`[API Config] 当前环境: ${currentEnv}`); +console.log(`[API Config] 主服务: ${currentConfig().baseURL}`); +if (currentConfig().pythonURL) { + console.log(`[API Config] Python服务: ${currentConfig().pythonURL}`); +} + +// API端点 +export const API = { + // 基础配置(动态获取) + get baseURL(): string { + return currentConfig().baseURL; + }, + get pythonURL(): string | undefined { + return currentConfig().pythonURL; + }, + get timeout(): number { + return currentConfig().timeout; + }, + + // 登录接口 + auth: { + wechatLogin: '/api/login/wechat', // 微信登录 + phoneLogin: '/api/login/phone' // 手机号登录 + }, + + // 员工端接口 + employee: { + profile: '/api/employee/profile', // 获取个人信息 + bindXHS: '/api/employee/bind-xhs', // 绑定小红书 + unbindXHS: '/api/employee/unbind-xhs', // 解绑小红书 + availableCopies: '/api/employee/available-copies', // 获取可领取文案 + claimCopy: '/api/employee/claim-copy', // 领取文案 + claimRandomCopy: '/api/employee/claim-random-copy', // 随机领取文案 + publish: '/api/employee/publish', // 发布内容 + myPublishRecords: '/api/employee/my-publish-records' // 我的发布记录 + }, + + // 公开接口(不需要登录) + public: { + products: '/api/products' // 获取产品列表 + }, + + // 小红书相关接口 + xhs: { + sendCode: '/api/xhs/send-code' // 发送小红书验证码 + } +}; + +// 构建完整URL +export function buildURL(path: string, usePython = false): string { + const baseURL = usePython ? (API.pythonURL || API.baseURL) : API.baseURL; + return `${baseURL}${path}`; +} + +// 获取请求头 +export function getHeaders(): Record { + const token = wx.getStorageSync('token') || ''; + return { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '' + }; +} diff --git a/miniprogram/miniprogram/custom-tab-bar/index.json b/miniprogram/miniprogram/custom-tab-bar/index.json new file mode 100644 index 0000000..e16a2dd --- /dev/null +++ b/miniprogram/miniprogram/custom-tab-bar/index.json @@ -0,0 +1,7 @@ +{ + "component": true, + "usingComponents": { + "t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar", + "t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item" + } +} diff --git a/miniprogram/miniprogram/custom-tab-bar/index.wxss b/miniprogram/miniprogram/custom-tab-bar/index.wxss new file mode 100644 index 0000000..b899594 --- /dev/null +++ b/miniprogram/miniprogram/custom-tab-bar/index.wxss @@ -0,0 +1 @@ +/* custom-tab-bar/index.wxss */ diff --git a/miniprogram/miniprogram/images/TABBAR_ICONS_GUIDE.md b/miniprogram/miniprogram/images/TABBAR_ICONS_GUIDE.md new file mode 100644 index 0000000..38df855 --- /dev/null +++ b/miniprogram/miniprogram/images/TABBAR_ICONS_GUIDE.md @@ -0,0 +1,111 @@ +# TabBar 图标添加指南 + +## 📋 当前状态 +- TabBar 已配置完成,只显示文字,暂无图标 +- 需要手动添加 PNG/JPG 格式的图标文件 + +## 🎨 图标要求 +- **格式**:PNG(推荐,支持透明背景)或 JPG +- **尺寸**:81x81 px(推荐)或 40x40 px(最小) +- **背景**:透明(PNG格式) +- **颜色**: + - 未选中:灰色 (#999999) + - 选中:绿色 (#07c160) + +## 📁 文件命名和位置 +创建 `images/tabbar/` 目录,添加以下 6 个图标文件: + +``` +miniprogram/ +└── miniprogram/ + └── images/ + └── tabbar/ + ├── home.png # 首页 - 未选中 + ├── home-active.png # 首页 - 选中 + ├── article.png # 我的发布 - 未选中 + ├── article-active.png # 我的发布 - 选中 + ├── user.png # 我的 - 未选中 + └── user-active.png # 我的 - 选中 +``` + +## 🔧 配置步骤 + +### 1. 准备图标文件 +可以使用以下方式获取图标: + +#### 方案1:使用 Iconfont(推荐) +1. 访问 https://www.iconfont.cn/ +2. 搜索以下图标: + - 首页:搜索 "home" 或 "房子" + - 文章:搜索 "file" 或 "文档" + - 用户:搜索 "user" 或 "用户" +3. 下载 PNG 格式(81x81 px) +4. 使用在线工具修改颜色: + - https://www.peko-step.com/zh/tool/pngcolor.html + - 未选中版本改为灰色 #999999 + - 选中版本改为绿色 #07c160 + +#### 方案2:使用 Icons8 +1. 访问 https://icons8.com/icons +2. 搜索图标并下载 +3. 选择 PNG 格式,81x81 px +4. 选择颜色(灰色/绿色) + +#### 方案3:使用 Flaticon +1. 访问 https://www.flaticon.com/ +2. 搜索并下载图标 +3. 编辑颜色 + +### 2. 修改 app.json 配置 + +在 `app.json` 的 `tabBar.list` 中添加图标路径: + +```json +{ + "tabBar": { + "list": [ + { + "pagePath": "pages/home/home", + "text": "首页", + "iconPath": "images/tabbar/home.png", + "selectedIconPath": "images/tabbar/home-active.png" + }, + { + "pagePath": "pages/profile/published/published", + "text": "我的发布", + "iconPath": "images/tabbar/article.png", + "selectedIconPath": "images/tabbar/article-active.png" + }, + { + "pagePath": "pages/profile/profile", + "text": "我的", + "iconPath": "images/tabbar/user.png", + "selectedIconPath": "images/tabbar/user-active.png" + } + ] + } +} +``` + +### 3. 重新编译小程序 +保存配置后,重新编译即可看到图标 + +## 🎯 快速方案:使用简单色块图标 + +如果暂时找不到合适的图标,可以使用纯色方块作为临时图标: + +1. 使用 Photoshop、Sketch 或在线工具创建 81x81 px 的图片 +2. 首页:蓝色方块 +3. 我的发布:橙色方块 +4. 我的:绿色方块 +5. 选中状态:同色但更亮或加边框 + +## ⚠️ 注意事项 +- 图标文件大小建议 < 40KB +- 确保图片路径正确(相对于 miniprogram 目录) +- PNG 格式推荐使用透明背景 +- 图标设计要简洁清晰,避免过于复杂的图案 +- 选中和未选中状态要有明显区别 + +## 📞 需要帮助? +如果需要我帮你生成简单的图标文件,请告诉我,我可以提供 base64 编码或其他方案。 diff --git a/miniprogram/miniprogram/images/article-active.svg b/miniprogram/miniprogram/images/article-active.svg new file mode 100644 index 0000000..d01456f --- /dev/null +++ b/miniprogram/miniprogram/images/article-active.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/miniprogram/miniprogram/images/article.svg b/miniprogram/miniprogram/images/article.svg new file mode 100644 index 0000000..3c41c58 --- /dev/null +++ b/miniprogram/miniprogram/images/article.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/miniprogram/miniprogram/images/create-icons.md b/miniprogram/miniprogram/images/create-icons.md new file mode 100644 index 0000000..323fdfb --- /dev/null +++ b/miniprogram/miniprogram/images/create-icons.md @@ -0,0 +1,40 @@ +# TabBar 图标说明 + +由于文件系统限制,无法直接创建二进制图标文件。 + +## 方案:使用 iconfont 或在线图标 + +建议使用以下方式添加 TabBar 图标: + +### 方案1:使用阿里 iconfont +1. 访问 https://www.iconfont.cn/ +2. 搜索并下载以下图标(81x81 px): + - 首页图标(home) + - 文章图标(article/document) + - 用户图标(user/profile) +3. 每个图标需要两个版本: + - 普通状态(灰色 #999999) + - 选中状态(绿色 #07c160) + +### 方案2:临时方案 - 使用 emoji 或纯文字 +在 app.json 的 tabBar 配置中暂时不设置 iconPath,只显示文字 + +### 方案3:使用在线图标生成工具 +可以使用以下工具快速生成图标: +- https://icon-icons.com/ +- https://www.flaticon.com/ +- https://icons8.com/ + +## 图标要求 +- 尺寸:81x81 px(推荐)或 40x40 px(最小) +- 格式:PNG +- 背景:透明 +- 命名规范: + ``` + images/tab-home.png + images/tab-home-active.png + images/tab-article.png + images/tab-article-active.png + images/tab-user.png + images/tab-user-active.png + ``` diff --git a/miniprogram/miniprogram/images/home-active.svg b/miniprogram/miniprogram/images/home-active.svg new file mode 100644 index 0000000..282f55a --- /dev/null +++ b/miniprogram/miniprogram/images/home-active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/miniprogram/images/home.svg b/miniprogram/miniprogram/images/home.svg new file mode 100644 index 0000000..0e5c223 --- /dev/null +++ b/miniprogram/miniprogram/images/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/miniprogram/images/tabbar/article-active.png b/miniprogram/miniprogram/images/tabbar/article-active.png new file mode 100644 index 0000000..0eb14b7 Binary files /dev/null and b/miniprogram/miniprogram/images/tabbar/article-active.png differ diff --git a/miniprogram/miniprogram/images/tabbar/article.png b/miniprogram/miniprogram/images/tabbar/article.png new file mode 100644 index 0000000..26aeaba Binary files /dev/null and b/miniprogram/miniprogram/images/tabbar/article.png differ diff --git a/miniprogram/miniprogram/images/tabbar/home-active.png b/miniprogram/miniprogram/images/tabbar/home-active.png new file mode 100644 index 0000000..6849027 Binary files /dev/null and b/miniprogram/miniprogram/images/tabbar/home-active.png differ diff --git a/miniprogram/miniprogram/images/tabbar/home.png b/miniprogram/miniprogram/images/tabbar/home.png new file mode 100644 index 0000000..f4f6709 Binary files /dev/null and b/miniprogram/miniprogram/images/tabbar/home.png differ diff --git a/miniprogram/miniprogram/images/tabbar/user-active.png b/miniprogram/miniprogram/images/tabbar/user-active.png new file mode 100644 index 0000000..9f8a46d Binary files /dev/null and b/miniprogram/miniprogram/images/tabbar/user-active.png differ diff --git a/miniprogram/miniprogram/images/tabbar/user.png b/miniprogram/miniprogram/images/tabbar/user.png new file mode 100644 index 0000000..1e5ede2 Binary files /dev/null and b/miniprogram/miniprogram/images/tabbar/user.png differ diff --git a/miniprogram/miniprogram/images/user-active.svg b/miniprogram/miniprogram/images/user-active.svg new file mode 100644 index 0000000..34da14d --- /dev/null +++ b/miniprogram/miniprogram/images/user-active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/miniprogram/images/user.svg b/miniprogram/miniprogram/images/user.svg new file mode 100644 index 0000000..19117b6 --- /dev/null +++ b/miniprogram/miniprogram/images/user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.json b/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.json new file mode 100644 index 0000000..b5bf3f9 --- /dev/null +++ b/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "隐私政策", + "navigationBarBackgroundColor": "#ff2442", + "navigationBarTextStyle": "white" +} diff --git a/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.ts b/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.ts new file mode 100644 index 0000000..b375c27 --- /dev/null +++ b/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.ts @@ -0,0 +1,10 @@ +// pages/agreement/privacy-policy/privacy-policy.ts +Page({ + data: { + + }, + + onLoad() { + + } +}); diff --git a/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.wxml b/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.wxml new file mode 100644 index 0000000..4f71d0b --- /dev/null +++ b/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.wxml @@ -0,0 +1,92 @@ + + + + 隐私政策 + 更新时间:2024年12月5日 + + + 一、信息收集 + + 我们会收集您在使用服务时主动提供或因使用服务而产生的信息,包括但不限于:\n + 1. 微信授权信息(昵称、头像等)\n + 2. 设备信息\n + 3. 日志信息\n + 4. 位置信息(如您授权) + + + + + 二、信息使用 + + 我们收集和使用您的个人信息用于:\n + 1. 为您提供、维护和改进我们的服务\n + 2. 与您沟通,包括发送通知和更新\n + 3. 保护服务的安全性和完整性\n + 4. 遵守法律法规要求 + + + + + 三、信息共享 + + 我们不会与第三方共享您的个人信息,除非:\n + 1. 获得您的明确同意\n + 2. 法律法规要求\n + 3. 与我们的服务提供商共享(仅限于提供服务所必需)\n + 4. 保护我们或他人的合法权益 + + + + + 四、信息安全 + + 我们采取各种安全措施来保护您的个人信息,包括:\n + 1. 使用加密技术保护数据传输\n + 2. 限制员工访问个人信息\n + 3. 定期审查信息收集、存储和处理实践\n + 4. 采取物理和技术措施防止未经授权的访问 + + + + + 五、您的权利 + + 您对自己的个人信息享有以下权利:\n + 1. 访问和更新您的个人信息\n + 2. 删除您的个人信息\n + 3. 撤回授权同意\n + 4. 注销账号 + + + + + 六、Cookie使用 + + 我们可能使用Cookie和类似技术来改善用户体验。您可以通过浏览器设置拒绝Cookie,但这可能影响某些功能的使用。 + + + + + 七、未成年人保护 + + 我们重视未成年人的个人信息保护。如果您是未成年人,请在监护人的陪同下阅读本政策,并在监护人同意后使用我们的服务。 + + + + + 八、政策更新 + + 我们可能会不时更新本隐私政策。更新后的政策将在平台上公布,请您定期查看。 + + + + + 九、联系我们 + + 如您对本隐私政策有任何疑问,请通过以下方式联系我们:\n + 客服邮箱:privacy@example.com\n + 客服电话:400-888-8888 + + + + diff --git a/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.wxss b/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.wxss new file mode 100644 index 0000000..09b45b8 --- /dev/null +++ b/miniprogram/miniprogram/pages/agreement/privacy-policy/privacy-policy.wxss @@ -0,0 +1,53 @@ +/* pages/agreement/privacy-policy/privacy-policy.wxss */ +page { + background: #FFFFFF; + height: 100%; +} + +.agreement-container { + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; + background: #FFFFFF; +} + +.content-scroll { + flex: 1; + width: 100%; + padding: 30rpx 40rpx 60rpx; + box-sizing: border-box; +} + +.agreement-title { + font-size: 44rpx; + font-weight: bold; + color: #1a1a1a; + text-align: center; + margin-bottom: 16rpx; +} + +.update-time { + font-size: 24rpx; + color: #999; + text-align: center; + margin-bottom: 40rpx; +} + +.section { + margin-bottom: 40rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 20rpx; +} + +.section-content { + font-size: 28rpx; + color: #666; + line-height: 1.8; + white-space: pre-line; +} diff --git a/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.json b/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.json new file mode 100644 index 0000000..a7baac5 --- /dev/null +++ b/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "用户协议", + "navigationBarBackgroundColor": "#ff2442", + "navigationBarTextStyle": "white" +} diff --git a/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.ts b/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.ts new file mode 100644 index 0000000..58d6591 --- /dev/null +++ b/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.ts @@ -0,0 +1,10 @@ +// pages/agreement/user-agreement/user-agreement.ts +Page({ + data: { + + }, + + onLoad() { + + } +}); diff --git a/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.wxml b/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.wxml new file mode 100644 index 0000000..a07f3fd --- /dev/null +++ b/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.wxml @@ -0,0 +1,71 @@ + + + + 用户协议 + 更新时间:2024年12月5日 + + + 一、协议的接受 + + 欢迎使用AI文章审核平台。本协议是您与本平台之间关于使用本平台服务所订立的协议。请您仔细阅读本协议,您使用本平台服务即表示您已阅读并同意本协议的全部内容。 + + + + + 二、服务说明 + + 本平台为用户提供AI文章生成、审核和管理服务。我们致力于为用户提供高效、便捷的内容管理工具。 + + + + + 三、用户账号 + + 1. 您需要通过微信授权登录使用本平台服务。\n + 2. 您应妥善保管您的账号信息,对账号下的所有活动负责。\n + 3. 如发现账号被盗用,请及时联系我们。 + + + + + 四、用户行为规范 + + 您在使用本平台服务时应遵守相关法律法规,不得利用本平台从事违法违规活动,包括但不限于:\n + 1. 发布违法违规内容\n + 2. 侵犯他人知识产权\n + 3. 传播虚假信息\n + 4. 其他危害平台安全的行为 + + + + + 五、知识产权 + + 本平台的所有内容,包括但不限于文字、图片、软件、程序等,均受著作权法等法律法规保护。未经授权,不得擅自使用。 + + + + + 六、免责声明 + + 本平台对因不可抗力或不可归责于本平台的原因导致的服务中断或其他缺陷不承担责任。用户理解并同意自行承担使用本平台服务的风险。 + + + + + 七、协议修改 + + 我们有权根据需要修改本协议,修改后的协议将在平台上公布。您继续使用本平台服务即表示同意修改后的协议。 + + + + + 八、联系我们 + + 如您对本协议有任何疑问,请通过以下方式联系我们:\n + 客服邮箱:support@example.com\n + 客服电话:400-888-8888 + + + + diff --git a/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.wxss b/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.wxss new file mode 100644 index 0000000..0ba6dc8 --- /dev/null +++ b/miniprogram/miniprogram/pages/agreement/user-agreement/user-agreement.wxss @@ -0,0 +1,53 @@ +/* pages/agreement/user-agreement/user-agreement.wxss */ +page { + background: #FFFFFF; + height: 100%; +} + +.agreement-container { + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; + background: #FFFFFF; +} + +.content-scroll { + flex: 1; + width: 100%; + padding: 30rpx 40rpx 60rpx; + box-sizing: border-box; +} + +.agreement-title { + font-size: 44rpx; + font-weight: bold; + color: #1a1a1a; + text-align: center; + margin-bottom: 16rpx; +} + +.update-time { + font-size: 24rpx; + color: #999; + text-align: center; + margin-bottom: 40rpx; +} + +.section { + margin-bottom: 40rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 20rpx; +} + +.section-content { + font-size: 28rpx; + color: #666; + line-height: 1.8; + white-space: pre-line; +} diff --git a/miniprogram/miniprogram/pages/article-detail/article-detail.json b/miniprogram/miniprogram/pages/article-detail/article-detail.json new file mode 100644 index 0000000..2051df5 --- /dev/null +++ b/miniprogram/miniprogram/pages/article-detail/article-detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "文章详情", + "usingComponents": {} +} diff --git a/miniprogram/miniprogram/pages/article-detail/article-detail.ts b/miniprogram/miniprogram/pages/article-detail/article-detail.ts new file mode 100644 index 0000000..dec4445 --- /dev/null +++ b/miniprogram/miniprogram/pages/article-detail/article-detail.ts @@ -0,0 +1,91 @@ +// pages/article-detail/article-detail.ts +import { formatDate, getStatusInfo, getChannelInfo, getCoverColor, getCoverIcon } from '../../utils/util' +import { EmployeeService } from '../../services/employee' + +Page({ + data: { + article: {} as any, + copyId: 0, // 文案ID + productId: 0, // 产品ID + showClaimButton: true // 是否显示领取按钮 + }, + + onLoad(options: any) { + const copyId = parseInt(options.id); + const productId = parseInt(options.productId || '0'); + + this.setData({ + copyId, + productId + }); + + // 如果有copyId,显示文案详情 + if (copyId) { + this.setData({ + article: { + id: copyId, + title: '正在加载...', + content: '' + } + }); + } + }, + + // 领取文案 + async claimCopy() { + const { copyId, productId } = this.data; + + if (!copyId || !productId) { + wx.showToast({ + title: '参数错误', + icon: 'none' + }); + return; + } + + try { + const response = await EmployeeService.claimCopy(copyId, productId); + + if (response.code === 200 && response.data) { + wx.showToast({ + title: '领取成功', + icon: 'success' + }); + + // 隐藏领取按钮 + this.setData({ + showClaimButton: false + }); + + // 延迟后跳转到发布页面,传递领取信息 + setTimeout(() => { + const copy = response.data!.copy; + wx.redirectTo({ + url: `/pages/article-generate/article-generate?copyId=${copyId}&claimId=${response.data!.claim_id}&productId=${productId}&productName=${encodeURIComponent(copy.title)}&title=${encodeURIComponent(copy.title)}&content=${encodeURIComponent(copy.content)}` + }); + }, 1500); + } + } catch (error) { + console.error('领取文案失败:', error); + } + }, + + // 分享功能 + onShareAppMessage() { + const article = this.data.article; + return { + title: article.title || '精彩种草文案', + path: `/pages/article-detail/article-detail?id=${this.data.copyId}&productId=${this.data.productId}`, + imageUrl: '' // 可以设置文章封面图 + }; + }, + + // 分享到朋友圈 + onShareTimeline() { + const article = this.data.article; + return { + title: article.title || '精彩种草文案', + imageUrl: '' // 可以设置文章封面图 + }; + } +}); diff --git a/miniprogram/miniprogram/pages/article-detail/article-detail.wxml b/miniprogram/miniprogram/pages/article-detail/article-detail.wxml new file mode 100644 index 0000000..c1f7b9a --- /dev/null +++ b/miniprogram/miniprogram/pages/article-detail/article-detail.wxml @@ -0,0 +1,115 @@ + + + + {{article.coverIcon}} + + + + {{article.title}} + + + + + + {{article.author_name ? article.author_name[0] : 'A'}} + + + {{article.author_name || '匿名'}} + 创建于 {{article.created_at}} + + {{article.statusText}} + + + {{article.content}} + + + + {{article.word_count}} + 字数 + + + {{article.image_count}} + 图片 + + + {{article.timeText}} + 创建时间 + + + + + + + + + + + + 审核意见 + + + + + + + + diff --git a/miniprogram/miniprogram/pages/article-detail/article-detail.wxss b/miniprogram/miniprogram/pages/article-detail/article-detail.wxss new file mode 100644 index 0000000..404ef46 --- /dev/null +++ b/miniprogram/miniprogram/pages/article-detail/article-detail.wxss @@ -0,0 +1,341 @@ +/* pages/article-detail/article-detail.wxss */ +page { + height: 100vh; + width: 100vw; + background: linear-gradient(to bottom, #fff 0%, #f8f9fa 100%); + overflow: hidden; +} + +.container { + height: 100vh; + width: 100vw; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; +} + +.article-cover { + width: 100%; + height: 450rpx; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.article-cover::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.15) 100%); +} + +.cover-icon { + font-size: 150rpx; + position: relative; + z-index: 1; + filter: drop-shadow(0 8rpx 24rpx rgba(0, 0, 0, 0.15)); +} + +.detail-content { + width: 100%; + padding: 50rpx 40rpx; + background: white; + border-radius: 40rpx 40rpx 0 0; + margin-top: -40rpx; + position: relative; + z-index: 10; + box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.06); + box-sizing: border-box; +} + +.detail-title { + font-size: 44rpx; + font-weight: bold; + color: #1a1a1a; + margin-bottom: 35rpx; + line-height: 1.5; + letter-spacing: 1rpx; + word-break: break-all; +} + +.article-meta-info { + display: flex; + flex-wrap: wrap; + gap: 24rpx; + margin-bottom: 35rpx; + padding: 35rpx; + background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%); + border-radius: 20rpx; + border: 1rpx solid #f0f0f0; + box-sizing: border-box; +} + +.meta-item { + flex: 0 0 auto; + font-size: 28rpx; +} + +.meta-item.full-width { + flex: 1 1 100%; +} + +.meta-label { + color: #999; + margin-right: 12rpx; + font-weight: 500; +} + +.meta-value { + color: #333; + font-weight: 600; +} + +.channel-tag { + padding: 6rpx 20rpx; + border-radius: 50rpx; + font-size: 24rpx; + font-weight: 600; + letter-spacing: 0.5rpx; +} + +.channel-1 { + background-color: #e6f7ff; + color: #1890ff; +} + +.channel-2 { + background-color: #fff7e6; + color: #fa8c16; +} + +.channel-3 { + background-color: #f0f9ff; + color: #07c160; +} + +.detail-author { + display: flex; + align-items: center; + margin-bottom: 40rpx; + padding: 30rpx; + background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%); + border-radius: 20rpx; + border: 1rpx solid #f0f0f0; +} + +.detail-author-avatar { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + margin-right: 24rpx; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + font-size: 36rpx; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); +} + +.author-info { + flex: 1; +} + +.author-name { + font-size: 34rpx; + font-weight: 600; + margin-bottom: 10rpx; + color: #1a1a1a; +} + +.author-time { + font-size: 26rpx; + color: #999; +} + +.article-status { + padding: 10rpx 28rpx; + border-radius: 50rpx; + font-size: 26rpx; + font-weight: 600; + letter-spacing: 0.5rpx; +} + +.status-topic { + background-color: #f0f5ff; + color: #2f54eb; +} + +.status-cover_image { + background-color: #e6f7ff; + color: #1890ff; +} + +.status-generate { + background-color: #fff7e6; + color: #fa8c16; +} + +.status-generate_failed { + background-color: #fff1f0; + color: #f5222d; +} + +.status-draft { + background-color: #f5f5f5; + color: #8c8c8c; +} + +.status-pending_review { + background-color: #fff7e6; + color: #fa8c16; +} + +.status-approved { + background-color: #f6ffed; + color: #52c41a; +} + +.status-rejected { + background-color: #fff1f0; + color: #f5222d; +} + +.status-published_review { + background-color: #e6fffb; + color: #13c2c2; +} + +.status-published { + background-color: #f6ffed; + color: #52c41a; +} + +.status-failed { + background-color: #fff1f0; + color: #f5222d; +} + +.detail-text { + font-size: 32rpx; + line-height: 2; + color: #444; + margin-bottom: 40rpx; + white-space: pre-wrap; + padding: 30rpx; + background: #fafafa; + border-radius: 20rpx; + border-left: 4rpx solid #07c160; + word-break: break-all; + box-sizing: border-box; +} + +.detail-stats { + display: flex; + justify-content: space-around; + margin-bottom: 50rpx; + padding: 40rpx 0; + background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%); + border-radius: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); +} + +.detail-stat { + text-align: center; + flex: 1; +} + +.detail-stat-value { + font-size: 48rpx; + font-weight: bold; + color: #07c160; + display: block; + margin-bottom: 12rpx; +} + +.detail-stat-label { + font-size: 26rpx; + color: #999; + font-weight: 500; +} + +.action-buttons { + display: flex; + gap: 24rpx; + margin-top: 40rpx; + flex-wrap: wrap; +} + +.action-btn { + flex: 1; + min-width: 200rpx; + padding: 32rpx; + border-radius: 20rpx; + font-size: 32rpx; + font-weight: bold; + border: none; + color: white; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12); + transition: all 0.3s; + box-sizing: border-box; +} + +.action-btn:active { + transform: scale(0.95); +} + +.approve-btn { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); +} + +.reject-btn { + background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%); +} + +.publish-btn { + background: #07c160; +} + +.publish-btn.disabled { + background: #d9d9d9; + color: #999; + box-shadow: none; +} + +.cancel-btn { + background: linear-gradient(135deg, #d9d9d9 0%, #e8e8e8 100%); + color: #666; +} + +.review-section { + margin-top: 50rpx; + padding: 40rpx; + background: linear-gradient(135deg, #fff5f5 0%, #fff 100%); + border-radius: 20rpx; + border: 2rpx solid #ffebee; +} + +.review-title { + font-size: 34rpx; + font-weight: bold; + margin-bottom: 30rpx; + color: #1a1a1a; +} + +.review-textarea { + width: 100%; + min-height: 220rpx; + padding: 28rpx; + border: 2rpx solid #f0f0f0; + border-radius: 16rpx; + font-size: 30rpx; + box-sizing: border-box; + margin-bottom: 30rpx; + background: white; + line-height: 1.8; +} diff --git a/miniprogram/miniprogram/pages/article-generate/article-generate.json b/miniprogram/miniprogram/pages/article-generate/article-generate.json new file mode 100644 index 0000000..0d18ecd --- /dev/null +++ b/miniprogram/miniprogram/pages/article-generate/article-generate.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "AI生成种草文章", + "navigationBarBackgroundColor": "#ff2442", + "navigationBarTextStyle": "white" +} diff --git a/miniprogram/miniprogram/pages/article-generate/article-generate.ts b/miniprogram/miniprogram/pages/article-generate/article-generate.ts new file mode 100644 index 0000000..7dc7d1c --- /dev/null +++ b/miniprogram/miniprogram/pages/article-generate/article-generate.ts @@ -0,0 +1,231 @@ +// pages/article-generate/article-generate.ts +import { EmployeeService } from '../../services/employee'; + +interface Article { + title: string; + content: string; + tags: string[]; + images: string[]; +} + +Page({ + data: { + productId: 0, + productName: '', + copyId: 0, // 领取的文案ID + claimId: 0, // 领取记录ID + article: { + title: '', + content: '', + tags: [], + images: [] + } as Article, + generating: false, + isFromClaim: false // 是否来自领取的文案 + }, + + onLoad(options: any) { + const { productId, productName, copyId, claimId, title, content } = options; + + this.setData({ + productId: parseInt(productId || '0'), + productName: decodeURIComponent(productName || ''), + copyId: parseInt(copyId || '0'), + claimId: parseInt(claimId || '0'), + isFromClaim: !!copyId + }); + + // 如果有copyId,说明是从领取文案过来的 + if (this.data.copyId && this.data.claimId) { + // 如果有传递文案内容,直接显示 + if (title && content) { + this.setData({ + article: { + title: decodeURIComponent(title), + content: decodeURIComponent(content), + tags: ['种草分享', '好物推荐'], + images: [] + } + }); + } else { + // 否则生成模拟文案 + this.generateArticle(); + } + } else { + // 生成文章 + this.generateArticle(); + } + }, + + // 生成文章 + generateArticle() { + this.setData({ generating: true }); + + wx.showLoading({ + title: '生成中...', + mask: true + }); + + // 模拟AI生成文章 + setTimeout(() => { + const mockArticles = [ + { + title: `【种草分享】${this.data.productName}使用体验💕`, + content: `姐妹们!今天必须来跟大家分享一下我最近入手的宝藏好物——${this.data.productName}! + +✨使用感受: +用了一段时间真的太爱了!质感超级好,完全超出我的预期。包装也非常精致,送人自用都很合适。 + +🌟推荐理由: +1. 品质优秀,性价比超高 +2. 使用体验一级棒 +3. 颜值在线,拿出来超有面子 + +💰价格也很美丽,趁着活动入手真的很划算!强烈推荐给大家,绝对不会踩雷!`, + tags: ['种草分享', '好物推荐', '必买清单', '真实测评'], + images: [ + 'https://picsum.photos/id/237/600/400', + 'https://picsum.photos/id/152/600/400' + ] + }, + { + title: `真香警告!${this.data.productName}实测分享`, + content: `集美们看过来!今天给大家带来${this.data.productName}的真实使用感受~ + +🎯第一印象: +收到货的那一刻就被惊艳到了!包装精美,细节满满,完全是高端货的质感。 + +💫使用体验: +用了几天下来,真的是越用越喜欢!质量很好,用起来特别顺手,完全就是我想要的样子! + +⭐️总结: +这个价位能买到这么好的产品,真的是太值了!强烈安利给大家,闭眼入不会错!`, + tags: ['真实测评', '使用心得', '好物安利', '值得入手'], + images: [ + 'https://picsum.photos/id/292/600/400', + 'https://picsum.photos/id/365/600/400', + 'https://picsum.photos/id/180/600/400' + ] + } + ]; + + const randomArticle = mockArticles[Math.floor(Math.random() * mockArticles.length)]; + + this.setData({ + article: randomArticle, + generating: false + }); + + wx.hideLoading(); + }, 2000); + }, + + // 删除图片 + deleteImage(e: any) { + const index = e.currentTarget.dataset.index; + const images = this.data.article.images; + images.splice(index, 1); + + this.setData({ + 'article.images': images + }); + }, + + // 返回上一页 + goBack() { + wx.navigateBack(); + }, + + // 重新生成文章 + regenerateArticle() { + if (this.data.generating) return; + this.generateArticle(); + }, + + // 发布文章 + async publishArticle() { + wx.showModal({ + title: '确认发布', + content: '确定要发布这篇种草文章吗?', + confirmText: '发布', + confirmColor: '#ff2442', + success: async (res) => { + if (res.confirm) { + // 如果是从领取的文案,调用后端API + if (this.data.isFromClaim && this.data.copyId) { + try { + const response = await EmployeeService.publish({ + copy_id: this.data.copyId, + title: this.data.article.title, + content: this.data.article.content, + publish_link: '', // 可以后续添加发布链接输入 + xhs_note_id: '' // 小红书笔记ID + }); + + if (response.code === 200) { + wx.showToast({ + title: '发布成功', + icon: 'success', + duration: 2000 + }); + + // 保存到本地 + const articles = wx.getStorageSync('myArticles') || []; + articles.unshift({ + id: (response.data && response.data.record_id) || Date.now(), + productName: this.data.productName, + title: this.data.article.title, + content: this.data.article.content, + tags: this.data.article.tags, + createTime: new Date().toISOString(), + status: 'published' + }); + wx.setStorageSync('myArticles', articles); + + // 2秒后返回首页 + setTimeout(() => { + wx.navigateBack(); + }, 2000); + } + } catch (error) { + console.error('发布失败:', error); + } + } else { + // 模拟发布(非领取文案的情况) + wx.showLoading({ + title: '发布中...', + mask: true + }); + + setTimeout(() => { + wx.hideLoading(); + wx.showToast({ + title: '发布成功', + icon: 'success', + duration: 2000 + }); + + // 保存到本地(模拟) + const articles = wx.getStorageSync('myArticles') || []; + articles.unshift({ + id: Date.now(), + productName: this.data.productName, + title: this.data.article.title, + content: this.data.article.content, + tags: this.data.article.tags, + createTime: new Date().toISOString(), + status: 'published' + }); + wx.setStorageSync('myArticles', articles); + + // 2秒后返回首页 + setTimeout(() => { + wx.navigateBack(); + }, 2000); + }, 1500); + } + } + } + }); + } +}); diff --git a/miniprogram/miniprogram/pages/article-generate/article-generate.wxml b/miniprogram/miniprogram/pages/article-generate/article-generate.wxml new file mode 100644 index 0000000..0809fb6 --- /dev/null +++ b/miniprogram/miniprogram/pages/article-generate/article-generate.wxml @@ -0,0 +1,52 @@ + + + + + + + + 生成具体内容 + + + + + + + + + + + + × + + + + + + {{article.title}} + + + + {{article.content}} + + + + + + + + + + diff --git a/miniprogram/miniprogram/pages/article-generate/article-generate.wxss b/miniprogram/miniprogram/pages/article-generate/article-generate.wxss new file mode 100644 index 0000000..f492cec --- /dev/null +++ b/miniprogram/miniprogram/pages/article-generate/article-generate.wxss @@ -0,0 +1,184 @@ +/* pages/article-generate/article-generate.wxss */ +page { + background: white; + height: 100%; +} + +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-bottom: calc(136rpx + env(safe-area-inset-bottom)); +} + +/* 页面头部 */ +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20rpx 30rpx; + background: white; + border-bottom: 1rpx solid #f0f0f0; +} + +.header-left, +.header-right { + width: 80rpx; +} + +.back-icon { + width: 40rpx; + height: 40rpx; + background-image: url('data:image/svg+xml;utf8,'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +.page-title { + font-size: 32rpx; + font-weight: 600; + color: #1a1a1a; +} + +/* 文章容器 */ +.article-container { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + background: white; +} + +.article-wrapper { + padding: 30rpx; +} + +/* 图片列表 */ +.article-images { + display: flex; + flex-wrap: nowrap; + gap: 16rpx; + margin-bottom: 30rpx; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.article-images::-webkit-scrollbar { + display: none; +} + +.image-item { + flex-shrink: 0; + width: 200rpx; + height: 200rpx; + position: relative; + border-radius: 12rpx; + overflow: hidden; +} + +.article-image { + width: 100%; + height: 100%; + background: #f5f5f5; +} + +.delete-icon { + position: absolute; + top: 8rpx; + right: 8rpx; + width: 40rpx; + height: 40rpx; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.delete-text { + font-size: 36rpx; + color: white; + line-height: 1; + font-weight: 300; +} + +/* 标题区域 */ +.article-header { + margin-bottom: 24rpx; +} + +.article-title { + font-size: 36rpx; + color: #1a1a1a; + font-weight: bold; + line-height: 1.4; +} + +/* 内容区域 */ +.article-content { + margin-bottom: 24rpx; +} + +.content-text { + font-size: 28rpx; + color: #333; + line-height: 1.8; + white-space: pre-line; +} + +/* 操作栏 */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + gap: 20rpx; + padding: 20rpx 30rpx; + background: white; + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08); + border-top: 1rpx solid #f0f0f0; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); +} + +.action-btn { + flex: 1; + height: 96rpx; + border: none; + border-radius: 48rpx; + font-size: 32rpx; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s; +} + +.action-btn::after { + border: none; +} + +.action-btn.secondary { + background: white; + color: #07c160; + border: 2rpx solid #07c160; +} + +.action-btn.secondary:active { + background: #f0f9f4; +} + +.action-btn.primary { + background: #07c160; + color: white; + box-shadow: 0 4rpx 16rpx rgba(7, 193, 96, 0.3); +} + +.action-btn.primary:active { + transform: scale(0.98); +} + +.btn-text { + font-size: 32rpx; +} diff --git a/miniprogram/miniprogram/pages/articles/articles.json b/miniprogram/miniprogram/pages/articles/articles.json new file mode 100644 index 0000000..df4088a --- /dev/null +++ b/miniprogram/miniprogram/pages/articles/articles.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "首页", + "usingComponents": {} +} diff --git a/miniprogram/miniprogram/pages/articles/articles.ts b/miniprogram/miniprogram/pages/articles/articles.ts new file mode 100644 index 0000000..0cd958b --- /dev/null +++ b/miniprogram/miniprogram/pages/articles/articles.ts @@ -0,0 +1,291 @@ +// pages/articles/articles.ts +import { formatDate, getStatusInfo, getChannelInfo, getCoverColor, getCoverIcon } from '../../utils/util' +import { EmployeeService } from '../../services/employee' + +Page({ + data: { + productId: 0, // 产品ID + productName: '', // 产品名称 + productImage: '', // 产品图片 + allCopies: [] as any[], // 所有文案 + currentIndex: 0, // 当前显示的文案索引 + currentCopy: null as any, // 当前显示的文案 + loading: false, + claiming: false, // 领取中 + publishing: false // 发布中 + }, + + onLoad(options: any) { + // 检查登录状态和Token + const token = wx.getStorageSync('token'); + if (!token) { + wx.showModal({ + title: '未登录', + content: '请先登录后再查看文案', + confirmText: '去登录', + success: (res) => { + if (res.confirm) { + wx.redirectTo({ + url: '/pages/login/login' + }); + } else { + wx.navigateBack(); + } + } + }); + return; + } + + // 检查小红书绑定状态 + this.checkXHSBinding().then(isBound => { + if (!isBound) { + wx.showModal({ + title: '未绑定小红书', + content: '查看文案前需要先绑定小红书账号', + confirmText: '去绑定', + success: (res) => { + if (res.confirm) { + wx.redirectTo({ + url: '/pages/profile/social-binding/social-binding' + }); + } else { + wx.navigateBack(); + } + } + }); + return; + } + + // 获取产品ID参数 + if (options.productId) { + this.setData({ + productId: parseInt(options.productId) + }); + } + + this.loadCopies(); + }); + }, + + // 检查小红书绑定状态 + async checkXHSBinding(): Promise { + try { + const response = await EmployeeService.getProfile(); + if (response.code === 200 && response.data) { + return response.data.is_bound_xhs === 1; + } + return false; + } catch (error) { + console.error('检查绑定状态失败:', error); + return false; + } + }, + + onShow() { + // 每次显示页面时刷新数据 + if (this.data.productId) { + this.loadCopies(); + } + }, + + // 加载文案列表 + async loadCopies() { + const productId = this.data.productId; + + if (!productId) { + wx.showToast({ + title: '请先选择产品', + icon: 'none' + }); + return; + } + + this.setData({ loading: true }); + + try { + // 从后端API获取可领取文案 + const response = await EmployeeService.getAvailableCopies(productId); + + console.log('===== 文案列表响应 ====='); + console.log('response:', response); + + if (response.code === 200 && response.data) { + const copies = response.data.copies || []; + const product = response.data.product || {}; + + console.log('文案数量:', copies.length); + if (copies.length > 0) { + console.log('第一篇文案:', copies[0]); + console.log('图片数量:', (copies[0].images && copies[0].images.length) || 0); + console.log('标签数量:', (copies[0].tags && copies[0].tags.length) || 0); + } + + this.setData({ + allCopies: copies, + productName: product.name || '', + productImage: product.image || '', + currentIndex: 0, + currentCopy: copies.length > 0 ? copies[0] : null, + loading: false + }); + + console.log('setData后的 currentCopy:', this.data.currentCopy); + + if (copies.length === 0) { + wx.showToast({ + title: '暂无可领取文案', + icon: 'none' + }); + } + } + } catch (error) { + console.error('加载文案失败:', error); + this.setData({ loading: false }); + wx.showToast({ + title: '加载文案失败', + icon: 'none' + }); + } + }, + + // 换一个文案 + changeArticle() { + if (this.data.claiming || this.data.publishing) return; + + const { allCopies, currentIndex } = this.data; + + if (allCopies.length === 0) { + wx.showToast({ + title: '暂无更多文案', + icon: 'none' + }); + return; + } + + // 切换到下一个文案 + const nextIndex = (currentIndex + 1) % allCopies.length; + + this.setData({ + currentIndex: nextIndex, + currentCopy: allCopies[nextIndex] + }); + + wx.showToast({ + title: `${nextIndex + 1}/${allCopies.length}`, + icon: 'none', + duration: 1000 + }); + }, + + // 一键发布(先领取,再发布) + async publishArticle() { + if (!this.data.currentCopy) { + wx.showToast({ + title: '请先选择文案', + icon: 'none' + }); + return; + } + + wx.showModal({ + title: '确认发布', + content: '确定要领取并发布这篇文案吗?', + confirmText: '发布', + confirmColor: '#07c160', + success: async (res) => { + if (res.confirm) { + await this.claimAndPublish(); + } + } + }); + }, + + // 领取并发布 + async claimAndPublish() { + this.setData({ claiming: true }); + + try { + // 1. 先领取文案 + const claimResponse = await EmployeeService.claimCopy( + this.data.currentCopy.id, + this.data.productId + ); + + if (claimResponse.code !== 200) { + throw new Error(claimResponse.message || '领取失败'); + } + + const claimId = (claimResponse.data && claimResponse.data.claim_id) || null; + const copyData = (claimResponse.data && claimResponse.data.copy) || null; + + this.setData({ claiming: false, publishing: true }); + + // 2. 再发布 + const publishResponse = await EmployeeService.publish({ + copy_id: this.data.currentCopy.id, + title: (copyData && copyData.title) || this.data.currentCopy.title, + content: (copyData && copyData.content) || this.data.currentCopy.content, + publish_link: '', + xhs_note_id: '' + }); + + if (publishResponse.code === 200) { + wx.showToast({ + title: '发布成功', + icon: 'success', + duration: 2000 + }); + + this.setData({ publishing: false }); + + // 2秒后返回 + setTimeout(() => { + wx.navigateBack(); + }, 2000); + } else { + throw new Error(publishResponse.message || '发布失败'); + } + } catch (error: any) { + console.error('发布失败:', error); + + this.setData({ + claiming: false, + publishing: false + }); + + wx.showToast({ + title: error.message || '操作失败', + icon: 'none' + }); + } + }, + + // 返回上一页 + goBack() { + wx.navigateBack(); + }, + + goToProfile() { + wx.redirectTo({ + url: '/pages/profile/profile' + }); + }, + + // 分享功能 + onShareAppMessage() { + const currentCopy = this.data.currentCopy; + return { + title: currentCopy ? currentCopy.title : '精彩种草文案', + path: `/pages/articles/articles?productId=${this.data.productId}`, + imageUrl: this.data.productImage || '' // 使用产品图片作为分享封面 + }; + }, + + // 分享到朋友圈 + onShareTimeline() { + return { + title: `${this.data.productName} - 精彩种草文案`, + imageUrl: this.data.productImage || '' + }; + } +}); diff --git a/miniprogram/miniprogram/pages/articles/articles.wxml b/miniprogram/miniprogram/pages/articles/articles.wxml new file mode 100644 index 0000000..2f27b8f --- /dev/null +++ b/miniprogram/miniprogram/pages/articles/articles.wxml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + {{currentCopy.title}} + + + + {{currentCopy.content}} + + + + + + + + + 📝 + 暂无可领取文案 + + + + + + 加载中... + + + + + + + + diff --git a/miniprogram/miniprogram/pages/articles/articles.wxss b/miniprogram/miniprogram/pages/articles/articles.wxss new file mode 100644 index 0000000..4b76531 --- /dev/null +++ b/miniprogram/miniprogram/pages/articles/articles.wxss @@ -0,0 +1,245 @@ +/* pages/articles/articles.wxss */ +page { + background: #f8f8f8; + height: 100%; +} + +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-bottom: calc(136rpx + env(safe-area-inset-bottom)); +} + +/* 文案容器 */ +.article-container { + flex: 1; + background: white; + overflow-y: auto; + overflow-x: hidden; +} + +.article-wrapper { + padding: 30rpx; +} + +/* 文章图片 - 3列网格布局 */ +.article-images { + margin-bottom: 24rpx; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12rpx; + box-sizing: border-box; + width: 100%; +} + +.image-item { + width: 100%; + padding-bottom: 100%; /* 1:1 比例 */ + position: relative; + border-radius: 8rpx; + overflow: hidden; + background: #f5f5f5; +} + +.article-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +/* 标签样式 */ +.article-tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + padding-bottom: 20rpx; + margin-bottom: 24rpx; +} + +.tag-item { + padding: 8rpx 20rpx; + background: linear-gradient(135deg, #e6f7ed 0%, #d0f0de 100%); + border-radius: 20rpx; + font-size: 24rpx; + color: #07c160; + font-weight: 500; +} + +.article-header { + margin-bottom: 24rpx; + padding-bottom: 24rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.article-title { + font-size: 36rpx; + color: #1a1a1a; + font-weight: bold; + line-height: 1.4; +} + +.article-content { + margin-bottom: 24rpx; +} + +.content-text { + font-size: 28rpx; + color: #333; + line-height: 1.8; + white-space: pre-line; +} + +.article-meta { + padding-top: 24rpx; + border-top: 1rpx solid #f0f0f0; + display: flex; + flex-direction: column; + gap: 12rpx; +} + +.meta-item { + display: flex; + align-items: center; + font-size: 26rpx; +} + +.meta-label { + color: #999; + min-width: 80rpx; +} + +.meta-value { + color: #333; + font-weight: 500; +} + +/* 空状态 */ +.empty-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 40rpx; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 24rpx; + opacity: 0.6; +} + +.empty-text { + font-size: 28rpx; + color: #999; + margin-bottom: 40rpx; +} + +.empty-btn { + padding: 20rpx 60rpx; + background: #07c160; + color: white; + border-radius: 50rpx; + font-size: 28rpx; + border: none; +} + +.empty-btn::after { + border: none; +} + +/* 加载状态 */ +.loading-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.loading-text { + font-size: 28rpx; + color: #999; +} + +/* 操作栏 */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + gap: 20rpx; + padding: 20rpx 30rpx; + background: white; + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08); + border-top: 1rpx solid #f0f0f0; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); +} + +.action-btn { + height: 80rpx; + border: none; + border-radius: 40rpx; + font-size: 28rpx; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; + transition: all 0.3s; +} + +.action-btn::after { + border: none; +} + +.action-btn.secondary { + flex: 0 0 180rpx; + background: #f5f5f5; + color: #333; +} + +.action-btn.secondary:active { + background: #e8e8e8; +} + +.action-btn.primary { + flex: 1; + background: #07c160; + color: white; +} + +.action-btn.primary:active { + opacity: 0.9; +} + +.action-btn[disabled] { + opacity: 0.6; +} + +.btn-icon { + width: 36rpx; + height: 36rpx; + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* 刷新图标 */ +.icon-refresh { + background-image: url('data:image/svg+xml;utf8,'); +} + +/* 确认图标 */ +.icon-check { + background-image: url('data:image/svg+xml;utf8,'); +} + +.btn-text { + font-size: 28rpx; +} diff --git a/miniprogram/miniprogram/pages/home/home.json b/miniprogram/miniprogram/pages/home/home.json new file mode 100644 index 0000000..4605b6d --- /dev/null +++ b/miniprogram/miniprogram/pages/home/home.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "商品选择", + "navigationBarBackgroundColor": "#07c160", + "navigationBarTextStyle": "white" +} diff --git a/miniprogram/miniprogram/pages/home/home.ts b/miniprogram/miniprogram/pages/home/home.ts new file mode 100644 index 0000000..28fc19e --- /dev/null +++ b/miniprogram/miniprogram/pages/home/home.ts @@ -0,0 +1,312 @@ +// pages/home/home.ts +import { EmployeeService, Product as ApiProduct } from '../../services/employee'; + +interface Product { + id: number; + name: string; + price: number; + sales: number; + image: string; + category: string; + tags: string[]; + hotLevel: number; // 1-5 热度等级 + description?: string; + available_copies?: number; +} + +interface Category { + id: string; + name: string; +} + +Page({ + data: { + selectedProduct: 0 as number, // 选中的商品ID + selectedProductName: '' as string, // 选中的商品名称 + products: [] as Product[], + hasMore: true, + loading: false, + page: 1, + pageSize: 6 + }, + + onLoad() { + // 不在onLoad检查登录,允许用户浏览首页 + this.loadProducts(); + }, + + // 加载商品列表 + async loadProducts() { + if (this.data.loading || !this.data.hasMore) return; + + this.setData({ loading: true }); + + try { + // 从后端API获取产品列表(公开接口,不需要登录) + const response = await EmployeeService.getProducts(); + + if (response.code === 200 && response.data) { + const apiProducts = response.data.list.map((product: ApiProduct, index: number) => ({ + id: product.id, + name: product.name, + price: 0, // 后端暂无价格字段 + sales: product.available_copies || 0, + image: product.image || `https://picsum.photos/id/${237 + index}/300/400`, + category: 'beauty', // 后端暂无分类字段 + tags: ['种草', '推荐'], + hotLevel: product.available_copies > 5 ? 5 : 3, + description: product.description, + available_copies: product.available_copies + })); + + this.setData({ + products: apiProducts, + loading: false, + hasMore: false + }); + return; + } + } catch (error) { + console.error('加载产品失败:', error); + // API失败,使用模拟数据 + } + + // 如果API失败,使用模拟数据 + const allMockProducts = [ + { + id: 1, + name: '兰蔻小黑瓶精华液', + price: 680, + sales: 10234, + image: 'https://picsum.photos/id/237/300/400', + category: 'beauty', + tags: ['美白', '抗老', '保湿'], + hotLevel: 5 + }, + { + id: 2, + name: 'SK-II神仙水', + price: 1299, + sales: 8765, + image: 'https://picsum.photos/id/152/300/400', + category: 'beauty', + tags: ['补水', '缩毛孔', '提亮'], + hotLevel: 5 + }, + { + id: 3, + name: '雅诗兰黛小棕瓶', + price: 790, + sales: 9432, + image: 'https://picsum.photos/id/292/300/400', + category: 'beauty', + tags: ['修复', '抗氧化', '紧致'], + hotLevel: 4 + }, + { + id: 4, + name: 'Dior烈艳蓝金口红', + price: 320, + sales: 15678, + image: 'https://picsum.photos/id/365/300/400', + category: 'beauty', + tags: ['段色', '滑顺', '持久'], + hotLevel: 5 + }, + { + id: 5, + name: 'AirPods Pro 2', + price: 1899, + sales: 23456, + image: 'https://picsum.photos/id/180/300/400', + category: 'digital', + tags: ['降噪', '音质', '舒适'], + hotLevel: 5 + }, + { + id: 6, + name: 'iPhone 15 Pro', + price: 7999, + sales: 34567, + image: 'https://picsum.photos/id/119/300/400', + category: 'digital', + tags: ['性能', '拍照', '长续航'], + hotLevel: 5 + }, + { + id: 7, + name: '海蓝之谜精华面霜', + price: 890, + sales: 7654, + image: 'https://picsum.photos/id/225/300/400', + category: 'beauty', + tags: ['保湿', '修复', '舒缓'], + hotLevel: 4 + }, + { + id: 8, + name: '迪奥烈影精华', + price: 1680, + sales: 5432, + image: 'https://picsum.photos/id/177/300/400', + category: 'beauty', + tags: ['抗衰老', '紧致', '提亮'], + hotLevel: 5 + }, + { + id: 9, + name: 'Zara复古风衣', + price: 299, + sales: 12345, + image: 'https://picsum.photos/id/111/300/400', + category: 'fashion', + tags: ['复古', '百搭', '时尚'], + hotLevel: 4 + }, + { + id: 10, + name: 'Uniqlo羊绒衫', + price: 199, + sales: 23456, + image: 'https://picsum.photos/id/222/300/400', + category: 'fashion', + tags: ['保暖', '舒适', '百搭'], + hotLevel: 5 + }, + { + id: 11, + name: '星巴克咖啡豆', + price: 88, + sales: 34567, + image: 'https://picsum.photos/id/431/300/400', + category: 'food', + tags: ['香浓', '精品', '中烘'], + hotLevel: 5 + }, + { + id: 12, + name: '三只松鼠零食礼盒', + price: 158, + sales: 19876, + image: 'https://picsum.photos/id/326/300/400', + category: 'food', + tags: ['零食', '礼盒', '美味'], + hotLevel: 4 + } + ]; + + // 模拟分页加载 + setTimeout(() => { + const start = (this.data.page - 1) * this.data.pageSize; + const end = start + this.data.pageSize; + const newProducts = allMockProducts.slice(start, end); + + this.setData({ + products: [...this.data.products, ...newProducts], + hasMore: end < allMockProducts.length, + loading: false, + page: this.data.page + 1 + }); + }, 800); + }, + + // 切换商品选中状态 + toggleProduct(e: any) { + const productId = e.currentTarget.dataset.id; + const productName = e.currentTarget.dataset.name; + + this.setData({ + selectedProduct: productId === this.data.selectedProduct ? 0 : productId, + selectedProductName: productId === this.data.selectedProduct ? '' : productName + }); + }, + + // 去生成内容 + async goToGenerate() { + if (!this.data.selectedProduct) { + wx.showToast({ + title: '请先选择商品', + icon: 'none' + }); + return; + } + // 1. 检查登录状态 + const token = wx.getStorageSync('token'); + if (!token) { + wx.showModal({ + title: '未登录', + content: '请先登录后再查看文案', + confirmText: '去登录', + success: (res) => { + if (res.confirm) { + wx.redirectTo({ + url: '/pages/login/login' + }); + } + } + }); + return; + } + + // 2. 检查小红书绑定状态 + try { + const response = await EmployeeService.getProfile(); + + if (response.code === 200 && response.data) { + const isBoundXHS = response.data.is_bound_xhs; + + if (!isBoundXHS) { + // 未绑定小红书,提示并跳转 + wx.showModal({ + title: '未绑定小红书', + content: '查看文案前需要先绑定小红书账号', + confirmText: '去绑定', + success: (res) => { + if (res.confirm) { + wx.navigateTo({ + url: '/pages/profile/social-binding/social-binding' + }); + } + } + }); + return; + } + + // 登录且已绑定,跳转到文案列表页面 + wx.navigateTo({ + url: `/pages/articles/articles?productId=${this.data.selectedProduct}` + }); + } else { + throw new Error('获取用户信息失败'); + } + } catch (error) { + console.error('检查绑定状态失败:', error); + wx.showToast({ + title: '获取用户信息失败', + icon: 'none' + }); + } + }, + + // 滚动到底部加载更多 + onScrollToLower() { + this.loadProducts(); + }, + + // 分享功能 + onShareAppMessage() { + return { + title: '万花筒AI助手 - 智能生成种草文案', + path: '/pages/home/home', + imageUrl: '' // 可以设置分享图片 + }; + }, + + // 分享到朋友圈 + onShareTimeline() { + return { + title: '万花筒AI助手 - 智能生成种草文案', + imageUrl: '' // 可以设置分享图片 + }; + } +}); diff --git a/miniprogram/miniprogram/pages/home/home.wxml b/miniprogram/miniprogram/pages/home/home.wxml new file mode 100644 index 0000000..da8b613 --- /dev/null +++ b/miniprogram/miniprogram/pages/home/home.wxml @@ -0,0 +1,54 @@ + + + + + 选择商品 + 运营提供 + + + + + + + + + + + + + {{item.name}} + + + + + + 加载更多... + + + 没有更多了 + + + + + + + + diff --git a/miniprogram/miniprogram/pages/home/home.wxss b/miniprogram/miniprogram/pages/home/home.wxss new file mode 100644 index 0000000..84d78bf --- /dev/null +++ b/miniprogram/miniprogram/pages/home/home.wxss @@ -0,0 +1,170 @@ +/* pages/home/home.wxss */ +page { + background: #f8f8f8; + height: 100%; +} + +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + background: white; +} + +/* 页面头部 */ +.page-header { + padding: 40rpx 30rpx 30rpx; + background: white; + border-bottom: 1rpx solid #f0f0f0; +} + +.page-title { + display: block; + font-size: 40rpx; + font-weight: bold; + color: #1a1a1a; + margin-bottom: 8rpx; +} + +.page-subtitle { + display: block; + font-size: 24rpx; + color: #999; +} + +/* 商品滚动容器 */ +.product-scroll { + flex: 1; + padding: 0 0 110rpx 0; + background: white; +} + +/* 商品网格布局 */ +.product-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20rpx; + padding: 20rpx; +} + +/* 商品卡片 */ +.product-card { + background: white; + border-radius: 12rpx; + overflow: hidden; + border: 2rpx solid #f0f0f0; + transition: all 0.3s; + position: relative; +} + +.product-card.selected { + border-color: #07c160; + box-shadow: 0 4rpx 16rpx rgba(7, 193, 96, 0.2); +} + +.product-image-wrapper { + width: 100%; + height: 330rpx; + background: #f5f5f5; + position: relative; + overflow: hidden; +} + +.product-image { + width: 100%; + height: 100%; +} + +/* 选中指示器 */ +.select-indicator { + position: absolute; + top: 12rpx; + right: 12rpx; + width: 48rpx; + height: 48rpx; + background: #07c160; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.4); +} + +.check-icon { + width: 24rpx; + height: 24rpx; + background-image: url('data:image/svg+xml;utf8,'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +.product-name { + display: block; + padding: 20rpx; + font-size: 28rpx; + color: #1a1a1a; + font-weight: 500; + line-height: 1.4; + height: 78rpx; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +/* 加载提示 */ +.loading-more, +.no-more { + text-align: center; + padding: 40rpx 0; +} + +.loading-text, +.no-more-text { + font-size: 24rpx; + color: #999; +} + +/* 底部操作栏 */ +.bottom-action { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 8rpx 30rpx; + background: white; + box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.05); + border-top: 1rpx solid #f0f0f0; + padding-bottom: calc(8rpx + env(safe-area-inset-bottom)); + display: flex; + align-items: center; +} + +.generate-btn { + width: 100%; + height: 80rpx; + background: linear-gradient(135deg, #07c160 0%, #06ad56 100%); + color: white; + border: none; + border-radius: 40rpx; + font-size: 30rpx; + font-weight: 500; + box-shadow: 0 2rpx 8rpx rgba(7, 193, 96, 0.25); + transition: all 0.3s; +} + +.generate-btn::after { + border: none; +} + +.generate-btn:active { + transform: scale(0.98); +} + +.generate-btn[disabled] { + background: #ddd; + color: #999; + box-shadow: none; +} diff --git a/miniprogram/miniprogram/pages/index/index.json b/miniprogram/miniprogram/pages/index/index.json new file mode 100644 index 0000000..b55b5a2 --- /dev/null +++ b/miniprogram/miniprogram/pages/index/index.json @@ -0,0 +1,4 @@ +{ + "usingComponents": { + } +} \ No newline at end of file diff --git a/miniprogram/miniprogram/pages/index/index.ts b/miniprogram/miniprogram/pages/index/index.ts new file mode 100644 index 0000000..c7aaf97 --- /dev/null +++ b/miniprogram/miniprogram/pages/index/index.ts @@ -0,0 +1,54 @@ +// index.ts +// 获取应用实例 +const app = getApp() +const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0' + +Component({ + data: { + motto: 'Hello World', + userInfo: { + avatarUrl: defaultAvatarUrl, + nickName: '', + }, + hasUserInfo: false, + canIUseGetUserProfile: wx.canIUse('getUserProfile'), + canIUseNicknameComp: wx.canIUse('input.type.nickname'), + }, + methods: { + // 事件处理函数 + bindViewTap() { + wx.navigateTo({ + url: '../logs/logs', + }) + }, + onChooseAvatar(e: any) { + const { avatarUrl } = e.detail + const { nickName } = this.data.userInfo + this.setData({ + "userInfo.avatarUrl": avatarUrl, + hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl, + }) + }, + onInputChange(e: any) { + const nickName = e.detail.value + const { avatarUrl } = this.data.userInfo + this.setData({ + "userInfo.nickName": nickName, + hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl, + }) + }, + getUserProfile() { + // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗 + wx.getUserProfile({ + desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写 + success: (res) => { + console.log(res) + this.setData({ + userInfo: res.userInfo, + hasUserInfo: true + }) + } + }) + }, + }, +}) diff --git a/miniprogram/miniprogram/pages/index/index.wxml b/miniprogram/miniprogram/pages/index/index.wxml new file mode 100644 index 0000000..0721ba0 --- /dev/null +++ b/miniprogram/miniprogram/pages/index/index.wxml @@ -0,0 +1,27 @@ + + + + + + + + 昵称 + + + + + + 请使用2.10.4及以上版本基础库 + + + + {{userInfo.nickName}} + + + + {{motto}} + + + diff --git a/miniprogram/miniprogram/pages/index/index.wxss b/miniprogram/miniprogram/pages/index/index.wxss new file mode 100644 index 0000000..1ebed4b --- /dev/null +++ b/miniprogram/miniprogram/pages/index/index.wxss @@ -0,0 +1,62 @@ +/**index.wxss**/ +page { + height: 100vh; + display: flex; + flex-direction: column; +} +.scrollarea { + flex: 1; + overflow-y: hidden; +} + +.userinfo { + display: flex; + flex-direction: column; + align-items: center; + color: #aaa; + width: 80%; +} + +.userinfo-avatar { + overflow: hidden; + width: 128rpx; + height: 128rpx; + margin: 20rpx; + border-radius: 50%; +} + +.usermotto { + margin-top: 200px; +} + +.avatar-wrapper { + padding: 0; + width: 56px !important; + border-radius: 8px; + margin-top: 40px; + margin-bottom: 40px; +} + +.avatar { + display: block; + width: 56px; + height: 56px; +} + +.nickname-wrapper { + display: flex; + width: 100%; + padding: 16px; + box-sizing: border-box; + border-top: .5px solid rgba(0, 0, 0, 0.1); + border-bottom: .5px solid rgba(0, 0, 0, 0.1); + color: black; +} + +.nickname-label { + width: 105px; +} + +.nickname-input { + flex: 1; +} diff --git a/miniprogram/miniprogram/pages/login/login.json b/miniprogram/miniprogram/pages/login/login.json new file mode 100644 index 0000000..7405de4 --- /dev/null +++ b/miniprogram/miniprogram/pages/login/login.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "万花筒AI助手", + "navigationBarBackgroundColor": "#07c160", + "navigationBarTextStyle": "white" +} diff --git a/miniprogram/miniprogram/pages/login/login.ts b/miniprogram/miniprogram/pages/login/login.ts new file mode 100644 index 0000000..b173485 --- /dev/null +++ b/miniprogram/miniprogram/pages/login/login.ts @@ -0,0 +1,145 @@ +// pages/login/login.ts +import { post } from '../../utils/request'; +import { API } from '../../config/api'; + +Page({ + data: { + loginLoading: false, + agreed: false + }, + + onLoad() { + // 检查是否已登录(检查token) + const token = wx.getStorageSync('token'); + if (token) { + wx.switchTab({ + url: '/pages/home/home' + }); + } + }, + + // 同意协议 + onAgreeChange(e: any) { + this.setData({ + agreed: e.detail.value.length > 0 + }); + }, + + // 跳转到用户协议 + goToUserAgreement() { + wx.navigateTo({ + url: '/pages/agreement/user-agreement/user-agreement' + }); + }, + + // 跳转到隐私政策 + goToPrivacyPolicy() { + wx.navigateTo({ + url: '/pages/agreement/privacy-policy/privacy-policy' + }); + }, + + // 未同意协议时点击登录 + handleAgreeFirst() { + if (this.data.loginLoading) return + + // 弹窗提示用户同意协议 + wx.showModal({ + title: '用户协议', + content: '请阅读并同意《用户协议》和《隐私政策》后再登录', + confirmText: '同意', + cancelText: '不同意', + success: (res) => { + if (res.confirm) { + // 用户点击了同意,勾选协议 + this.setData({ agreed: true }); + } + } + }); + }, + + // 微信登录(已同意协议后触发) + handleWechatLogin(e: any) { + if (this.data.loginLoading) return + + // 检查用户是否授权了手机号 + if (e.detail.errMsg && e.detail.errMsg !== 'getPhoneNumber:ok') { + // 用户拒绝授权手机号 + console.log('用户拒绝授权手机号:', e.detail.errMsg); + wx.showToast({ + title: '需要授权手机号才能登录', + icon: 'none', + duration: 2000 + }); + return; + } + + // 用户已授权手机号,直接登录 + this.performLogin(e.detail); + }, + + // 执行登录逻辑 + async performLogin(phoneDetail?: any) { + + this.setData({ loginLoading: true }); + + try { + // 1. 调用微信登录获取code + const loginRes = await wx.login(); + + if (!loginRes.code) { + throw new Error('获取微信code失败'); + } + + // 2. 处理手机号(如果用户授权了) + let phone = ''; + if (phoneDetail && phoneDetail.code) { + // 用户授权了手机号,将 code 发送给后端解密 + phone = phoneDetail.code; // 这是加密的code,需要后端解密 + } else if (phoneDetail && phoneDetail.errMsg && phoneDetail.errMsg !== 'getPhoneNumber:ok') { + // 用户拒绝授权手机号 + console.log('用户拒绝授权手机号'); + } + + // 3. 调用后端登录API + const response = await post(API.auth.wechatLogin, { + code: loginRes.code, + phone_code: phone // 将手机号code发送给后端 + }, false); + + if (response.code === 200 && response.data) { + // 保存token + wx.setStorageSync('token', response.data.token); + + // 保存员工信息 + if (response.data.employee) { + wx.setStorageSync('employeeInfo', response.data.employee); + wx.setStorageSync('username', response.data.employee.name); + } + + wx.showToast({ + title: '登录成功', + icon: 'success', + duration: 1500 + }); + + setTimeout(() => { + wx.switchTab({ + url: '/pages/home/home' + }); + }, 1500); + } else { + throw new Error(response.message || '登录失败'); + } + } catch (error: any) { + console.error('登录失败:', error); + + this.setData({ loginLoading: false }); + + wx.showToast({ + title: error.errMsg || error.message || '登录失败', + icon: 'none' + }); + } + } +}) diff --git a/miniprogram/miniprogram/pages/login/login.wxml b/miniprogram/miniprogram/pages/login/login.wxml new file mode 100644 index 0000000..d781b11 --- /dev/null +++ b/miniprogram/miniprogram/pages/login/login.wxml @@ -0,0 +1,58 @@ + + diff --git a/miniprogram/miniprogram/pages/login/login.wxss b/miniprogram/miniprogram/pages/login/login.wxss new file mode 100644 index 0000000..68b0c28 --- /dev/null +++ b/miniprogram/miniprogram/pages/login/login.wxss @@ -0,0 +1,236 @@ +/* pages/login/login.wxss */ +page { + height: 100vh; + background: #07c160; + overflow: hidden; + position: relative; +} + +page::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); + animation: rotate 30s linear infinite; +} + +@keyframes rotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.login-container { + display: flex; + flex-direction: column; + min-height: 100vh; + background: transparent; + position: relative; + z-index: 1; +} + +/* 顶部Logo区域 */ +.logo-section { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 60rpx; +} + +.logo-box { + width: 160rpx; + height: 160rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 32rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40rpx; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15); + backdrop-filter: blur(10rpx); + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10rpx); + } +} + +.logo-icon { + width: 80rpx; + height: 80rpx; + background: white; + border-radius: 16rpx; +} + +.app-name { + font-size: 48rpx; + font-weight: bold; + color: white; + margin-bottom: 16rpx; + letter-spacing: 2rpx; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15); +} + +.app-slogan { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.9); + letter-spacing: 1rpx; + text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); +} + +/* 登录区域 - 毛玻璃效果 */ +.login-section { + background: rgba(255, 255, 255, 0.85); + border-radius: 40rpx 40rpx 0 0; + padding: 60rpx 40rpx 80rpx; + box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.15); + backdrop-filter: blur(40rpx); + -webkit-backdrop-filter: blur(40rpx); + position: relative; + overflow: hidden; + border: 1rpx solid rgba(255, 255, 255, 0.3); +} + +.login-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2rpx; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent); +} + +.welcome-text { + margin-bottom: 60rpx; +} + +.welcome-title { + display: block; + font-size: 40rpx; + font-weight: bold; + color: #1a1a1a; + margin-bottom: 12rpx; +} + +.welcome-subtitle { + display: block; + font-size: 28rpx; + color: rgba(0, 0, 0, 0.5); +} + +.login-btn { + width: 80%; + max-width: 600rpx; + background: linear-gradient(135deg, #07c160 0%, #2dd573 100%); + color: white; + border: none; + border-radius: 48rpx; + padding: 36rpx; + font-size: 32rpx; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.25); + transition: all 0.3s; + margin: 0 auto 32rpx; +} + +.login-btn::after { + border: none; +} + +.btn-hover { + opacity: 0.9; + transform: translateY(2rpx); + box-shadow: 0 4rpx 16rpx rgba(7, 193, 96, 0.2); +} + +.login-btn.loading { + opacity: 0.7; + pointer-events: none; +} + +.btn-icon { + margin-right: 12rpx; + position: relative; + z-index: 1; +} + +.wechat-icon { + width: 44rpx; + height: 44rpx; + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTcuNSAxMi41QzExLjE1IDEyLjUgNiAxNy4xIDYgMjIuOEM2IDI2LjEgNy42NSAyOSAxMC4yIDMxTDkgMzUuNUwxMy41IDMzLjJDMTQuOCAzMy43IDE2LjEgMzQgMTcuNSAzNEMyMy44NSAzNCAyOSAyOS40IDI5IDIzLjdDMjkgMTguMSAyMy44NSAxMi41IDE3LjUgMTIuNVpNMTQuNSAyNkMxMy4xMiAyNiAxMiAyNC44OCAxMiAyMy41QzEyIDIyLjEyIDEzLjEyIDIxIDE0LjUgMjFDMTUuODggMjEgMTcgMjIuMTIgMTcgMjMuNUMxNyAyNC44OCAxNS44OCAyNiAxNC41IDI2Wk0yMC41IDI2QzE5LjEyIDI2IDE4IDI0Ljg4IDE4IDIzLjVDMTggMjIuMTIgMTkuMTIgMjEgMjAuNSAyMUMyMS44OCAyMSAyMyAyMi4xMiAyMyAyMy41QzIzIDI0Ljg4IDIxLjg4IDI2IDIwLjUgMjZaTTMxLjUgMjJDMzEuMiAyMiAzMC45IDIyIDMwLjYgMjIuMUMzMS41IDE5LjkgMzEuOCAxNy41IDMxLjMgMTUuMkMzNy4yIDE2LjYgNDEuNSAyMS4yIDQxLjUgMjYuN0M0MS41IDI5LjMgNDAuMiAzMS43IDM4LjEgMzMuNEwzOSAzNy41TDM1LjEgMzUuNkMzNCAxNi4wNSAzMi4yNSAzNi40IDMxLjUgMzYuNEMyNi4xNSAzNi40IDIxLjcgMzIuNSAyMS43IDI3LjdDMjEuNyAyNC40IDI0IDIxLjYgMjcuNSAyMC4xQzI4LjcgMjAuOSAzMC4xIDIxLjQgMzEuNSAyMS40VjIyWk0yNy41IDMwQzI2LjEyIDMwIDI1IDI4Ljg4IDI1IDI3LjVDMjUgMjYuMTIgMjYuMTIgMjUgMjcuNSAyNUMyOC44OCAyNSAzMCAyNi4xMiAzMCAyNy41QzMwIDI4Ljg4IDI4Ljg4IDMwIDI3LjUgMzBaTTM1LjUgMzBDMzQuMTIgMzAgMzMgMjguODggMzMgMjcuNUMzMyAyNi4xMiAzNC4xMiAyNSAzNS41IDI1QzM2Ljg4IDI1IDM4IDI2LjEyIDM4IDI3LjVDMzggMjguODggMzYuODggMzAgMzUuNSAzMFoiIGZpbGw9IndoaXRlIi8+PC9zdmc+'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +.btn-text { + position: relative; + z-index: 1; +} + +.login-tip { + margin-top: 40rpx; + font-size: 24rpx; + text-align: center; + color: #999; + line-height: 1.6; +} + +.tip-text { + color: #999; +} + +.tip-link { + color: #07c160; + font-weight: 500; +} + +/* 协议同意区域 */ +.agreement-section { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.agreement-label { + display: flex; + align-items: center; + gap: 12rpx; +} + +checkbox { + transform: scale(0.7); + margin: 0; + padding: 0; + flex-shrink: 0; +} + +.agreement-text { + flex: 1; + font-size: 24rpx; + line-height: 1.6; + color: rgba(0, 0, 0, 0.6); +} + +.normal-text { + color: rgba(0, 0, 0, 0.6); +} + +.link-text { + color: #07c160; + font-weight: 500; +} diff --git a/miniprogram/miniprogram/pages/logs/logs.json b/miniprogram/miniprogram/pages/logs/logs.json new file mode 100644 index 0000000..b55b5a2 --- /dev/null +++ b/miniprogram/miniprogram/pages/logs/logs.json @@ -0,0 +1,4 @@ +{ + "usingComponents": { + } +} \ No newline at end of file diff --git a/miniprogram/miniprogram/pages/logs/logs.ts b/miniprogram/miniprogram/pages/logs/logs.ts new file mode 100644 index 0000000..dba5c0a --- /dev/null +++ b/miniprogram/miniprogram/pages/logs/logs.ts @@ -0,0 +1,21 @@ +// logs.ts +// const util = require('../../utils/util.js') +import { formatTime } from '../../utils/util' + +Component({ + data: { + logs: [], + }, + lifetimes: { + attached() { + this.setData({ + logs: (wx.getStorageSync('logs') || []).map((log: string) => { + return { + date: formatTime(new Date(log)), + timeStamp: log + } + }), + }) + } + }, +}) diff --git a/miniprogram/miniprogram/pages/logs/logs.wxml b/miniprogram/miniprogram/pages/logs/logs.wxml new file mode 100644 index 0000000..85cf1bf --- /dev/null +++ b/miniprogram/miniprogram/pages/logs/logs.wxml @@ -0,0 +1,6 @@ + + + + {{index + 1}}. {{log.date}} + + diff --git a/miniprogram/miniprogram/pages/logs/logs.wxss b/miniprogram/miniprogram/pages/logs/logs.wxss new file mode 100644 index 0000000..33f9d9e --- /dev/null +++ b/miniprogram/miniprogram/pages/logs/logs.wxss @@ -0,0 +1,16 @@ +page { + height: 100vh; + display: flex; + flex-direction: column; +} +.scrollarea { + flex: 1; + overflow-y: hidden; +} +.log-item { + margin-top: 20rpx; + text-align: center; +} +.log-item:last-child { + padding-bottom: env(safe-area-inset-bottom); +} diff --git a/miniprogram/miniprogram/pages/profile/about/about.json b/miniprogram/miniprogram/pages/profile/about/about.json new file mode 100644 index 0000000..9854f02 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/about/about.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "关于我们", + "navigationBarBackgroundColor": "#ff2442", + "navigationBarTextStyle": "white" +} diff --git a/miniprogram/miniprogram/pages/profile/about/about.ts b/miniprogram/miniprogram/pages/profile/about/about.ts new file mode 100644 index 0000000..81e1f6e --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/about/about.ts @@ -0,0 +1,10 @@ +// pages/profile/about/about.ts +Page({ + data: { + + }, + + onLoad() { + + } +}); diff --git a/miniprogram/miniprogram/pages/profile/about/about.wxml b/miniprogram/miniprogram/pages/profile/about/about.wxml new file mode 100644 index 0000000..043b639 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/about/about.wxml @@ -0,0 +1,45 @@ + + + + + AI文章审核平台 + v1.0.0 + + + + + 产品介绍 + AI文章审核平台是一款智能内容生成与审核管理系统,通过AI技术帮助用户高效管理文章内容,提升内容审核效率。 + + + + 联系我们 + + 客服邮箱: + support@example.com + + + 客服电话: + 400-888-8888 + + + 工作时间: + 周一至周五 9:00-18:00 + + + + + 更新日志 + + v1.0.0 + 2024-12-05 + · 初始版本发布\n· 支持文章管理和审核\n· 优化用户体验 + + + + + + © 2024 AI文章审核平台 + All Rights Reserved + + diff --git a/miniprogram/miniprogram/pages/profile/about/about.wxss b/miniprogram/miniprogram/pages/profile/about/about.wxss new file mode 100644 index 0000000..689f33b --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/about/about.wxss @@ -0,0 +1,171 @@ +/* pages/profile/about/about.wxss */ +page { + background: #f8f8f8; + height: 100%; + width: 100%; +} + +.page-container { + min-height: 100vh; + width: 100%; + padding-bottom: 60rpx; + box-sizing: border-box; + overflow-x: hidden; +} + +.logo-section { + width: 100%; + background: #07c160; + padding: 80rpx 30rpx; + display: flex; + flex-direction: column; + align-items: center; + box-sizing: border-box; +} + +.app-logo { + width: 160rpx; + height: 160rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; + backdrop-filter: blur(10rpx); + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15); + position: relative; + margin-bottom: 30rpx; +} + +.app-logo::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80rpx; + height: 80rpx; + background: white; + border-radius: 50%; +} + +.app-logo::after { + content: ''; + position: absolute; + width: 32rpx; + height: 32rpx; + background: #07c160; + border-radius: 50%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.app-name { + font-size: 40rpx; + font-weight: bold; + color: white; + margin-bottom: 16rpx; +} + +.app-version { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.8); +} + +.info-section { + width: 100%; + padding: 20rpx 30rpx; + box-sizing: border-box; +} + +.info-card { + width: 100%; + background: white; + border-radius: 16rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); + box-sizing: border-box; + word-wrap: break-word; + word-break: break-all; +} + +.card-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 2rpx solid #f0f0f0; +} + +.card-content { + font-size: 28rpx; + color: #666; + line-height: 1.8; + word-wrap: break-word; + word-break: break-all; +} + +.contact-item { + display: flex; + margin-bottom: 16rpx; + word-wrap: break-word; + word-break: break-all; +} + +.contact-item:last-child { + margin-bottom: 0; +} + +.contact-label { + font-size: 28rpx; + color: #999; + min-width: 140rpx; +} + +.contact-value { + font-size: 28rpx; + color: #333; + word-wrap: break-word; + word-break: break-all; + flex: 1; +} + +.log-item { + display: flex; + flex-direction: column; +} + +.log-version { + font-size: 30rpx; + font-weight: 600; + color: #07c160; + margin-bottom: 8rpx; +} + +.log-date { + font-size: 24rpx; + color: #999; + margin-bottom: 16rpx; +} + +.log-content { + font-size: 28rpx; + color: #666; + line-height: 1.8; + white-space: pre-line; +} + +.footer-section { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + padding: 40rpx 30rpx; + box-sizing: border-box; +} + +.copyright { + font-size: 24rpx; + color: #999; + line-height: 1.6; +} diff --git a/miniprogram/miniprogram/pages/profile/article-detail/article-detail.json b/miniprogram/miniprogram/pages/profile/article-detail/article-detail.json new file mode 100644 index 0000000..2051df5 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/article-detail/article-detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "文章详情", + "usingComponents": {} +} diff --git a/miniprogram/miniprogram/pages/profile/article-detail/article-detail.ts b/miniprogram/miniprogram/pages/profile/article-detail/article-detail.ts new file mode 100644 index 0000000..8694d43 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/article-detail/article-detail.ts @@ -0,0 +1,114 @@ +// pages/profile/article-detail/article-detail.ts +import { EmployeeService } from '../../../services/employee'; + +Page({ + data: { + articleId: 0, + productName: '', + article: { + title: '', + content: '', + topic: '', + publishLink: '', // 小红书链接 + images: [] as Array<{ + id: number; + image_url: string; + image_thumb_url: string; + sort_order: number; + keywords_name: string; + }>, + tags: [] as string[] + }, + loading: true + }, + + onLoad(options: any) { + if (options.id) { + this.setData({ + articleId: parseInt(options.id) + }); + this.loadArticleDetail(); + } + }, + + async loadArticleDetail() { + try { + // 调用专门的详情API + const response = await EmployeeService.getPublishRecordDetail(this.data.articleId); + + console.log('=== 详情响应 ===', response); + console.log('response.data:', response.data); + + if (response.code === 200 && response.data) { + console.log('product_name:', response.data.product_name); + console.log('title:', response.data.title); + console.log('content:', response.data.content); + console.log('images:', response.data.images); + console.log('tags:', response.data.tags); + console.log('topic:', response.data.topic); + + this.setData({ + productName: response.data.product_name || '未知商品', + article: { + title: response.data.title || '', + content: response.data.content || '', + topic: response.data.topic || '', + publishLink: response.data.publish_link || '', // 获取小红书链接 + images: response.data.images || [], // 获取图片列表 + tags: response.data.tags || [] // 获取标签列表 + }, + loading: false + }); + + console.log('=== 设置后的数据 ==='); + console.log('productName:', this.data.productName); + console.log('article:', this.data.article); + } else { + throw new Error(response.message || '加载失败'); + } + } catch (error: any) { + console.error('加载文章详情失败:', error); + this.setData({ loading: false }); + + wx.showToast({ + title: error.message || '加载失败', + icon: 'none' + }); + + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } + }, + + // 分享链接(自动复制) + shareArticle() { + const publishLink = this.data.article.publishLink; + + if (!publishLink) { + wx.showToast({ + title: '暂无发布链接', + icon: 'none' + }); + return; + } + + // 复制到剪贴板 + wx.setClipboardData({ + data: publishLink, + success: () => { + wx.showToast({ + title: '链接已复制', + icon: 'success', + duration: 2000 + }); + }, + fail: () => { + wx.showToast({ + title: '复制失败', + icon: 'none' + }); + } + }); + } +}); diff --git a/miniprogram/miniprogram/pages/profile/article-detail/article-detail.wxml b/miniprogram/miniprogram/pages/profile/article-detail/article-detail.wxml new file mode 100644 index 0000000..616e585 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/article-detail/article-detail.wxml @@ -0,0 +1,78 @@ + + + + + 选中商品 + {{productName}} + + + + + 加载中... + + + + + + + {{article.title}} + + + + {{article.content}} + + + + + + + + + + + + + + + + + + + + 小红书链接 + + + {{article.publishLink}} + + + + + + + + + + diff --git a/miniprogram/miniprogram/pages/profile/article-detail/article-detail.wxss b/miniprogram/miniprogram/pages/profile/article-detail/article-detail.wxss new file mode 100644 index 0000000..ae51fbe --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/article-detail/article-detail.wxss @@ -0,0 +1,254 @@ +/* pages/profile/article-detail/article-detail.wxss */ +page { + background: #f8f8f8; + height: 100%; + width: 100%; + overflow-x: hidden; /* 防止横向滚动 */ +} + +.page-container { + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding-bottom: 120rpx; /* 为底部操作栏留出空间 */ + overflow-x: hidden; /* 防止横向滚动 */ +} + +/* 商品卡片 */ +.product-card { + background: linear-gradient(135deg, #ff2442 0%, #ff6b8b 100%); + border-radius: 16rpx; + padding: 30rpx; + margin: 20rpx; + box-shadow: 0 4rpx 16rpx rgba(255, 36, 66, 0.2); + box-sizing: border-box; + width: calc(100% - 40rpx); /* 确保不超出屏幕 */ +} + +.card-title { + display: block; + font-size: 24rpx; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 12rpx; +} + +.product-name { + display: block; + font-size: 32rpx; + color: white; + font-weight: bold; +} + +/* 加载状态 */ +.loading-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.loading-text { + font-size: 28rpx; + color: #999; +} + +/* 文章容器 */ +.article-container { + flex: 1; + background: white; + border-radius: 16rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); + margin: 0 20rpx 20rpx 20rpx; + overflow-y: auto; + overflow-x: hidden; + box-sizing: border-box; + width: calc(100% - 40rpx); /* 确保不超出屏幕 */ +} + +.article-wrapper { + padding: 30rpx; + box-sizing: border-box; + width: 100%; + overflow-x: hidden; /* 防止内容溢出 */ +} + +.article-header { + margin-bottom: 24rpx; + padding-bottom: 24rpx; + border-bottom: 1rpx solid #f0f0f0; + box-sizing: border-box; + width: 100%; +} + +.article-title { + font-size: 36rpx; + color: #1a1a1a; + font-weight: bold; + line-height: 1.4; + word-wrap: break-word; /* 允许换行 */ + word-break: break-word; /* 允许换行 */ +} + +.article-content { + margin-bottom: 24rpx; + box-sizing: border-box; + width: 100%; +} + +.content-text { + font-size: 28rpx; + color: #333; + line-height: 1.8; + white-space: pre-line; + word-wrap: break-word; /* 允许换行 */ + word-break: break-word; /* 允许换行 */ +} + +/* 文章图片 */ +.article-images { + margin: 24rpx 0; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12rpx; + box-sizing: border-box; + width: 100%; +} + +.image-item { + width: 100%; + padding-bottom: 100%; /* 1:1 比例 */ + position: relative; + border-radius: 8rpx; + overflow: hidden; + background: #f5f5f5; +} + +.article-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +/* 标签 */ +.article-tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + padding-bottom: 20rpx; + margin-bottom: 24rpx; + border-bottom: 1rpx solid #f0f0f0; + box-sizing: border-box; + width: 100%; +} + +.tag-item { + padding: 4rpx 16rpx; + background: linear-gradient(135deg, #fff5f7 0%, #ffe8ec 100%); + border-radius: 16rpx; + font-size: 24rpx; + color: #ff2442; + font-weight: 500; + line-height: 1.5; + word-wrap: break-word; /* 允许换行 */ +} + +/* 小红书链接区域 */ +.xhs-link-section { + background: #f8f9fa; + border-radius: 12rpx; + padding: 24rpx; + margin-top: 24rpx; + box-sizing: border-box; + width: 100%; + overflow-x: hidden; /* 防止链接超出 */ +} + +.link-label { + display: flex; + align-items: center; + gap: 8rpx; + margin-bottom: 12rpx; + box-sizing: border-box; +} + +.link-icon { + font-size: 28rpx; + flex-shrink: 0; /* 防止图标被压缩 */ + color: #ff2442; +} + +.link-text { + font-size: 26rpx; + color: #666; + font-weight: 500; + flex-shrink: 0; /* 防止文字被压缩 */ +} + +.link-value { + font-size: 24rpx; + color: #ff2442; + word-break: break-all; /* 强制换行 */ + line-height: 1.6; + overflow-wrap: break-word; /* 允许换行 */ + max-width: 100%; /* 确保不超出 */ + box-sizing: border-box; +} + +/* 底部操作栏 */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + padding: 20rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08); + z-index: 999; /* 提高层级 */ +} + +.share-btn { + width: 100%; + height: 88rpx; + background: linear-gradient(135deg, #ff2442 0%, #ff6b8b 100%); + border-radius: 44rpx; + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + border: none; + box-shadow: 0 6rpx 20rpx rgba(255, 36, 66, 0.3); + transition: all 0.3s; +} + +.share-btn::after { + border: none; +} + +.share-btn:active { + transform: scale(0.98); + opacity: 0.9; +} + +.btn-icon { + width: 36rpx; + height: 36rpx; + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +.icon-share { + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4IDhDMTkuNjU2OSA4IDIxIDYuNjU2ODUgMjEgNUMyMSAzLjM0MzE1IDE5LjY1NjkgMiAxOCAyQzE2LjM0MzEgMiAxNSAzLjM0MzE1IDE1IDVDMTUgNS4xMjk3MSAxNS4wMDk1IDUuMjU3MjMgMTUuMDI3NyA1LjM4MTk3TDguODg1OTMgOC43ODc0NUM4LjQzMzQ1IDguMjg5NTkgNy43OTQzNSA4IDcuMDggOCA1LjQyMzE1IDggNCAxMC42NTY5IDQgMTJDNCAxMi44NjkgNC4zNzcwNiAxMy42NDU5IDQuOTY5NTggMTQuMTk5OEw0Ljk3MTIzIDE0LjE5ODdDNC45ODA3NCAxNC4yMDQ1IDQuOTkwNDkgMTQuMjEgNSAxNC4yMTU0QzUuMzQ5MDUgMTQuNDk5MyA1Ljc1MzggMTQuNzE4NSA2LjE4OTggMTQuODUyNkM2LjQ0ODcxIDE0Ljk0NTYgNi43MjQzOCAxNSA3LjA4IDE1QzcuNzk0MzUgMTUgOC40MzM0NSAxNC43MTA0IDguODg1OTMgMTQuMjEyNUwxNS4wMjc3IDE3LjYxOEMxNS4wMDk1IDE3Ljc0MjggMTUgMTcuODcwMyAxNSAxOUMxNSAyMC42NTY5IDE2LjM0MzEgMjIgMTggMjJDMTkuNjU2OSAyMiAyMSAyMC42NTY5IDIxIDE5QzIxIDE3LjM0MzEgMTkuNjU2OSAxNiAxOCAxNkMxNy4yNDkyIDE2IDE2LjU3NTYgMTYuMjk5IDE2LjA5MjggMTYuNzg2OUw5Ljk3MTIzIDE0LjE5ODdDOS45ODA3NCAxNC4yMDQ1IDkuOTkwNDkgMTQuMjEgMTAgMTQuMjE1NEMxMC4zNDkgMTQuNDk5MyAxMC43NTM4IDE0LjcxODUgMTEuMTg5OCAxNC44NTI2QzExLjQ0ODcgMTQuOTQ1NiAxMS43MjQ0IDE1IDEyLjA4IDE1QzEyLjc5NDQgMTUgMTMuNDMzNCAxNC43MTA0IDEzLjg4NTkgMTQuMjEyNUwxOS4wMjc3IDE3LjYxOEMxOS4wMDk1IDE3Ljc0MjggMTkgMTcuODcwMyAxOSAxOUMxOSAyMC42NTY5IDIwLjM0MzEgMjIgMjIgMjJDMjMuNjU2OSAyMiAyNSAyMC42NTY5IDI1IDE5QzI1IDE3LjM0MzEgMjMuNjU2OSAxNiAyMiAxNkMyMS4yNDkyIDE2IDIwLjU3NTYgMTYuMjk5IDIwLjA5MjggMTYuNzg2OUwxNC45NzEyIDE0LjE5ODdDMTQuOTgwNyAxNC4yMDQ1IDE0Ljk5MDUgMTQuMjEgMTUgMTQuMjE1NFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='); +} + +.btn-text { + font-size: 32rpx; + color: white; + font-weight: 600; +} diff --git a/miniprogram/miniprogram/pages/profile/env-switch/env-switch.json b/miniprogram/miniprogram/pages/profile/env-switch/env-switch.json new file mode 100644 index 0000000..0b3a051 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/env-switch/env-switch.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "环境切换", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white" +} diff --git a/miniprogram/miniprogram/pages/profile/env-switch/env-switch.wxml b/miniprogram/miniprogram/pages/profile/env-switch/env-switch.wxml new file mode 100644 index 0000000..61c6480 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/env-switch/env-switch.wxml @@ -0,0 +1,62 @@ + + + + + 当前环境 + + {{currentEnv === 'dev' ? '开发环境' : currentEnv === 'test' ? '测试环境' : '生产环境'}} + + + + + + + + + + {{item.name}} + + + 当前 + 切换 + + + + + + 主服务: + + {{configs[item.key].baseURL}} + + + + Python: + + {{configs[item.key].pythonURL}} + + + + + + + + + ⚠️ 温馨提示 + • 切换环境后会清除登录状态,需要重新登录 + • 开发环境用于本地开发调试 + • 测试环境用于服务器功能测试 + • 生产环境为正式线上环境 + • 点击地址可以复制 + + + + + + + diff --git a/miniprogram/miniprogram/pages/profile/feedback/feedback.json b/miniprogram/miniprogram/pages/profile/feedback/feedback.json new file mode 100644 index 0000000..1d036d6 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/feedback/feedback.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "意见反馈", + "navigationBarBackgroundColor": "#ff2442", + "navigationBarTextStyle": "white" +} diff --git a/miniprogram/miniprogram/pages/profile/feedback/feedback.ts b/miniprogram/miniprogram/pages/profile/feedback/feedback.ts new file mode 100644 index 0000000..c922eea --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/feedback/feedback.ts @@ -0,0 +1,58 @@ +// pages/profile/feedback/feedback.ts +Page({ + data: { + typeList: ['功能建议', 'Bug反馈', '体验问题', '其他'], + typeIndex: 0, + content: '', + contact: '' + }, + + onLoad() { + + }, + + onTypeChange(e: any) { + this.setData({ + typeIndex: e.detail.value + }); + }, + + onContentInput(e: any) { + this.setData({ + content: e.detail.value + }); + }, + + onContactInput(e: any) { + this.setData({ + contact: e.detail.value + }); + }, + + handleSubmit() { + if (!this.data.content.trim()) { + wx.showToast({ + title: '请输入问题描述', + icon: 'none' + }); + return; + } + + wx.showLoading({ + title: '提交中...', + mask: true + }); + + setTimeout(() => { + wx.hideLoading(); + wx.showToast({ + title: '提交成功', + icon: 'success' + }); + + setTimeout(() => { + wx.navigateBack(); + }, 1500); + }, 1000); + } +}); diff --git a/miniprogram/miniprogram/pages/profile/feedback/feedback.wxml b/miniprogram/miniprogram/pages/profile/feedback/feedback.wxml new file mode 100644 index 0000000..b542b58 --- /dev/null +++ b/miniprogram/miniprogram/pages/profile/feedback/feedback.wxml @@ -0,0 +1,40 @@ + + + + + 反馈类型 + + + {{typeList[typeIndex]}} + + + + + + + 问题描述 + + {{content.length}}/1000 + + + + + + 🏷️ + 话题标签 + (可选) + + + + + #{{item}}# + + + + + + + 💡 + 发布前请确保已登录小红书账号 + + + + + + + + + {{toastMessage}} + + diff --git a/miniprogram/miniprogram/services/employee.ts b/miniprogram/miniprogram/services/employee.ts new file mode 100644 index 0000000..2423c7d --- /dev/null +++ b/miniprogram/miniprogram/services/employee.ts @@ -0,0 +1,213 @@ +// 员工端API服务 +import { get, post } from '../utils/request'; +import { API } from '../config/api'; + +// 员工信息接口 +export interface EmployeeInfo { + id: number; + name: string; + phone: string; + role: string; + enterprise_id: number; + enterprise_name: string; + avatar?: string; + is_bound_xhs: number; + xhs_account: string; + xhs_phone: string; + has_xhs_cookie?: boolean; // 是否有Cookie(不返回完整Cookie内容) + bound_at?: string; +} + +// 产品接口 +export interface Product { + id: number; + name: string; + image: string; + description: string; + available_copies: number; +} + +// 文案接口 +export interface Copy { + id: number; + product_id: number; + topic: string; + title: string; + content: string; + created_at: string; + images?: Array<{ + id: number; + image_url: string; + image_thumb_url: string; + sort_order: number; + keywords_name: string; + }>; + tags?: string[]; +} + +// 发布记录接口 +export interface PublishRecord { + id: number; + product_id: number; + product_name: string; + topic: string; + title: string; + publish_link: string; + publish_time: string; + images: Array<{ + id: number; + image_url: string; + image_thumb_url: string; + sort_order: number; + keywords_name: string; + }>; + tags: string[]; +} + +/** + * 员工API服务类 + */ +export class EmployeeService { + /** + * 发送小红书验证码 + */ + static async sendXHSCode(xhsPhone: string) { + return post(API.xhs.sendCode, { + xhs_phone: xhsPhone + }); + } + + /** + * 获取员工个人信息 + */ + static async getProfile() { + return get(API.employee.profile); + } + + /** + * 绑定小红书账号 + */ + static async bindXHS(xhsPhone: string, code: string) { + return post<{ xhs_account: string }>(API.employee.bindXHS, { + xhs_phone: xhsPhone, + code + }); + } + + /** + * 解绑小红书账号 + */ + static async unbindXHS() { + return post(API.employee.unbindXHS); + } + + /** + * 获取产品列表(公开接口,不需要登录) + */ + static async getProducts() { + return get<{ list: Product[] }>(API.public.products); + } + + /** + * 获取可领取文案列表 + */ + static async getAvailableCopies(productId: number) { + return get<{ + product: { + id: number; + name: string; + image: string; + }; + copies: Copy[]; + }>(API.employee.availableCopies, { product_id: productId }); + } + + /** + * 领取文案 + */ + static async claimCopy(copyId: number, productId: number) { + return post<{ + claim_id: number; + copy: { + id: number; + title: string; + content: string; + images: string[]; + }; + }>(API.employee.claimCopy, { + copy_id: copyId, + product_id: productId + }); + } + + /** + * 随机领取文案 + */ + static async claimRandomCopy(productId: number) { + return post<{ + claim_id: number; + copy: { + id: number; + title: string; + content: string; + images: string[]; + }; + }>(API.employee.claimRandomCopy, { + product_id: productId + }); + } + + /** + * 发布内容 + */ + static async publish(params: { + copy_id: number; + title: string; + content: string; + publish_link?: string; + xhs_note_id?: string; + }) { + return post<{ record_id: number }>(API.employee.publish, params); + } + + /** + * 获取我的发布记录 + */ + static async getMyPublishRecords(page: number = 1, pageSize: number = 10) { + return get<{ + total: number; + list: PublishRecord[]; + }>(API.employee.myPublishRecords, { + page, + page_size: pageSize + }); + } + + /** + * 获取发布记录详情 + */ + static async getPublishRecordDetail(recordId: number) { + return get<{ + id: number; + article_id: number; + product_id: number; + product_name: string; + topic: string; + title: string; + content: string; + images: Array<{ + id: number; + image_url: string; + image_thumb_url: string; + sort_order: number; + keywords_name: string; + }>; + tags: string[]; + coze_tag: string; + publish_link: string; + xhs_note_id: string; + status: string; + publish_time: string; + }>(`/api/employee/publish-record/${recordId}`); + } +} diff --git a/miniprogram/miniprogram/utils/request.ts b/miniprogram/miniprogram/utils/request.ts new file mode 100644 index 0000000..94a2b33 --- /dev/null +++ b/miniprogram/miniprogram/utils/request.ts @@ -0,0 +1,150 @@ +// HTTP请求工具类 +import { API, buildURL, getHeaders } from '../config/api'; + +// 请求响应接口 +interface ApiResponse { + code: number; + message: string; + data?: T; +} + +// 请求配置 +interface RequestOptions { + url: string; + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + data?: any; + showLoading?: boolean; + loadingText?: string; +} + +/** + * 统一请求方法 + */ +export function request(options: RequestOptions): Promise> { + const { + url, + method = 'GET', + data, + showLoading = true, + loadingText = '加载中...' + } = options; + + // 显示加载提示 + if (showLoading) { + wx.showLoading({ + title: loadingText, + mask: true + }); + } + + return new Promise((resolve, reject) => { + wx.request({ + url: buildURL(url), + method, + data, + header: getHeaders(), + timeout: API.timeout, + success: (res: any) => { + if (showLoading) { + wx.hideLoading(); + } + + const response = res.data as ApiResponse; + + // 处理业务状态码 + if (response.code === 200) { + resolve(response); + } else if (response.code === 401) { + // Token过期或未登录 + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + // 清除token并跳转到登录页 + wx.removeStorageSync('token'); + wx.removeStorageSync('userInfo'); + setTimeout(() => { + wx.redirectTo({ + url: '/pages/login/login' + }); + }, 1500); + reject(response); + } else { + // 其他错误 + wx.showToast({ + title: response.message || '请求失败', + icon: 'none' + }); + reject(response); + } + }, + fail: (error) => { + if (showLoading) { + wx.hideLoading(); + } + + console.error('请求失败:', error); + wx.showToast({ + title: '网络错误,请稍后重试', + icon: 'none' + }); + reject(error); + } + }); + }); +} + +/** + * GET请求 + */ +export function get(url: string, data?: any, showLoading = true): Promise> { + // 将参数拼接到URL + if (data) { + const params = Object.keys(data) + .map(key => `${key}=${encodeURIComponent(data[key])}`) + .join('&'); + url = `${url}?${params}`; + } + + return request({ + url, + method: 'GET', + showLoading + }); +} + +/** + * POST请求 + */ +export function post(url: string, data?: any, showLoading = true): Promise> { + return request({ + url, + method: 'POST', + data, + showLoading + }); +} + +/** + * PUT请求 + */ +export function put(url: string, data?: any, showLoading = true): Promise> { + return request({ + url, + method: 'PUT', + data, + showLoading + }); +} + +/** + * DELETE请求 + */ +export function del(url: string, data?: any, showLoading = true): Promise> { + return request({ + url, + method: 'DELETE', + data, + showLoading + }); +} diff --git a/miniprogram/miniprogram/utils/util.ts b/miniprogram/miniprogram/utils/util.ts new file mode 100644 index 0000000..9c346a7 --- /dev/null +++ b/miniprogram/miniprogram/utils/util.ts @@ -0,0 +1,95 @@ +export const formatTime = (date: Date) => { + const year = date.getFullYear() + const month = date.getMonth() + 1 + const day = date.getDate() + const hour = date.getHours() + const minute = date.getMinutes() + const second = date.getSeconds() + + return ( + [year, month, day].map(formatNumber).join('/') + + ' ' + + [hour, minute, second].map(formatNumber).join(':') + ) +} + +const formatNumber = (n: number) => { + const s = n.toString() + return s[1] ? s : '0' + s +} + +// 格式化日期 +export const formatDate = (dateStr: string) => { + if (!dateStr) return '-' + + // 修复iOS兼容性:将 "2023-12-05 11:00:00" 格式转换为 "2023-12-05T11:00:00" + const iosCompatibleDateStr = dateStr.replace(' ', 'T') + + const date = new Date(iosCompatibleDateStr) + const now = new Date() + const diff = now.getTime() - date.getTime() + const days = Math.floor(diff / (1000 * 60 * 60 * 24)) + + if (days === 0) { + const hours = Math.floor(diff / (1000 * 60 * 60)) + if (hours === 0) { + const minutes = Math.floor(diff / (1000 * 60)) + return minutes <= 0 ? '刚刚' : `${minutes}分钟前` + } + return `${hours}小时前` + } else if (days === 1) { + return '昨天' + } else if (days < 7) { + return `${days}天前` + } else { + return dateStr.slice(5, 10) // MM-DD + } +} + +// 获取状态信息 +export const getStatusInfo = (status: string) => { + const statusMap: Record = { + 'topic': { text: '话题', class: 'status-topic' }, + 'cover_image': { text: '封面图', class: 'status-cover_image' }, + 'generate': { text: '生成中', class: 'status-generate' }, + 'generate_failed': { text: '生成失败', class: 'status-generate_failed' }, + 'draft': { text: '草稿', class: 'status-draft' }, + 'pending_review': { text: '待审核', class: 'status-pending_review' }, + 'approved': { text: '已通过', class: 'status-approved' }, + 'rejected': { text: '已驳回', class: 'status-rejected' }, + 'published_review': { text: '发布审核中', class: 'status-published_review' }, + 'published': { text: '已发布', class: 'status-published' }, + 'failed': { text: '失败', class: 'status-failed' } + } + return statusMap[status] || { text: status, class: 'status-draft' } +} + +// 获取渠道信息 +export const getChannelInfo = (channel: number) => { + const channelMap: Record = { + 1: { text: '百度', class: 'channel-baidu' }, + 2: { text: '头条', class: 'channel-toutiao' }, + 3: { text: '微信', class: 'channel-weixin' } + } + return channelMap[channel] || { text: '未知', class: '' } +} + +// 获取封面颜色 +export const getCoverColor = (topicTypeId: number) => { + const colors = [ + '#FFB6C1', '#FFD700', '#98FB98', '#DDA0DD', + '#87CEEB', '#F0E68C', '#FFA07A', '#B0C4DE', + '#8B4513', '#32CD32', '#FF69B4', '#4169E1' + ] + return colors[topicTypeId % colors.length] +} + +// 获取封面图标 +export const getCoverIcon = (topicTypeId: number) => { + const icons = [ + '☀️', '🍽️', '🏠', '👔', + '⛺', '💪', '💆', '📚', + '☕', '💰', '❤️', '⭐' + ] + return icons[topicTypeId % icons.length] +} diff --git a/miniprogram/package-lock.json b/miniprogram/package-lock.json new file mode 100644 index 0000000..96f8fa0 --- /dev/null +++ b/miniprogram/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "miniprogram-ts-quickstart", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "miniprogram-ts-quickstart", + "version": "1.0.0", + "dependencies": { + "tdesign-miniprogram": "^1.11.2" + }, + "devDependencies": { + "miniprogram-api-typings": "^2.8.3-1" + } + }, + "node_modules/miniprogram-api-typings": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/miniprogram-api-typings/-/miniprogram-api-typings-2.12.0.tgz", + "integrity": "sha512-ibvbqeslVFur0IAvTxLMvsbtvVcMo6gwvOnj0YZHV7aeDLu091VQRrETT2QuiG9P6aZWRcxeNGJChRKVPCp9VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tdesign-miniprogram": { + "version": "1.11.2", + "resolved": "https://registry.npmmirror.com/tdesign-miniprogram/-/tdesign-miniprogram-1.11.2.tgz", + "integrity": "sha512-lXcry3vRa9jHzjpOdIxuIAh7F85kImym82VwLbCqr/TkMhycOsOepx+r1S9fum7u2nsWiYRTV+HuvDHN3KlIuA==", + "license": "MIT" + } + } +} diff --git a/miniprogram/package.json b/miniprogram/package.json new file mode 100644 index 0000000..46b0e42 --- /dev/null +++ b/miniprogram/package.json @@ -0,0 +1,15 @@ +{ + "name": "miniprogram-ts-quickstart", + "version": "1.0.0", + "description": "", + "scripts": {}, + "keywords": [], + "author": "", + "license": "", + "dependencies": { + "tdesign-miniprogram": "^1.11.2" + }, + "devDependencies": { + "miniprogram-api-typings": "^2.8.3-1" + } +} diff --git a/miniprogram/project.config.json b/miniprogram/project.config.json new file mode 100644 index 0000000..930efec --- /dev/null +++ b/miniprogram/project.config.json @@ -0,0 +1,50 @@ +{ + "description": "项目配置文件", + "miniprogramRoot": "miniprogram/", + "compileType": "miniprogram", + "setting": { + "useCompilerPlugins": [ + "typescript" + ], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "coverView": false, + "postcss": false, + "minified": false, + "enhance": false, + "showShadowRootInWxmlPanel": false, + "packNpmRelationList": [], + "ignoreUploadUnusedFiles": true, + "compileHotReLoad": false, + "skylineRenderEnable": true, + "es6": false, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true, + "disableUseStrict": false + }, + "simulatorType": "wechat", + "simulatorPluginLibVersion": {}, + "condition": {}, + "srcMiniprogramRoot": "miniprogram/", + "editorSetting": { + "tabIndent": "insertSpaces", + "tabSize": 2 + }, + "libVersion": "trial", + "packOptions": { + "ignore": [], + "include": [] + }, + "appid": "wxa5bf062342ef754d" +} \ No newline at end of file diff --git a/miniprogram/tsconfig.json b/miniprogram/tsconfig.json new file mode 100644 index 0000000..ade784e --- /dev/null +++ b/miniprogram/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "strictNullChecks": true, + "noImplicitAny": true, + "module": "CommonJS", + "target": "ES2020", + "allowJs": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strict": true, + "strictPropertyInitialization": true, + "lib": ["ES2020"], + "typeRoots": [ + "./typings" + ] + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/miniprogram/typings/index.d.ts b/miniprogram/typings/index.d.ts new file mode 100644 index 0000000..3ee60c8 --- /dev/null +++ b/miniprogram/typings/index.d.ts @@ -0,0 +1,8 @@ +/// + +interface IAppOption { + globalData: { + userInfo?: WechatMiniprogram.UserInfo, + } + userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback, +} \ No newline at end of file diff --git a/miniprogram/typings/types/index.d.ts b/miniprogram/typings/types/index.d.ts new file mode 100644 index 0000000..a5e8a7c --- /dev/null +++ b/miniprogram/typings/types/index.d.ts @@ -0,0 +1 @@ +/// diff --git a/miniprogram/typings/types/wx/index.d.ts b/miniprogram/typings/types/wx/index.d.ts new file mode 100644 index 0000000..db82722 --- /dev/null +++ b/miniprogram/typings/types/wx/index.d.ts @@ -0,0 +1,74 @@ +/*! ***************************************************************************** +Copyright (c) 2021 Tencent, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +***************************************************************************** */ + +/// +/// +/// +/// +/// +/// +/// + +declare namespace WechatMiniprogram { + type IAnyObject = Record + type Optional = F extends (arg: infer P) => infer R ? (arg?: P) => R : F + type OptionalInterface = { [K in keyof T]: Optional } + interface AsyncMethodOptionLike { + success?: (...args: any[]) => void + } + type PromisifySuccessResult< + P, + T extends AsyncMethodOptionLike + > = P extends { success: any } + ? void + : P extends { fail: any } + ? void + : P extends { complete: any } + ? void + : Promise>[0]> +} + +declare const console: WechatMiniprogram.Console +declare const wx: WechatMiniprogram.Wx +/** 引入模块。返回模块通过 `module.exports` 或 `exports` 暴露的接口。 */ +declare function require( + /** 需要引入模块文件相对于当前文件的相对路径,或 npm 模块名,或 npm 模块路径。不支持绝对路径 */ + module: string +): any +/** 引入插件。返回插件通过 `main` 暴露的接口。 */ +declare function requirePlugin( + /** 需要引入的插件的 alias */ + module: string +): any +/** 插件引入当前使用者小程序。返回使用者小程序通过 [插件配置中 `export` 暴露的接口](https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/using.html#%E5%AF%BC%E5%87%BA%E5%88%B0%E6%8F%92%E4%BB%B6)。 + * + * 该接口只在插件中存在 + * + * 最低基础库: `2.11.1` */ +declare function requireMiniProgram(): any +/** 当前模块对象 */ +declare let module: { + /** 模块向外暴露的对象,使用 `require` 引用该模块时可以获取 */ + exports: any +} +/** `module.exports` 的引用 */ +declare let exports: any diff --git a/miniprogram/typings/types/wx/lib.wx.api.d.ts b/miniprogram/typings/types/wx/lib.wx.api.d.ts new file mode 100644 index 0000000..4c6047a --- /dev/null +++ b/miniprogram/typings/types/wx/lib.wx.api.d.ts @@ -0,0 +1,19671 @@ +/*! ***************************************************************************** +Copyright (c) 2021 Tencent, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +***************************************************************************** */ + +declare namespace WechatMiniprogram { + interface AccessFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail no such file or directory ${path}': 文件/目录不存在; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface AccessOption { + /** 要判断是否存在的文件/目录路径 (本地路径) */ + path: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AccessCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AccessFailCallback + /** 接口调用成功的回调函数 */ + success?: AccessSuccessCallback + } + /** 帐号信息 */ + interface AccountInfo { + /** 小程序帐号信息 */ + miniProgram: MiniProgram + /** 插件帐号信息(仅在插件中调用时包含这一项) */ + plugin: Plugin + } + interface AddCardOption { + /** 需要添加的卡券列表 */ + cardList: AddCardRequestInfo[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AddCardCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AddCardFailCallback + /** 接口调用成功的回调函数 */ + success?: AddCardSuccessCallback + } + /** 需要添加的卡券列表 */ + interface AddCardRequestInfo { + /** 卡券的扩展参数。需将 CardExt 对象 JSON 序列化为**字符串**传入 */ + cardExt: string + /** 卡券 ID */ + cardId: string + } + /** 卡券添加结果列表 */ + interface AddCardResponseInfo { + /** 卡券的扩展参数,结构请参考下文 */ + cardExt: string + /** 用户领取到卡券的 ID */ + cardId: string + /** 加密 code,为用户领取到卡券的code加密后的字符串,解密请参照:[code 解码接口](https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1499332673_Unm7V) */ + code: string + /** 是否成功 */ + isSuccess: boolean + } + interface AddCardSuccessCallbackResult { + /** 卡券添加结果列表 */ + cardList: AddCardResponseInfo[] + errMsg: string + } + interface AddCustomLayerOption { + /** 个性化图层id */ + layerId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AddCustomLayerCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AddCustomLayerFailCallback + /** 接口调用成功的回调函数 */ + success?: AddCustomLayerSuccessCallback + } + interface AddGroundOverlayOption { + /** 图片覆盖的经纬度范围 */ + bounds: MapBounds + /** 图片图层 id */ + id: string + /** 图片路径,支持网络图片、临时路径、代码包路径 */ + src: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AddGroundOverlayCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AddGroundOverlayFailCallback + /** 图层透明度 */ + opacity?: number + /** 接口调用成功的回调函数 */ + success?: AddGroundOverlaySuccessCallback + /** 是否可见 */ + visible?: boolean + /** 图层绘制顺序 */ + zIndex?: number + } + interface AddMarkersOption { + /** 同传入 map 组件的 marker 属性 */ + markers: any[] + /** 是否先清空地图上所有 marker */ + clear?: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AddMarkersCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AddMarkersFailCallback + /** 接口调用成功的回调函数 */ + success?: AddMarkersSuccessCallback + } + interface AddPhoneCalendarOption { + /** 开始时间的 unix 时间戳 */ + startTime: number + /** 日历事件标题 */ + title: string + /** 是否提醒,默认 true */ + alarm?: boolean + /** 提醒提前量,单位秒,默认 0 表示开始时提醒 */ + alarmOffset?: number + /** 是否全天事件,默认 false */ + allDay?: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AddPhoneCalendarCompleteCallback + /** 事件说明 */ + description?: string + /** 结束时间的 unix 时间戳,默认与开始时间相同 */ + endTime?: string + /** 接口调用失败的回调函数 */ + fail?: AddPhoneCalendarFailCallback + /** 事件位置 */ + location?: string + /** 接口调用成功的回调函数 */ + success?: AddPhoneCalendarSuccessCallback + } + interface AddPhoneContactOption { + /** 名字 */ + firstName: string + /** 联系地址城市 */ + addressCity?: string + /** 联系地址国家 */ + addressCountry?: string + /** 联系地址邮政编码 */ + addressPostalCode?: string + /** 联系地址省份 */ + addressState?: string + /** 联系地址街道 */ + addressStreet?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AddPhoneContactCompleteCallback + /** 电子邮件 */ + email?: string + /** 接口调用失败的回调函数 */ + fail?: AddPhoneContactFailCallback + /** 住宅地址城市 */ + homeAddressCity?: string + /** 住宅地址国家 */ + homeAddressCountry?: string + /** 住宅地址邮政编码 */ + homeAddressPostalCode?: string + /** 住宅地址省份 */ + homeAddressState?: string + /** 住宅地址街道 */ + homeAddressStreet?: string + /** 住宅传真 */ + homeFaxNumber?: string + /** 住宅电话 */ + homePhoneNumber?: string + /** 公司电话 */ + hostNumber?: string + /** 姓氏 */ + lastName?: string + /** 中间名 */ + middleName?: string + /** 手机号 */ + mobilePhoneNumber?: string + /** 昵称 */ + nickName?: string + /** 公司 */ + organization?: string + /** 头像本地文件路径 */ + photoFilePath?: string + /** 备注 */ + remark?: string + /** 接口调用成功的回调函数 */ + success?: AddPhoneContactSuccessCallback + /** 职位 */ + title?: string + /** 网站 */ + url?: string + /** 微信号 */ + weChatNumber?: string + /** 工作地址城市 */ + workAddressCity?: string + /** 工作地址国家 */ + workAddressCountry?: string + /** 工作地址邮政编码 */ + workAddressPostalCode?: string + /** 工作地址省份 */ + workAddressState?: string + /** 工作地址街道 */ + workAddressStreet?: string + /** 工作传真 */ + workFaxNumber?: string + /** 工作电话 */ + workPhoneNumber?: string + } + interface AddPhoneRepeatCalendarOption { + /** 开始时间的 unix 时间戳 (1970年1月1日开始所经过的秒数) */ + startTime: number + /** 日历事件标题 */ + title: string + /** 是否提醒,默认 true */ + alarm?: boolean + /** 提醒提前量,单位秒,默认 0 表示开始时提醒 */ + alarmOffset?: number + /** 是否全天事件,默认 false */ + allDay?: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AddPhoneRepeatCalendarCompleteCallback + /** 事件说明 */ + description?: string + /** 结束时间的 unix 时间戳,默认与开始时间相同 */ + endTime?: string + /** 接口调用失败的回调函数 */ + fail?: AddPhoneRepeatCalendarFailCallback + /** 事件位置 */ + location?: string + /** 重复周期结束时间的 unix 时间戳,不填表示一直重复 */ + repeatEndTime?: number + /** 重复周期,默认 month 每月重复 */ + repeatInterval?: string + /** 接口调用成功的回调函数 */ + success?: AddPhoneRepeatCalendarSuccessCallback + } + interface AddServiceOption { + /** 描述service的Object */ + service: BLEPeripheralService + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AddServiceCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AddServiceFailCallback + /** 接口调用成功的回调函数 */ + success?: AddServiceSuccessCallback + } + /** 广播自定义参数 */ + interface AdvertiseReqObj { + /** 当前Service是否可连接 */ + connectable?: boolean + /** 广播中deviceName字段,默认为空 */ + deviceName?: string + /** 广播的制造商信息, 仅安卓支持 */ + manufacturerData?: ManufacturerData[] + /** 要广播的serviceUuid列表 */ + serviceUuids?: string[] + } + /** animationData */ + interface AnimationExportResult { + actions: IAnyObject[] + } + /** 动画效果 */ + interface AnimationOption { + /** 动画变化时间,单位 ms */ + duration?: number + /** 动画变化方式 + * + * 可选值: + * - 'linear': 动画从头到尾的速度是相同的; + * - 'easeIn': 动画以低速开始; + * - 'easeOut': 动画以低速结束; + * - 'easeInOut': 动画以低速开始和结束; */ + timingFunc?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' + } + interface AppendFileFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail no such file or directory, open ${filePath}': 指定的 filePath 文件不存在; + * - 'fail illegal operation on a directory, open "${filePath}"': 指定的 filePath 是一个已经存在的目录; + * - 'fail permission denied, open ${dirPath}': 指定的 filePath 路径没有写权限; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface AppendFileOption { + /** 要追加的文本或二进制数据 */ + data: string | ArrayBuffer + /** 要追加内容的文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AppendFileCompleteCallback + /** 指定写入文件的字符编码 + * + * 可选值: + * - 'ascii': ; + * - 'base64': ; + * - 'binary': ; + * - 'hex': ; + * - 'ucs2': 以小端序读取; + * - 'ucs-2': 以小端序读取; + * - 'utf16le': 以小端序读取; + * - 'utf-16le': 以小端序读取; + * - 'utf-8': ; + * - 'utf8': ; + * - 'latin1': ; */ + encoding?: + | 'ascii' + | 'base64' + | 'binary' + | 'hex' + | 'ucs2' + | 'ucs-2' + | 'utf16le' + | 'utf-16le' + | 'utf-8' + | 'utf8' + | 'latin1' + /** 接口调用失败的回调函数 */ + fail?: AppendFileFailCallback + /** 接口调用成功的回调函数 */ + success?: AppendFileSuccessCallback + } + interface AuthPrivateMessageOption { + /** shareTicket。可以从 wx.onShow 中获取。详情 [shareTicket](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html) */ + shareTicket: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AuthPrivateMessageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AuthPrivateMessageFailCallback + /** 接口调用成功的回调函数 */ + success?: AuthPrivateMessageSuccessCallback + } + interface AuthPrivateMessageSuccessCallbackResult { + /** 经过加密的activityId,解密后可得到原始的activityId。若解密后得到的activityId可以与开发者后台的活动id对应上则验证通过,否则表明valid字段不可靠(被篡改) 详细见[加密数据解密算法](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html) */ + encryptedData: string + /** 错误信息 */ + errMsg: string + /** 加密算法的初始向量,详细见[加密数据解密算法](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html) */ + iv: string + /** 验证是否通过 */ + valid: boolean + } + /** 用户授权设置信息,详情参考[权限](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html) */ + interface AuthSetting { + /** 是否授权通讯地址,已取消此项授权,会默认返回true */ + 'scope.address'?: boolean + /** 是否授权摄像头,对应[[camera](https://developers.weixin.qq.com/miniprogram/dev/component/camera.html)](https://developers.weixin.qq.com/miniprogram/dev/component/camera.html) 组件 */ + 'scope.camera'?: boolean + /** 是否授权获取发票,已取消此项授权,会默认返回true */ + 'scope.invoice'?: boolean + /** 是否授权发票抬头,已取消此项授权,会默认返回true */ + 'scope.invoiceTitle'?: boolean + /** 是否授权录音功能,对应接口 [wx.startRecord](https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/wx.startRecord.html) */ + 'scope.record'?: boolean + /** 是否授权用户信息,对应接口 [wx.getUserInfo](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html) */ + 'scope.userInfo'?: boolean + /** 是否授权地理位置,对应接口 [wx.getLocation](https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.getLocation.html), [wx.chooseLocation](https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.chooseLocation.html) */ + 'scope.userLocation'?: boolean + /** 是否授权微信运动步数,对应接口 [wx.getWeRunData](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/werun/wx.getWeRunData.html) */ + 'scope.werun'?: boolean + /** 是否授权保存到相册 [wx.saveImageToPhotosAlbum](https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.saveImageToPhotosAlbum.html), [wx.saveVideoToPhotosAlbum](https://developers.weixin.qq.com/miniprogram/dev/api/media/video/wx.saveVideoToPhotosAlbum.html) */ + 'scope.writePhotosAlbum'?: boolean + } + interface AuthorizeForMiniProgramOption { + /** 需要获取权限的 scope,详见 [scope 列表]((authorize#scope-列表)) + * + * 可选值: + * - 'scope.record': ; + * - 'scope.writePhotosAlbum': ; + * - 'scope.camera': ; */ + scope: 'scope.record' | 'scope.writePhotosAlbum' | 'scope.camera' + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AuthorizeForMiniProgramCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AuthorizeForMiniProgramFailCallback + /** 接口调用成功的回调函数 */ + success?: AuthorizeForMiniProgramSuccessCallback + } + interface AuthorizeOption { + /** 需要获取权限的 scope,详见 [scope 列表]((authorize#scope-列表)) */ + scope: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: AuthorizeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: AuthorizeFailCallback + /** 接口调用成功的回调函数 */ + success?: AuthorizeSuccessCallback + } + /** 设备特征值列表 */ + interface BLECharacteristic { + /** 该特征值支持的操作类型 */ + properties: BLECharacteristicProperties + /** 蓝牙设备特征值的 uuid */ + uuid: string + } + /** 该特征值支持的操作类型 */ + interface BLECharacteristicProperties { + /** 该特征值是否支持 indicate 操作 */ + indicate: boolean + /** 该特征值是否支持 notify 操作 */ + notify: boolean + /** 该特征值是否支持 read 操作 */ + read: boolean + /** 该特征值是否支持 write 操作 */ + write: boolean + } + interface BLEPeripheralServerCloseOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SocketTaskCloseCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SocketTaskCloseFailCallback + /** 接口调用成功的回调函数 */ + success?: SocketTaskCloseSuccessCallback + } + /** 描述service的Object */ + interface BLEPeripheralService { + /** characteristics列表 */ + characteristics: Characteristic[] + /** service 的 uuid */ + uuid: string + } + /** 设备服务列表 */ + interface BLEService { + /** 该服务是否为主服务 */ + isPrimary: boolean + /** 蓝牙设备服务的 uuid */ + uuid: string + } + /** BackgroundAudioManager 实例,可通过 [wx.getBackgroundAudioManager](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/wx.getBackgroundAudioManager.html) 获取。 +* +* **示例代码** +* +* +* ```js +const backgroundAudioManager = wx.getBackgroundAudioManager() + +backgroundAudioManager.title = '此时此刻' +backgroundAudioManager.epname = '此时此刻' +backgroundAudioManager.singer = '许巍' +backgroundAudioManager.coverImgUrl = 'http://y.gtimg.cn/music/photo_new/T002R300x300M000003rsKF44GyaSk.jpg?max_age=2592000' +// 设置了 src 之后会自动播放 +backgroundAudioManager.src = 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E061FF02C31F716658E5C81F5594D561F2E88B854E81CAAB7806D5E4F103E55D33C16F3FAC506D1AB172DE8600B37E43FAD&fromtag=46' +``` */ + interface BackgroundAudioManager { + /** 音频已缓冲的时间,仅保证当前播放时间点到此时间点内容已缓冲。(只读) */ + buffered: number + /** 封面图 URL,用于做原生音频播放器背景图。原生音频播放器中的分享功能,分享出去的卡片配图及背景也将使用该图。 */ + coverImgUrl: string + /** 当前音频的播放位置(单位:s),只有在有合法 src 时返回。(只读) */ + currentTime: number + /** 当前音频的长度(单位:s),只有在有合法 src 时返回。(只读) */ + duration: number + /** 专辑名,原生音频播放器中的分享功能,分享出去的卡片简介,也将使用该值。 */ + epname: string + /** 当前是否暂停或停止。(只读) */ + paused: boolean + /** 播放速度。范围 0.5-2.0,默认为 1。(Android 需要 6 及以上版本) + * + * 最低基础库: `2.11.0` */ + playbackRate: number + /** 音频协议。默认值为 'http',设置 'hls' 可以支持播放 HLS 协议的直播音频。 + * + * 最低基础库: `1.9.94` */ + protocol: string + /** 歌手名,原生音频播放器中的分享功能,分享出去的卡片简介,也将使用该值。 */ + singer: string + /** 音频的数据源([2.2.3](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 开始支持云文件ID)。默认为空字符串,**当设置了新的 src 时,会自动开始播放**,目前支持的格式有 m4a, aac, mp3, wav。 */ + src: string + /** 音频开始播放的位置(单位:s)。 */ + startTime: number + /** 音频标题,用于原生音频播放器音频标题(必填)。原生音频播放器中的分享功能,分享出去的卡片标题,也将使用该值。 */ + title: string + /** 页面链接,原生音频播放器中的分享功能,分享出去的卡片简介,也将使用该值。 */ + webUrl: string + } + interface BlueToothDevice { + /** 当前蓝牙设备的信号强度 */ + RSSI: number + /** 当前蓝牙设备的广播数据段中的 ManufacturerData 数据段。 */ + advertisData: ArrayBuffer + /** 当前蓝牙设备的广播数据段中的 ServiceUUIDs 数据段 */ + advertisServiceUUIDs: string[] + /** 用于区分设备的 id */ + deviceId: string + /** 当前蓝牙设备的广播数据段中的 LocalName 数据段 */ + localName: string + /** 蓝牙设备名称,某些设备可能没有 */ + name: string + /** 当前蓝牙设备的广播数据段中的 ServiceData 数据段 */ + serviceData: IAnyObject + } + /** 搜索到的设备列表 */ + interface BluetoothDeviceInfo { + /** 用于区分设备的 id */ + deviceId: string + /** 蓝牙设备名称,某些设备可能没有 */ + name: string + } + interface BlurOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: BlurCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: BlurFailCallback + /** 接口调用成功的回调函数 */ + success?: BlurSuccessCallback + } + interface BoundingClientRectCallbackResult { + /** 节点的下边界坐标 */ + bottom: number + /** 节点的 dataset */ + dataset: IAnyObject + /** 节点的高度 */ + height: number + /** 节点的 ID */ + id: string + /** 节点的左边界坐标 */ + left: number + /** 节点的右边界坐标 */ + right: number + /** 节点的上边界坐标 */ + top: number + /** 节点的宽度 */ + width: number + } + /** 目标边界 */ + interface BoundingClientRectResult { + /** 下边界 */ + bottom: number + /** 高度 */ + height: number + /** 左边界 */ + left: number + /** 右边界 */ + right: number + /** 上边界 */ + top: number + /** 宽度 */ + width: number + } + interface CameraContextStartRecordOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartRecordCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartRecordFailCallback + /** 接口调用成功的回调函数 */ + success?: CameraContextStartRecordSuccessCallback + /** 超过30s或页面 `onHide` 时会结束录像 */ + timeoutCallback?: StartRecordTimeoutCallback + } + interface CameraContextStopRecordOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopRecordCompleteCallback + /** 启动视频压缩,压缩效果同`chooseVideo` */ + compressed?: boolean + /** 接口调用失败的回调函数 */ + fail?: StopRecordFailCallback + /** 接口调用成功的回调函数 */ + success?: CameraContextStopRecordSuccessCallback + } + interface CameraFrameListenerStartOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartFailCallback + /** 接口调用成功的回调函数 */ + success?: StartSuccessCallback + } + /** Canvas 实例,可通过 [SelectorQuery](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/SelectorQuery.html) 获取。 + * + * **示例代码** + * + * + * + * 2D Canvas 示例 + * [在微信开发者工具中查看示例](https://developers.weixin.qq.com/s/SHfgCmmq7UcM) + * + * WebGL 示例 + * [在微信开发者工具中查看示例](https://developers.weixin.qq.com/s/qEGUOqmf7T8z) + * + * 最低基础库: `2.7.0` */ + interface Canvas { + /** 画布高度 */ + height: number + /** 画布宽度 */ + width: number + } + /** canvas 组件的绘图上下文。CanvasContext 是旧版的接口, 新版 Canvas 2D 接口与 Web 一致。 */ + interface CanvasContext { + /** 填充颜色。用法同 [CanvasContext.setFillStyle()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setFillStyle.html)。 + * + * 最低基础库: `1.9.90` */ + fillStyle: string | CanvasGradient + /** 当前字体样式的属性。符合 [CSS font 语法](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font) 的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif。 + * + * 最低基础库: `1.9.90` */ + font: string + /** 全局画笔透明度。范围 0-1,0 表示完全透明,1 表示完全不透明。 */ + globalAlpha: number + /** 在绘制新形状时应用的合成操作的类型。目前安卓版本只适用于 `fill` 填充块的合成,用于 `stroke` 线段的合成效果都是 `source-over`。 + * + * 目前支持的操作有 + * - 安卓:xor, source-over, source-atop, destination-out, lighter, overlay, darken, lighten, hard-light + * - iOS:xor, source-over, source-atop, destination-over, destination-out, lighter, multiply, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, saturation, luminosity + * + * 最低基础库: `1.9.90` */ + globalCompositeOperation: string + /** 线条的端点样式。用法同 [CanvasContext.setLineCap()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setLineCap.html)。 + * + * 最低基础库: `1.9.90` */ + lineCap: string + /** 虚线偏移量,初始值为0 + * + * 最低基础库: `1.9.90` */ + lineDashOffset: number + /** 线条的交点样式。用法同 [CanvasContext.setLineJoin()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setLineJoin.html)。 + * + * 可选值: + * - 'bevel': 斜角; + * - 'round': 圆角; + * - 'miter': 尖角; + * + * 最低基础库: `1.9.90` */ + lineJoin: 'bevel' | 'round' | 'miter' + /** 线条的宽度。用法同 [CanvasContext.setLineWidth()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setLineWidth.html)。 + * + * 最低基础库: `1.9.90` */ + lineWidth: number + /** 最大斜接长度。用法同 [CanvasContext.setMiterLimit()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setMiterLimit.html)。 + * + * 最低基础库: `1.9.90` */ + miterLimit: number + /** 阴影的模糊级别 + * + * 最低基础库: `1.9.90` */ + shadowBlur: number + /** 阴影的颜色 + * + * 最低基础库: `1.9.90` */ + shadowColor: number + /** 阴影相对于形状在水平方向的偏移 + * + * 最低基础库: `1.9.90` */ + shadowOffsetX: number + /** 阴影相对于形状在竖直方向的偏移 + * + * 最低基础库: `1.9.90` */ + shadowOffsetY: number + /** 边框颜色。用法同 [CanvasContext.setStrokeStyle()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setStrokeStyle.html)。 + * + * 最低基础库: `1.9.90` */ + strokeStyle: string | CanvasGradient + } + interface CanvasGetImageDataOption { + /** 画布标识,传入 [canvas](https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html) 组件的 `canvas-id` 属性。 */ + canvasId: string + /** 将要被提取的图像数据矩形区域的高度 */ + height: number + /** 将要被提取的图像数据矩形区域的宽度 */ + width: number + /** 将要被提取的图像数据矩形区域的左上角横坐标 */ + x: number + /** 将要被提取的图像数据矩形区域的左上角纵坐标 */ + y: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CanvasGetImageDataCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CanvasGetImageDataFailCallback + /** 接口调用成功的回调函数 */ + success?: CanvasGetImageDataSuccessCallback + } + interface CanvasGetImageDataSuccessCallbackResult { + /** 图像像素点数据,一维数组,每四项表示一个像素点的 rgba */ + data: Uint8ClampedArray + /** 图像数据矩形的高度 */ + height: number + /** 图像数据矩形的宽度 */ + width: number + errMsg: string + } + interface CanvasPutImageDataOption { + /** 画布标识,传入 [canvas](https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html) 组件的 canvas-id 属性。 */ + canvasId: string + /** 图像像素点数据,一维数组,每四项表示一个像素点的 rgba */ + data: Uint8ClampedArray + /** 源图像数据矩形区域的高度 */ + height: number + /** 源图像数据矩形区域的宽度 */ + width: number + /** 源图像数据在目标画布中的位置偏移量(x 轴方向的偏移量) */ + x: number + /** 源图像数据在目标画布中的位置偏移量(y 轴方向的偏移量) */ + y: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CanvasPutImageDataCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CanvasPutImageDataFailCallback + /** 接口调用成功的回调函数 */ + success?: CanvasPutImageDataSuccessCallback + } + interface CanvasToTempFilePathOption { + /** 画布标识,传入 [canvas](https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html) 组件实例 (canvas type="2d" 时使用该属性)。 */ + canvas?: IAnyObject + /** 画布标识,传入 [canvas](https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html) 组件的 canvas-id */ + canvasId?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CanvasToTempFilePathCompleteCallback + /** 输出的图片的高度 + * + * 最低基础库: `1.2.0` */ + destHeight?: number + /** 输出的图片的宽度 + * + * 最低基础库: `1.2.0` */ + destWidth?: number + /** 接口调用失败的回调函数 */ + fail?: CanvasToTempFilePathFailCallback + /** 目标文件的类型 + * + * 可选值: + * - 'jpg': jpg 图片; + * - 'png': png 图片; + * + * 最低基础库: `1.7.0` */ + fileType?: 'jpg' | 'png' + /** 指定的画布区域的高度 + * + * 最低基础库: `1.2.0` */ + height?: number + /** 图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。 + * + * 最低基础库: `1.7.0` */ + quality?: number + /** 接口调用成功的回调函数 */ + success?: CanvasToTempFilePathSuccessCallback + /** 指定的画布区域的宽度 + * + * 最低基础库: `1.2.0` */ + width?: number + /** 指定的画布区域的左上角横坐标 + * + * 最低基础库: `1.2.0` */ + x?: number + /** 指定的画布区域的左上角纵坐标 + * + * 最低基础库: `1.2.0` */ + y?: number + } + interface CanvasToTempFilePathSuccessCallbackResult { + /** 生成文件的临时路径 (本地路径) */ + tempFilePath: string + errMsg: string + } + /** characteristics列表 */ + interface Characteristic { + /** Characteristic 的 uuid */ + uuid: string + /** 描述符数据 */ + descriptors?: CharacteristicDescriptor[] + /** 特征值权限 */ + permission?: CharacteristicPermission + /** 特征值支持的操作 */ + properties?: CharacteristicProperties + /** 特征值对应的二进制值 */ + value?: ArrayBuffer + } + /** 描述符数据 */ + interface CharacteristicDescriptor { + /** Descriptor 的 uuid */ + uuid: string + /** 描述符的权限 */ + permission?: DescriptorPermission + /** 描述符数据 */ + value?: ArrayBuffer + } + /** 特征值权限 */ + interface CharacteristicPermission { + /** 加密读请求 */ + readEncryptionRequired?: boolean + /** 可读 */ + readable?: boolean + /** 加密写请求 */ + writeEncryptionRequired?: boolean + /** 可写 */ + writeable?: boolean + } + /** 特征值支持的操作 */ + interface CharacteristicProperties { + /** 回包 */ + indicate?: boolean + /** 订阅 */ + notify?: boolean + /** 读 */ + read?: boolean + /** 写 */ + write?: boolean + } + interface CheckIsOpenAccessibilityOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CheckIsOpenAccessibilityCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CheckIsOpenAccessibilityFailCallback + /** 接口调用成功的回调函数 */ + success?: CheckIsOpenAccessibilitySuccessCallback + } + interface CheckIsOpenAccessibilitySuccessCallbackOption { + /** iOS 上开启辅助功能旁白,安卓开启 talkback 时返回 true */ + open: boolean + } + interface CheckIsSoterEnrolledInDeviceOption { + /** 认证方式 + * + * 可选值: + * - 'fingerPrint': 指纹识别; + * - 'facial': 人脸识别; + * - 'speech': 声纹识别(暂未支持); */ + checkAuthMode: 'fingerPrint' | 'facial' | 'speech' + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CheckIsSoterEnrolledInDeviceCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CheckIsSoterEnrolledInDeviceFailCallback + /** 接口调用成功的回调函数 */ + success?: CheckIsSoterEnrolledInDeviceSuccessCallback + } + interface CheckIsSoterEnrolledInDeviceSuccessCallbackResult { + /** 错误信息 */ + errMsg: string + /** 是否已录入信息 */ + isEnrolled: boolean + } + interface CheckIsSupportSoterAuthenticationOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CheckIsSupportSoterAuthenticationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CheckIsSupportSoterAuthenticationFailCallback + /** 接口调用成功的回调函数 */ + success?: CheckIsSupportSoterAuthenticationSuccessCallback + } + interface CheckIsSupportSoterAuthenticationSuccessCallbackResult { + /** 该设备支持的可被SOTER识别的生物识别方式 + * + * 可选值: + * - 'fingerPrint': 指纹识别; + * - 'facial': 人脸识别; + * - 'speech': 声纹识别(暂未支持); */ + supportMode: Array<'fingerPrint' | 'facial' | 'speech'> + errMsg: string + } + interface CheckSessionOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CheckSessionCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CheckSessionFailCallback + /** 接口调用成功的回调函数 */ + success?: CheckSessionSuccessCallback + } + interface ChooseAddressOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ChooseAddressCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ChooseAddressFailCallback + /** 接口调用成功的回调函数 */ + success?: ChooseAddressSuccessCallback + } + interface ChooseAddressSuccessCallbackResult { + /** 国标收货地址第二级地址 */ + cityName: string + /** 国标收货地址第三级地址 */ + countyName: string + /** 详细收货地址信息 */ + detailInfo: string + /** 错误信息 */ + errMsg: string + /** 收货地址国家码 */ + nationalCode: string + /** 邮编 */ + postalCode: string + /** 国标收货地址第一级地址 */ + provinceName: string + /** 收货人手机号码 */ + telNumber: string + /** 收货人姓名 */ + userName: string + } + /** 返回选择的文件的本地临时文件对象数组 */ + interface ChooseFile { + /** 选择的文件名称 */ + name: string + /** 本地临时文件路径 (本地路径) */ + path: string + /** 本地临时文件大小,单位 B */ + size: number + /** 选择的文件的会话发送时间,Unix时间戳,工具暂不支持此属性 */ + time: number + /** 选择的文件类型 + * + * 可选值: + * - 'video': 选择了视频文件; + * - 'image': 选择了图片文件; + * - 'file': 选择了除图片和视频的文件; */ + type: 'video' | 'image' | 'file' + } + interface ChooseImageOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ChooseImageCompleteCallback + /** 最多可以选择的图片张数 */ + count?: number + /** 接口调用失败的回调函数 */ + fail?: ChooseImageFailCallback + /** 所选的图片的尺寸 + * + * 可选值: + * - 'original': 原图; + * - 'compressed': 压缩图; */ + sizeType?: Array<'original' | 'compressed'> + /** 选择图片的来源 + * + * 可选值: + * - 'album': 从相册选图; + * - 'camera': 使用相机; */ + sourceType?: Array<'album' | 'camera'> + /** 接口调用成功的回调函数 */ + success?: ChooseImageSuccessCallback + } + interface ChooseImageSuccessCallbackResult { + /** 图片的本地临时文件路径列表 (本地路径) */ + tempFilePaths: string[] + /** 图片的本地临时文件列表 + * + * 最低基础库: `1.2.0` */ + tempFiles: ImageFile[] + errMsg: string + } + interface ChooseInvoiceOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ChooseInvoiceCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ChooseInvoiceFailCallback + /** 接口调用成功的回调函数 */ + success?: ChooseInvoiceSuccessCallback + } + interface ChooseInvoiceSuccessCallbackResult { + /** 用户选中的发票信息,格式为一个 JSON 字符串,包含三个字段: card_id:所选发票卡券的 cardId,encrypt_code:所选发票卡券的加密 code,报销方可以通过 cardId 和 encryptCode 获得报销发票的信息,app_id: 发票方的 appId。 */ + invoiceInfo: string + errMsg: string + } + interface ChooseInvoiceTitleOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ChooseInvoiceTitleCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ChooseInvoiceTitleFailCallback + /** 接口调用成功的回调函数 */ + success?: ChooseInvoiceTitleSuccessCallback + } + interface ChooseInvoiceTitleSuccessCallbackResult { + /** 银行账号 */ + bankAccount: string + /** 银行名称 */ + bankName: string + /** 单位地址 */ + companyAddress: string + /** 错误信息 */ + errMsg: string + /** 抬头税号 */ + taxNumber: string + /** 手机号码 */ + telephone: string + /** 抬头名称 */ + title: string + /** 抬头类型 + * + * 可选值: + * - 0: 单位; + * - 1: 个人; */ + type: 0 | 1 + } + interface ChooseLocationOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ChooseLocationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ChooseLocationFailCallback + /** 目标地纬度 + * + * 最低基础库: `2.9.0` */ + latitude?: number + /** 目标地经度 + * + * 最低基础库: `2.9.0` */ + longitude?: number + /** 接口调用成功的回调函数 */ + success?: ChooseLocationSuccessCallback + } + interface ChooseLocationSuccessCallbackResult { + /** 详细地址 */ + address: string + /** 纬度,浮点数,范围为-90~90,负数表示南纬。使用 gcj02 国测局坐标系 */ + latitude: string + /** 经度,浮点数,范围为-180~180,负数表示西经。使用 gcj02 国测局坐标系 */ + longitude: string + /** 位置名称 */ + name: string + errMsg: string + } + interface ChooseMediaOption { + /** 仅在 sourceType 为 camera 时生效,使用前置或后置摄像头 + * + * 可选值: + * - 'back': 使用后置摄像头; + * - 'front': 使用前置摄像头; */ + camera?: 'back' | 'front' + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ChooseMediaCompleteCallback + /** 最多可以选择的文件个数 */ + count?: number + /** 接口调用失败的回调函数 */ + fail?: ChooseMediaFailCallback + /** 拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 30s 之间 */ + maxDuration?: number + /** 文件类型 + * + * 可选值: + * - 'image': 只能拍摄图片或从相册选择图片; + * - 'video': 只能拍摄视频或从相册选择视频; */ + mediaType?: Array<'image' | 'video'> + /** 仅对 mediaType 为 image 时有效,是否压缩所选文件 */ + sizeType?: string[] + /** 图片和视频选择的来源 + * + * 可选值: + * - 'album': 从相册选择; + * - 'camera': 使用相机拍摄; */ + sourceType?: Array<'album' | 'camera'> + /** 接口调用成功的回调函数 */ + success?: ChooseMediaSuccessCallback + } + interface ChooseMediaSuccessCallbackResult { + /** 本地临时文件列表 */ + tempFiles: MediaFile[] + /** 文件类型,有效值有 image 、video */ + type: string + errMsg: string + } + interface ChooseMessageFileOption { + /** 最多可以选择的文件个数,可以 0~100 */ + count: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ChooseMessageFileCompleteCallback + /** 根据文件拓展名过滤,仅 type==file 时有效。每一项都不能是空字符串。默认不过滤。 + * + * 最低基础库: `2.6.0` */ + extension?: string[] + /** 接口调用失败的回调函数 */ + fail?: ChooseMessageFileFailCallback + /** 接口调用成功的回调函数 */ + success?: ChooseMessageFileSuccessCallback + /** 所选的文件的类型 + * + * 可选值: + * - 'all': 从所有文件选择; + * - 'video': 只能选择视频文件; + * - 'image': 只能选择图片文件; + * - 'file': 可以选择除了图片和视频之外的其它的文件; */ + type?: 'all' | 'video' | 'image' | 'file' + } + interface ChooseMessageFileSuccessCallbackResult { + /** 返回选择的文件的本地临时文件对象数组 */ + tempFiles: ChooseFile[] + errMsg: string + } + interface ChooseVideoOption { + /** 默认拉起的是前置或者后置摄像头。部分 Android 手机下由于系统 ROM 不支持无法生效 + * + * 可选值: + * - 'back': 默认拉起后置摄像头; + * - 'front': 默认拉起前置摄像头; */ + camera?: 'back' | 'front' + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ChooseVideoCompleteCallback + /** 是否压缩所选择的视频文件 + * + * 最低基础库: `1.6.0` */ + compressed?: boolean + /** 接口调用失败的回调函数 */ + fail?: ChooseVideoFailCallback + /** 拍摄视频最长拍摄时间,单位秒 */ + maxDuration?: number + /** 视频选择的来源 + * + * 可选值: + * - 'album': 从相册选择视频; + * - 'camera': 使用相机拍摄视频; */ + sourceType?: Array<'album' | 'camera'> + /** 接口调用成功的回调函数 */ + success?: ChooseVideoSuccessCallback + } + interface ChooseVideoSuccessCallbackResult { + /** 选定视频的时间长度 */ + duration: number + /** 返回选定视频的高度 */ + height: number + /** 选定视频的数据量大小 */ + size: number + /** 选定视频的临时文件路径 (本地路径) */ + tempFilePath: string + /** 返回选定视频的宽度 */ + width: number + errMsg: string + } + interface ClearOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ClearCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ClearFailCallback + /** 接口调用成功的回调函数 */ + success?: ClearSuccessCallback + } + interface ClearStorageOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ClearStorageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ClearStorageFailCallback + /** 接口调用成功的回调函数 */ + success?: ClearStorageSuccessCallback + } + interface CloseBLEConnectionOption { + /** 用于区分设备的 id */ + deviceId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CloseBLEConnectionCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CloseBLEConnectionFailCallback + /** 接口调用成功的回调函数 */ + success?: CloseBLEConnectionSuccessCallback + } + interface CloseBluetoothAdapterOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CloseBluetoothAdapterCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CloseBluetoothAdapterFailCallback + /** 接口调用成功的回调函数 */ + success?: CloseBluetoothAdapterSuccessCallback + } + interface CloseSocketOption { + /** 一个数字值表示关闭连接的状态号,表示连接被关闭的原因。 */ + code?: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CloseSocketCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CloseSocketFailCallback + /** 一个可读的字符串,表示连接被关闭的原因。这个字符串必须是不长于 123 字节的 UTF-8 文本(不是字符)。 */ + reason?: string + /** 接口调用成功的回调函数 */ + success?: CloseSocketSuccessCallback + } + /** 颜色。可以用以下几种方式来表示 canvas 中使用的颜色: + * + * - RGB 颜色: 如 `'rgb(255, 0, 0)'` + * - RGBA 颜色:如 `'rgba(255, 0, 0, 0.3)'` + * - 16 进制颜色: 如 `'#FF0000'` + * - 预定义的颜色: 如 `'red'` + * + * 其中预定义颜色有以下148个: + * *注意**: Color Name 大小写不敏感 + * + * | Color Name | HEX | + * | -------------------- | ------- | + * | AliceBlue | #F0F8FF | + * | AntiqueWhite | #FAEBD7 | + * | Aqua | #00FFFF | + * | Aquamarine | #7FFFD4 | + * | Azure | #F0FFFF | + * | Beige | #F5F5DC | + * | Bisque | #FFE4C4 | + * | Black | #000000 | + * | BlanchedAlmond | #FFEBCD | + * | Blue | #0000FF | + * | BlueViolet | #8A2BE2 | + * | Brown | #A52A2A | + * | BurlyWood | #DEB887 | + * | CadetBlue | #5F9EA0 | + * | Chartreuse | #7FFF00 | + * | Chocolate | #D2691E | + * | Coral | #FF7F50 | + * | CornflowerBlue | #6495ED | + * | Cornsilk | #FFF8DC | + * | Crimson | #DC143C | + * | Cyan | #00FFFF | + * | DarkBlue | #00008B | + * | DarkCyan | #008B8B | + * | DarkGoldenRod | #B8860B | + * | DarkGray | #A9A9A9 | + * | DarkGrey | #A9A9A9 | + * | DarkGreen | #006400 | + * | DarkKhaki | #BDB76B | + * | DarkMagenta | #8B008B | + * | DarkOliveGreen | #556B2F | + * | DarkOrange | #FF8C00 | + * | DarkOrchid | #9932CC | + * | DarkRed | #8B0000 | + * | DarkSalmon | #E9967A | + * | DarkSeaGreen | #8FBC8F | + * | DarkSlateBlue | #483D8B | + * | DarkSlateGray | #2F4F4F | + * | DarkSlateGrey | #2F4F4F | + * | DarkTurquoise | #00CED1 | + * | DarkViolet | #9400D3 | + * | DeepPink | #FF1493 | + * | DeepSkyBlue | #00BFFF | + * | DimGray | #696969 | + * | DimGrey | #696969 | + * | DodgerBlue | #1E90FF | + * | FireBrick | #B22222 | + * | FloralWhite | #FFFAF0 | + * | ForestGreen | #228B22 | + * | Fuchsia | #FF00FF | + * | Gainsboro | #DCDCDC | + * | GhostWhite | #F8F8FF | + * | Gold | #FFD700 | + * | GoldenRod | #DAA520 | + * | Gray | #808080 | + * | Grey | #808080 | + * | Green | #008000 | + * | GreenYellow | #ADFF2F | + * | HoneyDew | #F0FFF0 | + * | HotPink | #FF69B4 | + * | IndianRed | #CD5C5C | + * | Indigo | #4B0082 | + * | Ivory | #FFFFF0 | + * | Khaki | #F0E68C | + * | Lavender | #E6E6FA | + * | LavenderBlush | #FFF0F5 | + * | LawnGreen | #7CFC00 | + * | LemonChiffon | #FFFACD | + * | LightBlue | #ADD8E6 | + * | LightCoral | #F08080 | + * | LightCyan | #E0FFFF | + * | LightGoldenRodYellow | #FAFAD2 | + * | LightGray | #D3D3D3 | + * | LightGrey | #D3D3D3 | + * | LightGreen | #90EE90 | + * | LightPink | #FFB6C1 | + * | LightSalmon | #FFA07A | + * | LightSeaGreen | #20B2AA | + * | LightSkyBlue | #87CEFA | + * | LightSlateGray | #778899 | + * | LightSlateGrey | #778899 | + * | LightSteelBlue | #B0C4DE | + * | LightYellow | #FFFFE0 | + * | Lime | #00FF00 | + * | LimeGreen | #32CD32 | + * | Linen | #FAF0E6 | + * | Magenta | #FF00FF | + * | Maroon | #800000 | + * | MediumAquaMarine | #66CDAA | + * | MediumBlue | #0000CD | + * | MediumOrchid | #BA55D3 | + * | MediumPurple | #9370DB | + * | MediumSeaGreen | #3CB371 | + * | MediumSlateBlue | #7B68EE | + * | MediumSpringGreen | #00FA9A | + * | MediumTurquoise | #48D1CC | + * | MediumVioletRed | #C71585 | + * | MidnightBlue | #191970 | + * | MintCream | #F5FFFA | + * | MistyRose | #FFE4E1 | + * | Moccasin | #FFE4B5 | + * | NavajoWhite | #FFDEAD | + * | Navy | #000080 | + * | OldLace | #FDF5E6 | + * | Olive | #808000 | + * | OliveDrab | #6B8E23 | + * | Orange | #FFA500 | + * | OrangeRed | #FF4500 | + * | Orchid | #DA70D6 | + * | PaleGoldenRod | #EEE8AA | + * | PaleGreen | #98FB98 | + * | PaleTurquoise | #AFEEEE | + * | PaleVioletRed | #DB7093 | + * | PapayaWhip | #FFEFD5 | + * | PeachPuff | #FFDAB9 | + * | Peru | #CD853F | + * | Pink | #FFC0CB | + * | Plum | #DDA0DD | + * | PowderBlue | #B0E0E6 | + * | Purple | #800080 | + * | RebeccaPurple | #663399 | + * | Red | #FF0000 | + * | RosyBrown | #BC8F8F | + * | RoyalBlue | #4169E1 | + * | SaddleBrown | #8B4513 | + * | Salmon | #FA8072 | + * | SandyBrown | #F4A460 | + * | SeaGreen | #2E8B57 | + * | SeaShell | #FFF5EE | + * | Sienna | #A0522D | + * | Silver | #C0C0C0 | + * | SkyBlue | #87CEEB | + * | SlateBlue | #6A5ACD | + * | SlateGray | #708090 | + * | SlateGrey | #708090 | + * | Snow | #FFFAFA | + * | SpringGreen | #00FF7F | + * | SteelBlue | #4682B4 | + * | Tan | #D2B48C | + * | Teal | #008080 | + * | Thistle | #D8BFD8 | + * | Tomato | #FF6347 | + * | Turquoise | #40E0D0 | + * | Violet | #EE82EE | + * | Wheat | #F5DEB3 | + * | White | #FFFFFF | + * | WhiteSmoke | #F5F5F5 | + * | Yellow | #FFFF00 | + * | YellowGreen | #9ACD32 | */ + interface Color {} + interface CompressImageOption { + /** 图片路径,图片的路径,支持本地路径、代码包路径 */ + src: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CompressImageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CompressImageFailCallback + /** 压缩质量,范围0~100,数值越小,质量越低,压缩率越高(仅对jpg有效)。 */ + quality?: number + /** 接口调用成功的回调函数 */ + success?: CompressImageSuccessCallback + } + interface CompressImageSuccessCallbackResult { + /** 压缩后图片的临时文件路径 (本地路径) */ + tempFilePath: string + errMsg: string + } + interface CompressVideoOption { + /** 码率,单位 kbps */ + bitrate: number + /** 帧率 */ + fps: number + /** 压缩质量 + * + * 可选值: + * - 'low': 低; + * - 'medium': 中; + * - 'high': 高; */ + quality: 'low' | 'medium' | 'high' + /** 相对于原视频的分辨率比例,取值范围(0, 1] */ + resolution: number + /** 视频文件路径,可以是临时文件路径也可以是永久文件路径 */ + src: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CompressVideoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CompressVideoFailCallback + /** 接口调用成功的回调函数 */ + success?: CompressVideoSuccessCallback + } + interface CompressVideoSuccessCallbackResult { + /** 压缩后的大小,单位 kB */ + size: string + /** 压缩后的临时文件地址 */ + tempFilePath: string + errMsg: string + } + interface ConnectOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ConnectCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ConnectFailCallback + /** 接口调用成功的回调函数 */ + success?: ConnectSuccessCallback + } + interface ConnectSocketOption { + /** 开发者服务器 wss 接口地址 */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ConnectSocketCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ConnectSocketFailCallback + /** HTTP Header,Header 中不能设置 Referer */ + header?: IAnyObject + /** 是否开启压缩扩展 + * + * 最低基础库: `2.8.0` */ + perMessageDeflate?: boolean + /** 子协议数组 + * + * 最低基础库: `1.4.0` */ + protocols?: string[] + /** 接口调用成功的回调函数 */ + success?: ConnectSocketSuccessCallback + /** 建立 TCP 连接的时候的 TCP_NODELAY 设置 + * + * 最低基础库: `2.4.0` */ + tcpNoDelay?: boolean + /** 超时时间,单位为毫秒 + * + * 最低基础库: `2.10.0` */ + timeout?: number + } + interface ConnectWifiOption { + /** Wi-Fi 设备 SSID */ + SSID: string + /** Wi-Fi 设备密码 */ + password: string + /** Wi-Fi 设备 BSSID */ + BSSID?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ConnectWifiCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ConnectWifiFailCallback + /** 跳转到系统设置页进行连接,仅安卓生效 + * + * 最低基础库: `2.12.0` */ + maunal?: boolean + /** 接口调用成功的回调函数 */ + success?: ConnectWifiSuccessCallback + } + interface ContextCallbackResult { + /** 节点对应的 Context 对象 */ + context: IAnyObject + } + interface CopyFileFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail permission denied, copyFile ${srcPath} -> ${destPath}': 指定目标文件路径没有写权限; + * - 'fail no such file or directory, copyFile ${srcPath} -> ${destPath}': 源文件不存在,或目标文件路径的上层目录不存在; + * - 'fail the maximum size of the file storage limit is exceeded': 存储空间不足; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface CopyFileOption { + /** 目标文件路径,支持本地路径 */ + destPath: string + /** 源文件路径,支持本地路径 */ + srcPath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CopyFileCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CopyFileFailCallback + /** 接口调用成功的回调函数 */ + success?: CopyFileSuccessCallback + } + interface CreateBLEConnectionOption { + /** 用于区分设备的 id */ + deviceId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CreateBLEConnectionCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CreateBLEConnectionFailCallback + /** 接口调用成功的回调函数 */ + success?: CreateBLEConnectionSuccessCallback + /** 超时时间,单位ms,不填表示不会超时 */ + timeout?: number + } + interface CreateBLEPeripheralServerOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: CreateBLEPeripheralServerCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: CreateBLEPeripheralServerFailCallback + /** 接口调用成功的回调函数 */ + success?: CreateBLEPeripheralServerSuccessCallback + } + interface CreateBLEPeripheralServerSuccessCallbackResult { + /** [BLEPeripheralServer](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.html) + * + * 外围设备的服务端。 */ + server: BLEPeripheralServer + errMsg: string + } + /** 选项 */ + interface CreateIntersectionObserverOption { + /** 初始的相交比例,如果调用时检测到的相交比例与这个值不相等且达到阈值,则会触发一次监听器的回调函数。 */ + initialRatio?: number + /** 是否同时观测多个目标节点(而非一个),如果设为 true ,observe 的 targetSelector 将选中多个节点(注意:同时选中过多节点将影响渲染性能) + * + * 最低基础库: `2.0.0` */ + observeAll?: boolean + /** 一个数值数组,包含所有阈值。 */ + thresholds?: number[] + } + interface CreateInterstitialAdOption { + /** 广告单元 id */ + adUnitId: string + } + interface CreateMediaRecorderOption { + /** 指定录制的时长(s),到达自动停止。最大 7200,最小 5 */ + duration?: number + /** 视频 fps */ + fps?: number + /** 视频关键帧间隔 */ + gop?: number + /** 视频比特率(kbps),最小值 600,最大值 3000 */ + videoBitsPerSecond?: number + } + interface CreateRewardedVideoAdOption { + /** 广告单元 id */ + adUnitId: string + /** 是否启用多例模式,默认为false + * + * 最低基础库: `2.8.0` */ + multiton?: boolean + } + /** 可选参数 */ + interface CreateWorkerOption { + /** 是否使用实验worker。在iOS下,实验worker的JS运行效率比非实验worker提升近十倍,如需在worker内进行重度计算的建议开启此选项。 + * + * 最低基础库: `2.13.0` */ + useExperimentalWorker?: boolean + } + /** 弹幕内容 */ + interface Danmu { + /** 弹幕文字 */ + text: string + /** 弹幕颜色 */ + color?: string + } + /** 可选的字体描述符 */ + interface DescOption { + /** 字体样式,可选值为 normal / italic / oblique */ + style?: string + /** 设置小型大写字母的字体显示文本,可选值为 normal / small-caps / inherit */ + variant?: string + /** 字体粗细,可选值为 normal / bold / 100 / 200../ 900 */ + weight?: string + } + /** 描述符的权限 */ + interface DescriptorPermission { + /** 读 */ + read?: boolean + /** 写 */ + write?: boolean + } + /** 指定 marker 移动到的目标点 */ + interface DestinationOption { + /** 纬度 */ + latitude: number + /** 经度 */ + longitude: number + } + interface DisableAlertBeforeUnloadOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: DisableAlertBeforeUnloadCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: DisableAlertBeforeUnloadFailCallback + /** 接口调用成功的回调函数 */ + success?: DisableAlertBeforeUnloadSuccessCallback + } + interface DownloadFileOption { + /** 下载资源的 url */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: DownloadFileCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: DownloadFileFailCallback + /** 指定文件下载后存储的路径 (本地路径) + * + * 最低基础库: `1.8.0` */ + filePath?: string + /** HTTP 请求的 Header,Header 中不能设置 Referer */ + header?: IAnyObject + /** 接口调用成功的回调函数 */ + success?: DownloadFileSuccessCallback + /** 超时时间,单位为毫秒 + * + * 最低基础库: `2.10.0` */ + timeout?: number + } + interface DownloadFileSuccessCallbackResult { + /** 用户文件路径 (本地路径)。传入 filePath 时会返回,跟传入的 filePath 一致 */ + filePath: string + /** 网络请求过程中一些调试信息 + * + * 最低基础库: `2.10.4` */ + profile: DownloadProfile + /** 开发者服务器返回的 HTTP 状态码 */ + statusCode: number + /** 临时文件路径 (本地路径)。没传入 filePath 指定文件存储路径时会返回,下载后的文件会存储到一个临时文件 */ + tempFilePath: string + errMsg: string + } + /** 网络请求过程中一些调试信息 + * + * 最低基础库: `2.10.4` */ + interface DownloadProfile { + /** SSL建立完成的时间,如果不是安全连接,则值为 0 */ + SSLconnectionEnd: number + /** SSL建立连接的时间,如果不是安全连接,则值为 0 */ + SSLconnectionStart: number + /** HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等。注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间。注意这里握手结束,包括安全连接建立完成、SOCKS 授权通过 */ + connectEnd: number + /** HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等。注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间 */ + connectStart: number + /** DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 */ + domainLookupEnd: number + /** DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 */ + domainLookupStart: number + /** 评估当前网络下载的kbps */ + downstreamThroughputKbpsEstimate: number + /** 评估的网络状态 slow 2g/2g/3g/4g */ + estimate_nettype: string + /** 组件准备好使用 HTTP 请求抓取资源的时间,这发生在检查本地缓存之前 */ + fetchStart: number + /** 协议层根据多个请求评估当前网络的 rtt(仅供参考) */ + httpRttEstimate: number + /** 当前请求的IP */ + peerIP: string + /** 当前请求的端口 */ + port: number + /** 收到字节数 */ + receivedBytedCount: number + /** 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0 */ + redirectEnd: number + /** 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0 */ + redirectStart: number + /** HTTP请求读取真实文档结束的时间 */ + requestEnd: number + /** HTTP请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存。连接错误重连时,这里显示的也是新建立连接的时间 */ + requestStart: number + /** HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存 */ + responseEnd: number + /** HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存 */ + responseStart: number + /** 当次请求连接过程中实时 rtt */ + rtt: number + /** 发送的字节数 */ + sendBytesCount: number + /** 是否复用连接 */ + socketReused: boolean + /** 当前网络的实际下载kbps */ + throughputKbps: number + /** 传输层根据多个请求评估的当前网络的 rtt(仅供参考) */ + transportRttEstimate: number + } + interface DownloadTaskOnProgressUpdateCallbackResult { + /** 下载进度百分比 */ + progress: number + /** 预期需要下载的数据总长度,单位 Bytes */ + totalBytesExpectedToWrite: number + /** 已经下载的数据长度,单位 Bytes */ + totalBytesWritten: number + } + interface EnableAlertBeforeUnloadOption { + /** 询问对话框内容 */ + message: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: EnableAlertBeforeUnloadCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: EnableAlertBeforeUnloadFailCallback + /** 接口调用成功的回调函数 */ + success?: EnableAlertBeforeUnloadSuccessCallback + } + interface ExitFullScreenOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ExitFullScreenCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ExitFullScreenFailCallback + /** 接口调用成功的回调函数 */ + success?: ExitFullScreenSuccessCallback + } + interface ExitPictureInPictureOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ExitPictureInPictureCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ExitPictureInPictureFailCallback + /** 接口调用成功的回调函数 */ + success?: ExitPictureInPictureSuccessCallback + } + interface ExitVoIPChatOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ExitVoIPChatCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ExitVoIPChatFailCallback + /** 接口调用成功的回调函数 */ + success?: ExitVoIPChatSuccessCallback + } + interface ExtractDataSourceOption { + /** 视频源地址,只支持本地文件 */ + source: string + } + interface Fields { + /** 指定样式名列表,返回节点对应样式名的当前值 + * + * 最低基础库: `2.1.0` */ + computedStyle?: string[] + /** 是否返回节点对应的 Context 对象 + * + * 最低基础库: `2.4.2` */ + context?: boolean + /** 是否返回节点 dataset */ + dataset?: boolean + /** 是否返回节点 id */ + id?: boolean + /** 是否返回节点 mark */ + mark?: boolean + /** 是否返回节点对应的 Node 实例 + * + * 最低基础库: `2.7.0` */ + node?: boolean + /** 指定属性名列表,返回节点对应属性名的当前属性值(只能获得组件文档中标注的常规属性值,id class style 和事件绑定的属性值不可获取) */ + properties?: string[] + /** 是否返回节点布局位置(`left` `right` `top` `bottom`) */ + rect?: boolean + /** 否 是否返回节点的 `scrollLeft` `scrollTop`,节点必须是 `scroll-view` 或者 `viewport` */ + scrollOffset?: boolean + /** 是否返回节点尺寸(`width` `height`) */ + size?: boolean + } + interface FileItem { + /** 文件保存时的时间戳,从1970/01/01 08:00:00 到当前时间的秒数 */ + createTime: number + /** 文件路径 (本地路径) */ + filePath: string + /** 本地文件大小,以字节为单位 */ + size: number + } + interface FileSystemManagerGetFileInfoOption { + /** 要读取的文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetFileInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: FileSystemManagerGetFileInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: FileSystemManagerGetFileInfoSuccessCallback + } + interface FileSystemManagerGetFileInfoSuccessCallbackResult { + /** 文件大小,以字节为单位 */ + size: number + errMsg: string + } + interface FileSystemManagerGetSavedFileListOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSavedFileListCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSavedFileListFailCallback + /** 接口调用成功的回调函数 */ + success?: FileSystemManagerGetSavedFileListSuccessCallback + } + interface FileSystemManagerGetSavedFileListSuccessCallbackResult { + /** 文件数组 */ + fileList: FileItem[] + errMsg: string + } + interface FileSystemManagerRemoveSavedFileOption { + /** 需要删除的文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveSavedFileCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: FileSystemManagerRemoveSavedFileFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveSavedFileSuccessCallback + } + interface FileSystemManagerSaveFileOption { + /** 临时存储文件路径 (本地路径) */ + tempFilePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SaveFileCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: FileSystemManagerSaveFileFailCallback + /** 要存储的文件路径 (本地路径) */ + filePath?: string + /** 接口调用成功的回调函数 */ + success?: SaveFileSuccessCallback + } + /** 打开的文件信息数组,只有从聊天素材场景打开(scene为1173)才会携带该参数 */ + interface ForwardMaterials { + /** 文件名 */ + name: string + /** 文件路径(如果是webview则是url) */ + path: string + /** 文件大小 */ + size: number + /** 文件的mimetype类型 */ + type: string + } + /** 视频帧数据,若取不到则返回 null。当缓冲区为空的时候可能暂停取不到数据。 */ + interface FrameDataOptions { + /** 帧数据 */ + data: ArrayBuffer + /** 帧数据高度 */ + height: number + /** 帧原始 dts */ + pkDts: number + /** 帧原始 pts */ + pkPts: number + /** 帧数据宽度 */ + width: number + } + interface FromScreenLocationOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: FromScreenLocationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: FromScreenLocationFailCallback + /** 接口调用成功的回调函数 */ + success?: FromScreenLocationSuccessCallback + } + interface GetAtqaOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetAtqaCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetAtqaFailCallback + /** 接口调用成功的回调函数 */ + success?: GetAtqaSuccessCallback + } + interface GetAtqaSuccessCallbackResult { + /** 返回 ATQA/SENS_RES 数据 */ + atqa: ArrayBuffer + errMsg: string + } + interface GetAvailableAudioSourcesOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetAvailableAudioSourcesCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetAvailableAudioSourcesFailCallback + /** 接口调用成功的回调函数 */ + success?: GetAvailableAudioSourcesSuccessCallback + } + interface GetAvailableAudioSourcesSuccessCallbackResult { + /** 支持的音频输入源列表,可在 [RecorderManager.start()](https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/RecorderManager.start.html) 接口中使用。返回值定义参考 https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource + * + * 可选值: + * - 'auto': 自动设置,默认使用手机麦克风,插上耳麦后自动切换使用耳机麦克风,所有平台适用; + * - 'buildInMic': 手机麦克风,仅限 iOS; + * - 'headsetMic': 耳机麦克风,仅限 iOS; + * - 'mic': 麦克风(没插耳麦时是手机麦克风,插耳麦时是耳机麦克风),仅限 Android; + * - 'camcorder': 同 mic,适用于录制音视频内容,仅限 Android; + * - 'voice_communication': 同 mic,适用于实时沟通,仅限 Android; + * - 'voice_recognition': 同 mic,适用于语音识别,仅限 Android; */ + audioSources: Array< + | 'auto' + | 'buildInMic' + | 'headsetMic' + | 'mic' + | 'camcorder' + | 'voice_communication' + | 'voice_recognition' + > + errMsg: string + } + interface GetBLEDeviceCharacteristicsOption { + /** 蓝牙设备 id */ + deviceId: string + /** 蓝牙服务 uuid,需要使用 `getBLEDeviceServices` 获取 */ + serviceId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBLEDeviceCharacteristicsCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBLEDeviceCharacteristicsFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBLEDeviceCharacteristicsSuccessCallback + } + interface GetBLEDeviceCharacteristicsSuccessCallbackResult { + /** 设备特征值列表 */ + characteristics: BLECharacteristic[] + errMsg: string + } + interface GetBLEDeviceRSSIOption { + /** 蓝牙设备 id */ + deviceId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBLEDeviceRSSICompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBLEDeviceRSSIFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBLEDeviceRSSISuccessCallback + } + interface GetBLEDeviceRSSISuccessCallbackResult { + /** 信号强度 */ + RSSI: number + errMsg: string + } + interface GetBLEDeviceServicesOption { + /** 蓝牙设备 id */ + deviceId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBLEDeviceServicesCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBLEDeviceServicesFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBLEDeviceServicesSuccessCallback + } + interface GetBLEDeviceServicesSuccessCallbackResult { + /** 设备服务列表 */ + services: BLEService[] + errMsg: string + } + interface GetBackgroundAudioPlayerStateOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBackgroundAudioPlayerStateCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBackgroundAudioPlayerStateFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBackgroundAudioPlayerStateSuccessCallback + } + interface GetBackgroundAudioPlayerStateSuccessCallbackResult { + /** 选定音频的播放位置(单位:s),只有在音乐播放中时返回 */ + currentPosition: number + /** 歌曲数据链接,只有在音乐播放中时返回 */ + dataUrl: string + /** 音频的下载进度百分比,只有在音乐播放中时返回 */ + downloadPercent: number + /** 选定音频的长度(单位:s),只有在音乐播放中时返回 */ + duration: number + /** 播放状态 + * + * 可选值: + * - 0: 暂停中; + * - 1: 播放中; + * - 2: 没有音乐播放; */ + status: 0 | 1 | 2 + errMsg: string + } + interface GetBackgroundFetchDataOption { + /** 取值为 periodic */ + fetchType: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBackgroundFetchDataCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBackgroundFetchDataFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBackgroundFetchDataSuccessCallback + } + interface GetBackgroundFetchTokenOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBackgroundFetchTokenCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBackgroundFetchTokenFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBackgroundFetchTokenSuccessCallback + } + interface GetBatteryInfoOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBatteryInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBatteryInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBatteryInfoSuccessCallback + } + interface GetBatteryInfoSuccessCallbackResult { + /** 是否正在充电中 */ + isCharging: boolean + /** 设备电量,范围 1 - 100 */ + level: string + errMsg: string + } + interface GetBatteryInfoSyncResult { + /** 是否正在充电中 */ + isCharging: boolean + /** 设备电量,范围 1 - 100 */ + level: string + } + interface GetBeaconsOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBeaconsCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBeaconsFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBeaconsSuccessCallback + } + interface GetBeaconsSuccessCallbackResult { + /** iBeacon 设备列表 */ + beacons: IBeaconInfo[] + errMsg: string + } + interface GetBluetoothAdapterStateOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBluetoothAdapterStateCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBluetoothAdapterStateFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBluetoothAdapterStateSuccessCallback + } + interface GetBluetoothAdapterStateSuccessCallbackResult { + /** 蓝牙适配器是否可用 */ + available: boolean + /** 是否正在搜索设备 */ + discovering: boolean + errMsg: string + } + interface GetBluetoothDevicesOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetBluetoothDevicesCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetBluetoothDevicesFailCallback + /** 接口调用成功的回调函数 */ + success?: GetBluetoothDevicesSuccessCallback + } + interface GetBluetoothDevicesSuccessCallbackResult { + /** uuid 对应的的已连接设备列表 */ + devices: BlueToothDevice[] + errMsg: string + } + interface GetCenterLocationOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetCenterLocationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetCenterLocationFailCallback + /** 接口调用成功的回调函数 */ + success?: GetCenterLocationSuccessCallback + } + interface GetCenterLocationSuccessCallbackResult { + /** 纬度 */ + latitude: number + /** 经度 */ + longitude: number + errMsg: string + } + interface GetClipboardDataOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetClipboardDataCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetClipboardDataFailCallback + /** 接口调用成功的回调函数 */ + success?: GetClipboardDataSuccessCallback + } + interface GetClipboardDataSuccessCallbackOption { + /** 剪贴板的内容 */ + data: string + } + interface GetConnectedBluetoothDevicesOption { + /** 蓝牙设备主 service 的 uuid 列表 */ + services: string[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetConnectedBluetoothDevicesCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetConnectedBluetoothDevicesFailCallback + /** 接口调用成功的回调函数 */ + success?: GetConnectedBluetoothDevicesSuccessCallback + } + interface GetConnectedBluetoothDevicesSuccessCallbackResult { + /** 搜索到的设备列表 */ + devices: BluetoothDeviceInfo[] + errMsg: string + } + interface GetConnectedWifiOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetConnectedWifiCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetConnectedWifiFailCallback + /** 接口调用成功的回调函数 */ + success?: GetConnectedWifiSuccessCallback + } + interface GetConnectedWifiSuccessCallbackResult { + /** [WifiInfo](https://developers.weixin.qq.com/miniprogram/dev/api/device/wifi/WifiInfo.html) + * + * Wi-Fi 信息 */ + wifi: WifiInfo + errMsg: string + } + interface GetContentsOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetContentsCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetContentsFailCallback + /** 接口调用成功的回调函数 */ + success?: GetContentsSuccessCallback + } + interface GetContentsSuccessCallbackResult { + /** 表示内容的delta对象 */ + delta: IAnyObject + /** 带标签的HTML内容 */ + html: string + /** 纯文本内容 */ + text: string + errMsg: string + } + interface GetExtConfigOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetExtConfigCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetExtConfigFailCallback + /** 接口调用成功的回调函数 */ + success?: GetExtConfigSuccessCallback + } + interface GetExtConfigSuccessCallbackResult { + /** 第三方平台自定义的数据 */ + extConfig: IAnyObject + errMsg: string + } + interface GetFileInfoFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail file not exist': 指定的 filePath 找不到文件; */ + errMsg: string + } + interface GetGroupEnterInfoOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetGroupEnterInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetGroupEnterInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: GetGroupEnterInfoSuccessCallback + } + interface GetGroupEnterInfoSuccessCallbackResult { + /** 敏感数据对应的云 ID,开通[云开发](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html)的小程序才会返回,可通过云调用直接获取开放数据,详细见[云调用直接获取开放数据](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html#method-cloud) + * + * 最低基础库: `2.7.0` */ + cloudID: string + /** 包括敏感数据在内的完整转发信息的加密数据,详细见[加密数据解密算法](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html) */ + encryptedData: string + /** 错误信息 */ + errMsg: string + /** 加密算法的初始向量,详细见[加密数据解密算法](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html) */ + iv: string + } + interface GetHCEStateOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetHCEStateCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetHCEStateFailCallback + /** 接口调用成功的回调函数 */ + success?: GetHCEStateSuccessCallback + } + interface GetHistoricalBytesOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetHistoricalBytesCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetHistoricalBytesFailCallback + /** 接口调用成功的回调函数 */ + success?: GetHistoricalBytesSuccessCallback + } + interface GetHistoricalBytesSuccessCallbackResult { + /** 返回历史二进制数据 */ + histBytes: ArrayBuffer + errMsg: string + } + interface GetImageInfoOption { + /** 图片的路径,支持网络路径、本地路径、代码包路径 */ + src: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetImageInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetImageInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: GetImageInfoSuccessCallback + } + interface GetImageInfoSuccessCallbackResult { + /** 图片原始高度,单位px。不考虑旋转。 */ + height: number + /** [拍照时设备方向](http://sylvana.net/jpegcrop/exif_orientation.html) + * + * 可选值: + * - 'up': 默认方向(手机横持拍照),对应 Exif 中的 1。或无 orientation 信息。; + * - 'up-mirrored': 同 up,但镜像翻转,对应 Exif 中的 2; + * - 'down': 旋转180度,对应 Exif 中的 3; + * - 'down-mirrored': 同 down,但镜像翻转,对应 Exif 中的 4; + * - 'left-mirrored': 同 left,但镜像翻转,对应 Exif 中的 5; + * - 'right': 顺时针旋转90度,对应 Exif 中的 6; + * - 'right-mirrored': 同 right,但镜像翻转,对应 Exif 中的 7; + * - 'left': 逆时针旋转90度,对应 Exif 中的 8; + * + * 最低基础库: `1.9.90` */ + orientation: + | 'up' + | 'up-mirrored' + | 'down' + | 'down-mirrored' + | 'left-mirrored' + | 'right' + | 'right-mirrored' + | 'left' + /** 图片的本地路径 */ + path: string + /** 图片格式 + * + * 最低基础库: `1.9.90` */ + type: string + /** 图片原始宽度,单位px。不考虑旋转。 */ + width: number + errMsg: string + } + interface GetLocationOption { + /** 传入 true 会返回高度信息,由于获取高度需要较高精确度,会减慢接口返回速度 + * + * 最低基础库: `1.6.0` */ + altitude?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetLocationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetLocationFailCallback + /** 高精度定位超时时间(ms),指定时间内返回最高精度,该值3000ms以上高精度定位才有效果 + * + * 最低基础库: `2.9.0` */ + highAccuracyExpireTime?: number + /** 开启高精度定位 + * + * 最低基础库: `2.9.0` */ + isHighAccuracy?: boolean + /** 接口调用成功的回调函数 */ + success?: GetLocationSuccessCallback + /** wgs84 返回 gps 坐标,gcj02 返回可用于 wx.openLocation 的坐标 */ + type?: string + } + interface GetLocationSuccessCallbackResult { + /** 位置的精确度 */ + accuracy: number + /** 高度,单位 m + * + * 最低基础库: `1.2.0` */ + altitude: number + /** 水平精度,单位 m + * + * 最低基础库: `1.2.0` */ + horizontalAccuracy: number + /** 纬度,范围为 -90~90,负数表示南纬 */ + latitude: number + /** 经度,范围为 -180~180,负数表示西经 */ + longitude: number + /** 速度,单位 m/s */ + speed: number + /** 垂直精度,单位 m(Android 无法获取,返回 0) + * + * 最低基础库: `1.2.0` */ + verticalAccuracy: number + errMsg: string + } + interface GetLogManagerOption { + /** 取值为0/1,取值为0表示是否会把 `App`、`Page` 的生命周期函数和 `wx` 命名空间下的函数调用写入日志,取值为1则不会。默认值是 0 + * + * 最低基础库: `2.3.2` */ + level?: number + } + interface GetMaxTransceiveLengthOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetMaxTransceiveLengthCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetMaxTransceiveLengthFailCallback + /** 接口调用成功的回调函数 */ + success?: GetMaxTransceiveLengthSuccessCallback + } + interface GetMaxTransceiveLengthSuccessCallbackResult { + /** 最大传输长度 */ + length: number + errMsg: string + } + interface GetNetworkTypeOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetNetworkTypeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetNetworkTypeFailCallback + /** 接口调用成功的回调函数 */ + success?: GetNetworkTypeSuccessCallback + } + interface GetNetworkTypeSuccessCallbackResult { + /** 网络类型 + * + * 可选值: + * - 'wifi': wifi 网络; + * - '2g': 2g 网络; + * - '3g': 3g 网络; + * - '4g': 4g 网络; + * - '5g': 5g 网络; + * - 'unknown': Android 下不常见的网络类型; + * - 'none': 无网络; */ + networkType: 'wifi' | '2g' | '3g' | '4g' | '5g' | 'unknown' | 'none' + errMsg: string + } + interface GetRandomValuesOption { + /** 整数,生成随机数的字节数,最大 1048576 */ + length: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetRandomValuesCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetRandomValuesFailCallback + /** 接口调用成功的回调函数 */ + success?: GetRandomValuesSuccessCallback + } + interface GetRandomValuesSuccessCallbackResult { + /** 随机数内容,长度为传入的字节数 */ + randomValues: ArrayBuffer + errMsg: string + } + interface GetRegionOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetRegionCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetRegionFailCallback + /** 接口调用成功的回调函数 */ + success?: GetRegionSuccessCallback + } + interface GetRegionSuccessCallbackResult { + /** 东北角经纬度 */ + northeast: MapPostion + /** 西南角经纬度 */ + southwest: MapPostion + errMsg: string + } + interface GetRotateOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetRotateCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetRotateFailCallback + /** 接口调用成功的回调函数 */ + success?: GetRotateSuccessCallback + } + interface GetRotateSuccessCallbackResult { + /** 旋转角 */ + rotate: number + errMsg: string + } + interface GetSakOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSakCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSakFailCallback + /** 接口调用成功的回调函数 */ + success?: GetSakSuccessCallback + } + interface GetSakSuccessCallbackResult { + /** 返回 SAK/SEL_RES 数据 */ + sak: number + errMsg: string + } + interface GetSavedFileInfoOption { + /** 文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSavedFileInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSavedFileInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: GetSavedFileInfoSuccessCallback + } + interface GetSavedFileInfoSuccessCallbackResult { + /** 文件保存时的时间戳,从1970/01/01 08:00:00 到该时刻的秒数 */ + createTime: number + /** 文件大小,单位 B */ + size: number + errMsg: string + } + interface GetScaleOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetScaleCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetScaleFailCallback + /** 接口调用成功的回调函数 */ + success?: GetScaleSuccessCallback + } + interface GetScaleSuccessCallbackResult { + /** 缩放值 */ + scale: number + errMsg: string + } + interface GetScreenBrightnessOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetScreenBrightnessCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetScreenBrightnessFailCallback + /** 接口调用成功的回调函数 */ + success?: GetScreenBrightnessSuccessCallback + } + interface GetScreenBrightnessSuccessCallbackOption { + /** 屏幕亮度值,范围 0 ~ 1,0 最暗,1 最亮 */ + value: number + } + interface GetSelectedTextRangeOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSelectedTextRangeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSelectedTextRangeFailCallback + /** 接口调用成功的回调函数 */ + success?: GetSelectedTextRangeSuccessCallback + } + interface GetSelectedTextRangeSuccessCallbackResult { + /** 输入框光标结束位置 */ + end: number + /** 输入框光标起始位置 */ + start: number + errMsg: string + } + interface GetSelectionTextOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSelectionTextCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSelectionTextFailCallback + /** 接口调用成功的回调函数 */ + success?: GetSelectionTextSuccessCallback + } + interface GetSelectionTextSuccessCallbackResult { + /** 纯文本内容 */ + text: string + errMsg: string + } + interface GetSettingOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSettingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSettingFailCallback + /** 接口调用成功的回调函数 */ + success?: GetSettingSuccessCallback + /** 是否同时获取用户订阅消息的订阅状态,默认不获取。注意:withSubscriptions 只返回用户勾选过订阅面板中的“总是保持以上选择,不再询问”的订阅消息。 + * + * 最低基础库: `2.10.1` */ + withSubscriptions?: boolean + } + interface GetSettingSuccessCallbackResult { + /** [AuthSetting](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/setting/AuthSetting.html) + * + * 用户授权结果 */ + authSetting: AuthSetting + /** [SubscriptionsSetting](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/setting/SubscriptionsSetting.html) + * + * 用户订阅消息设置,接口参数`withSubscriptions`值为`true`时才会返回。 + * + * 最低基础库: `2.10.1` */ + subscriptionsSetting: SubscriptionsSetting + /** [AuthSetting](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/setting/AuthSetting.html) + * + * 在插件中调用时,当前宿主小程序的用户授权结果 */ + miniprogramAuthSetting?: AuthSetting + errMsg: string + } + interface GetShareInfoOption { + /** shareTicket */ + shareTicket: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetShareInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetShareInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: GetShareInfoSuccessCallback + /** 超时时间,单位 ms + * + * 最低基础库: `1.9.90` */ + timeout?: number + } + interface GetSkewOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSkewCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSkewFailCallback + /** 接口调用成功的回调函数 */ + success?: GetSkewSuccessCallback + } + interface GetSkewSuccessCallbackResult { + /** 倾斜角 */ + skew: number + errMsg: string + } + interface GetStorageInfoOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetStorageInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetStorageInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: GetStorageInfoSuccessCallback + } + interface GetStorageInfoSuccessCallbackOption { + /** 当前占用的空间大小, 单位 KB */ + currentSize: number + /** 当前 storage 中所有的 key */ + keys: string[] + /** 限制的空间大小,单位 KB */ + limitSize: number + } + interface GetStorageInfoSyncOption { + /** 当前占用的空间大小, 单位 KB */ + currentSize: number + /** 当前 storage 中所有的 key */ + keys: string[] + /** 限制的空间大小,单位 KB */ + limitSize: number + } + interface GetStorageOption { + /** 本地缓存中指定的 key */ + key: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetStorageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetStorageFailCallback + /** 接口调用成功的回调函数 */ + success?: GetStorageSuccessCallback + } + interface GetStorageSuccessCallbackResult { + /** key对应的内容 */ + data: T + errMsg: string + } + interface GetSystemInfoAsyncOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSystemInfoAsyncCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSystemInfoAsyncFailCallback + /** 接口调用成功的回调函数 */ + success?: GetSystemInfoAsyncSuccessCallback + } + interface GetSystemInfoOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSystemInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSystemInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: GetSystemInfoSuccessCallback + } + interface GetUserInfoOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetUserInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetUserInfoFailCallback + /** 显示用户信息的语言 + * + * 可选值: + * - 'en': 英文; + * - 'zh_CN': 简体中文; + * - 'zh_TW': 繁体中文; */ + lang?: 'en' | 'zh_CN' | 'zh_TW' + /** 接口调用成功的回调函数 */ + success?: GetUserInfoSuccessCallback + /** 是否带上登录态信息。当 withCredentials 为 true 时,要求此前有调用过 wx.login 且登录态尚未过期,此时返回的数据会包含 encryptedData, iv 等敏感信息;当 withCredentials 为 false 时,不要求有登录态,返回的数据不包含 encryptedData, iv 等敏感信息。 */ + withCredentials?: boolean + } + interface GetUserInfoSuccessCallbackResult { + /** 敏感数据对应的云 ID,开通[云开发](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html)的小程序才会返回,可通过云调用直接获取开放数据,详细见[云调用直接获取开放数据](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html#method-cloud) + * + * 最低基础库: `2.7.0` */ + cloudID: string + /** 包括敏感数据在内的完整用户信息的加密数据,详见 [用户数据的签名验证和加解密]((signature#加密数据解密算法)) */ + encryptedData: string + /** 加密算法的初始向量,详见 [用户数据的签名验证和加解密]((signature#加密数据解密算法)) */ + iv: string + /** 不包括敏感信息的原始数据字符串,用于计算签名 */ + rawData: string + /** 使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息,详见 [用户数据的签名验证和加解密](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html) */ + signature: string + /** [UserInfo](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/UserInfo.html) + * + * 用户信息对象,不包含 openid 等敏感信息 */ + userInfo: UserInfo + errMsg: string + } + interface GetUserProfileOption { + /** 声明获取用户个人信息后的用途,不超过30个字符 */ + desc: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetUserProfileCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetUserProfileFailCallback + /** 显示用户信息的语言 + * + * 可选值: + * - 'en': 英文; + * - 'zh_CN': 简体中文; + * - 'zh_TW': 繁体中文; */ + lang?: 'en' | 'zh_CN' | 'zh_TW' + /** 接口调用成功的回调函数 */ + success?: GetUserProfileSuccessCallback + } + interface GetUserProfileSuccessCallbackResult { + /** [UserInfo](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/UserInfo.html) + * + * 用户信息对象 */ + userInfo: UserInfo + errMsg: string + } + interface GetVideoInfoOption { + /** 视频文件路径,可以是临时文件路径也可以是永久文件路径 */ + src: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetVideoInfoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetVideoInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: GetVideoInfoSuccessCallback + } + interface GetVideoInfoSuccessCallbackResult { + /** 视频码率,单位 kbps */ + bitrate: number + /** 视频长度 */ + duration: number + /** 视频帧率 */ + fps: number + /** 视频的长,单位 px */ + height: number + /** 画面方向 + * + * 可选值: + * - 'up': 默认; + * - 'down': 180度旋转; + * - 'left': 逆时针旋转90度; + * - 'right': 顺时针旋转90度; + * - 'up-mirrored': 同up,但水平翻转; + * - 'down-mirrored': 同down,但水平翻转; + * - 'left-mirrored': 同left,但垂直翻转; + * - 'right-mirrored': 同right,但垂直翻转; */ + orientation: + | 'up' + | 'down' + | 'left' + | 'right' + | 'up-mirrored' + | 'down-mirrored' + | 'left-mirrored' + | 'right-mirrored' + /** 视频大小,单位 kB */ + size: number + /** 视频格式 */ + type: string + /** 视频的宽,单位 px */ + width: number + errMsg: string + } + interface GetWeRunDataOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetWeRunDataCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetWeRunDataFailCallback + /** 接口调用成功的回调函数 */ + success?: GetWeRunDataSuccessCallback + } + interface GetWeRunDataSuccessCallbackResult { + /** 敏感数据对应的云 ID,开通云开发的小程序才会返回,可通过云调用直接获取开放数据,详细见[云调用直接获取开放数据](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html#method-cloud) + * + * 最低基础库: `2.7.0` */ + cloudID: string + /** 包括敏感数据在内的完整用户信息的加密数据,详细见[加密数据解密算法](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html)。解密后得到的数据结构见后文 */ + encryptedData: string + /** 加密算法的初始向量,详细见[加密数据解密算法](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html) */ + iv: string + errMsg: string + } + interface GetWifiListOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetWifiListCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetWifiListFailCallback + /** 接口调用成功的回调函数 */ + success?: GetWifiListSuccessCallback + } + interface HideHomeButtonOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideHomeButtonCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideHomeButtonFailCallback + /** 接口调用成功的回调函数 */ + success?: HideHomeButtonSuccessCallback + } + interface HideKeyboardOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideKeyboardCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideKeyboardFailCallback + /** 接口调用成功的回调函数 */ + success?: HideKeyboardSuccessCallback + } + interface HideLoadingOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideLoadingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideLoadingFailCallback + /** 接口调用成功的回调函数 */ + success?: HideLoadingSuccessCallback + } + interface HideNavigationBarLoadingOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideNavigationBarLoadingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideNavigationBarLoadingFailCallback + /** 接口调用成功的回调函数 */ + success?: HideNavigationBarLoadingSuccessCallback + } + interface HideShareMenuOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideShareMenuCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideShareMenuFailCallback + /** 本接口为 Beta 版本,暂只在 Android 平台支持。需要隐藏的转发按钮名称列表,默认['shareAppMessage', 'shareTimeline']。按钮名称合法值包含 "shareAppMessage"、"shareTimeline" 两种 + * + * 最低基础库: `2.11.3` */ + menus?: string[] + /** 接口调用成功的回调函数 */ + success?: HideShareMenuSuccessCallback + } + interface HideTabBarOption { + /** 是否需要动画效果 */ + animation?: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideTabBarCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideTabBarFailCallback + /** 接口调用成功的回调函数 */ + success?: HideTabBarSuccessCallback + } + interface HideTabBarRedDotOption { + /** tabBar 的哪一项,从左边算起 */ + index: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideTabBarRedDotCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideTabBarRedDotFailCallback + /** 接口调用成功的回调函数 */ + success?: HideTabBarRedDotSuccessCallback + } + interface HideToastOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideToastCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideToastFailCallback + /** 接口调用成功的回调函数 */ + success?: HideToastSuccessCallback + } + interface IBeaconInfo { + /** iBeacon 设备的距离 */ + accuracy: number + /** iBeacon 设备的主 id */ + major: string + /** iBeacon 设备的次 id */ + minor: string + /** 表示设备距离的枚举值 */ + proximity: number + /** 表示设备的信号强度 */ + rssi: number + /** iBeacon 设备广播的 uuid */ + uuid: string + } + /** 图片对象 + * + * 最低基础库: `2.7.0` */ + interface Image { + /** 图片的真实高度 */ + height: number + /** 图片加载发生错误后触发的回调函数 */ + onerror: (...args: any[]) => any + /** 图片加载完成后触发的回调函数 */ + onload: (...args: any[]) => any + /** 图片的 URL。v2.11.0 起支持传递 base64 Data URI */ + src: string + /** 图片的真实宽度 */ + width: number + } + /** ImageData 对象 + * + * 最低基础库: `2.9.0` */ + interface ImageData { + /** 一维数组,包含以 RGBA 顺序的数据,数据使用 0 至 255(包含)的整数表示 */ + data: Uint8ClampedArray + /** 使用像素描述 ImageData 的实际高度 */ + height: number + /** 使用像素描述 ImageData 的实际宽度 */ + width: number + } + /** 图片的本地临时文件列表 + * + * 最低基础库: `1.2.0` */ + interface ImageFile { + /** 本地临时文件路径 (本地路径) */ + path: string + /** 本地临时文件大小,单位 B */ + size: number + } + interface IncludePointsOption { + /** 要显示在可视区域内的坐标点列表 */ + points: MapPostion[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: IncludePointsCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: IncludePointsFailCallback + /** 坐标点形成的矩形边缘到地图边缘的距离,单位像素。格式为[上,右,下,左],安卓上只能识别数组第一项,上下左右的padding一致。开发者工具暂不支持padding参数。 */ + padding?: number[] + /** 接口调用成功的回调函数 */ + success?: IncludePointsSuccessCallback + } + interface InitMarkerClusterOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: InitMarkerClusterCompleteCallback + /** 启用默认的聚合样式 */ + enableDefaultStyle?: boolean + /** 接口调用失败的回调函数 */ + fail?: InitMarkerClusterFailCallback + /** 聚合算法的可聚合距离,即距离小于该值的点会聚合至一起,以像素为单位 */ + gridSize?: boolean + /** 接口调用成功的回调函数 */ + success?: InitMarkerClusterSuccessCallback + /** 点击已经聚合的标记点时是否实现聚合分离 */ + zoomOnClick?: boolean + } + /** InnerAudioContext 实例,可通过 [wx.createInnerAudioContext](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/wx.createInnerAudioContext.html) 接口获取实例。注意,音频播放过程中,可能被系统中断,可通过 [wx.onAudioInterruptionBegin](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onAudioInterruptionBegin.html)、[wx.onAudioInterruptionEnd](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onAudioInterruptionEnd.html)事件来处理这种情况。 +* +* **支持格式** +* +* +* | 格式 | iOS | Android | +* | ---- | ---- | ------- | +* | flac | x | √ | +* | m4a | √ | √ | +* | ogg | x | √ | +* | ape | x | √ | +* | amr | x | √ | +* | wma | x | √ | +* | wav | √ | √ | +* | mp3 | √ | √ | +* | mp4 | x | √ | +* | aac | √ | √ | +* | aiff | √ | x | +* | caf | √ | x | +* +* **示例代码** +* +* +* ```js +const innerAudioContext = wx.createInnerAudioContext() +innerAudioContext.autoplay = true +innerAudioContext.src = 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E061FF02C31F716658E5C81F5594D561F2E88B854E81CAAB7806D5E4F103E55D33C16F3FAC506D1AB172DE8600B37E43FAD&fromtag=46' +innerAudioContext.onPlay(() => { + console.log('开始播放') +}) +innerAudioContext.onError((res) => { + console.log(res.errMsg) + console.log(res.errCode) +}) +``` */ + interface InnerAudioContext { + /** 是否自动开始播放,默认为 `false` */ + autoplay: boolean + /** 音频缓冲的时间点,仅保证当前播放时间点到此时间点内容已缓冲(只读) */ + buffered: number + /** 当前音频的播放位置(单位 s)。只有在当前有合法的 src 时返回,时间保留小数点后 6 位(只读) */ + currentTime: number + /** 当前音频的长度(单位 s)。只有在当前有合法的 src 时返回(只读) */ + duration: number + /** 是否循环播放,默认为 `false` */ + loop: boolean + /** 是否遵循系统静音开关,默认为 `true`。当此参数为 `false` 时,即使用户打开了静音开关,也能继续发出声音。从 2.3.0 版本开始此参数不生效,使用 [wx.setInnerAudioOption](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/wx.setInnerAudioOption.html) 接口统一设置。 */ + obeyMuteSwitch: boolean + /** 当前是是否暂停或停止状态(只读) */ + paused: boolean + /** 播放速度。范围 0.5-2.0,默认为 1。(Android 需要 6 及以上版本) + * + * 最低基础库: `2.11.0` */ + playbackRate: number + /** 音频资源的地址,用于直接播放。[2.2.3](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 开始支持云文件ID */ + src: string + /** 开始播放的位置(单位:s),默认为 0 */ + startTime: number + /** 音量。范围 0~1。默认为 1 + * + * 最低基础库: `1.9.90` */ + volume: number + } + interface InnerAudioContextOnErrorCallbackResult { + /** + * + * 可选值: + * - 10001: 系统错误; + * - 10002: 网络错误; + * - 10003: 文件错误; + * - 10004: 格式错误; + * - -1: 未知错误; */ + errCode: 10001 | 10002 | 10003 | 10004 | -1 + errMsg: string + } + interface InsertDividerOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: InsertDividerCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: InsertDividerFailCallback + /** 接口调用成功的回调函数 */ + success?: InsertDividerSuccessCallback + } + interface InsertImageOption { + /** 图片地址,仅支持 http(s)、base64、云图片(2.8.0)、临时文件(2.8.3)。 */ + src: string + /** 图像无法显示时的替代文本 */ + alt?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: InsertImageCompleteCallback + /** data 被序列化为 name=value;name1=value2 的格式挂在属性 data-custom 上 */ + data?: IAnyObject + /** 添加到图片 img 标签上的类名 */ + extClass?: string + /** 接口调用失败的回调函数 */ + fail?: InsertImageFailCallback + /** 图片高度 (pixels/百分比) */ + height?: string + /** 接口调用成功的回调函数 */ + success?: InsertImageSuccessCallback + /** 图片宽度(pixels/百分比) */ + width?: string + } + interface InsertTextOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: InsertTextCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: InsertTextFailCallback + /** 接口调用成功的回调函数 */ + success?: InsertTextSuccessCallback + /** 文本内容 */ + text?: string + } + interface IntersectionObserverObserveCallbackResult { + /** 目标边界 */ + boundingClientRect: BoundingClientRectResult + /** 相交比例 */ + intersectionRatio: number + /** 相交区域的边界 */ + intersectionRect: IntersectionRectResult + /** 参照区域的边界 */ + relativeRect: RelativeRectResult + /** 相交检测时的时间戳 */ + time: number + } + /** 相交区域的边界 */ + interface IntersectionRectResult { + /** 下边界 */ + bottom: number + /** 高度 */ + height: number + /** 左边界 */ + left: number + /** 右边界 */ + right: number + /** 上边界 */ + top: number + /** 宽度 */ + width: number + } + interface InterstitialAdOnErrorCallbackResult { + /** 错误码 + * + * 可选值: + * - 1000: 后端接口调用失败; + * - 1001: 参数错误; + * - 1002: 广告单元无效; + * - 1003: 内部错误; + * - 1004: 无合适的广告; + * - 1005: 广告组件审核中; + * - 1006: 广告组件被驳回; + * - 1007: 广告组件被封禁; + * - 1008: 广告单元已关闭; */ + errCode: 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 + /** 错误信息 */ + errMsg: string + } + interface IsConnectedOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: IsConnectedCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: IsConnectedFailCallback + /** 接口调用成功的回调函数 */ + success?: IsConnectedSuccessCallback + } + interface JoinVoIPChatOption { + /** 小游戏内此房间/群聊的 ID。同一时刻传入相同 groupId 的用户会进入到同个实时语音房间。 */ + groupId: string + /** 验证所需的随机字符串 */ + nonceStr: string + /** 签名,用于验证小游戏的身份 */ + signature: string + /** 验证所需的时间戳 */ + timeStamp: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: JoinVoIPChatCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: JoinVoIPChatFailCallback + /** 静音设置 */ + muteConfig?: MuteConfig + /** 房间类型 + * + * 可选值: + * - 'voice': 音频房间,用于语音通话; + * - 'video': 视频房间,结合 [voip-room](https://developers.weixin.qq.com/miniprogram/dev/component/voip-room.html) 组件可显示成员画面; */ + roomType?: 'voice' | 'video' + /** 接口调用成功的回调函数 */ + success?: JoinVoIPChatSuccessCallback + } + interface JoinVoIPChatSuccessCallbackResult { + /** 错误码 */ + errCode: number + /** 调用结果 */ + errMsg: string + /** 在此通话中的成员 openId 名单 */ + openIdList: string[] + } + /** 启动参数 */ + interface LaunchOptionsApp { + /** 打开的文件信息数组,只有从聊天素材场景打开(scene为1173)才会携带该参数 */ + forwardMaterials: ForwardMaterials[] + /** 启动小程序的路径 (代码包路径) */ + path: string + /** 启动小程序的 query 参数 */ + query: IAnyObject + /** 来源信息。从另一个小程序、公众号或 App 进入小程序时返回。否则返回 `{}`。(参见后文注意) */ + referrerInfo: ReferrerInfo + /** 启动小程序的[场景值](https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/scene.html) */ + scene: number + /** shareTicket,详见[获取更多转发信息](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html) */ + shareTicket?: string + } + interface LivePlayerContextRequestFullScreenOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RequestFullScreenCompleteCallback + /** 设置全屏时的方向 + * + * 可选值: + * - 0: 正常竖向; + * - 90: 屏幕逆时针90度; + * - -90: 屏幕顺时针90度; */ + direction?: 0 | 90 | -90 + /** 接口调用失败的回调函数 */ + fail?: RequestFullScreenFailCallback + /** 接口调用成功的回调函数 */ + success?: RequestFullScreenSuccessCallback + } + interface LivePlayerContextSnapshotOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SnapshotCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SnapshotFailCallback + /** 图片的质量,默认原图。有效值为 raw、compressed + * + * 最低基础库: `2.10.0` */ + quality?: string + /** 接口调用成功的回调函数 */ + success?: LivePlayerContextSnapshotSuccessCallback + } + interface LivePlayerContextSnapshotSuccessCallbackResult { + /** 图片的高度 */ + height: string + /** 图片文件的临时路径 (本地路径) */ + tempImagePath: string + /** 图片的宽度 */ + width: string + errMsg: string + } + interface LivePusherContextSnapshotOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SnapshotCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SnapshotFailCallback + /** 图片的质量,默认原图。有效值为 raw、compressed + * + * 最低基础库: `2.10.0` */ + quality?: string + /** 接口调用成功的回调函数 */ + success?: LivePusherContextSnapshotSuccessCallback + } + interface LivePusherContextSnapshotSuccessCallbackResult { + /** 图片的高度 */ + height: string + /** 图片文件的临时路径 */ + tempImagePath: string + /** 图片的宽度 */ + width: string + errMsg: string + } + interface LoadFontFaceCompleteCallbackResult { + /** 加载字体结果 */ + status: string + } + interface LoadFontFaceOption { + /** 定义的字体名称 */ + family: string + /** 字体资源的地址。建议格式为 TTF 和 WOFF,WOFF2 在低版本的iOS上会不兼容。 */ + source: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: LoadFontFaceCompleteCallback + /** 可选的字体描述符 */ + desc?: DescOption + /** 接口调用失败的回调函数 */ + fail?: LoadFontFaceFailCallback + /** 是否全局生效 + * + * 最低基础库: `2.10.0` */ + global?: boolean + /** 字体作用范围,可选值为 webview / native,默认 webview,设置 native 可在 Canvas 2D 下使用 */ + scopes?: any[] + /** 接口调用成功的回调函数 */ + success?: LoadFontFaceSuccessCallback + } + interface LoginOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: LoginCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: LoginFailCallback + /** 接口调用成功的回调函数 */ + success?: LoginSuccessCallback + /** 超时时间,单位ms + * + * 最低基础库: `1.9.90` */ + timeout?: number + } + interface LoginSuccessCallbackResult { + /** 用户登录凭证(有效期五分钟)。开发者需要在开发者服务器后台调用 [auth.code2Session](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html),使用 code 换取 openid 和 session_key 等信息 */ + code: string + errMsg: string + } + interface MakeBluetoothPairOption { + /** 蓝牙设备 id */ + deviceId: string + /** pin 码,Base64 格式。 */ + pin: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: MakeBluetoothPairCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: MakeBluetoothPairFailCallback + /** 接口调用成功的回调函数 */ + success?: MakeBluetoothPairSuccessCallback + /** 超时时间 */ + timeout?: number + } + interface MakePhoneCallOption { + /** 需要拨打的电话号码 */ + phoneNumber: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: MakePhoneCallCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: MakePhoneCallFailCallback + /** 接口调用成功的回调函数 */ + success?: MakePhoneCallSuccessCallback + } + /** 广播的制造商信息, 仅安卓支持 */ + interface ManufacturerData { + /** 制造商ID,0x 开头的十六进制 */ + manufacturerId: string + /** 制造商信息 */ + manufacturerSpecificData?: ArrayBuffer + } + /** 图片覆盖的经纬度范围 */ + interface MapBounds { + /** 东北角经纬度 */ + northeast: MapPostion + /** 西南角经纬度 */ + southwest: MapPostion + } + interface MapPostion { + /** 纬度 */ + latitude: number + /** 经度 */ + longitude: number + } + /** 用来扩展(或收缩)参照节点布局区域的边界 */ + interface Margins { + /** 节点布局区域的下边界 */ + bottom?: number + /** 节点布局区域的左边界 */ + left?: number + /** 节点布局区域的右边界 */ + right?: number + /** 节点布局区域的上边界 */ + top?: number + } + /** MediaAudioPlayer 实例,可通过 [wx.createMediaAudioPlayer](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/wx.createMediaAudioPlayer.html) 接口获取实例。 */ + interface MediaAudioPlayer { + /** 音量。范围 0~1。默认为 1 */ + volume: number + } + /** 本地临时文件列表 */ + interface MediaFile { + /** 视频的时间长度 */ + duration: number + /** 视频的高度 */ + height: number + /** 本地临时文件大小,单位 B */ + size: number + /** 本地临时文件路径 (本地路径) */ + tempFilePath: string + /** 视频缩略图临时文件路径 */ + thumbTempFilePath: string + /** 视频的宽度 */ + width: number + } + interface MediaQueryObserverObserveCallbackResult { + /** 页面的当前状态是否满足所指定的 media query */ + matches: boolean + } + /** 需要预览的资源列表 */ + interface MediaSource { + /** 图片或视频的地址 */ + url: string + /** 视频的封面图片 */ + poster?: string + /** 资源的类型,默认为图片 + * + * 可选值: + * - 'image': 图片; + * - 'video': 视频; */ + type?: 'image' | 'video' + } + /** 可通过 [MediaContainer.extractDataSource](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaContainer.extractDataSource.html) 返回。 + * + * [MediaTrack](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaTrack.html) 音频或视频轨道,可以对轨道进行一些操作 + * + * 最低基础库: `2.9.0` */ + interface MediaTrack { + /** 轨道长度,只读 */ + duration: number + /** 轨道类型,只读 + * + * 可选值: + * - 'audio': 音频轨道; + * - 'video': 视频轨道; */ + kind: 'audio' | 'video' + /** 音量,音频轨道下有效,可写 */ + volume: number + } + /** 小程序帐号信息 */ + interface MiniProgram { + /** 小程序 appId */ + appId: string + /** 小程序版本 + * + * 可选值: + * - 'develop': 开发版; + * - 'trial': 体验版; + * - 'release': 正式版; + * + * 最低基础库: `2.10.0` */ + envVersion: 'develop' | 'trial' | 'release' + /** 线上小程序版本号 + * + * 最低基础库: `2.10.2` */ + version: string + } + interface MkdirFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail no such file or directory ${dirPath}': 上级目录不存在; + * - 'fail permission denied, open ${dirPath}': 指定的 filePath 路径没有写权限; + * - 'fail file already exists ${dirPath}': 有同名文件或目录; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface MkdirOption { + /** 创建的目录路径 (本地路径) */ + dirPath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: MkdirCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: MkdirFailCallback + /** 是否在递归创建该目录的上级目录后再创建该目录。如果对应的上级目录已经存在,则不创建该上级目录。如 dirPath 为 a/b/c/d 且 recursive 为 true,将创建 a 目录,再在 a 目录下创建 b 目录,以此类推直至创建 a/b/c 目录下的 d 目录。 + * + * 最低基础库: `2.3.0` */ + recursive?: boolean + /** 接口调用成功的回调函数 */ + success?: MkdirSuccessCallback + } + interface MoveAlongOption { + /** 平滑移动的时间 */ + duration: number + /** 指定 marker */ + markerId: number + /** 移动路径的坐标串,坐标点格式 `{longitude, latitude}` */ + path: any[] + /** 根据路径方向自动改变 marker 的旋转角度 */ + autoRotate?: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: MoveAlongCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: MoveAlongFailCallback + /** 接口调用成功的回调函数 */ + success?: MoveAlongSuccessCallback + } + interface MoveToLocationOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: MoveToLocationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: MoveToLocationFailCallback + /** 纬度 + * + * 最低基础库: `2.8.0` */ + latitude?: number + /** 经度 + * + * 最低基础库: `2.8.0` */ + longitude?: number + /** 接口调用成功的回调函数 */ + success?: MoveToLocationSuccessCallback + } + /** 静音设置 */ + interface MuteConfig { + /** 是否静音耳机 */ + muteEarphone?: boolean + /** 是否静音麦克风 */ + muteMicrophone?: boolean + } + interface MuteOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: MuteCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: MuteFailCallback + /** 接口调用成功的回调函数 */ + success?: MuteSuccessCallback + } + /** + * + * 最低基础库: `2.11.2` */ + interface NFCAdapter { + /** 标签类型枚举 */ + tech: TechType + } + interface NavigateBackMiniProgramOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: NavigateBackMiniProgramCompleteCallback + /** 需要返回给上一个小程序的数据,上一个小程序可在 `App.onShow` 中获取到这份数据。 [详情](https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html)。 */ + extraData?: IAnyObject + /** 接口调用失败的回调函数 */ + fail?: NavigateBackMiniProgramFailCallback + /** 接口调用成功的回调函数 */ + success?: NavigateBackMiniProgramSuccessCallback + } + interface NavigateBackOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: NavigateBackCompleteCallback + /** 返回的页面数,如果 delta 大于现有页面数,则返回到首页。 */ + delta?: number + /** 接口调用失败的回调函数 */ + fail?: NavigateBackFailCallback + /** 接口调用成功的回调函数 */ + success?: NavigateBackSuccessCallback + } + interface NavigateToMiniProgramOption { + /** 要打开的小程序 appId */ + appId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: NavigateToMiniProgramCompleteCallback + /** 要打开的小程序版本。仅在当前小程序为开发版或体验版时此参数有效。如果当前小程序是正式版,则打开的小程序必定是正式版。 + * + * 可选值: + * - 'develop': 开发版; + * - 'trial': 体验版; + * - 'release': 正式版; */ + envVersion?: 'develop' | 'trial' | 'release' + /** 需要传递给目标小程序的数据,目标小程序可在 `App.onLaunch`,`App.onShow` 中获取到这份数据。如果跳转的是小游戏,可以在 [wx.onShow](#)、[wx.getLaunchOptionsSync](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/life-cycle/wx.getLaunchOptionsSync.html) 中可以获取到这份数据数据。 */ + extraData?: IAnyObject + /** 接口调用失败的回调函数 */ + fail?: NavigateToMiniProgramFailCallback + /** 打开的页面路径,如果为空则打开首页。path 中 ? 后面的部分会成为 query,在小程序的 `App.onLaunch`、`App.onShow` 和 `Page.onLoad` 的回调函数或小游戏的 [wx.onShow](#) 回调函数、[wx.getLaunchOptionsSync](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/life-cycle/wx.getLaunchOptionsSync.html) 中可以获取到 query 数据。对于小游戏,可以只传入 query 部分,来实现传参效果,如:传入 "?foo=bar"。 */ + path?: string + /** 接口调用成功的回调函数 */ + success?: NavigateToMiniProgramSuccessCallback + } + interface NavigateToOption { + /** 需要跳转的应用内非 tabBar 的页面的路径 (代码包路径), 路径后可以带参数。参数与路径之间使用 `?` 分隔,参数键与参数值用 `=` 相连,不同参数用 `&` 分隔;如 'path?key=value&key2=value2' */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: NavigateToCompleteCallback + /** 页面间通信接口,用于监听被打开页面发送到当前页面的数据。基础库 2.7.3 开始支持。 */ + events?: IAnyObject + /** 接口调用失败的回调函数 */ + fail?: NavigateToFailCallback + /** 接口调用成功的回调函数 */ + success?: NavigateToSuccessCallback + } + interface NavigateToSuccessCallbackResult { + /** [EventChannel](https://developers.weixin.qq.com/miniprogram/dev/api/route/EventChannel.html) + * + * 和被打开页面进行通信 */ + eventChannel: EventChannel + errMsg: string + } + interface NdefCloseOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: NdefCloseCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: NdefCloseFailCallback + /** 接口调用成功的回调函数 */ + success?: NdefCloseSuccessCallback + } + interface NodeCallbackResult { + /** 节点对应的 Node 实例 */ + node: IAnyObject + } + interface NotifyBLECharacteristicValueChangeOption { + /** 蓝牙特征值的 uuid */ + characteristicId: string + /** 蓝牙设备 id */ + deviceId: string + /** 蓝牙特征值对应服务的 uuid */ + serviceId: string + /** 是否启用 notify */ + state: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: NotifyBLECharacteristicValueChangeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: NotifyBLECharacteristicValueChangeFailCallback + /** 接口调用成功的回调函数 */ + success?: NotifyBLECharacteristicValueChangeSuccessCallback + } + /** media query 描述符 */ + interface ObserveDescriptor { + /** 页面高度( px 为单位) */ + height: number + /** 页面最大高度( px 为单位) */ + maxHeight: number + /** 页面最大宽度( px 为单位) */ + maxWidth: number + /** 页面最小高度( px 为单位) */ + minHeight: number + /** 页面最小宽度( px 为单位) */ + minWidth: number + /** 屏幕方向( `landscape` 或 `portrait` ) */ + orientation: string + /** 页面宽度( px 为单位) */ + width: number + } + interface OnAccelerometerChangeCallbackResult { + /** X 轴 */ + x: number + /** Y 轴 */ + y: number + /** Z 轴 */ + z: number + } + interface OnAppShowCallbackResult { + /** 打开的文件信息数组,只有从聊天素材场景打开(scene为1173)才会携带该参数 */ + forwardMaterials: ForwardMaterials[] + /** 小程序切前台的路径 (代码包路径) */ + path: string + /** 小程序切前台的 query 参数 */ + query: IAnyObject + /** 来源信息。从另一个小程序、公众号或 App 进入小程序时返回。否则返回 `{}`。(参见后文注意) */ + referrerInfo: ReferrerInfo + /** 小程序切前台的[场景值](https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/scene.html) */ + scene: number + /** shareTicket,详见[获取更多转发信息](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html) */ + shareTicket?: string + } + interface OnBLECharacteristicValueChangeCallbackResult { + /** 蓝牙特征值的 uuid */ + characteristicId: string + /** 蓝牙设备 id */ + deviceId: string + /** 蓝牙特征值对应服务的 uuid */ + serviceId: string + /** 特征值最新的值 */ + value: ArrayBuffer + } + interface OnBLEConnectionStateChangeCallbackResult { + /** 是否处于已连接状态 */ + connected: boolean + /** 蓝牙设备ID */ + deviceId: string + } + interface OnBLEPeripheralConnectionStateChangedCallbackResult { + /** 连接目前状态 */ + connected: boolean + /** 连接状态变化的设备 id */ + deviceId: string + /** server 的 uuid */ + serverId: string + } + interface OnBackgroundFetchDataCallbackResult { + /** 缓存数据类别 (periodic) */ + fetchType: string + /** 缓存数据 */ + fetchedData: string + /** 客户端拿到缓存数据的时间戳 */ + timeStamp: number + } + interface OnBeaconServiceChangeCallbackResult { + /** 服务目前是否可用 */ + available: boolean + /** 目前是否处于搜索状态 */ + discovering: boolean + } + interface OnBeaconUpdateCallbackResult { + /** 当前搜寻到的所有 iBeacon 设备列表 */ + beacons: IBeaconInfo[] + } + interface OnBluetoothAdapterStateChangeCallbackResult { + /** 蓝牙适配器是否可用 */ + available: boolean + /** 蓝牙适配器是否处于搜索状态 */ + discovering: boolean + } + interface OnBluetoothDeviceFoundCallbackResult { + /** 新搜索到的设备列表 */ + devices: BlueToothDevice[] + } + interface OnCameraFrameCallbackResult { + /** 图像像素点数据,一维数组,每四项表示一个像素点的 rgba */ + data: ArrayBuffer + /** 图像数据矩形的高度 */ + height: number + /** 图像数据矩形的宽度 */ + width: number + } + interface OnCharacteristicReadRequestCallbackResult { + /** 唯一标识码,调用 writeCharacteristicValue 时使用 */ + callbackId: number + /** characteristic对应的uuid */ + characteristicId: string + /** service对应的uuid */ + serviceId: string + } + interface OnCharacteristicSubscribedCallbackResult { + /** characteristic对应的uuid */ + characteristicId: string + /** service对应的uuid */ + serviceId: string + } + interface OnCharacteristicWriteRequestCallbackResult { + /** 唯一标识码,调用 writeCharacteristicValue 时使用 */ + callbackId: number + /** characteristic对应的uuid */ + characteristicId: string + /** service对应的uuid */ + serviceId: string + /** 请求写入的特征值数据 */ + value: ArrayBuffer + } + interface OnCheckForUpdateCallbackResult { + /** 是否有新版本 */ + hasUpdate: boolean + } + interface OnCompassChangeCallbackResult { + /** 精度 + * + * 最低基础库: `2.4.0` */ + accuracy: number | string + /** 面对的方向度数 */ + direction: number + } + interface OnCopyUrlCallbackResult { + /** 用短链打开小程序时当前页面携带的查询字符串。小程序中使用时,应在进入页面时调用 `wx.onCopyUrl` 自定义 `query`,退出页面时调用 `wx.offCopyUrl`,防止影响其它页面。 */ + query: string + } + interface OnDeviceMotionChangeCallbackResult { + /** 当 手机坐标 X/Y 和 地球 X/Y 重合时,绕着 Z 轴转动的夹角为 alpha,范围值为 [0, 2*PI)。逆时针转动为正。 */ + alpha: number + /** 当手机坐标 Y/Z 和地球 Y/Z 重合时,绕着 X 轴转动的夹角为 beta。范围值为 [-1*PI, PI) 。顶部朝着地球表面转动为正。也有可能朝着用户为正。 */ + beta: number + /** 当手机 X/Z 和地球 X/Z 重合时,绕着 Y 轴转动的夹角为 gamma。范围值为 [-1*PI/2, PI/2)。右边朝着地球表面转动为正。 */ + gamma: number + } + interface OnDiscoveredCallbackResult { + /** NdefMessage 数组,消息格式为 {id: ArrayBuffer, type: ArrayBuffer, payload: ArrayBuffer} */ + messages: any[] + /** tech 数组,用于匹配NFC卡片具体可以使用什么标准(NfcA等实例)处理 */ + techs: any[] + } + interface OnFrameRecordedCallbackResult { + /** 录音分片数据 */ + frameBuffer: ArrayBuffer + /** 当前帧是否正常录音结束前的最后一帧 */ + isLastFrame: boolean + } + interface OnGetWifiListCallbackResult { + /** Wi-Fi 列表数据 */ + wifiList: WifiInfo[] + } + interface OnGyroscopeChangeCallbackResult { + /** x 轴的角速度 */ + x: number + /** y 轴的角速度 */ + y: number + /** z 轴的角速度 */ + z: number + } + interface OnHCEMessageCallbackResult { + /** `messageType=1` 时 ,客户端接收到 NFC 设备的指令 */ + data: ArrayBuffer + /** 消息类型 + * + * 可选值: + * - 1: HCE APDU Command类型,小程序需对此指令进行处理,并调用 sendHCEMessage 接口返回处理指令; + * - 2: 设备离场事件类型; */ + messageType: 1 | 2 + /** `messageType=2` 时,原因 */ + reason: number + } + interface OnHeadersReceivedCallbackResult { + /** 开发者服务器返回的 HTTP Response Header */ + header: IAnyObject + } + interface OnKeyboardHeightChangeCallbackResult { + /** 键盘高度 */ + height: number + } + interface OnLocalServiceFoundCallbackResult { + /** 服务的 ip 地址 */ + ip: string + /** 服务的端口 */ + port: number + /** 服务的名称 */ + serviceName: string + /** 服务的类型 */ + serviceType: string + } + interface OnLocalServiceLostCallbackResult { + /** 服务的名称 */ + serviceName: string + /** 服务的类型 */ + serviceType: string + } + interface OnLocationChangeCallbackResult { + /** 位置的精确度 */ + accuracy: number + /** 高度,单位 m + * + * 最低基础库: `1.2.0` */ + altitude: number + /** 水平精度,单位 m + * + * 最低基础库: `1.2.0` */ + horizontalAccuracy: number + /** 纬度,范围为 -90~90,负数表示南纬 */ + latitude: number + /** 经度,范围为 -180~180,负数表示西经 */ + longitude: number + /** 速度,单位 m/s */ + speed: number + /** 垂直精度,单位 m(Android 无法获取,返回 0) + * + * 最低基础库: `1.2.0` */ + verticalAccuracy: number + } + interface OnMemoryWarningCallbackResult { + /** 内存告警等级,只有 Android 才有,对应系统宏定义 + * + * 可选值: + * - 5: TRIM_MEMORY_RUNNING_MODERATE; + * - 10: TRIM_MEMORY_RUNNING_LOW; + * - 15: TRIM_MEMORY_RUNNING_CRITICAL; */ + level: 5 | 10 | 15 + } + interface OnNetworkStatusChangeCallbackResult { + /** 当前是否有网络连接 */ + isConnected: boolean + /** 网络类型 + * + * 可选值: + * - 'wifi': wifi 网络; + * - '2g': 2g 网络; + * - '3g': 3g 网络; + * - '4g': 4g 网络; + * - 'unknown': Android 下不常见的网络类型; + * - 'none': 无网络; */ + networkType: 'wifi' | '2g' | '3g' | '4g' | 'unknown' | 'none' + } + interface OnOpenCallbackResult { + /** 连接成功的 HTTP 响应 Header + * + * 最低基础库: `2.0.0` */ + header: IAnyObject + /** 网络请求过程中一些调试信息 + * + * 最低基础库: `2.10.4` */ + profile: SocketProfile + } + interface OnPageNotFoundCallbackResult { + /** 是否本次启动的首个页面(例如从分享等入口进来,首个页面是开发者配置的分享页面) */ + isEntryPage: boolean + /** 不存在页面的路径 (代码包路径) */ + path: string + /** 打开不存在页面的 query 参数 */ + query: IAnyObject + } + interface OnSocketOpenCallbackResult { + /** 连接成功的 HTTP 响应 Header + * + * 最低基础库: `2.0.0` */ + header: IAnyObject + } + interface OnStopCallbackResult { + /** 录音总时长,单位:ms */ + duration: number + /** 录音文件大小,单位:Byte */ + fileSize: number + /** 录音文件的临时路径 (本地路径) */ + tempFilePath: string + } + interface OnThemeChangeCallbackResult { + /** 系统当前的主题,取值为`light`或`dark` + * + * 可选值: + * - 'dark': 深色主题; + * - 'light': 浅色主题; */ + theme: 'dark' | 'light' + } + interface OnUnhandledRejectionCallbackResult { + /** 被拒绝的 Promise 对象 */ + promise: Promise + /** 拒绝原因,一般是一个 Error 对象 */ + reason: string + } + interface OnVoIPChatInterruptedCallbackResult { + /** 错误码 */ + errCode: number + /** 调用结果(错误原因) */ + errMsg: string + } + interface OnVoIPChatMembersChangedCallbackResult { + /** 错误码 */ + errCode: number + /** 调用结果 */ + errMsg: string + /** 还在实时语音通话中的成员 openId 名单 */ + openIdList: string[] + } + interface OnVoIPChatSpeakersChangedCallbackResult { + /** 错误码 */ + errCode: number + /** 调用结果(错误原因) */ + errMsg: string + /** 还在实时语音通话中的成员 openId 名单 */ + openIdList: string[] + } + interface OnVoIPVideoMembersChangedCallbackResult { + /** 错误码 */ + errCode: number + /** 调用结果 */ + errMsg: string + /** 开启视频的成员名单 */ + openIdList: string[] + } + interface OnWifiConnectedCallbackResult { + /** [WifiInfo](https://developers.weixin.qq.com/miniprogram/dev/api/device/wifi/WifiInfo.html) + * + * Wi-Fi 信息 */ + wifi: WifiInfo + } + interface OnWindowResizeCallbackResult { + size: Size + } + interface OpenBluetoothAdapterOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: OpenBluetoothAdapterCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: OpenBluetoothAdapterFailCallback + /** 蓝牙模式,可作为主/从设备,仅 iOS 需要。 + * + * 可选值: + * - 'central': 主机模式; + * - 'peripheral': 从机模式; + * + * 最低基础库: `2.10.0` */ + mode?: 'central' | 'peripheral' + /** 接口调用成功的回调函数 */ + success?: OpenBluetoothAdapterSuccessCallback + } + interface OpenCardOption { + /** 需要打开的卡券列表 */ + cardList: OpenCardRequestInfo[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: OpenCardCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: OpenCardFailCallback + /** 接口调用成功的回调函数 */ + success?: OpenCardSuccessCallback + } + /** 需要打开的卡券列表 */ + interface OpenCardRequestInfo { + /** 卡券 ID */ + cardId: string + /** 由 [wx.addCard](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/card/wx.addCard.html) 的返回对象中的加密 code 通过解密后得到,解密请参照:[code 解码接口](https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1499332673_Unm7V) */ + code: string + } + interface OpenDocumentOption { + /** 文件路径 (本地路径) ,可通过 downloadFile 获得 */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: OpenDocumentCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: OpenDocumentFailCallback + /** 文件类型,指定文件类型打开文件 + * + * 可选值: + * - 'doc': doc 格式; + * - 'docx': docx 格式; + * - 'xls': xls 格式; + * - 'xlsx': xlsx 格式; + * - 'ppt': ppt 格式; + * - 'pptx': pptx 格式; + * - 'pdf': pdf 格式; + * + * 最低基础库: `1.4.0` */ + fileType?: 'doc' | 'docx' | 'xls' | 'xlsx' | 'ppt' | 'pptx' | 'pdf' + /** 是否显示右上角菜单 + * + * 最低基础库: `2.11.0` */ + showMenu?: boolean + /** 接口调用成功的回调函数 */ + success?: OpenDocumentSuccessCallback + } + interface OpenLocationOption { + /** 纬度,范围为-90~90,负数表示南纬。使用 gcj02 国测局坐标系 */ + latitude: number + /** 经度,范围为-180~180,负数表示西经。使用 gcj02 国测局坐标系 */ + longitude: number + /** 地址的详细说明 */ + address?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: OpenLocationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: OpenLocationFailCallback + /** 位置名 */ + name?: string + /** 缩放比例,范围5~18 */ + scale?: number + /** 接口调用成功的回调函数 */ + success?: OpenLocationSuccessCallback + } + interface OpenMapAppOption { + /** 目的地名称 */ + destination: string + /** 目的地纬度 */ + latitude: number + /** 目的地经度 */ + longitude: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: OpenMapAppCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: OpenMapAppFailCallback + /** 接口调用成功的回调函数 */ + success?: OpenMapAppSuccessCallback + } + interface OpenSettingOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: OpenSettingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: OpenSettingFailCallback + /** 接口调用成功的回调函数 */ + success?: OpenSettingSuccessCallback + /** 是否同时获取用户订阅消息的订阅状态,默认不获取。注意:withSubscriptions 只返回用户勾选过订阅面板中的“总是保持以上选择,不再询问”的订阅消息。 + * + * 最低基础库: `2.10.3` */ + withSubscriptions?: boolean + } + interface OpenSettingSuccessCallbackResult { + /** [AuthSetting](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/setting/AuthSetting.html) + * + * 用户授权结果 */ + authSetting: AuthSetting + /** [SubscriptionsSetting](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/setting/SubscriptionsSetting.html) + * + * 用户订阅消息设置,接口参数`withSubscriptions`值为`true`时才会返回。 + * + * 最低基础库: `2.10.3` */ + subscriptionsSetting: SubscriptionsSetting + errMsg: string + } + interface OpenVideoEditorOption { + /** 视频源的路径,只支持本地路径 */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: OpenVideoEditorCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: OpenVideoEditorFailCallback + /** 接口调用成功的回调函数 */ + success?: OpenVideoEditorSuccessCallback + } + interface OpenVideoEditorSuccessCallbackResult { + /** 剪辑后生成的视频文件的时长,单位毫秒(ms) */ + duration: number + /** 剪辑后生成的视频文件大小,单位字节数(byte) */ + size: number + /** 编辑后生成的视频文件的临时路径 */ + tempFilePath: string + /** 编辑后生成的缩略图文件的临时路径 */ + tempThumbPath: string + errMsg: string + } + interface PageScrollToOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PageScrollToCompleteCallback + /** 滚动动画的时长,单位 ms */ + duration?: number + /** 接口调用失败的回调函数 */ + fail?: PageScrollToFailCallback + /** 滚动到页面的目标位置,单位 px */ + scrollTop?: number + /** 选择器 + * + * 最低基础库: `2.7.3` */ + selector?: string + /** 接口调用成功的回调函数 */ + success?: PageScrollToSuccessCallback + } + /** Canvas 2D API 的接口 Path2D 用来声明路径,此路径稍后会被CanvasRenderingContext2D 对象使用。CanvasRenderingContext2D 接口的 路径方法 也存在于 Path2D 这个接口中,允许你在 canvas 中根据需要创建可以保留并重用的路径。 + * + * 最低基础库: `2.11.0` */ + interface Path2D {} + interface PauseBGMOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PauseBGMCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: PauseBGMFailCallback + /** 接口调用成功的回调函数 */ + success?: PauseBGMSuccessCallback + } + interface PauseBackgroundAudioOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PauseBackgroundAudioCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: PauseBackgroundAudioFailCallback + /** 接口调用成功的回调函数 */ + success?: PauseBackgroundAudioSuccessCallback + } + interface PauseOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PauseCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: PauseFailCallback + /** 接口调用成功的回调函数 */ + success?: PauseSuccessCallback + } + interface PauseVoiceOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PauseVoiceCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: PauseVoiceFailCallback + /** 接口调用成功的回调函数 */ + success?: PauseVoiceSuccessCallback + } + /** PerformanceObserver 对象, 用于监听性能相关事件 + * + * 最低基础库: `2.11.0` */ + interface PerformanceObserver { + /** 获取当前支持的所有性能指标类型 */ + supportedEntryTypes: any[] + } + interface PlayBGMOption { + /** 加入背景混音的资源地址 */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PlayBGMCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: PlayBGMFailCallback + /** 接口调用成功的回调函数 */ + success?: PlayBGMSuccessCallback + } + interface PlayBackgroundAudioOption { + /** 音乐链接,目前支持的格式有 m4a, aac, mp3, wav */ + dataUrl: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PlayBackgroundAudioCompleteCallback + /** 封面URL */ + coverImgUrl?: string + /** 接口调用失败的回调函数 */ + fail?: PlayBackgroundAudioFailCallback + /** 接口调用成功的回调函数 */ + success?: PlayBackgroundAudioSuccessCallback + /** 音乐标题 */ + title?: string + } + interface PlayOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PlayCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: PlayFailCallback + /** 接口调用成功的回调函数 */ + success?: PlaySuccessCallback + } + interface PlayVoiceOption { + /** 需要播放的语音文件的文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PlayVoiceCompleteCallback + /** 指定播放时长,到达指定的播放时长后会自动停止播放,单位:秒 + * + * 最低基础库: `1.6.0` */ + duration?: number + /** 接口调用失败的回调函数 */ + fail?: PlayVoiceFailCallback + /** 接口调用成功的回调函数 */ + success?: PlayVoiceSuccessCallback + } + /** 插件帐号信息(仅在插件中调用时包含这一项) */ + interface Plugin { + /** 插件 appId */ + appId: string + /** 插件版本号 */ + version: string + } + interface PreviewImageOption { + /** 需要预览的图片链接列表。[2.2.3](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起支持云文件ID。 */ + urls: string[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PreviewImageCompleteCallback + /** 当前显示图片的链接 */ + current?: string + /** 接口调用失败的回调函数 */ + fail?: PreviewImageFailCallback + /** 是否显示长按菜单 + * + * 最低基础库: `2.13.0` */ + showmenu?: boolean + /** 接口调用成功的回调函数 */ + success?: PreviewImageSuccessCallback + } + interface PreviewMediaOption { + /** 需要预览的资源列表 */ + sources: MediaSource[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: PreviewMediaCompleteCallback + /** 当前显示的资源序号 */ + current?: number + /** 接口调用失败的回调函数 */ + fail?: PreviewMediaFailCallback + /** 是否显示长按菜单 + * + * 最低基础库: `2.13.0` */ + showmenu?: boolean + /** 接口调用成功的回调函数 */ + success?: PreviewMediaSuccessCallback + } + interface ReLaunchOption { + /** 需要跳转的应用内页面路径 (代码包路径),路径后可以带参数。参数与路径之间使用?分隔,参数键与参数值用=相连,不同参数用&分隔;如 'path?key=value&key2=value2' */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ReLaunchCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ReLaunchFailCallback + /** 接口调用成功的回调函数 */ + success?: ReLaunchSuccessCallback + } + interface ReadBLECharacteristicValueOption { + /** 蓝牙特征值的 uuid */ + characteristicId: string + /** 蓝牙设备 id */ + deviceId: string + /** 蓝牙特征值对应服务的 uuid */ + serviceId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ReadBLECharacteristicValueCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ReadBLECharacteristicValueFailCallback + /** 接口调用成功的回调函数 */ + success?: ReadBLECharacteristicValueSuccessCallback + } + interface ReadFileFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail no such file or directory, open ${filePath}': 指定的 filePath 所在目录不存在; + * - 'fail permission denied, open ${dirPath}': 指定的 filePath 路径没有读权限; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface ReadFileOption { + /** 要读取的文件的路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ReadFileCompleteCallback + /** 指定读取文件的字符编码,如果不传 encoding,则以 ArrayBuffer 格式读取文件的二进制内容 + * + * 可选值: + * - 'ascii': ; + * - 'base64': ; + * - 'binary': ; + * - 'hex': ; + * - 'ucs2': 以小端序读取; + * - 'ucs-2': 以小端序读取; + * - 'utf16le': 以小端序读取; + * - 'utf-16le': 以小端序读取; + * - 'utf-8': ; + * - 'utf8': ; + * - 'latin1': ; */ + encoding?: + | 'ascii' + | 'base64' + | 'binary' + | 'hex' + | 'ucs2' + | 'ucs-2' + | 'utf16le' + | 'utf-16le' + | 'utf-8' + | 'utf8' + | 'latin1' + /** 接口调用失败的回调函数 */ + fail?: ReadFileFailCallback + /** 指定文件的长度,如果不指定,则读到文件末尾。有效范围:[1, fileLength]。单位:byte + * + * 最低基础库: `2.10.0` */ + length?: number + /** 从文件指定位置开始读,如果不指定,则从文件头开始读。读取的范围应该是左闭右开区间 [position, position+length)。有效范围:[0, fileLength - 1]。单位:byte + * + * 最低基础库: `2.10.0` */ + position?: number + /** 接口调用成功的回调函数 */ + success?: ReadFileSuccessCallback + } + interface ReadFileSuccessCallbackResult { + /** 文件内容 */ + data: string | ArrayBuffer + errMsg: string + } + interface ReaddirFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail no such file or directory ${dirPath}': 目录不存在; + * - 'fail not a directory ${dirPath}': dirPath 不是目录; + * - 'fail permission denied, open ${dirPath}': 指定的 filePath 路径没有读权限; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface ReaddirOption { + /** 要读取的目录路径 (本地路径) */ + dirPath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ReaddirCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ReaddirFailCallback + /** 接口调用成功的回调函数 */ + success?: ReaddirSuccessCallback + } + interface ReaddirSuccessCallbackResult { + /** 指定目录下的文件名数组。 */ + files: string[] + errMsg: string + } + interface RecorderManagerStartOption { + /** 指定录音的音频输入源,可通过 [wx.getAvailableAudioSources()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/wx.getAvailableAudioSources.html) 获取当前可用的音频源 + * + * 可选值: + * - 'auto': 自动设置,默认使用手机麦克风,插上耳麦后自动切换使用耳机麦克风,所有平台适用; + * - 'buildInMic': 手机麦克风,仅限 iOS; + * - 'headsetMic': 有线耳机麦克风,仅限 iOS; + * - 'mic': 麦克风(没插耳麦时是手机麦克风,插耳麦时是耳机麦克风),仅限 Android; + * - 'camcorder': 同 mic,适用于录制音视频内容,仅限 Android; + * - 'voice_communication': 同 mic,适用于实时沟通,仅限 Android; + * - 'voice_recognition': 同 mic,适用于语音识别,仅限 Android; + * + * 最低基础库: `2.1.0` */ + audioSource?: + | 'auto' + | 'buildInMic' + | 'headsetMic' + | 'mic' + | 'camcorder' + | 'voice_communication' + | 'voice_recognition' + /** 录音的时长,单位 ms,最大值 600000(10 分钟) */ + duration?: number + /** 编码码率,有效值见下表格 */ + encodeBitRate?: number + /** 音频格式 + * + * 可选值: + * - 'mp3': mp3 格式; + * - 'aac': aac 格式; + * - 'wav': wav 格式; + * - 'PCM': pcm 格式; */ + format?: 'mp3' | 'aac' | 'wav' | 'PCM' + /** 指定帧大小,单位 KB。传入 frameSize 后,每录制指定帧大小的内容后,会回调录制的文件内容,不指定则不会回调。暂仅支持 mp3 格式。 */ + frameSize?: number + /** 录音通道数 + * + * 可选值: + * - 1: 1 个通道; + * - 2: 2 个通道; */ + numberOfChannels?: 1 | 2 + /** 采样率 + * + * 可选值: + * - 8000: 8000 采样率; + * - 11025: 11025 采样率; + * - 12000: 12000 采样率; + * - 16000: 16000 采样率; + * - 22050: 22050 采样率; + * - 24000: 24000 采样率; + * - 32000: 32000 采样率; + * - 44100: 44100 采样率; + * - 48000: 48000 采样率; */ + sampleRate?: + | 8000 + | 11025 + | 12000 + | 16000 + | 22050 + | 24000 + | 32000 + | 44100 + | 48000 + } + /** 菜单按钮的布局位置信息 */ + interface Rect { + /** 下边界坐标,单位:px */ + bottom: number + /** 高度,单位:px */ + height: number + /** 左边界坐标,单位:px */ + left: number + /** 右边界坐标,单位:px */ + right: number + /** 上边界坐标,单位:px */ + top: number + /** 宽度,单位:px */ + width: number + } + interface RedirectToOption { + /** 需要跳转的应用内非 tabBar 的页面的路径 (代码包路径), 路径后可以带参数。参数与路径之间使用 `?` 分隔,参数键与参数值用 `=` 相连,不同参数用 `&` 分隔;如 'path?key=value&key2=value2' */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RedirectToCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RedirectToFailCallback + /** 接口调用成功的回调函数 */ + success?: RedirectToSuccessCallback + } + interface RedoOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RedoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RedoFailCallback + /** 接口调用成功的回调函数 */ + success?: RedoSuccessCallback + } + /** 来源信息。从另一个小程序、公众号或 App 进入小程序时返回。否则返回 `{}`。(参见后文注意) */ + interface ReferrerInfo { + /** 来源小程序、公众号或 App 的 appId */ + appId: string + /** 来源小程序传过来的数据,scene=1037或1038时支持 */ + extraData: IAnyObject + } + /** 参照区域的边界 */ + interface RelativeRectResult { + /** 下边界 */ + bottom: number + /** 左边界 */ + left: number + /** 右边界 */ + right: number + /** 上边界 */ + top: number + } + /** 消息来源的结构化信息 */ + interface RemoteInfo { + /** 发送消息的 socket 的地址 */ + address: string + /** 使用的协议族,为 IPv4 或者 IPv6 */ + family: string + /** 端口号 */ + port: number + /** message 的大小,单位:字节 */ + size: number + } + interface RemoveCustomLayerOption { + /** 个性化图层id */ + layerId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveCustomLayerCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RemoveCustomLayerFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveCustomLayerSuccessCallback + } + interface RemoveFormatOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveFormatCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RemoveFormatFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveFormatSuccessCallback + } + interface RemoveGroundOverlayOption { + /** 图片图层 id */ + id: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveGroundOverlayCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RemoveGroundOverlayFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveGroundOverlaySuccessCallback + } + interface RemoveMarkersOption { + /** marker 的 id 集合。 */ + markerIds: any[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveMarkersCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RemoveMarkersFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveMarkersSuccessCallback + } + interface RemoveSavedFileFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail file not exist': 指定的 tempFilePath 找不到文件; */ + errMsg: string + } + interface RemoveServiceOption { + /** service 的 uuid */ + serviceId: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveServiceCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RemoveServiceFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveServiceSuccessCallback + } + interface RemoveStorageOption { + /** 本地缓存中指定的 key */ + key: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveStorageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RemoveStorageFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveStorageSuccessCallback + } + interface RemoveTabBarBadgeOption { + /** tabBar 的哪一项,从左边算起 */ + index: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveTabBarBadgeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RemoveTabBarBadgeFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveTabBarBadgeSuccessCallback + } + interface RenameFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail permission denied, rename ${oldPath} -> ${newPath}': 指定源文件或目标文件没有写权限; + * - 'fail no such file or directory, rename ${oldPath} -> ${newPath}': 源文件不存在,或目标文件路径的上层目录不存在; */ + errMsg: string + } + interface RenameOption { + /** 新文件路径,支持本地路径 */ + newPath: string + /** 源文件路径,支持本地路径 */ + oldPath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RenameCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RenameFailCallback + /** 接口调用成功的回调函数 */ + success?: RenameSuccessCallback + } + /** Canvas 绘图上下文。 + * + * **** + * + * - 通过 Canvas.getContext('2d') 接口可以获取 CanvasRenderingContext2D 对象,实现了 [HTML Canvas 2D Context](https://www.w3.org/TR/2dcontext/) 定义的属性、方法。 + * - 通过 Canvas.getContext('webgl') 或 OffscreenCanvas.getContext('webgl') 接口可以获取 WebGLRenderingContext 对象,实现了 [WebGL 1.0](https://www.khronos.org/registry/webgl/specs/latest/1.0/) 定义的所有属性、方法、常量。 + * - CanvasRenderingContext2D 的 drawImage 方法 2.10.0 起支持传入通过 [SelectorQuery](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/SelectorQuery.html) 获取的 video 对象 + * + * **示例代码** + * + * + * + * video 画到 2D Canvas 示例 + * [在微信开发者工具中查看示例](https://developers.weixin.qq.com/s/tJTak7mU7sfX) */ + interface RenderingContext {} + interface RequestOption< + T extends string | IAnyObject | ArrayBuffer = + | string + | IAnyObject + | ArrayBuffer + > { + /** 开发者服务器接口地址 */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RequestCompleteCallback + /** 请求的参数 */ + data?: string | IAnyObject | ArrayBuffer + /** 返回的数据格式 + * + * 可选值: + * - 'json': 返回的数据为 JSON,返回后会对返回的数据进行一次 JSON.parse; + * - '其他': 不对返回的内容进行 JSON.parse; */ + dataType?: 'json' | '其他' + /** 开启 cache + * + * 最低基础库: `2.10.4` */ + enableCache?: boolean + /** 开启 http2 + * + * 最低基础库: `2.10.4` */ + enableHttp2?: boolean + /** 开启 quic + * + * 最低基础库: `2.10.4` */ + enableQuic?: boolean + /** 接口调用失败的回调函数 */ + fail?: RequestFailCallback + /** 设置请求的 header,header 中不能设置 Referer。 + * + * `content-type` 默认为 `application/json` */ + header?: IAnyObject + /** HTTP 请求方法 + * + * 可选值: + * - 'OPTIONS': HTTP 请求 OPTIONS; + * - 'GET': HTTP 请求 GET; + * - 'HEAD': HTTP 请求 HEAD; + * - 'POST': HTTP 请求 POST; + * - 'PUT': HTTP 请求 PUT; + * - 'DELETE': HTTP 请求 DELETE; + * - 'TRACE': HTTP 请求 TRACE; + * - 'CONNECT': HTTP 请求 CONNECT; */ + method?: + | 'OPTIONS' + | 'GET' + | 'HEAD' + | 'POST' + | 'PUT' + | 'DELETE' + | 'TRACE' + | 'CONNECT' + /** 响应的数据类型 + * + * 可选值: + * - 'text': 响应的数据为文本; + * - 'arraybuffer': 响应的数据为 ArrayBuffer; + * + * 最低基础库: `1.7.0` */ + responseType?: 'text' | 'arraybuffer' + /** 接口调用成功的回调函数 */ + success?: RequestSuccessCallback + /** 超时时间,单位为毫秒 + * + * 最低基础库: `2.10.0` */ + timeout?: number + } + interface RequestPaymentOption { + /** 随机字符串,长度为32个字符以下 */ + nonceStr: string + /** 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*** */ + package: string + /** 签名,具体见微信支付文档 */ + paySign: string + /** 时间戳,从 1970 年 1 月 1 日 00:00:00 至今的秒数,即当前的时间 */ + timeStamp: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RequestPaymentCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RequestPaymentFailCallback + /** 签名算法,应与后台下单时的值一致 + * + * 可选值: + * - 'MD5': 仅在 v2 版本接口适用; + * - 'HMAC-SHA256': 仅在 v2 版本接口适用; + * - 'RSA': 仅在 v3 版本接口适用; */ + signType?: 'MD5' | 'HMAC-SHA256' | 'RSA' + /** 接口调用成功的回调函数 */ + success?: RequestPaymentSuccessCallback + } + interface RequestPictureInPictureOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RequestPictureInPictureCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RequestPictureInPictureFailCallback + /** 接口调用成功的回调函数 */ + success?: RequestPictureInPictureSuccessCallback + } + /** 网络请求过程中一些调试信息 + * + * 最低基础库: `2.10.4` */ + interface RequestProfile { + /** SSL建立完成的时间,如果不是安全连接,则值为 0 */ + SSLconnectionEnd: number + /** SSL建立连接的时间,如果不是安全连接,则值为 0 */ + SSLconnectionStart: number + /** HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等。注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间。注意这里握手结束,包括安全连接建立完成、SOCKS 授权通过 */ + connectEnd: number + /** HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等。注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间 */ + connectStart: number + /** DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 */ + domainLookupEnd: number + /** DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 */ + domainLookupStart: number + /** 评估当前网络下载的kbps */ + downstreamThroughputKbpsEstimate: number + /** 评估的网络状态 slow 2g/2g/3g/4g */ + estimate_nettype: string + /** 组件准备好使用 HTTP 请求抓取资源的时间,这发生在检查本地缓存之前 */ + fetchStart: number + /** 协议层根据多个请求评估当前网络的 rtt(仅供参考) */ + httpRttEstimate: number + /** 当前请求的IP */ + peerIP: string + /** 当前请求的端口 */ + port: number + /** 收到字节数 */ + receivedBytedCount: number + /** 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0 */ + redirectEnd: number + /** 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0 */ + redirectStart: number + /** HTTP请求读取真实文档结束的时间 */ + requestEnd: number + /** HTTP请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存。连接错误重连时,这里显示的也是新建立连接的时间 */ + requestStart: number + /** HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存 */ + responseEnd: number + /** HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存 */ + responseStart: number + /** 当次请求连接过程中实时 rtt */ + rtt: number + /** 发送的字节数 */ + sendBytesCount: number + /** 是否复用连接 */ + socketReused: boolean + /** 当前网络的实际下载kbps */ + throughputKbps: number + /** 传输层根据多个请求评估的当前网络的 rtt(仅供参考) */ + transportRttEstimate: number + } + interface RequestSubscribeMessageFailCallbackResult { + /** 接口调用失败错误码 */ + errCode: number + /** 接口调用失败错误信息 */ + errMsg: string + } + interface RequestSubscribeMessageOption { + /** 需要订阅的消息模板的id的集合,一次调用最多可订阅3条消息(注意:iOS客户端7.0.6版本、Android客户端7.0.7版本之后的一次性订阅/长期订阅才支持多个模板消息,iOS客户端7.0.5版本、Android客户端7.0.6版本之前的一次订阅只支持一个模板消息)消息模板id在[微信公众平台(mp.weixin.qq.com)-功能-订阅消息]中配置。每个tmplId对应的模板标题需要不相同,否则会被过滤。 */ + tmplIds: any[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RequestSubscribeMessageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RequestSubscribeMessageFailCallback + /** 接口调用成功的回调函数 */ + success?: RequestSubscribeMessageSuccessCallback + } + interface RequestSubscribeMessageSuccessCallbackResult { + /** [TEMPLATE_ID]是动态的键,即模板id,值包括'accept'、'reject'、'ban'、'filter'。'accept'表示用户同意订阅该条id对应的模板消息,'reject'表示用户拒绝订阅该条id对应的模板消息,'ban'表示已被后台封禁,'filter'表示该模板因为模板标题同名被后台过滤。例如 { errMsg: "requestSubscribeMessage:ok", zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: "accept"} 表示用户同意订阅zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE这条消息 */ + [TEMPLATE_ID: string]: string + /** 接口调用成功时errMsg值为'requestSubscribeMessage:ok' */ + errMsg: string + } + interface RequestSuccessCallbackResult< + T extends string | IAnyObject | ArrayBuffer = + | string + | IAnyObject + | ArrayBuffer + > { + /** 开发者服务器返回的 cookies,格式为字符串数组 + * + * 最低基础库: `2.10.0` */ + cookies: string[] + /** 开发者服务器返回的数据 */ + data: T + /** 开发者服务器返回的 HTTP Response Header + * + * 最低基础库: `1.2.0` */ + header: IAnyObject + /** 网络请求过程中一些调试信息 + * + * 最低基础库: `2.10.4` */ + profile: RequestProfile + /** 开发者服务器返回的 HTTP 状态码 */ + statusCode: number + errMsg: string + } + interface ResumeBGMOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ResumeBGMCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ResumeBGMFailCallback + /** 接口调用成功的回调函数 */ + success?: ResumeBGMSuccessCallback + } + interface ResumeOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ResumeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ResumeFailCallback + /** 接口调用成功的回调函数 */ + success?: ResumeSuccessCallback + } + interface RewardedVideoAdOnCloseCallbackResult { + /** 视频是否是在用户完整观看的情况下被关闭的 + * + * 最低基础库: `2.1.0` */ + isEnded: boolean + } + interface RewardedVideoAdOnErrorCallbackResult { + /** 错误码 + * + * 可选值: + * - 1000: 后端接口调用失败; + * - 1001: 参数错误; + * - 1002: 广告单元无效; + * - 1003: 内部错误; + * - 1004: 无合适的广告; + * - 1005: 广告组件审核中; + * - 1006: 广告组件被驳回; + * - 1007: 广告组件被封禁; + * - 1008: 广告单元已关闭; + * + * 最低基础库: `2.2.2` */ + errCode: 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 + /** 错误信息 */ + errMsg: string + } + interface RmdirFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail no such file or directory ${dirPath}': 目录不存在; + * - 'fail directory not empty': 目录不为空; + * - 'fail permission denied, open ${dirPath}': 指定的 dirPath 路径没有写权限; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface RmdirOption { + /** 要删除的目录路径 (本地路径) */ + dirPath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RmdirCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: RmdirFailCallback + /** 是否递归删除目录。如果为 true,则删除该目录和该目录下的所有子目录以及文件。 + * + * 最低基础库: `2.3.0` */ + recursive?: boolean + /** 接口调用成功的回调函数 */ + success?: RmdirSuccessCallback + } + /** 在竖屏正方向下的安全区域 + * + * 最低基础库: `2.7.0` */ + interface SafeArea { + /** 安全区域右下角纵坐标 */ + bottom: number + /** 安全区域的高度,单位逻辑像素 */ + height: number + /** 安全区域左上角横坐标 */ + left: number + /** 安全区域右下角横坐标 */ + right: number + /** 安全区域左上角纵坐标 */ + top: number + /** 安全区域的宽度,单位逻辑像素 */ + width: number + } + interface SaveFileFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail tempFilePath file not exist': 指定的 tempFilePath 找不到文件; + * - 'fail permission denied, open "${filePath}"': 指定的 filePath 路径没有写权限; + * - 'fail no such file or directory "${dirPath}"': 上级目录不存在; + * - 'fail the maximum size of the file storage limit is exceeded': 存储空间不足; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface SaveFileSuccessCallbackResult { + /** 存储后的文件路径 (本地路径) */ + savedFilePath: string + errMsg: string + } + interface SaveFileToDiskOption { + /** 待保存文件路径 */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SaveFileToDiskCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SaveFileToDiskFailCallback + /** 接口调用成功的回调函数 */ + success?: SaveFileToDiskSuccessCallback + } + interface SaveImageToPhotosAlbumOption { + /** 图片文件路径,可以是临时文件路径或永久文件路径 (本地路径) ,不支持网络路径 */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SaveImageToPhotosAlbumCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SaveImageToPhotosAlbumFailCallback + /** 接口调用成功的回调函数 */ + success?: SaveImageToPhotosAlbumSuccessCallback + } + interface SaveVideoToPhotosAlbumOption { + /** 视频文件路径,可以是临时文件路径也可以是永久文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SaveVideoToPhotosAlbumCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SaveVideoToPhotosAlbumFailCallback + /** 接口调用成功的回调函数 */ + success?: SaveVideoToPhotosAlbumSuccessCallback + } + interface ScanCodeOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ScanCodeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ScanCodeFailCallback + /** 是否只能从相机扫码,不允许从相册选择图片 + * + * 最低基础库: `1.2.0` */ + onlyFromCamera?: boolean + /** 扫码类型 + * + * 可选值: + * - 'barCode': 一维码; + * - 'qrCode': 二维码; + * - 'datamatrix': Data Matrix 码; + * - 'pdf417': PDF417 条码; + * + * 最低基础库: `1.7.0` */ + scanType?: Array<'barCode' | 'qrCode' | 'datamatrix' | 'pdf417'> + /** 接口调用成功的回调函数 */ + success?: ScanCodeSuccessCallback + } + interface ScanCodeSuccessCallbackResult { + /** 所扫码的字符集 */ + charSet: string + /** 当所扫的码为当前小程序二维码时,会返回此字段,内容为二维码携带的 path */ + path: string + /** 原始数据,base64编码 */ + rawData: string + /** 所扫码的内容 */ + result: string + /** 所扫码的类型 + * + * 可选值: + * - 'QR_CODE': 二维码; + * - 'AZTEC': 一维码; + * - 'CODABAR': 一维码; + * - 'CODE_39': 一维码; + * - 'CODE_93': 一维码; + * - 'CODE_128': 一维码; + * - 'DATA_MATRIX': 二维码; + * - 'EAN_8': 一维码; + * - 'EAN_13': 一维码; + * - 'ITF': 一维码; + * - 'MAXICODE': 一维码; + * - 'PDF_417': 二维码; + * - 'RSS_14': 一维码; + * - 'RSS_EXPANDED': 一维码; + * - 'UPC_A': 一维码; + * - 'UPC_E': 一维码; + * - 'UPC_EAN_EXTENSION': 一维码; + * - 'WX_CODE': 二维码; + * - 'CODE_25': 一维码; */ + scanType: + | 'QR_CODE' + | 'AZTEC' + | 'CODABAR' + | 'CODE_39' + | 'CODE_93' + | 'CODE_128' + | 'DATA_MATRIX' + | 'EAN_8' + | 'EAN_13' + | 'ITF' + | 'MAXICODE' + | 'PDF_417' + | 'RSS_14' + | 'RSS_EXPANDED' + | 'UPC_A' + | 'UPC_E' + | 'UPC_EAN_EXTENSION' + | 'WX_CODE' + | 'CODE_25' + errMsg: string + } + interface ScrollOffsetCallbackResult { + /** 节点的 dataset */ + dataset: IAnyObject + /** 节点的 ID */ + id: string + /** 节点的水平滚动位置 */ + scrollLeft: number + /** 节点的竖直滚动位置 */ + scrollTop: number + } + interface ScrollToOption { + /** 是否启用滚动动画 */ + animated?: boolean + /** 滚动动画时长 */ + duration?: number + /** 左边界距离 */ + left?: number + /** 顶部距离 */ + top?: number + /** 初始速度 */ + velocity?: number + } + /** 增强 ScrollView 实例 + * + * 最低基础库: `2.14.4` */ + interface ScrollViewContext { + /** 设置滚动边界弹性 (仅在 iOS 下生效) */ + bounces: boolean + /** 取消滚动惯性 (仅在 iOS 下生效) */ + decelerationDisabled: boolean + /** 设置滚动减速速率 */ + fastDeceleration: boolean + /** 分页滑动开关 */ + pagingEnabled: boolean + /** 滚动开关 */ + scrollEnabled: boolean + /** 设置是否显示滚动条 */ + showScrollbar: boolean + } + interface SeekBackgroundAudioOption { + /** 音乐位置,单位:秒 */ + position: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SeekBackgroundAudioCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SeekBackgroundAudioFailCallback + /** 接口调用成功的回调函数 */ + success?: SeekBackgroundAudioSuccessCallback + } + interface SendHCEMessageOption { + /** 二进制数据 */ + data: ArrayBuffer + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SendHCEMessageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SendHCEMessageFailCallback + /** 接口调用成功的回调函数 */ + success?: SendHCEMessageSuccessCallback + } + interface SendMessageOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SendMessageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SendMessageFailCallback + /** 接口调用成功的回调函数 */ + success?: SendMessageSuccessCallback + } + interface SendSocketMessageOption { + /** 需要发送的内容 */ + data: string | ArrayBuffer + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SendSocketMessageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SendSocketMessageFailCallback + /** 接口调用成功的回调函数 */ + success?: SendSocketMessageSuccessCallback + } + interface SetBGMVolumeOption { + /** 音量大小,范围是 0-1 */ + volume: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetBGMVolumeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetBGMVolumeFailCallback + /** 接口调用成功的回调函数 */ + success?: SetBGMVolumeSuccessCallback + } + interface SetBLEMTUOption { + /** 用于区分设备的 id */ + deviceId: string + /** 最大传输单元(22,512) 区间内,单位 bytes */ + mtu: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetBLEMTUCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetBLEMTUFailCallback + /** 接口调用成功的回调函数 */ + success?: SetBLEMTUSuccessCallback + } + interface SetBackgroundColorOption { + /** 窗口的背景色,必须为十六进制颜色值 */ + backgroundColor?: string + /** 底部窗口的背景色,必须为十六进制颜色值,仅 iOS 支持 */ + backgroundColorBottom?: string + /** 顶部窗口的背景色,必须为十六进制颜色值,仅 iOS 支持 */ + backgroundColorTop?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetBackgroundColorCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetBackgroundColorFailCallback + /** 接口调用成功的回调函数 */ + success?: SetBackgroundColorSuccessCallback + } + interface SetBackgroundFetchTokenOption { + /** 自定义的登录态 */ + token: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetBackgroundFetchTokenCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetBackgroundFetchTokenFailCallback + /** 接口调用成功的回调函数 */ + success?: SetBackgroundFetchTokenSuccessCallback + } + interface SetBackgroundTextStyleOption { + /** 下拉背景字体、loading 图的样式。 + * + * 可选值: + * - 'dark': dark 样式; + * - 'light': light 样式; */ + textStyle: 'dark' | 'light' + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetBackgroundTextStyleCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetBackgroundTextStyleFailCallback + /** 接口调用成功的回调函数 */ + success?: SetBackgroundTextStyleSuccessCallback + } + interface SetCenterOffsetOption { + /** 偏移量,两位数组 */ + offset: number[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetCenterOffsetCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetCenterOffsetFailCallback + /** 接口调用成功的回调函数 */ + success?: SetCenterOffsetSuccessCallback + } + interface SetClipboardDataOption { + /** 剪贴板的内容 */ + data: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetClipboardDataCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetClipboardDataFailCallback + /** 接口调用成功的回调函数 */ + success?: SetClipboardDataSuccessCallback + } + interface SetContentsOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetContentsCompleteCallback + /** 表示内容的delta对象 */ + delta?: IAnyObject + /** 接口调用失败的回调函数 */ + fail?: SetContentsFailCallback + /** 带标签的HTML内容 */ + html?: string + /** 接口调用成功的回调函数 */ + success?: SetContentsSuccessCallback + } + interface SetEnableDebugOption { + /** 是否打开调试 */ + enableDebug: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetEnableDebugCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetEnableDebugFailCallback + /** 接口调用成功的回调函数 */ + success?: SetEnableDebugSuccessCallback + } + interface SetInnerAudioOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetInnerAudioOptionCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetInnerAudioOptionFailCallback + /** 是否与其他音频混播,设置为 true 之后,不会终止其他应用或微信内的音乐 */ + mixWithOther?: boolean + /** (仅在 iOS 生效)是否遵循静音开关,设置为 false 之后,即使是在静音模式下,也能播放声音 */ + obeyMuteSwitch?: boolean + /** true 代表用扬声器播放,false 代表听筒播放,默认值为 true。 */ + speakerOn?: boolean + /** 接口调用成功的回调函数 */ + success?: SetInnerAudioOptionSuccessCallback + } + interface SetKeepScreenOnOption { + /** 是否保持屏幕常亮 */ + keepScreenOn: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetKeepScreenOnCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetKeepScreenOnFailCallback + /** 接口调用成功的回调函数 */ + success?: SetKeepScreenOnSuccessCallback + } + interface SetMICVolumeOption { + /** 音量大小,范围是 0.0-1.0 */ + volume: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetMICVolumeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetMICVolumeFailCallback + /** 接口调用成功的回调函数 */ + success?: SetMICVolumeSuccessCallback + } + interface SetNavigationBarColorOption { + /** 背景颜色值,有效值为十六进制颜色 */ + backgroundColor: string + /** 前景颜色值,包括按钮、标题、状态栏的颜色,仅支持 #ffffff 和 #000000 */ + frontColor: string + /** 动画效果 */ + animation?: AnimationOption + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetNavigationBarColorCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetNavigationBarColorFailCallback + /** 接口调用成功的回调函数 */ + success?: SetNavigationBarColorSuccessCallback + } + interface SetNavigationBarTitleOption { + /** 页面标题 */ + title: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetNavigationBarTitleCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetNavigationBarTitleFailCallback + /** 接口调用成功的回调函数 */ + success?: SetNavigationBarTitleSuccessCallback + } + interface SetScreenBrightnessOption { + /** 屏幕亮度值,范围 0 ~ 1。0 最暗,1 最亮 */ + value: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetScreenBrightnessCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetScreenBrightnessFailCallback + /** 接口调用成功的回调函数 */ + success?: SetScreenBrightnessSuccessCallback + } + interface SetStorageOption { + /** 需要存储的内容。只支持原生类型、Date、及能够通过`JSON.stringify`序列化的对象。 */ + data: T + /** 本地缓存中指定的 key */ + key: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetStorageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetStorageFailCallback + /** 接口调用成功的回调函数 */ + success?: SetStorageSuccessCallback + } + interface SetTabBarBadgeOption { + /** tabBar 的哪一项,从左边算起 */ + index: number + /** 显示的文本,超过 4 个字符则显示成 ... */ + text: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetTabBarBadgeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetTabBarBadgeFailCallback + /** 接口调用成功的回调函数 */ + success?: SetTabBarBadgeSuccessCallback + } + interface SetTabBarItemOption { + /** tabBar 的哪一项,从左边算起 */ + index: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetTabBarItemCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetTabBarItemFailCallback + /** 图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,当 postion 为 top 时,此参数无效 */ + iconPath?: string + /** 选中时的图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px ,当 postion 为 top 时,此参数无效 */ + selectedIconPath?: string + /** 接口调用成功的回调函数 */ + success?: SetTabBarItemSuccessCallback + /** tab 上的按钮文字 */ + text?: string + } + interface SetTabBarStyleOption { + /** tab 的背景色,HexColor */ + backgroundColor?: string + /** tabBar上边框的颜色, 仅支持 black/white */ + borderStyle?: string + /** tab 上的文字默认颜色,HexColor */ + color?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetTabBarStyleCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetTabBarStyleFailCallback + /** tab 上的文字选中时的颜色,HexColor */ + selectedColor?: string + /** 接口调用成功的回调函数 */ + success?: SetTabBarStyleSuccessCallback + } + interface SetTimeoutOption { + /** 设置超时时间 (ms) */ + timeout: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetTimeoutCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetTimeoutFailCallback + /** 接口调用成功的回调函数 */ + success?: SetTimeoutSuccessCallback + } + interface SetTopBarTextOption { + /** 置顶栏文字 */ + text: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetTopBarTextCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetTopBarTextFailCallback + /** 接口调用成功的回调函数 */ + success?: SetTopBarTextSuccessCallback + } + interface SetWifiListOption { + /** 提供预设的 Wi-Fi 信息列表 */ + wifiList: WifiData[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetWifiListCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetWifiListFailCallback + /** 接口调用成功的回调函数 */ + success?: SetWifiListSuccessCallback + } + interface SetWindowSizeOption { + /** 窗口高度,以像素为单位 */ + height: number + /** 窗口宽度,以像素为单位 */ + width: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetWindowSizeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetWindowSizeFailCallback + /** 接口调用成功的回调函数 */ + success?: SetWindowSizeSuccessCallback + } + interface SetZoomOption { + /** 缩放级别,范围[1, maxZoom]。zoom 可取小数,精确到小数后一位。maxZoom 可在 bindinitdone 返回值中获取。 */ + zoom: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SetZoomCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SetZoomFailCallback + /** 接口调用成功的回调函数 */ + success?: SetZoomSuccessCallback + } + interface SetZoomSuccessCallbackResult { + /** 实际设置的缩放级别。由于系统限制,某些机型可能无法设置成指定值,会改用最接近的可设值。 */ + zoom: number + errMsg: string + } + interface ShowActionSheetOption { + /** 按钮的文字数组,数组长度最大为 6 */ + itemList: string[] + /** 警示文案 + * + * 最低基础库: `2.14.0` */ + alertText?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowActionSheetCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ShowActionSheetFailCallback + /** 按钮的文字颜色 */ + itemColor?: string + /** 接口调用成功的回调函数 */ + success?: ShowActionSheetSuccessCallback + } + interface ShowActionSheetSuccessCallbackResult { + /** 用户点击的按钮序号,从上到下的顺序,从0开始 */ + tapIndex: number + errMsg: string + } + interface ShowLoadingOption { + /** 提示的内容 */ + title: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowLoadingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ShowLoadingFailCallback + /** 是否显示透明蒙层,防止触摸穿透 */ + mask?: boolean + /** 接口调用成功的回调函数 */ + success?: ShowLoadingSuccessCallback + } + interface ShowModalOption { + /** 取消按钮的文字颜色,必须是 16 进制格式的颜色字符串 */ + cancelColor?: string + /** 取消按钮的文字,最多 4 个字符 */ + cancelText?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowModalCompleteCallback + /** 确认按钮的文字颜色,必须是 16 进制格式的颜色字符串 */ + confirmColor?: string + /** 确认按钮的文字,最多 4 个字符 */ + confirmText?: string + /** 提示的内容,editable 为 true 时,会输入框默认文本 */ + content?: string + /** 是否显示输入框 + * + * 最低基础库: `2.15.0` */ + editable?: boolean + /** 接口调用失败的回调函数 */ + fail?: ShowModalFailCallback + /** 输入框提示文本 + * + * 最低基础库: `2.15.0` */ + placeholderText?: string + /** 是否显示取消按钮 */ + showCancel?: boolean + /** 接口调用成功的回调函数 */ + success?: ShowModalSuccessCallback + /** 提示的标题 */ + title?: string + } + interface ShowModalSuccessCallbackResult { + /** 为 true 时,表示用户点击了取消(用于 Android 系统区分点击蒙层关闭还是点击取消按钮关闭) + * + * 最低基础库: `1.1.0` */ + cancel: boolean + /** 为 true 时,表示用户点击了确定按钮 */ + confirm: boolean + /** editable 为 true 时,用户输入的文本 */ + content: string + errMsg: string + } + interface ShowNavigationBarLoadingOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowNavigationBarLoadingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ShowNavigationBarLoadingFailCallback + /** 接口调用成功的回调函数 */ + success?: ShowNavigationBarLoadingSuccessCallback + } + interface ShowRedPackageOption { + /** 封面地址 */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowRedPackageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ShowRedPackageFailCallback + /** 接口调用成功的回调函数 */ + success?: ShowRedPackageSuccessCallback + } + interface ShowShareImageMenuOption { + /** 要分享的图片地址,必须为本地路径或临时路径 */ + path: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowShareImageMenuCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ShowShareImageMenuFailCallback + /** 接口调用成功的回调函数 */ + success?: ShowShareImageMenuSuccessCallback + } + interface ShowShareMenuOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowShareMenuCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ShowShareMenuFailCallback + /** 本接口为 Beta 版本,暂只在 Android 平台支持。需要显示的转发按钮名称列表,默认['shareAppMessage']。按钮名称合法值包含 "shareAppMessage"、"shareTimeline" 两种 + * + * 最低基础库: `2.11.3` */ + menus?: string[] + /** 接口调用成功的回调函数 */ + success?: ShowShareMenuSuccessCallback + /** 是否使用带 shareTicket 的转发[详情](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html) */ + withShareTicket?: boolean + } + interface ShowTabBarOption { + /** 是否需要动画效果 */ + animation?: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowTabBarCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ShowTabBarFailCallback + /** 接口调用成功的回调函数 */ + success?: ShowTabBarSuccessCallback + } + interface ShowTabBarRedDotOption { + /** tabBar 的哪一项,从左边算起 */ + index: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowTabBarRedDotCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ShowTabBarRedDotFailCallback + /** 接口调用成功的回调函数 */ + success?: ShowTabBarRedDotSuccessCallback + } + interface ShowToastOption { + /** 提示的内容 */ + title: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ShowToastCompleteCallback + /** 提示的延迟时间 */ + duration?: number + /** 接口调用失败的回调函数 */ + fail?: ShowToastFailCallback + /** 图标 + * + * 可选值: + * - 'success': 显示成功图标,此时 title 文本最多显示 7 个汉字长度; + * - 'error': 显示失败图标,此时 title 文本最多显示 7 个汉字长度; + * - 'loading': 显示加载图标,此时 title 文本最多显示 7 个汉字长度; + * - 'none': 不显示图标,此时 title 文本最多可显示两行,[1.9.0](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)及以上版本支持; */ + icon?: 'success' | 'error' | 'loading' | 'none' + /** 自定义图标的本地路径,image 的优先级高于 icon + * + * 最低基础库: `1.1.0` */ + image?: string + /** 是否显示透明蒙层,防止触摸穿透 */ + mask?: boolean + /** 接口调用成功的回调函数 */ + success?: ShowToastSuccessCallback + } + interface Size { + /** 变化后的窗口高度,单位 px */ + windowHeight: number + /** 变化后的窗口宽度,单位 px */ + windowWidth: number + } + /** 网络请求过程中一些调试信息 + * + * 最低基础库: `2.10.4` */ + interface SocketProfile { + /** 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等。注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间。注意这里握手结束,包括安全连接建立完成、SOCKS 授权通过 */ + connectEnd: number + /** 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等。注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间 */ + connectStart: number + /** 上层请求到返回的耗时 */ + cost: number + /** DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 */ + domainLookupEnd: number + /** DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 */ + domainLookupStart: number + /** 组件准备好使用 SOCKET 建立请求的时间,这发生在检查本地缓存之前 */ + fetchStart: number + /** 握手耗时 */ + handshakeCost: number + /** 单次连接的耗时,包括 connect ,tls */ + rtt: number + } + interface SocketTaskCloseOption { + /** 一个数字值表示关闭连接的状态号,表示连接被关闭的原因。 */ + code?: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SocketTaskCloseCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SocketTaskCloseFailCallback + /** 一个可读的字符串,表示连接被关闭的原因。这个字符串必须是不长于 123 字节的 UTF-8 文本(不是字符)。 */ + reason?: string + /** 接口调用成功的回调函数 */ + success?: SocketTaskCloseSuccessCallback + } + interface SocketTaskOnCloseCallbackResult { + /** 一个数字值表示关闭连接的状态号,表示连接被关闭的原因。 */ + code: number + /** 一个可读的字符串,表示连接被关闭的原因。 */ + reason: string + } + interface SocketTaskOnMessageCallbackResult { + /** 服务器返回的消息 */ + data: string | ArrayBuffer + } + interface SocketTaskSendOption { + /** 需要发送的内容 */ + data: string | ArrayBuffer + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SendCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SendFailCallback + /** 接口调用成功的回调函数 */ + success?: SendSuccessCallback + } + interface StartAccelerometerOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartAccelerometerCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartAccelerometerFailCallback + /** 监听加速度数据回调函数的执行频率 + * + * 可选值: + * - 'game': 适用于更新游戏的回调频率,在 20ms/次 左右; + * - 'ui': 适用于更新 UI 的回调频率,在 60ms/次 左右; + * - 'normal': 普通的回调频率,在 200ms/次 左右; + * + * 最低基础库: `2.1.0` */ + interval?: 'game' | 'ui' | 'normal' + /** 接口调用成功的回调函数 */ + success?: StartAccelerometerSuccessCallback + } + interface StartAdvertisingObject { + /** 广播自定义参数 */ + advertiseRequest: AdvertiseReqObj + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartAdvertisingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartAdvertisingFailCallback + /** 广播功率 + * + * 可选值: + * - 'low': 功率低; + * - 'medium': 功率适中; + * - 'high': 功率高; */ + powerLevel?: 'low' | 'medium' | 'high' + /** 接口调用成功的回调函数 */ + success?: StartAdvertisingSuccessCallback + } + interface StartBeaconDiscoveryOption { + /** iBeacon 设备广播的 uuid 列表 */ + uuids: string[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartBeaconDiscoveryCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartBeaconDiscoveryFailCallback + /** 是否校验蓝牙开关,仅在 iOS 下有效 */ + ignoreBluetoothAvailable?: boolean + /** 接口调用成功的回调函数 */ + success?: StartBeaconDiscoverySuccessCallback + } + interface StartBluetoothDevicesDiscoveryOption { + /** 是否允许重复上报同一设备。如果允许重复上报,则 [wx.onBlueToothDeviceFound](#) 方法会多次上报同一设备,但是 RSSI 值会有不同。 */ + allowDuplicatesKey?: boolean + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartBluetoothDevicesDiscoveryCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartBluetoothDevicesDiscoveryFailCallback + /** 上报设备的间隔。0 表示找到新设备立即上报,其他数值根据传入的间隔上报。 */ + interval?: number + /** 扫描模式,越高扫描越快,也越耗电, 仅安卓 7.0.12 及以上支持。 + * + * 可选值: + * - 'low': 低; + * - 'medium': 中; + * - 'high': 高; */ + powerLevel?: 'low' | 'medium' | 'high' + /** 要搜索的蓝牙设备主 service 的 uuid 列表。某些蓝牙设备会广播自己的主 service 的 uuid。如果设置此参数,则只搜索广播包有对应 uuid 的主服务的蓝牙设备。建议主要通过该参数过滤掉周边不需要处理的其他蓝牙设备。 */ + services?: string[] + /** 接口调用成功的回调函数 */ + success?: StartBluetoothDevicesDiscoverySuccessCallback + } + interface StartCompassOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartCompassCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartCompassFailCallback + /** 接口调用成功的回调函数 */ + success?: StartCompassSuccessCallback + } + interface StartDeviceMotionListeningOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartDeviceMotionListeningCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartDeviceMotionListeningFailCallback + /** 监听设备方向的变化回调函数的执行频率 + * + * 可选值: + * - 'game': 适用于更新游戏的回调频率,在 20ms/次 左右; + * - 'ui': 适用于更新 UI 的回调频率,在 60ms/次 左右; + * - 'normal': 普通的回调频率,在 200ms/次 左右; */ + interval?: 'game' | 'ui' | 'normal' + /** 接口调用成功的回调函数 */ + success?: StartDeviceMotionListeningSuccessCallback + } + interface StartDiscoveryOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartDiscoveryCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartDiscoveryFailCallback + /** 接口调用成功的回调函数 */ + success?: StartDiscoverySuccessCallback + } + interface StartGyroscopeOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartGyroscopeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartGyroscopeFailCallback + /** 监听陀螺仪数据回调函数的执行频率 + * + * 可选值: + * - 'game': 适用于更新游戏的回调频率,在 20ms/次 左右; + * - 'ui': 适用于更新 UI 的回调频率,在 60ms/次 左右; + * - 'normal': 普通的回调频率,在 200ms/次 左右; */ + interval?: 'game' | 'ui' | 'normal' + /** 接口调用成功的回调函数 */ + success?: StartGyroscopeSuccessCallback + } + interface StartHCEOption { + /** 需要注册到系统的 AID 列表 */ + aid_list: string[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartHCECompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartHCEFailCallback + /** 接口调用成功的回调函数 */ + success?: StartHCESuccessCallback + } + interface StartLocalServiceDiscoveryFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'invalid param': serviceType 为空; + * - 'scan task already exist': 在当前 startLocalServiceDiscovery 发起的搜索未停止的情况下,再次调用 startLocalServiceDiscovery; */ + errMsg: string + } + interface StartLocalServiceDiscoveryOption { + /** 要搜索的服务类型 */ + serviceType: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartLocalServiceDiscoveryCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartLocalServiceDiscoveryFailCallback + /** 接口调用成功的回调函数 */ + success?: StartLocalServiceDiscoverySuccessCallback + } + interface StartLocationUpdateBackgroundOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartLocationUpdateBackgroundCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartLocationUpdateBackgroundFailCallback + /** 接口调用成功的回调函数 */ + success?: StartLocationUpdateBackgroundSuccessCallback + } + interface StartLocationUpdateOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartLocationUpdateCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartLocationUpdateFailCallback + /** 接口调用成功的回调函数 */ + success?: StartLocationUpdateSuccessCallback + } + interface StartPreviewOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartPreviewCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartPreviewFailCallback + /** 接口调用成功的回调函数 */ + success?: StartPreviewSuccessCallback + } + interface StartPullDownRefreshOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartPullDownRefreshCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartPullDownRefreshFailCallback + /** 接口调用成功的回调函数 */ + success?: StartPullDownRefreshSuccessCallback + } + interface StartRecordSuccessCallbackResult { + /** 录音文件的临时路径 (本地路径) */ + tempFilePath: string + errMsg: string + } + interface StartRecordTimeoutCallbackResult { + /** 封面图片文件的临时路径 (本地路径) */ + tempThumbPath: string + /** 视频的文件的临时路径 (本地路径) */ + tempVideoPath: string + } + interface StartSoterAuthenticationOption { + /** 挑战因子。挑战因子为调用者为此次生物鉴权准备的用于签名的字符串关键识别信息,将作为 `resultJSON` 的一部分,供调用者识别本次请求。例如:如果场景为请求用户对某订单进行授权确认,则可以将订单号填入此参数。 */ + challenge: string + /** 请求使用的可接受的生物认证方式 + * + * 可选值: + * - 'fingerPrint': 指纹识别; + * - 'facial': 人脸识别; + * - 'speech': 声纹识别(暂未支持); */ + requestAuthModes: Array<'fingerPrint' | 'facial' | 'speech'> + /** 验证描述,即识别过程中显示在界面上的对话框提示内容 */ + authContent?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartSoterAuthenticationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartSoterAuthenticationFailCallback + /** 接口调用成功的回调函数 */ + success?: StartSoterAuthenticationSuccessCallback + } + interface StartSoterAuthenticationSuccessCallbackResult { + /** 生物认证方式 */ + authMode: string + /** 错误码 */ + errCode: number + /** 错误信息 */ + errMsg: string + /** 在设备安全区域(TEE)内获得的本机安全信息(如TEE名称版本号等以及防重放参数)以及本次认证信息(仅Android支持,本次认证的指纹ID)。具体说明见下文 */ + resultJSON: string + /** 用SOTER安全密钥对 `resultJSON` 的签名(SHA256 with RSA/PSS, saltlen=20) */ + resultJSONSignature: string + } + interface StartWifiOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartWifiCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartWifiFailCallback + /** 接口调用成功的回调函数 */ + success?: StartWifiSuccessCallback + } + interface StatFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail permission denied, open ${path}': 指定的 path 路径没有读权限; + * - 'fail no such file or directory ${path}': 文件不存在; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface StatOption { + /** 文件/目录路径 (本地路径) */ + path: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StatCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StatFailCallback + /** 是否递归获取目录下的每个文件的 Stats 信息 + * + * 最低基础库: `2.3.0` */ + recursive?: boolean + /** 接口调用成功的回调函数 */ + success?: StatSuccessCallback + } + interface StatSuccessCallbackResult { + /** [Stats](https://developers.weixin.qq.com/miniprogram/dev/api/file/Stats.html)|Object + * + * 当 recursive 为 false 时,res.stats 是一个 Stats 对象。当 recursive 为 true 且 path 是一个目录的路径时,res.stats 是一个 Object,key 以 path 为根路径的相对路径,value 是该路径对应的 Stats 对象。 */ + stats: Stats | IAnyObject + errMsg: string + } + /** 描述文件状态的对象 */ + interface Stats { + /** 文件最近一次被存取或被执行的时间,UNIX 时间戳,对应 POSIX stat.st_atime */ + lastAccessedTime: number + /** 文件最后一次被修改的时间,UNIX 时间戳,对应 POSIX stat.st_mtime */ + lastModifiedTime: number + /** 文件的类型和存取的权限,对应 POSIX stat.st_mode */ + mode: string + /** 文件大小,单位:B,对应 POSIX stat.st_size */ + size: number + } + interface StepOption { + /** 动画延迟时间,单位 ms */ + delay?: number + /** 动画持续时间,单位 ms */ + duration?: number + /** 动画的效果 + * + * 可选值: + * - 'linear': 动画从头到尾的速度是相同的; + * - 'ease': 动画以低速开始,然后加快,在结束前变慢; + * - 'ease-in': 动画以低速开始; + * - 'ease-in-out': 动画以低速开始和结束; + * - 'ease-out': 动画以低速结束; + * - 'step-start': 动画第一帧就跳至结束状态直到结束; + * - 'step-end': 动画一直保持开始状态,最后一帧跳到结束状态; */ + timingFunction?: + | 'linear' + | 'ease' + | 'ease-in' + | 'ease-in-out' + | 'ease-out' + | 'step-start' + | 'step-end' + transformOrigin?: string + } + interface StopAccelerometerOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopAccelerometerCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopAccelerometerFailCallback + /** 接口调用成功的回调函数 */ + success?: StopAccelerometerSuccessCallback + } + interface StopAdvertisingOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopAdvertisingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopAdvertisingFailCallback + /** 接口调用成功的回调函数 */ + success?: StopAdvertisingSuccessCallback + } + interface StopBGMOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopBGMCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopBGMFailCallback + /** 接口调用成功的回调函数 */ + success?: StopBGMSuccessCallback + } + interface StopBackgroundAudioOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopBackgroundAudioCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopBackgroundAudioFailCallback + /** 接口调用成功的回调函数 */ + success?: StopBackgroundAudioSuccessCallback + } + interface StopBeaconDiscoveryOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopBeaconDiscoveryCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopBeaconDiscoveryFailCallback + /** 接口调用成功的回调函数 */ + success?: StopBeaconDiscoverySuccessCallback + } + interface StopBluetoothDevicesDiscoveryOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopBluetoothDevicesDiscoveryCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopBluetoothDevicesDiscoveryFailCallback + /** 接口调用成功的回调函数 */ + success?: StopBluetoothDevicesDiscoverySuccessCallback + } + interface StopCompassOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopCompassCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopCompassFailCallback + /** 接口调用成功的回调函数 */ + success?: StopCompassSuccessCallback + } + interface StopDeviceMotionListeningOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopDeviceMotionListeningCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopDeviceMotionListeningFailCallback + /** 接口调用成功的回调函数 */ + success?: StopDeviceMotionListeningSuccessCallback + } + interface StopDiscoveryOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopDiscoveryCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopDiscoveryFailCallback + /** 接口调用成功的回调函数 */ + success?: StopDiscoverySuccessCallback + } + interface StopGyroscopeOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopGyroscopeCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopGyroscopeFailCallback + /** 接口调用成功的回调函数 */ + success?: StopGyroscopeSuccessCallback + } + interface StopHCEOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopHCECompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopHCEFailCallback + /** 接口调用成功的回调函数 */ + success?: StopHCESuccessCallback + } + interface StopLocalServiceDiscoveryFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'task not found': 在当前没有处在搜索服务中的情况下调用 stopLocalServiceDiscovery; */ + errMsg: string + } + interface StopLocalServiceDiscoveryOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopLocalServiceDiscoveryCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopLocalServiceDiscoveryFailCallback + /** 接口调用成功的回调函数 */ + success?: StopLocalServiceDiscoverySuccessCallback + } + interface StopLocationUpdateOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopLocationUpdateCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopLocationUpdateFailCallback + /** 接口调用成功的回调函数 */ + success?: StopLocationUpdateSuccessCallback + } + interface StopOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopFailCallback + /** 接口调用成功的回调函数 */ + success?: StopSuccessCallback + } + interface StopPreviewOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopPreviewCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopPreviewFailCallback + /** 接口调用成功的回调函数 */ + success?: StopPreviewSuccessCallback + } + interface StopPullDownRefreshOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopPullDownRefreshCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopPullDownRefreshFailCallback + /** 接口调用成功的回调函数 */ + success?: StopPullDownRefreshSuccessCallback + } + interface StopRecordSuccessCallbackResult { + /** 封面图片文件的临时路径 (本地路径) */ + tempThumbPath: string + /** 视频的文件的临时路径 (本地路径) */ + tempVideoPath: string + errMsg: string + } + interface StopVoiceOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopVoiceCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopVoiceFailCallback + /** 接口调用成功的回调函数 */ + success?: StopVoiceSuccessCallback + } + interface StopWifiOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopWifiCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopWifiFailCallback + /** 接口调用成功的回调函数 */ + success?: StopWifiSuccessCallback + } + interface SubscribeVoIPVideoMembersOption { + /** 订阅的成员列表 */ + openIdList: string[] + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SubscribeVoIPVideoMembersCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SubscribeVoIPVideoMembersFailCallback + /** 接口调用成功的回调函数 */ + success?: SubscribeVoIPVideoMembersSuccessCallback + } + /** 订阅消息设置 +* +* **示例代码** +* +* +* ```javascript +wx.getSetting({ + withSubscriptions: true, + success (res) { + console.log(res.authSetting) + // res.authSetting = { + // "scope.userInfo": true, + // "scope.userLocation": true + // } + console.log(res.subscriptionsSetting) + // res.subscriptionsSetting = { + // mainSwitch: true, // 订阅消息总开关 + // itemSettings: { // 每一项开关 + // SYS_MSG_TYPE_INTERACTIVE: 'accept', // 小游戏系统订阅消息 + // SYS_MSG_TYPE_RANK: 'accept' + // zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: 'reject', // 普通一次性订阅消息 + // ke_OZC_66gZxALLcsuI7ilCJSP2OJ2vWo2ooUPpkWrw: 'ban', + // } + // } + } +}) +``` */ + interface SubscriptionsSetting { + /** 订阅消息总开关,true为开启,false为关闭 */ + mainSwitch: boolean + /** 每一项订阅消息的订阅状态。itemSettings对象的键为**一次性订阅消息的模板id**或**系统订阅消息的类型**,值为'accept'、'reject'、'ban'中的其中一种。'accept'表示用户同意订阅这条消息,'reject'表示用户拒绝订阅这条消息,'ban'表示已被后台封禁。一次性订阅消息使用方法详见 [wx.requestSubscribeMessage](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/subscribe-message/wx.requestSubscribeMessage.html),永久订阅消息(仅小游戏可用)使用方法详见[wx.requestSubscribeSystemMessage](/minigame/dev/api/open-api/subscribe-message/wx.requestSubscribeSystemMessage.html) + * ## 注意事项 + * - itemSettings 只返回用户勾选过订阅面板中的“总是保持以上选择,不再询问”的订阅消息。 */ + itemSettings?: IAnyObject + } + interface SwitchCameraOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SwitchCameraCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SwitchCameraFailCallback + /** 接口调用成功的回调函数 */ + success?: SwitchCameraSuccessCallback + } + interface SwitchTabOption { + /** 需要跳转的 tabBar 页面的路径 (代码包路径)(需在 app.json 的 [tabBar](https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#tabbar) 字段定义的页面),路径后不能带参数。 */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SwitchTabCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: SwitchTabFailCallback + /** 接口调用成功的回调函数 */ + success?: SwitchTabSuccessCallback + } + interface SystemInfo { + /** 客户端基础库版本 + * + * 最低基础库: `1.1.0` */ + SDKVersion: string + /** 允许微信使用相册的开关(仅 iOS 有效) + * + * 最低基础库: `2.6.0` */ + albumAuthorized: boolean + /** 设备性能等级(仅 Android)。取值为:-2 或 0(该设备无法运行小游戏),-1(性能未知),>=1(设备性能值,该值越高,设备性能越好,目前最高不到50) + * + * 最低基础库: `1.8.0` */ + benchmarkLevel: number + /** 蓝牙的系统开关 + * + * 最低基础库: `2.6.0` */ + bluetoothEnabled: boolean + /** 设备品牌 + * + * 最低基础库: `1.5.0` */ + brand: string + /** 允许微信使用摄像头的开关 + * + * 最低基础库: `2.6.0` */ + cameraAuthorized: boolean + /** 设备方向 + * + * 可选值: + * - 'portrait': 竖屏; + * - 'landscape': 横屏; */ + deviceOrientation: 'portrait' | 'landscape' + /** 是否已打开调试。可通过右上角菜单或 [wx.setEnableDebug](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.setEnableDebug.html) 打开调试。 + * + * 最低基础库: `2.15.0` */ + enableDebug: boolean + /** 用户字体大小(单位px)。以微信客户端「我-设置-通用-字体大小」中的设置为准 + * + * 最低基础库: `1.5.0` */ + fontSizeSetting: number + /** 微信设置的语言 */ + language: string + /** 允许微信使用定位的开关 + * + * 最低基础库: `2.6.0` */ + locationAuthorized: boolean + /** 地理位置的系统开关 + * + * 最低基础库: `2.6.0` */ + locationEnabled: boolean + /** `true` 表示模糊定位,`false` 表示精确定位,仅 iOS 支持 */ + locationReducedAccuracy: boolean + /** 允许微信使用麦克风的开关 + * + * 最低基础库: `2.6.0` */ + microphoneAuthorized: boolean + /** 设备型号。新机型刚推出一段时间会显示unknown,微信会尽快进行适配。 */ + model: string + /** 允许微信通知带有提醒的开关(仅 iOS 有效) + * + * 最低基础库: `2.6.0` */ + notificationAlertAuthorized: boolean + /** 允许微信通知的开关 + * + * 最低基础库: `2.6.0` */ + notificationAuthorized: boolean + /** 允许微信通知带有标记的开关(仅 iOS 有效) + * + * 最低基础库: `2.6.0` */ + notificationBadgeAuthorized: boolean + /** 允许微信通知带有声音的开关(仅 iOS 有效) + * + * 最低基础库: `2.6.0` */ + notificationSoundAuthorized: boolean + /** 设备像素比 */ + pixelRatio: number + /** 客户端平台 */ + platform: string + /** 在竖屏正方向下的安全区域 + * + * 最低基础库: `2.7.0` */ + safeArea: SafeArea + /** 屏幕高度,单位px + * + * 最低基础库: `1.1.0` */ + screenHeight: number + /** 屏幕宽度,单位px + * + * 最低基础库: `1.1.0` */ + screenWidth: number + /** 状态栏的高度,单位px + * + * 最低基础库: `1.9.0` */ + statusBarHeight: number + /** 操作系统及版本 */ + system: string + /** 微信版本号 */ + version: string + /** Wi-Fi 的系统开关 + * + * 最低基础库: `2.6.0` */ + wifiEnabled: boolean + /** 可使用窗口高度,单位px */ + windowHeight: number + /** 可使用窗口宽度,单位px */ + windowWidth: number + /** 系统当前主题,取值为`light`或`dark`,全局配置`"darkmode":true`时才能获取,否则为 undefined (不支持小游戏) + * + * 可选值: + * - 'dark': 深色主题; + * - 'light': 浅色主题; + * + * 最低基础库: `2.11.0` */ + theme?: 'dark' | 'light' + } + interface TakePhotoOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: TakePhotoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: TakePhotoFailCallback + /** 成像质量 + * + * 可选值: + * - 'high': 高质量; + * - 'normal': 普通质量; + * - 'low': 低质量; */ + quality?: 'high' | 'normal' | 'low' + /** 接口调用成功的回调函数 */ + success?: TakePhotoSuccessCallback + } + interface TakePhotoSuccessCallbackResult { + /** 照片文件的临时路径 (本地路径),安卓是jpg图片格式,ios是png */ + tempImagePath: string + errMsg: string + } + /** 标签类型枚举 */ + interface TechType { + /** 对应IsoDep实例,实例支持ISO-DEP (ISO 14443-4)标准的读写 */ + isoDep: string + /** 对应MifareClassic实例,实例支持MIFARE Classic标签的读写 */ + mifareClassic: string + /** 对应MifareUltralight实例,实例支持MIFARE Ultralight标签的读写 */ + mifareUltralight: string + /** 对应Ndef实例,实例支持对NDEF格式的NFC标签上的NDEF数据的读写 */ + ndef: string + /** 对应NfcA实例,实例支持NFC-A (ISO 14443-3A)标准的读写 */ + nfcA: string + /** 对应NfcB实例,实例支持NFC-B (ISO 14443-3B)标准的读写 */ + nfcB: string + /** 对应NfcF实例,实例支持NFC-F (JIS 6319-4)标准的读写 */ + nfcF: string + /** 对应NfcV实例,实例支持NFC-V (ISO 15693)标准的读写 */ + nfcV: string + } + interface TextMetrics { + /** 文本的宽度 */ + width: number + } + interface ToScreenLocationOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ToScreenLocationCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ToScreenLocationFailCallback + /** 接口调用成功的回调函数 */ + success?: ToScreenLocationSuccessCallback + } + interface ToScreenLocationSuccessCallbackResult { + /** x 坐标值 */ + x: number + /** y 坐标值 */ + y: number + errMsg: string + } + interface ToggleTorchOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: ToggleTorchCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: ToggleTorchFailCallback + /** 接口调用成功的回调函数 */ + success?: ToggleTorchSuccessCallback + } + interface TransceiveOption { + /** 需要传递的二进制数据 */ + data: ArrayBuffer + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: TransceiveCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: TransceiveFailCallback + /** 接口调用成功的回调函数 */ + success?: TransceiveSuccessCallback + } + interface TransceiveSuccessCallbackResult { + data: ArrayBuffer + errMsg: string + } + interface TranslateMarkerOption { + /** 移动过程中是否自动旋转 marker */ + autoRotate: boolean + /** 指定 marker 移动到的目标点 */ + destination: DestinationOption + /** 指定 marker */ + markerId: number + /** marker 的旋转角度 */ + rotate: number + /** 动画结束回调函数 */ + animationEnd?: (...args: any[]) => any + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: TranslateMarkerCompleteCallback + /** 动画持续时长,平移与旋转分别计算 */ + duration?: number + /** 接口调用失败的回调函数 */ + fail?: TranslateMarkerFailCallback + /** 平移和旋转同时进行 + * + * 最低基础库: `2.13.0` */ + moveWithRotate?: boolean + /** 接口调用成功的回调函数 */ + success?: TranslateMarkerSuccessCallback + } + interface UDPSocketOnErrorCallbackResult { + /** 错误信息 */ + errMsg: string + } + interface UDPSocketOnMessageCallbackResult { + /** 收到的消息 */ + message: ArrayBuffer + /** 消息来源的结构化信息 */ + remoteInfo: RemoteInfo + } + interface UDPSocketSendOption { + /** 要发消息的地址。在基础库 2.9.3 及之前版本可以是一个和本机同网段的 IP 地址,也可以是在安全域名列表内的域名地址;在基础库 2.9.4 及之后版本,可以是任意 IP 和域名 */ + address: string + /** 要发送的数据 */ + message: string | ArrayBuffer + /** 要发送消息的端口号 */ + port: number + /** 发送数据的长度,仅当 message 为 ArrayBuffer 类型时有效 */ + length?: number + /** 发送数据的偏移量,仅当 message 为 ArrayBuffer 类型时有效 */ + offset?: number + } + interface UndoOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: UndoCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: UndoFailCallback + /** 接口调用成功的回调函数 */ + success?: UndoSuccessCallback + } + interface UnlinkFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail permission denied, open ${path}': 指定的 path 路径没有读权限; + * - 'fail no such file or directory ${path}': 文件不存在; + * - 'fail operation not permitted, unlink ${filePath}': 传入的 filePath 是一个目录; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface UnlinkOption { + /** 要删除的文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: UnlinkCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: UnlinkFailCallback + /** 接口调用成功的回调函数 */ + success?: UnlinkSuccessCallback + } + interface UnzipFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail permission denied, unzip ${zipFilePath} -> ${destPath}': 指定目标文件路径没有写权限; + * - 'fail no such file or directory, unzip ${zipFilePath} -> "${destPath}': 源文件不存在,或目标文件路径的上层目录不存在; */ + errMsg: string + } + interface UnzipOption { + /** 目标目录路径, 支持本地路径 */ + targetPath: string + /** 源文件路径,支持本地路径, 只可以是 zip 压缩文件 */ + zipFilePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: UnzipCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: UnzipFailCallback + /** 接口调用成功的回调函数 */ + success?: UnzipSuccessCallback + } + /** 参数列表 */ + interface UpdatableMessageFrontEndParameter { + /** 参数名 */ + name: string + /** 参数值 */ + value: string + } + /** 动态消息的模板信息 + * + * 最低基础库: `2.4.0` */ + interface UpdatableMessageFrontEndTemplateInfo { + /** 参数列表 */ + parameterList: UpdatableMessageFrontEndParameter[] + } + interface UpdateGroundOverlayOption { + /** 图片覆盖的经纬度范围 */ + bounds: MapBounds + /** 图片图层 id */ + id: string + /** 图片路径,支持网络图片、临时路径、代码包路径 */ + src: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: UpdateGroundOverlayCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: UpdateGroundOverlayFailCallback + /** 图层透明度 */ + opacity?: number + /** 接口调用成功的回调函数 */ + success?: UpdateGroundOverlaySuccessCallback + /** 是否可见 */ + visible?: boolean + /** 图层绘制顺序 */ + zIndex?: number + } + interface UpdateShareMenuOption { + /** 动态消息的 activityId。通过 [updatableMessage.createActivityId](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/updatable-message/updatableMessage.createActivityId.html) 接口获取 + * + * 最低基础库: `2.4.0` */ + activityId?: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: UpdateShareMenuCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: UpdateShareMenuFailCallback + /** 是否是私密消息。详见 [小程序私密消息](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share/private-message.html) + * + * 最低基础库: `2.13.0` */ + isPrivateMessage?: boolean + /** 是否是动态消息,详见[动态消息](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share/updatable-message.html) + * + * 最低基础库: `2.4.0` */ + isUpdatableMessage?: boolean + /** 接口调用成功的回调函数 */ + success?: UpdateShareMenuSuccessCallback + /** 动态消息的模板信息 + * + * 最低基础库: `2.4.0` */ + templateInfo?: UpdatableMessageFrontEndTemplateInfo + /** 群待办消息的id,通过toDoActivityId可以把多个群待办消息聚合为同一个。通过 [updatableMessage.createActivityId](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/updatable-message/updatableMessage.createActivityId.html) 接口获取。详见[群待办消息](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html) + * + * 最低基础库: `2.11.0` */ + toDoActivityId?: string + /** 是否使用带 shareTicket 的转发[详情](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html) */ + withShareTicket?: boolean + } + interface UpdateVoIPChatMuteConfigOption { + /** 静音设置 */ + muteConfig: MuteConfig + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: UpdateVoIPChatMuteConfigCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: UpdateVoIPChatMuteConfigFailCallback + /** 接口调用成功的回调函数 */ + success?: UpdateVoIPChatMuteConfigSuccessCallback + } + interface UpdateWeChatAppOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: UpdateWeChatAppCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: UpdateWeChatAppFailCallback + /** 接口调用成功的回调函数 */ + success?: UpdateWeChatAppSuccessCallback + } + interface UploadFileOption { + /** 要上传文件资源的路径 (本地路径) */ + filePath: string + /** 文件对应的 key,开发者在服务端可以通过这个 key 获取文件的二进制内容 */ + name: string + /** 开发者服务器地址 */ + url: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: UploadFileCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: UploadFileFailCallback + /** HTTP 请求中其他额外的 form data */ + formData?: IAnyObject + /** HTTP 请求 Header,Header 中不能设置 Referer */ + header?: IAnyObject + /** 接口调用成功的回调函数 */ + success?: UploadFileSuccessCallback + /** 超时时间,单位为毫秒 + * + * 最低基础库: `2.10.0` */ + timeout?: number + } + interface UploadFileSuccessCallbackResult { + /** 开发者服务器返回的数据 */ + data: string + /** 开发者服务器返回的 HTTP 状态码 */ + statusCode: number + errMsg: string + } + interface UploadTaskOnProgressUpdateCallbackResult { + /** 上传进度百分比 */ + progress: number + /** 预期需要上传的数据总长度,单位 Bytes */ + totalBytesExpectedToSend: number + /** 已经上传的数据长度,单位 Bytes */ + totalBytesSent: number + } + /** 用户信息 */ + interface UserInfo { + /** 用户头像图片的 URL。URL 最后一个数值代表正方形头像大小(有 0、46、64、96、132 数值可选,0 代表 640x640 的正方形头像,46 表示 46x46 的正方形头像,剩余数值以此类推。默认132),用户没有头像时该项为空。若用户更换头像,原有头像 URL 将失效。 */ + avatarUrl: string + /** 用户所在城市 */ + city: string + /** 用户所在国家 */ + country: string + /** 用户性别 + * + * 可选值: + * - 0: 未知; + * - 1: 男性; + * - 2: 女性; */ + gender: 0 | 1 | 2 + /** 显示 country,province,city 所用的语言 + * + * 可选值: + * - 'en': 英文; + * - 'zh_CN': 简体中文; + * - 'zh_TW': 繁体中文; */ + language: 'en' | 'zh_CN' | 'zh_TW' + /** 用户昵称 */ + nickName: string + /** 用户所在省份 */ + province: string + } + interface VibrateLongOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: VibrateLongCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: VibrateLongFailCallback + /** 接口调用成功的回调函数 */ + success?: VibrateLongSuccessCallback + } + interface VibrateShortOption { + /** 震动强度类型,有效值为:heavy、medium、light + * + * 最低基础库: `2.13.0` */ + type: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: VibrateShortCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: VibrateShortFailCallback + /** 接口调用成功的回调函数 */ + success?: VibrateShortSuccessCallback + } + interface VideoContextRequestFullScreenOption { + /** 设置全屏时视频的方向,不指定则根据宽高比自动判断。 + * + * 可选值: + * - 0: 正常竖向; + * - 90: 屏幕逆时针90度; + * - -90: 屏幕顺时针90度; + * + * 最低基础库: `1.7.0` */ + direction?: 0 | 90 | -90 + } + interface VideoDecoderStartOption { + /** 需要解码的视频源文件。基础库 2.13.0 以下的版本只支持本地路径。 2.13.0 开始支持 http:// 和 https:// 协议的远程路径。 */ + source: string + /** 解码模式。0:按 pts 解码;1:以最快速度解码 */ + mode?: number + } + /** 提供预设的 Wi-Fi 信息列表 */ + interface WifiData { + /** Wi-Fi 的 BSSID */ + BSSID?: string + /** Wi-Fi 的 SSID */ + SSID?: string + /** Wi-Fi 设备密码 */ + password?: string + } + /** Wifi 信息 */ + interface WifiInfo { + /** Wi-Fi 的 BSSID */ + BSSID: string + /** Wi-Fi 的 SSID */ + SSID: string + /** Wi-Fi 频段单位 MHz + * + * 最低基础库: `2.12.0` */ + frequency: number + /** Wi-Fi 是否安全 */ + secure: boolean + /** Wi-Fi 信号强度 */ + signalStrength: number + } + interface WorkerOnMessageCallbackResult { + /** 主线程/Worker 线程向当前线程发送的消息 */ + message: IAnyObject + } + interface WriteBLECharacteristicValueOption { + /** 蓝牙特征值的 uuid */ + characteristicId: string + /** 蓝牙设备 id */ + deviceId: string + /** 蓝牙特征值对应服务的 uuid */ + serviceId: string + /** 蓝牙设备特征值对应的二进制值 */ + value: ArrayBuffer + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: WriteBLECharacteristicValueCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: WriteBLECharacteristicValueFailCallback + /** 接口调用成功的回调函数 */ + success?: WriteBLECharacteristicValueSuccessCallback + } + interface WriteCharacteristicValueObject { + /** characteristic对应的uuid */ + characteristicId: string + /** 是否需要通知主机value已更新 */ + needNotify: boolean + /** service 的 uuid */ + serviceId: string + /** 特征值对应的二进制值 */ + value: ArrayBuffer + /** 可选,处理回包时使用 */ + callbackId?: number + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: WriteCharacteristicValueCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: WriteCharacteristicValueFailCallback + /** 接口调用成功的回调函数 */ + success?: WriteCharacteristicValueSuccessCallback + } + interface WriteFileFailCallbackResult { + /** 错误信息 + * + * 可选值: + * - 'fail no such file or directory, open ${filePath}': 指定的 filePath 所在目录不存在; + * - 'fail permission denied, open ${dirPath}': 指定的 filePath 路径没有写权限; + * - 'fail the maximum size of the file storage limit is exceeded': 存储空间不足; + * - 'fail sdcard not mounted': Android sdcard 挂载失败; */ + errMsg: string + } + interface WriteFileOption { + /** 要写入的文本或二进制数据 */ + data: string | ArrayBuffer + /** 要写入的文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: WriteFileCompleteCallback + /** 指定写入文件的字符编码 + * + * 可选值: + * - 'ascii': ; + * - 'base64': ; + * - 'binary': ; + * - 'hex': ; + * - 'ucs2': 以小端序读取; + * - 'ucs-2': 以小端序读取; + * - 'utf16le': 以小端序读取; + * - 'utf-16le': 以小端序读取; + * - 'utf-8': ; + * - 'utf8': ; + * - 'latin1': ; */ + encoding?: + | 'ascii' + | 'base64' + | 'binary' + | 'hex' + | 'ucs2' + | 'ucs-2' + | 'utf16le' + | 'utf-16le' + | 'utf-8' + | 'utf8' + | 'latin1' + /** 接口调用失败的回调函数 */ + fail?: WriteFileFailCallback + /** 接口调用成功的回调函数 */ + success?: WriteFileSuccessCallback + } + interface WriteNdefMessageOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: WriteNdefMessageCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: WriteNdefMessageFailCallback + /** 二进制对象数组, 需要指明 id, type 以及 payload (均为 ArrayBuffer 类型) */ + records?: any[] + /** 接口调用成功的回调函数 */ + success?: WriteNdefMessageSuccessCallback + /** text 数组 */ + texts?: any[] + /** uri 数组 */ + uris?: any[] + } + interface WxGetFileInfoOption { + /** 本地文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetFileInfoCompleteCallback + /** 计算文件摘要的算法 + * + * 可选值: + * - 'md5': md5 算法; + * - 'sha1': sha1 算法; */ + digestAlgorithm?: 'md5' | 'sha1' + /** 接口调用失败的回调函数 */ + fail?: WxGetFileInfoFailCallback + /** 接口调用成功的回调函数 */ + success?: WxGetFileInfoSuccessCallback + } + interface WxGetFileInfoSuccessCallbackResult { + /** 按照传入的 digestAlgorithm 计算得出的的文件摘要 */ + digest: string + /** 文件大小,以字节为单位 */ + size: number + errMsg: string + } + interface WxGetSavedFileListOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: GetSavedFileListCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: GetSavedFileListFailCallback + /** 接口调用成功的回调函数 */ + success?: WxGetSavedFileListSuccessCallback + } + interface WxGetSavedFileListSuccessCallbackResult { + /** 文件数组,每一项是一个 FileItem */ + fileList: FileItem[] + errMsg: string + } + interface WxRemoveSavedFileOption { + /** 需要删除的文件路径 (本地路径) */ + filePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: RemoveSavedFileCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: WxRemoveSavedFileFailCallback + /** 接口调用成功的回调函数 */ + success?: RemoveSavedFileSuccessCallback + } + interface WxSaveFileOption { + /** 需要保存的文件的临时路径 (本地路径) */ + tempFilePath: string + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: SaveFileCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: WxSaveFileFailCallback + /** 接口调用成功的回调函数 */ + success?: SaveFileSuccessCallback + } + interface WxStartRecordOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StartRecordCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StartRecordFailCallback + /** 接口调用成功的回调函数 */ + success?: WxStartRecordSuccessCallback + } + interface WxStopRecordOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: StopRecordCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: StopRecordFailCallback + /** 接口调用成功的回调函数 */ + success?: WxStopRecordSuccessCallback + } + interface Animation { + /** [Object Animation.export()](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.export.html) + * + * 导出动画队列。**export 方法每次调用后会清掉之前的动画操作。** */ + export(): AnimationExportResult + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.backgroundColor(string value)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.backgroundColor.html) + * + * 设置背景色 */ + backgroundColor( + /** 颜色值 */ + value: string + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.bottom(number|string value)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.bottom.html) + * + * 设置 bottom 值 */ + bottom( + /** 长度值,如果传入 number 则默认使用 px,可传入其他自定义单位的长度值 */ + value: number | string + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.height(number|string value)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.height.html) + * + * 设置高度 */ + height( + /** 长度值,如果传入 number 则默认使用 px,可传入其他自定义单位的长度值 */ + value: number | string + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.left(number|string value)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.left.html) + * + * 设置 left 值 */ + left( + /** 长度值,如果传入 number 则默认使用 px,可传入其他自定义单位的长度值 */ + value: number | string + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.matrix()](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.matrix.html) + * + * 同 [transform-function matrix](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix) */ + matrix(): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.matrix3d()](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.matrix3d.html) + * + * 同 [transform-function matrix3d](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix3d) */ + matrix3d(): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.opacity(number value)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.opacity.html) + * + * 设置透明度 */ + opacity( + /** 透明度,范围 0-1 */ + value: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.right(number|string value)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.right.html) + * + * 设置 right 值 */ + right( + /** 长度值,如果传入 number 则默认使用 px,可传入其他自定义单位的长度值 */ + value: number | string + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.rotate(number angle)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.rotate.html) + * + * 从原点顺时针旋转一个角度 */ + rotate( + /** 旋转的角度。范围 [-180, 180] */ + angle: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.rotate3d(number x, number y, number z, number angle)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.rotate3d.html) + * + * 从 固定 轴顺时针旋转一个角度 */ + rotate3d( + /** 旋转轴的 x 坐标 */ + x: number, + /** 旋转轴的 y 坐标 */ + y: number, + /** 旋转轴的 z 坐标 */ + z: number, + /** 旋转的角度。范围 [-180, 180] */ + angle: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.rotateX(number angle)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.rotateX.html) + * + * 从 X 轴顺时针旋转一个角度 */ + rotateX( + /** 旋转的角度。范围 [-180, 180] */ + angle: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.rotateY(number angle)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.rotateY.html) + * + * 从 Y 轴顺时针旋转一个角度 */ + rotateY( + /** 旋转的角度。范围 [-180, 180] */ + angle: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.rotateZ(number angle)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.rotateZ.html) + * + * 从 Z 轴顺时针旋转一个角度 */ + rotateZ( + /** 旋转的角度。范围 [-180, 180] */ + angle: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.scale(number sx, number sy)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.scale.html) + * + * 缩放 */ + scale( + /** 当仅有 sx 参数时,表示在 X 轴、Y 轴同时缩放sx倍数 */ + sx: number, + /** 在 Y 轴缩放 sy 倍数 */ + sy?: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.scale3d(number sx, number sy, number sz)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.scale3d.html) + * + * 缩放 */ + scale3d( + /** x 轴的缩放倍数 */ + sx: number, + /** y 轴的缩放倍数 */ + sy: number, + /** z 轴的缩放倍数 */ + sz: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.scaleX(number scale)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.scaleX.html) + * + * 缩放 X 轴 */ + scaleX( + /** X 轴的缩放倍数 */ + scale: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.scaleY(number scale)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.scaleY.html) + * + * 缩放 Y 轴 */ + scaleY( + /** Y 轴的缩放倍数 */ + scale: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.scaleZ(number scale)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.scaleZ.html) + * + * 缩放 Z 轴 */ + scaleZ( + /** Z 轴的缩放倍数 */ + scale: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.skew(number ax, number ay)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.skew.html) + * + * 对 X、Y 轴坐标进行倾斜 */ + skew( + /** 对 X 轴坐标倾斜的角度,范围 [-180, 180] */ + ax: number, + /** 对 Y 轴坐标倾斜的角度,范围 [-180, 180] */ + ay: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.skewX(number angle)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.skewX.html) + * + * 对 X 轴坐标进行倾斜 */ + skewX( + /** 倾斜的角度,范围 [-180, 180] */ + angle: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.skewY(number angle)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.skewY.html) + * + * 对 Y 轴坐标进行倾斜 */ + skewY( + /** 倾斜的角度,范围 [-180, 180] */ + angle: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.step(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.step.html) + * + * 表示一组动画完成。可以在一组动画中调用任意多个动画方法,一组动画中的所有动画会同时开始,一组动画完成后才会进行下一组动画。 */ + step(option?: StepOption): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.top(number|string value)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.top.html) + * + * 设置 top 值 */ + top( + /** 长度值,如果传入 number 则默认使用 px,可传入其他自定义单位的长度值 */ + value: number | string + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.translate(number tx, number ty)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.translate.html) + * + * 平移变换 */ + translate( + /** 当仅有该参数时表示在 X 轴偏移 tx,单位 px */ + tx?: number, + /** 在 Y 轴平移的距离,单位为 px */ + ty?: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.translate3d(number tx, number ty, number tz)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.translate3d.html) + * + * 对 xyz 坐标进行平移变换 */ + translate3d( + /** 在 X 轴平移的距离,单位为 px */ + tx?: number, + /** 在 Y 轴平移的距离,单位为 px */ + ty?: number, + /** 在 Z 轴平移的距离,单位为 px */ + tz?: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.translateX(number translation)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.translateX.html) + * + * 对 X 轴平移 */ + translateX( + /** 在 X 轴平移的距离,单位为 px */ + translation: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.translateY(number translation)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.translateY.html) + * + * 对 Y 轴平移 */ + translateY( + /** 在 Y 轴平移的距离,单位为 px */ + translation: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.translateZ(number translation)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.translateZ.html) + * + * 对 Z 轴平移 */ + translateZ( + /** 在 Z 轴平移的距离,单位为 px */ + translation: number + ): Animation + /** [[Animation](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.html) Animation.width(number|string value)](https://developers.weixin.qq.com/miniprogram/dev/api/ui/animation/Animation.width.html) + * + * 设置宽度 */ + width( + /** 长度值,如果传入 number 则默认使用 px,可传入其他自定义单位的长度值 */ + value: number | string + ): Animation + } + interface AudioContext { + /** [AudioContext.pause()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.pause.html) + * + * 暂停音频。 */ + pause(): void + /** [AudioContext.play()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.play.html) + * + * 播放音频。 */ + play(): void + /** [AudioContext.seek(number position)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.seek.html) + * + * 跳转到指定位置。 */ + seek( + /** 跳转位置,单位 s */ + position: number + ): void + /** [AudioContext.setSrc(string src)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.setSrc.html) + * + * 设置音频地址 */ + setSrc( + /** 音频地址 */ + src: string + ): void + } + interface BLEPeripheralServer { + /** [BLEPeripheralServer.addService(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.addService.html) + * + * 添加服务。 + * + * 最低基础库: `2.10.3` */ + addService(option: AddServiceOption): void + /** [BLEPeripheralServer.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.close.html) + * + * 关闭当前服务端。 + * + * 最低基础库: `2.10.3` */ + close(option?: BLEPeripheralServerCloseOption): void + /** [BLEPeripheralServer.offCharacteristicReadRequest(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.offCharacteristicReadRequest.html) + * + * 取消监听已连接的设备请求读当前外围设备的特征值事件 + * + * 最低基础库: `2.10.3` */ + offCharacteristicReadRequest( + /** 已连接的设备请求读当前外围设备的特征值事件的回调函数 */ + callback?: OffCharacteristicReadRequestCallback + ): void + /** [BLEPeripheralServer.offCharacteristicSubscribed(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.offCharacteristicSubscribed.html) + * + * 取消监听特征值订阅事件 + * + * 最低基础库: `2.13.0` */ + offCharacteristicSubscribed( + /** 特征值订阅事件的回调函数 */ + callback?: OffCharacteristicSubscribedCallback + ): void + /** [BLEPeripheralServer.offCharacteristicUnsubscribed(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.offCharacteristicUnsubscribed.html) + * + * 取消监听取消特征值订阅事件 + * + * 最低基础库: `2.13.0` */ + offCharacteristicUnsubscribed( + /** 取消特征值订阅事件的回调函数 */ + callback?: OffCharacteristicUnsubscribedCallback + ): void + /** [BLEPeripheralServer.offCharacteristicWriteRequest(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.offCharacteristicWriteRequest.html) + * + * 取消监听已连接的设备请求写当前外围设备的特征值事件 + * + * 最低基础库: `2.10.3` */ + offCharacteristicWriteRequest( + /** 已连接的设备请求写当前外围设备的特征值事件的回调函数 */ + callback?: OffCharacteristicWriteRequestCallback + ): void + /** [BLEPeripheralServer.onCharacteristicReadRequest(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.onCharacteristicReadRequest.html) + * + * 监听已连接的设备请求读当前外围设备的特征值事件。收到该消息后需要立刻调用 `writeCharacteristicValue` 写回数据,否则主机不会收到响应。 + * + * 最低基础库: `2.10.3` */ + onCharacteristicReadRequest( + /** 已连接的设备请求读当前外围设备的特征值事件的回调函数 */ + callback: OnCharacteristicReadRequestCallback + ): void + /** [BLEPeripheralServer.onCharacteristicSubscribed(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.onCharacteristicSubscribed.html) + * + * 监听特征值订阅事件,仅 iOS 支持。 + * + * 最低基础库: `2.13.0` */ + onCharacteristicSubscribed( + /** 特征值订阅事件的回调函数 */ + callback: OnCharacteristicSubscribedCallback + ): void + /** [BLEPeripheralServer.onCharacteristicUnsubscribed(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.onCharacteristicUnsubscribed.html) + * + * 监听取消特征值订阅事件,仅 iOS 支持。 + * + * 最低基础库: `2.13.0` */ + onCharacteristicUnsubscribed( + /** 取消特征值订阅事件的回调函数 */ + callback: OnCharacteristicUnsubscribedCallback + ): void + /** [BLEPeripheralServer.onCharacteristicWriteRequest(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.onCharacteristicWriteRequest.html) + * + * 监听已连接的设备请求写当前外围设备的特征值事件。收到该消息后需要立刻调用 `writeCharacteristicValue` 写回数据,否则主机不会收到响应。 + * + * 最低基础库: `2.10.3` */ + onCharacteristicWriteRequest( + /** 已连接的设备请求写当前外围设备的特征值事件的回调函数 */ + callback: OnCharacteristicWriteRequestCallback + ): void + /** [BLEPeripheralServer.removeService(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.removeService.html) + * + * 移除服务。 + * + * 最低基础库: `2.10.3` */ + removeService(option: RemoveServiceOption): void + /** [BLEPeripheralServer.startAdvertising(Object Object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.startAdvertising.html) + * + * 开始广播本地创建的外围设备。 + * + * 最低基础库: `2.10.3` */ + startAdvertising(Object: StartAdvertisingObject): void + /** [BLEPeripheralServer.stopAdvertising(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.stopAdvertising.html) + * + * 停止广播。 + * + * 最低基础库: `2.10.3` */ + stopAdvertising(option?: StopAdvertisingOption): void + /** [BLEPeripheralServer.writeCharacteristicValue(Object Object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth-peripheral/BLEPeripheralServer.writeCharacteristicValue.html) + * + * 往指定特征值写入数据,并通知已连接的主机,从机的特征值已发生变化,该接口会处理是走回包还是走订阅。 + * + * 最低基础库: `2.10.3` */ + writeCharacteristicValue(Object: WriteCharacteristicValueObject): void + } + interface BackgroundAudioError { + /** 错误信息 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 10001 | | 系统错误 | + * | 10002 | | 网络错误 | + * | 10003 | | 文件错误,请检查是否responseheader是否缺少Content-Length | + * | 10004 | | 格式错误 | + * | -1 | | 未知错误 | */ errMsg: string + /** 错误码 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 10001 | | 系统错误 | + * | 10002 | | 网络错误 | + * | 10003 | | 文件错误,请检查是否responseheader是否缺少Content-Length | + * | 10004 | | 格式错误 | + * | -1 | | 未知错误 | */ errCode: number + } + interface BackgroundAudioManager { + /** [BackgroundAudioManager.onCanplay(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onCanplay.html) + * + * 监听背景音频进入可播放状态事件。 但不保证后面可以流畅播放 */ + onCanplay( + /** 背景音频进入可播放状态事件的回调函数 */ + callback: OnCanplayCallback + ): void + /** [BackgroundAudioManager.onEnded(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onEnded.html) + * + * 监听背景音频自然播放结束事件 */ + onEnded( + /** 背景音频自然播放结束事件的回调函数 */ + callback: OnEndedCallback + ): void + /** [BackgroundAudioManager.onError(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onError.html) + * + * 监听背景音频播放错误事件 */ + onError( + /** 背景音频播放错误事件的回调函数 */ + callback: BackgroundAudioManagerOnErrorCallback + ): void + /** [BackgroundAudioManager.onNext(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onNext.html) + * + * 监听用户在系统音乐播放面板点击下一曲事件(仅iOS) */ + onNext( + /** 用户在系统音乐播放面板点击下一曲事件的回调函数 */ + callback: OnNextCallback + ): void + /** [BackgroundAudioManager.onPause(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onPause.html) + * + * 监听背景音频暂停事件 */ + onPause( + /** 背景音频暂停事件的回调函数 */ + callback: OnPauseCallback + ): void + /** [BackgroundAudioManager.onPlay(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onPlay.html) + * + * 监听背景音频播放事件 */ + onPlay( + /** 背景音频播放事件的回调函数 */ + callback: OnPlayCallback + ): void + /** [BackgroundAudioManager.onPrev(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onPrev.html) + * + * 监听用户在系统音乐播放面板点击上一曲事件(仅iOS) */ + onPrev( + /** 用户在系统音乐播放面板点击上一曲事件的回调函数 */ + callback: OnPrevCallback + ): void + /** [BackgroundAudioManager.onSeeked(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onSeeked.html) + * + * 监听背景音频完成跳转操作事件 */ + onSeeked( + /** 背景音频完成跳转操作事件的回调函数 */ + callback: OnSeekedCallback + ): void + /** [BackgroundAudioManager.onSeeking(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onSeeking.html) + * + * 监听背景音频开始跳转操作事件 */ + onSeeking( + /** 背景音频开始跳转操作事件的回调函数 */ + callback: OnSeekingCallback + ): void + /** [BackgroundAudioManager.onStop(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onStop.html) + * + * 监听背景音频停止事件 */ + onStop( + /** 背景音频停止事件的回调函数 */ + callback: InnerAudioContextOnStopCallback + ): void + /** [BackgroundAudioManager.onTimeUpdate(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onTimeUpdate.html) + * + * 监听背景音频播放进度更新事件,只有小程序在前台时会回调。 */ + onTimeUpdate( + /** 背景音频播放进度更新事件的回调函数 */ + callback: OnTimeUpdateCallback + ): void + /** [BackgroundAudioManager.onWaiting(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.onWaiting.html) + * + * 监听音频加载中事件。当音频因为数据不足,需要停下来加载时会触发 */ + onWaiting( + /** 音频加载中事件的回调函数 */ + callback: OnWaitingCallback + ): void + /** [BackgroundAudioManager.pause()](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.pause.html) + * + * 暂停音乐 */ + pause(): void + /** [BackgroundAudioManager.play()](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.play.html) + * + * 播放音乐 */ + play(): void + /** [BackgroundAudioManager.seek(number currentTime)](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.seek.html) + * + * 跳转到指定位置 */ + seek( + /** 跳转的位置,单位 s。精确到小数点后 3 位,即支持 ms 级别精确度 */ + currentTime: number + ): void + /** [BackgroundAudioManager.stop()](https://developers.weixin.qq.com/miniprogram/dev/api/media/background-audio/BackgroundAudioManager.stop.html) + * + * 停止音乐 */ + stop(): void + } + interface BluetoothError { + /** 错误信息 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 0 | ok | 正常 | + * | -1 | already connet | 已连接 | + * | 10000 | not init | 未初始化蓝牙适配器 | + * | 10001 | not available | 当前蓝牙适配器不可用 | + * | 10002 | no device | 没有找到指定设备 | + * | 10003 | connection fail | 连接失败 | + * | 10004 | no service | 没有找到指定服务 | + * | 10005 | no characteristic | 没有找到指定特征值 | + * | 10006 | no connection | 当前连接已断开 | + * | 10007 | property not support | 当前特征值不支持此操作 | + * | 10008 | system error | 其余所有系统上报的异常 | + * | 10009 | system not support | Android 系统特有,系统版本低于 4.3 不支持 BLE | + * | 10012 | operate time out | 连接超时 | + * | 10013 | invalid_data | 连接 deviceId 为空或者是格式不正确 | */ errMsg: string + /** 错误码 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 0 | ok | 正常 | + * | -1 | already connet | 已连接 | + * | 10000 | not init | 未初始化蓝牙适配器 | + * | 10001 | not available | 当前蓝牙适配器不可用 | + * | 10002 | no device | 没有找到指定设备 | + * | 10003 | connection fail | 连接失败 | + * | 10004 | no service | 没有找到指定服务 | + * | 10005 | no characteristic | 没有找到指定特征值 | + * | 10006 | no connection | 当前连接已断开 | + * | 10007 | property not support | 当前特征值不支持此操作 | + * | 10008 | system error | 其余所有系统上报的异常 | + * | 10009 | system not support | Android 系统特有,系统版本低于 4.3 不支持 BLE | + * | 10012 | operate time out | 连接超时 | + * | 10013 | invalid_data | 连接 deviceId 为空或者是格式不正确 | */ errCode: number + } + interface CameraContext { + /** [CameraContext.setZoom(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraContext.setZoom.html) + * + * 设置缩放级别 + * + * 最低基础库: `2.10.0` */ + setZoom(option: SetZoomOption): void + /** [CameraContext.startRecord(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraContext.startRecord.html) + * + * 开始录像 */ + startRecord(option: CameraContextStartRecordOption): void + /** [CameraContext.stopRecord(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraContext.stopRecord.html) + * + * 结束录像 */ + stopRecord(option: CameraContextStopRecordOption): void + /** [CameraContext.takePhoto(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraContext.takePhoto.html) + * + * 拍摄照片 */ + takePhoto(option: TakePhotoOption): void + /** [[CameraFrameListener](https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraFrameListener.html) CameraContext.onCameraFrame(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraContext.onCameraFrame.html) +* +* 获取 Camera 实时帧数据 +* +* **** +* +* 注: 使用该接口需同时在 [camera](https://developers.weixin.qq.com/miniprogram/dev/component/camera.html) 组件属性中指定 frame-size。 +* +* **示例代码** +* +* +* ```js +const context = wx.createCameraContext() +const listener = context.onCameraFrame((frame) => { + console.log(frame.data instanceof ArrayBuffer, frame.width, frame.height) +}) +listener.start() +``` +* +* 最低基础库: `2.7.0` */ + onCameraFrame( + /** 回调函数 */ + callback: OnCameraFrameCallback + ): CameraFrameListener + } + interface CameraFrameListener { + /** [CameraFrameListener.start(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraFrameListener.start.html) + * + * 开始监听帧数据 */ + start(option?: CameraFrameListenerStartOption): void + /** [CameraFrameListener.stop(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/camera/CameraFrameListener.stop.html) + * + * 停止监听帧数据 */ + stop(option?: StopOption): void + } + interface Canvas { + /** [Canvas.cancelAnimationFrame(number requestID)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Canvas.cancelAnimationFrame.html) + * + * 取消由 requestAnimationFrame 添加到计划中的动画帧请求。支持在 2D Canvas 和 WebGL Canvas 下使用, 但不支持混用 2D 和 WebGL 的方法。 + * + * 最低基础库: `2.7.0` */ + cancelAnimationFrame(requestID: number): void + /** [[ImageData](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/ImageData.html) Canvas.createImageData()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Canvas.createImageData.html) + * + * 创建一个 ImageData 对象。仅支持在 2D Canvas 中使用。 + * + * 最低基础库: `2.9.0` */ + createImageData(): ImageData + /** [[Image](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Image.html) Canvas.createImage()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Canvas.createImage.html) + * + * 创建一个图片对象。 支持在 2D Canvas 和 WebGL Canvas 下使用, 但不支持混用 2D 和 WebGL 的方法。 + * + * 最低基础库: `2.7.0` */ + createImage(): Image + /** [[Path2D](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Path2D.html) Canvas.createPath2D([Path2D](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Path2D.html) path)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Canvas.createPath2D.html) + * + * 创建 Path2D 对象 + * + * 最低基础库: `2.11.0` */ + createPath2D( + /** [Path2D](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Path2D.html) + * + * */ + path: Path2D + ): Path2D + /** [[RenderingContext](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/RenderingContext.html) Canvas.getContext(string contextType)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Canvas.getContext.html) + * + * 该方法返回 Canvas 的绘图上下文 + * + * **** + * + * 支持获取 2D 和 WebGL 绘图上下文 + * + * 最低基础库: `2.7.0` */ + getContext(contextType: string): any + /** [number Canvas.requestAnimationFrame(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Canvas.requestAnimationFrame.html) + * + * 在下次进行重绘时执行。 支持在 2D Canvas 和 WebGL Canvas 下使用, 但不支持混用 2D 和 WebGL 的方法。 + * + * 最低基础库: `2.7.0` */ + requestAnimationFrame( + /** 执行的 callback */ + callback: (...args: any[]) => any + ): number + /** [string Canvas.toDataURL(string type, number encoderOptions)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/Canvas.toDataURL.html) + * + * 返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。 + * + * 最低基础库: `2.11.0` */ + toDataURL( + /** 图片格式,默认为 image/png */ + type: string, + /** 在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。 */ + encoderOptions: number + ): string + } + interface CanvasContext { + /** [CanvasContext.arc(number x, number y, number r, number sAngle, number eAngle, boolean counterclockwise)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.arc.html) +* +* 创建一条弧线。 +* +* - 创建一个圆可以指定起始弧度为 0,终止弧度为 2 * Math.PI。 +* - 用 `stroke` 或者 `fill` 方法来在 `canvas` 中画弧线。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +// Draw coordinates +ctx.arc(100, 75, 50, 0, 2 * Math.PI) +ctx.setFillStyle('#EEEEEE') +ctx.fill() + +ctx.beginPath() +ctx.moveTo(40, 75) +ctx.lineTo(160, 75) +ctx.moveTo(100, 15) +ctx.lineTo(100, 135) +ctx.setStrokeStyle('#AAAAAA') +ctx.stroke() + +ctx.setFontSize(12) +ctx.setFillStyle('black') +ctx.fillText('0', 165, 78) +ctx.fillText('0.5*PI', 83, 145) +ctx.fillText('1*PI', 15, 78) +ctx.fillText('1.5*PI', 83, 10) + +// Draw points +ctx.beginPath() +ctx.arc(100, 75, 2, 0, 2 * Math.PI) +ctx.setFillStyle('lightgreen') +ctx.fill() + +ctx.beginPath() +ctx.arc(100, 25, 2, 0, 2 * Math.PI) +ctx.setFillStyle('blue') +ctx.fill() + +ctx.beginPath() +ctx.arc(150, 75, 2, 0, 2 * Math.PI) +ctx.setFillStyle('red') +ctx.fill() + +// Draw arc +ctx.beginPath() +ctx.arc(100, 75, 50, 0, 1.5 * Math.PI) +ctx.setStrokeStyle('#333333') +ctx.stroke() + +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/arc.png) +* +* 针对 arc(100, 75, 50, 0, 1.5 * Math.PI)的三个关键坐标如下: +* +* - 绿色: 圆心 (100, 75) +* - 红色: 起始弧度 (0) +* - 蓝色: 终止弧度 (1.5 * Math.PI) */ + arc( + /** 圆心的 x 坐标 */ + x: number, + /** 圆心的 y 坐标 */ + y: number, + /** 圆的半径 */ + r: number, + /** 起始弧度,单位弧度(在3点钟方向) */ + sAngle: number, + /** 终止弧度 */ + eAngle: number, + /** 弧度的方向是否是逆时针 */ + counterclockwise?: boolean + ): void + /** [CanvasContext.arcTo(number x1, number y1, number x2, number y2, number radius)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.arcTo.html) + * + * 根据控制点和半径绘制圆弧路径。 + * + * 最低基础库: `1.9.90` */ + arcTo( + /** 第一个控制点的 x 轴坐标 */ + x1: number, + /** 第一个控制点的 y 轴坐标 */ + y1: number, + /** 第二个控制点的 x 轴坐标 */ + x2: number, + /** 第二个控制点的 y 轴坐标 */ + y2: number, + /** 圆弧的半径 */ + radius: number + ): void + /** [CanvasContext.beginPath()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.beginPath.html) +* +* 开始创建一个路径。需要调用 `fill` 或者 `stroke` 才会使用路径进行填充或描边 +* +* - 在最开始的时候相当于调用了一次 `beginPath`。 +* - 同一个路径内的多次 `setFillStyle`、`setStrokeStyle`、`setLineWidth`等设置,以最后一次设置为准。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +// begin path +ctx.rect(10, 10, 100, 30) +ctx.setFillStyle('yellow') +ctx.fill() + +// begin another path +ctx.beginPath() +ctx.rect(10, 40, 100, 30) + +// only fill this rect, not in current path +ctx.setFillStyle('blue') +ctx.fillRect(10, 70, 100, 30) + +ctx.rect(10, 100, 100, 30) + +// it will fill current path +ctx.setFillStyle('red') +ctx.fill() +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/fill-path.png) */ + beginPath(): void + /** [CanvasContext.bezierCurveTo(number cp1x, number cp1y, number cp2x, number cp2y, number x, number y)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.bezierCurveTo.html) +* +* 创建三次方贝塞尔曲线路径。曲线的起始点为路径中前一个点。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +// Draw points +ctx.beginPath() +ctx.arc(20, 20, 2, 0, 2 * Math.PI) +ctx.setFillStyle('red') +ctx.fill() + +ctx.beginPath() +ctx.arc(200, 20, 2, 0, 2 * Math.PI) +ctx.setFillStyle('lightgreen') +ctx.fill() + +ctx.beginPath() +ctx.arc(20, 100, 2, 0, 2 * Math.PI) +ctx.arc(200, 100, 2, 0, 2 * Math.PI) +ctx.setFillStyle('blue') +ctx.fill() + +ctx.setFillStyle('black') +ctx.setFontSize(12) + +// Draw guides +ctx.beginPath() +ctx.moveTo(20, 20) +ctx.lineTo(20, 100) +ctx.lineTo(150, 75) + +ctx.moveTo(200, 20) +ctx.lineTo(200, 100) +ctx.lineTo(70, 75) +ctx.setStrokeStyle('#AAAAAA') +ctx.stroke() + +// Draw quadratic curve +ctx.beginPath() +ctx.moveTo(20, 20) +ctx.bezierCurveTo(20, 100, 200, 100, 200, 20) +ctx.setStrokeStyle('black') +ctx.stroke() + +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/bezier-curve.png) +* +* 针对 moveTo(20, 20) bezierCurveTo(20, 100, 200, 100, 200, 20) 的三个关键坐标如下: +* +* - 红色:起始点(20, 20) +* - 蓝色:两个控制点(20, 100) (200, 100) +* - 绿色:终止点(200, 20) */ + bezierCurveTo( + /** 第一个贝塞尔控制点的 x 坐标 */ + cp1x: number, + /** 第一个贝塞尔控制点的 y 坐标 */ + cp1y: number, + /** 第二个贝塞尔控制点的 x 坐标 */ + cp2x: number, + /** 第二个贝塞尔控制点的 y 坐标 */ + cp2y: number, + /** 结束点的 x 坐标 */ + x: number, + /** 结束点的 y 坐标 */ + y: number + ): void + /** [CanvasContext.clearRect(number x, number y, number width, number height)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.clearRect.html) +* +* 清除画布上在该矩形区域内的内容 +* +* **示例代码** +* +* +* clearRect 并非画一个白色的矩形在地址区域,而是清空,为了有直观感受,对 canvas 加了一层背景色。 +* ```html +* +* ``` +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.setFillStyle('red') +ctx.fillRect(0, 0, 150, 200) +ctx.setFillStyle('blue') +ctx.fillRect(150, 0, 150, 200) +ctx.clearRect(10, 10, 150, 75) +ctx.draw() +``` +* ![](@program/dev/image/canvas/clear-rect.png) */ + clearRect( + /** 矩形路径左上角的横坐标 */ + x: number, + /** 矩形路径左上角的纵坐标 */ + y: number, + /** 矩形路径的宽度 */ + width: number, + /** 矩形路径的高度 */ + height: number + ): void + /** [CanvasContext.clip()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.clip.html) +* +* 从原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。可以在使用 `clip` 方法前通过使用 `save` 方法对当前画布区域进行保存,并在以后的任意时间通过`restore`方法对其进行恢复。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +wx.downloadFile({ + url: 'http://is5.mzstatic.com/image/thumb/Purple128/v4/75/3b/90/753b907c-b7fb-5877-215a-759bd73691a4/source/50x50bb.jpg', + success: function(res) { + ctx.save() + ctx.beginPath() + ctx.arc(50, 50, 25, 0, 2*Math.PI) + ctx.clip() + ctx.drawImage(res.tempFilePath, 25, 25) + ctx.restore() + ctx.draw() + } +}) +``` +* ![](@program/dev/image/canvas/clip.png) +* +* 最低基础库: `1.6.0` */ + clip(): void + /** [CanvasContext.closePath()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.closePath.html) +* +* 关闭一个路径。会连接起点和终点。如果关闭路径后没有调用 `fill` 或者 `stroke` 并开启了新的路径,那之前的路径将不会被渲染。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.moveTo(10, 10) +ctx.lineTo(100, 10) +ctx.lineTo(100, 100) +ctx.closePath() +ctx.stroke() +ctx.draw() +``` +* ![](@program/dev/image/canvas/close-line.png) +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +// begin path +ctx.rect(10, 10, 100, 30) +ctx.closePath() + +// begin another path +ctx.beginPath() +ctx.rect(10, 40, 100, 30) + +// only fill this rect, not in current path +ctx.setFillStyle('blue') +ctx.fillRect(10, 70, 100, 30) + +ctx.rect(10, 100, 100, 30) + +// it will fill current path +ctx.setFillStyle('red') +ctx.fill() +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/close-path.png) */ + closePath(): void + /** [CanvasContext.createPattern(string image, string repetition)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.createPattern.html) + * + * 对指定的图像创建模式的方法,可在指定的方向上重复元图像 + * + * 最低基础库: `1.9.90` */ + createPattern( + /** 重复的图像源,支持代码包路径和本地临时路径 (本地路径) */ + image: string, + /** 如何重复图像 + * + * 参数 repetition 可选值: + * - 'repeat': 水平竖直方向都重复; + * - 'repeat-x': 水平方向重复; + * - 'repeat-y': 竖直方向重复; + * - 'no-repeat': 不重复; */ + repetition: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' + ): void + /** [CanvasContext.draw(boolean reserve, function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.draw.html) +* +* 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中。 +* +* **示例代码** +* +* +* 第二次 draw() reserve 为 true。所以保留了上一次的绘制结果,在上下文设置的 fillStyle 'red' 也变成了默认的 'black'。 +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.setFillStyle('red') +ctx.fillRect(10, 10, 150, 100) +ctx.draw() +ctx.fillRect(50, 50, 150, 100) +ctx.draw(true) +``` +* ![](@program/dev/image/canvas/reserve.png) +* +* **示例代码** +* +* +* 第二次 draw() reserve 为 false。所以没有保留了上一次的绘制结果和在上下文设置的 fillStyle 'red'。 +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.setFillStyle('red') +ctx.fillRect(10, 10, 150, 100) +ctx.draw() +ctx.fillRect(50, 50, 150, 100) +ctx.draw() +``` +* ![](@program/dev/image/canvas/un-reserve.png) */ + draw( + /** 本次绘制是否接着上一次绘制。即 reserve 参数为 false,则在本次调用绘制之前 native 层会先清空画布再继续绘制;若 reserve 参数为 true,则保留当前画布上的内容,本次调用 drawCanvas 绘制的内容覆盖在上面,默认 false。 */ + reserve?: boolean, + /** 绘制完成后执行的回调函数 */ + callback?: (...args: any[]) => any + ): void + /** [CanvasContext.drawImage(string imageResource, number sx, number sy, number sWidth, number sHeight, number dx, number dy, number dWidth, number dHeight)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.drawImage.html) +* +* 绘制图像到画布 +* +* **示例代码** +* +* +* +* 有三个版本的写法: +* +* - drawImage(imageResource, dx, dy) +* - drawImage(imageResource, dx, dy, dWidth, dHeight) +* - drawImage(imageResource, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 从 1.9.0 起支持 +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +wx.chooseImage({ + success: function(res){ + ctx.drawImage(res.tempFilePaths[0], 0, 0, 150, 100) + ctx.draw() + } +}) + +``` +* ![](@program/dev/image/canvas/draw-image.png) */ + drawImage( + /** 所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载) */ + imageResource: string, + /** imageResource的左上角在目标 canvas 上 x 轴的位置 */ + dx: number, + /** imageResource的左上角在目标 canvas 上 y 轴的位置 */ + dy: number + ): void + /** [CanvasContext.drawImage(string imageResource, number sx, number sy, number sWidth, number sHeight, number dx, number dy, number dWidth, number dHeight)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.drawImage.html) +* +* 绘制图像到画布 +* +* **示例代码** +* +* +* +* 有三个版本的写法: +* +* - drawImage(imageResource, dx, dy) +* - drawImage(imageResource, dx, dy, dWidth, dHeight) +* - drawImage(imageResource, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 从 1.9.0 起支持 +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +wx.chooseImage({ + success: function(res){ + ctx.drawImage(res.tempFilePaths[0], 0, 0, 150, 100) + ctx.draw() + } +}) + +``` +* ![](@program/dev/image/canvas/draw-image.png) */ + drawImage( + /** 所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载) */ + imageResource: string, + /** imageResource的左上角在目标 canvas 上 x 轴的位置 */ + dx: number, + /** imageResource的左上角在目标 canvas 上 y 轴的位置 */ + dy: number, + /** 在目标画布上绘制imageResource的宽度,允许对绘制的imageResource进行缩放 */ + dWidth: number, + /** 在目标画布上绘制imageResource的高度,允许对绘制的imageResource进行缩放 */ + dHeight: number + ): void + /** [CanvasContext.drawImage(string imageResource, number sx, number sy, number sWidth, number sHeight, number dx, number dy, number dWidth, number dHeight)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.drawImage.html) +* +* 绘制图像到画布 +* +* **示例代码** +* +* +* +* 有三个版本的写法: +* +* - drawImage(imageResource, dx, dy) +* - drawImage(imageResource, dx, dy, dWidth, dHeight) +* - drawImage(imageResource, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 从 1.9.0 起支持 +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +wx.chooseImage({ + success: function(res){ + ctx.drawImage(res.tempFilePaths[0], 0, 0, 150, 100) + ctx.draw() + } +}) + +``` +* ![](@program/dev/image/canvas/draw-image.png) */ + drawImage( + /** 所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载) */ + imageResource: string, + /** 需要绘制到画布中的,imageResource的矩形(裁剪)选择框的左上角 x 坐标 */ + sx: number, + /** 需要绘制到画布中的,imageResource的矩形(裁剪)选择框的左上角 y 坐标 */ + sy: number, + /** 需要绘制到画布中的,imageResource的矩形(裁剪)选择框的宽度 */ + sWidth: number, + /** 需要绘制到画布中的,imageResource的矩形(裁剪)选择框的高度 */ + sHeight: number, + /** imageResource的左上角在目标 canvas 上 x 轴的位置 */ + dx: number, + /** imageResource的左上角在目标 canvas 上 y 轴的位置 */ + dy: number, + /** 在目标画布上绘制imageResource的宽度,允许对绘制的imageResource进行缩放 */ + dWidth: number, + /** 在目标画布上绘制imageResource的高度,允许对绘制的imageResource进行缩放 */ + dHeight: number + ): void + /** [CanvasContext.fill()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.fill.html) +* +* 对当前路径中的内容进行填充。默认的填充色为黑色。 +* +* **示例代码** +* +* +* +* 如果当前路径没有闭合,fill() 方法会将起点和终点进行连接,然后填充。 +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.moveTo(10, 10) +ctx.lineTo(100, 10) +ctx.lineTo(100, 100) +ctx.fill() +ctx.draw() +``` +* +* fill() 填充的的路径是从 beginPath() 开始计算,但是不会将 fillRect() 包含进去。 +* +* ![](@program/dev/image/canvas/fill-line.png) +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +// begin path +ctx.rect(10, 10, 100, 30) +ctx.setFillStyle('yellow') +ctx.fill() + +// begin another path +ctx.beginPath() +ctx.rect(10, 40, 100, 30) + +// only fill this rect, not in current path +ctx.setFillStyle('blue') +ctx.fillRect(10, 70, 100, 30) + +ctx.rect(10, 100, 100, 30) + +// it will fill current path +ctx.setFillStyle('red') +ctx.fill() +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/fill-path.png) */ + fill(): void + /** [CanvasContext.fillRect(number x, number y, number width, number height)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.fillRect.html) +* +* 填充一个矩形。用 [`setFillStyle`](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setFillStyle.html) 设置矩形的填充色,如果没设置默认是黑色。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.setFillStyle('red') +ctx.fillRect(10, 10, 150, 75) +ctx.draw() +``` +* ![](@program/dev/image/canvas/fill-rect.png) */ + fillRect( + /** 矩形路径左上角的横坐标 */ + x: number, + /** 矩形路径左上角的纵坐标 */ + y: number, + /** 矩形路径的宽度 */ + width: number, + /** 矩形路径的高度 */ + height: number + ): void + /** [CanvasContext.fillText(string text, number x, number y, number maxWidth)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.fillText.html) +* +* 在画布上绘制被填充的文本 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.setFontSize(20) +ctx.fillText('Hello', 20, 20) +ctx.fillText('MINA', 100, 100) + +ctx.draw() +``` +* ![](@program/dev/image/canvas/text.png) */ + fillText( + /** 在画布上输出的文本 */ + text: string, + /** 绘制文本的左上角 x 坐标位置 */ + x: number, + /** 绘制文本的左上角 y 坐标位置 */ + y: number, + /** 需要绘制的最大宽度,可选 */ + maxWidth?: number + ): void + /** [CanvasContext.lineTo(number x, number y)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.lineTo.html) +* +* 增加一个新点,然后创建一条从上次指定点到目标点的线。用 `stroke` 方法来画线条 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.moveTo(10, 10) +ctx.rect(10, 10, 100, 50) +ctx.lineTo(110, 60) +ctx.stroke() +ctx.draw() +``` +* ![](@program/dev/image/canvas/line-to.png) */ + lineTo( + /** 目标位置的 x 坐标 */ + x: number, + /** 目标位置的 y 坐标 */ + y: number + ): void + /** [CanvasContext.moveTo(number x, number y)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.moveTo.html) +* +* 把路径移动到画布中的指定点,不创建线条。用 `stroke` 方法来画线条 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.moveTo(10, 10) +ctx.lineTo(100, 10) + +ctx.moveTo(10, 50) +ctx.lineTo(100, 50) +ctx.stroke() +ctx.draw() +``` +* ![](@program/dev/image/canvas/move-to.png) */ + moveTo( + /** 目标位置的 x 坐标 */ + x: number, + /** 目标位置的 y 坐标 */ + y: number + ): void + /** [CanvasContext.quadraticCurveTo(number cpx, number cpy, number x, number y)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.quadraticCurveTo.html) +* +* 创建二次贝塞尔曲线路径。曲线的起始点为路径中前一个点。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +// Draw points +ctx.beginPath() +ctx.arc(20, 20, 2, 0, 2 * Math.PI) +ctx.setFillStyle('red') +ctx.fill() + +ctx.beginPath() +ctx.arc(200, 20, 2, 0, 2 * Math.PI) +ctx.setFillStyle('lightgreen') +ctx.fill() + +ctx.beginPath() +ctx.arc(20, 100, 2, 0, 2 * Math.PI) +ctx.setFillStyle('blue') +ctx.fill() + +ctx.setFillStyle('black') +ctx.setFontSize(12) + +// Draw guides +ctx.beginPath() +ctx.moveTo(20, 20) +ctx.lineTo(20, 100) +ctx.lineTo(200, 20) +ctx.setStrokeStyle('#AAAAAA') +ctx.stroke() + +// Draw quadratic curve +ctx.beginPath() +ctx.moveTo(20, 20) +ctx.quadraticCurveTo(20, 100, 200, 20) +ctx.setStrokeStyle('black') +ctx.stroke() + +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/quadratic-curve-to.png) +* +* 针对 moveTo(20, 20) quadraticCurveTo(20, 100, 200, 20) 的三个关键坐标如下: +* +* - 红色:起始点(20, 20) +* - 蓝色:控制点(20, 100) +* - 绿色:终止点(200, 20) */ + quadraticCurveTo( + /** 贝塞尔控制点的 x 坐标 */ + cpx: number, + /** 贝塞尔控制点的 y 坐标 */ + cpy: number, + /** 结束点的 x 坐标 */ + x: number, + /** 结束点的 y 坐标 */ + y: number + ): void + /** [CanvasContext.rect(number x, number y, number width, number height)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.rect.html) +* +* 创建一个矩形路径。需要用 [`fill`](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.fill.html) 或者 [`stroke`](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.stroke.html) 方法将矩形真正的画到 `canvas` 中 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.rect(10, 10, 150, 75) +ctx.setFillStyle('red') +ctx.fill() +ctx.draw() +``` +* ![](@program/dev/image/canvas/fill-rect.png) */ + rect( + /** 矩形路径左上角的横坐标 */ + x: number, + /** 矩形路径左上角的纵坐标 */ + y: number, + /** 矩形路径的宽度 */ + width: number, + /** 矩形路径的高度 */ + height: number + ): void + /** [CanvasContext.restore()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.restore.html) +* +* 恢复之前保存的绘图上下文。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +// save the default fill style +ctx.save() +ctx.setFillStyle('red') +ctx.fillRect(10, 10, 150, 100) + +// restore to the previous saved state +ctx.restore() +ctx.fillRect(50, 50, 150, 100) + +ctx.draw() +``` +* ![](@program/dev/image/canvas/save-restore.png) */ + restore(): void + /** [CanvasContext.rotate(number rotate)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.rotate.html) +* +* 以原点为中心顺时针旋转当前坐标轴。多次调用旋转的角度会叠加。原点可以用 `translate` 方法修改。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.strokeRect(100, 10, 150, 100) +ctx.rotate(20 * Math.PI / 180) +ctx.strokeRect(100, 10, 150, 100) +ctx.rotate(20 * Math.PI / 180) +ctx.strokeRect(100, 10, 150, 100) + +ctx.draw() +``` +* ![](@program/dev/image/canvas/rotate.png) */ + rotate( + /** 旋转角度,以弧度计 degrees * Math.PI/180;degrees 范围为 0-360 */ + rotate: number + ): void + /** [CanvasContext.save()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.save.html) +* +* 保存绘图上下文。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +// save the default fill style +ctx.save() +ctx.setFillStyle('red') +ctx.fillRect(10, 10, 150, 100) + +// restore to the previous saved state +ctx.restore() +ctx.fillRect(50, 50, 150, 100) + +ctx.draw() +``` +* ![](@program/dev/image/canvas/save-restore.png) */ + save(): void + /** [CanvasContext.scale(number scaleWidth, number scaleHeight)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.scale.html) +* +* 在调用后,之后创建的路径其横纵坐标会被缩放。多次调用倍数会相乘。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.strokeRect(10, 10, 25, 15) +ctx.scale(2, 2) +ctx.strokeRect(10, 10, 25, 15) +ctx.scale(2, 2) +ctx.strokeRect(10, 10, 25, 15) + +ctx.draw() +``` +* ![](@program/dev/image/canvas/scale.png) */ + scale( + /** 横坐标缩放的倍数 (1 = 100%,0.5 = 50%,2 = 200%) */ + scaleWidth: number, + /** 纵坐标轴缩放的倍数 (1 = 100%,0.5 = 50%,2 = 200%) */ + scaleHeight: number + ): void + /** [CanvasContext.setFillStyle(string|[CanvasGradient](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasGradient.html) color)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setFillStyle.html) +* +* 设置填充色。 +* +* **代码示例** +* +* +* ```js +const ctx = wx.createCanvasContext('myCanvas') +ctx.setFillStyle('red') +ctx.fillRect(10, 10, 150, 75) +ctx.draw() +``` +* ![](@program/dev/image/canvas/fill-rect.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.fillStyle](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setFillStyle( + /** 填充的颜色,默认颜色为 black。 */ + color: string | CanvasGradient + ): void + /** [CanvasContext.setFontSize(number fontSize)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setFontSize.html) +* +* 设置字体的字号 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.setFontSize(20) +ctx.fillText('20', 20, 20) +ctx.setFontSize(30) +ctx.fillText('30', 40, 40) +ctx.setFontSize(40) +ctx.fillText('40', 60, 60) +ctx.setFontSize(50) +ctx.fillText('50', 90, 90) + +ctx.draw() +``` +* ![](@program/dev/image/canvas/font-size.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.font](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setFontSize( + /** 字体的字号 */ + fontSize: number + ): void + /** [CanvasContext.setGlobalAlpha(number alpha)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setGlobalAlpha.html) +* +* 设置全局画笔透明度。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.setFillStyle('red') +ctx.fillRect(10, 10, 150, 100) +ctx.setGlobalAlpha(0.2) +ctx.setFillStyle('blue') +ctx.fillRect(50, 50, 150, 100) +ctx.setFillStyle('yellow') +ctx.fillRect(100, 100, 150, 100) + +ctx.draw() +``` +* ![](@program/dev/image/canvas/global-alpha.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.globalAlpha](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setGlobalAlpha( + /** 透明度。范围 0-1,0 表示完全透明,1 表示完全不透明。 */ + alpha: number + ): void + /** [CanvasContext.setLineCap(string lineCap)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setLineCap.html) +* +* 设置线条的端点样式 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.beginPath() +ctx.moveTo(10, 10) +ctx.lineTo(150, 10) +ctx.stroke() + +ctx.beginPath() +ctx.setLineCap('butt') +ctx.setLineWidth(10) +ctx.moveTo(10, 30) +ctx.lineTo(150, 30) +ctx.stroke() + +ctx.beginPath() +ctx.setLineCap('round') +ctx.setLineWidth(10) +ctx.moveTo(10, 50) +ctx.lineTo(150, 50) +ctx.stroke() + +ctx.beginPath() +ctx.setLineCap('square') +ctx.setLineWidth(10) +ctx.moveTo(10, 70) +ctx.lineTo(150, 70) +ctx.stroke() + +ctx.draw() +``` +* ![](@program/dev/image/canvas/line-cap.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.lineCap](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setLineCap( + /** 线条的结束端点样式 + * + * 参数 lineCap 可选值: + * - 'butt': 向线条的每个末端添加平直的边缘。; + * - 'round': 向线条的每个末端添加圆形线帽。; + * - 'square': 向线条的每个末端添加正方形线帽。; */ + lineCap: 'butt' | 'round' | 'square' + ): void + /** [CanvasContext.setLineDash(Array.<number> pattern, number offset)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setLineDash.html) +* +* 设置虚线样式。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.setLineDash([10, 20], 5); + +ctx.beginPath(); +ctx.moveTo(0,100); +ctx.lineTo(400, 100); +ctx.stroke(); + +ctx.draw() +``` +* ![](@program/dev/image/canvas/set-line-dash.png) +* +* 最低基础库: `1.6.0` +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.lineDashOffset](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setLineDash( + /** 一组描述交替绘制线段和间距(坐标空间单位)长度的数字 */ + pattern: number[], + /** 虚线偏移量 */ + offset: number + ): void + /** [CanvasContext.setLineJoin(string lineJoin)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setLineJoin.html) +* +* 设置线条的交点样式 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.beginPath() +ctx.moveTo(10, 10) +ctx.lineTo(100, 50) +ctx.lineTo(10, 90) +ctx.stroke() + +ctx.beginPath() +ctx.setLineJoin('bevel') +ctx.setLineWidth(10) +ctx.moveTo(50, 10) +ctx.lineTo(140, 50) +ctx.lineTo(50, 90) +ctx.stroke() + +ctx.beginPath() +ctx.setLineJoin('round') +ctx.setLineWidth(10) +ctx.moveTo(90, 10) +ctx.lineTo(180, 50) +ctx.lineTo(90, 90) +ctx.stroke() + +ctx.beginPath() +ctx.setLineJoin('miter') +ctx.setLineWidth(10) +ctx.moveTo(130, 10) +ctx.lineTo(220, 50) +ctx.lineTo(130, 90) +ctx.stroke() + +ctx.draw() +``` +* ![](@program/dev/image/canvas/line-join.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.lineJoin](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setLineJoin( + /** 线条的结束交点样式 + * + * 参数 lineJoin 可选值: + * - 'bevel': 斜角; + * - 'round': 圆角; + * - 'miter': 尖角; */ + lineJoin: 'bevel' | 'round' | 'miter' + ): void + /** [CanvasContext.setLineWidth(number lineWidth)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setLineWidth.html) +* +* 设置线条的宽度 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.beginPath() +ctx.moveTo(10, 10) +ctx.lineTo(150, 10) +ctx.stroke() + +ctx.beginPath() +ctx.setLineWidth(5) +ctx.moveTo(10, 30) +ctx.lineTo(150, 30) +ctx.stroke() + +ctx.beginPath() +ctx.setLineWidth(10) +ctx.moveTo(10, 50) +ctx.lineTo(150, 50) +ctx.stroke() + +ctx.beginPath() +ctx.setLineWidth(15) +ctx.moveTo(10, 70) +ctx.lineTo(150, 70) +ctx.stroke() + +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/line-width.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.lineWidth](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setLineWidth( + /** 线条的宽度,单位px */ + lineWidth: number + ): void + /** [CanvasContext.setMiterLimit(number miterLimit)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setMiterLimit.html) +* +* 设置最大斜接长度。斜接长度指的是在两条线交汇处内角和外角之间的距离。当 [CanvasContext.setLineJoin()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setLineJoin.html) 为 miter 时才有效。超过最大倾斜长度的,连接处将以 lineJoin 为 bevel 来显示。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.beginPath() +ctx.setLineWidth(10) +ctx.setLineJoin('miter') +ctx.setMiterLimit(1) +ctx.moveTo(10, 10) +ctx.lineTo(100, 50) +ctx.lineTo(10, 90) +ctx.stroke() + +ctx.beginPath() +ctx.setLineWidth(10) +ctx.setLineJoin('miter') +ctx.setMiterLimit(2) +ctx.moveTo(50, 10) +ctx.lineTo(140, 50) +ctx.lineTo(50, 90) +ctx.stroke() + +ctx.beginPath() +ctx.setLineWidth(10) +ctx.setLineJoin('miter') +ctx.setMiterLimit(3) +ctx.moveTo(90, 10) +ctx.lineTo(180, 50) +ctx.lineTo(90, 90) +ctx.stroke() + +ctx.beginPath() +ctx.setLineWidth(10) +ctx.setLineJoin('miter') +ctx.setMiterLimit(4) +ctx.moveTo(130, 10) +ctx.lineTo(220, 50) +ctx.lineTo(130, 90) +ctx.stroke() + +ctx.draw() +``` +* ![](@program/dev/image/canvas/miter-limit.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.miterLimit](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setMiterLimit( + /** 最大斜接长度 */ + miterLimit: number + ): void + /** [CanvasContext.setShadow(number offsetX, number offsetY, number blur, string color)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setShadow.html) +* +* 设定阴影样式。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.setFillStyle('red') +ctx.setShadow(10, 50, 50, 'blue') +ctx.fillRect(10, 10, 150, 75) +ctx.draw() +``` +* ![](@program/dev/image/canvas/shadow.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.shadowOffsetX|CanvasContext.shadowOffsetY|CanvasContext.shadowColor|CanvasContext.shadowBlur](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setShadow( + /** 阴影相对于形状在水平方向的偏移,默认值为 0。 */ + offsetX: number, + /** 阴影相对于形状在竖直方向的偏移,默认值为 0。 */ + offsetY: number, + /** 阴影的模糊级别,数值越大越模糊。范围 0- 100。,默认值为 0。 */ + blur: number, + /** 阴影的颜色。默认值为 black。 */ + color: string + ): void + /** [CanvasContext.setStrokeStyle(string|[CanvasGradient](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasGradient.html) color)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setStrokeStyle.html) +* +* 设置描边颜色。 +* +* **代码示例** +* +* +* ```js +const ctx = wx.createCanvasContext('myCanvas') +ctx.setStrokeStyle('red') +ctx.strokeRect(10, 10, 150, 75) +ctx.draw() +``` +* ![](@program/dev/image/canvas/stroke-rect.png) +* @deprecated 基础库版本 [1.9.90](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起已废弃,请使用 [CanvasContext.strokeStyle](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 替换 +* */ + setStrokeStyle( + /** 描边的颜色,默认颜色为 black。 */ + color: string | CanvasGradient + ): void + /** [CanvasContext.setTextAlign(string align)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setTextAlign.html) +* +* 设置文字的对齐 +* +* **示例代码** +* +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.setStrokeStyle('red') +ctx.moveTo(150, 20) +ctx.lineTo(150, 170) +ctx.stroke() + +ctx.setFontSize(15) +ctx.setTextAlign('left') +ctx.fillText('textAlign=left', 150, 60) + +ctx.setTextAlign('center') +ctx.fillText('textAlign=center', 150, 80) + +ctx.setTextAlign('right') +ctx.fillText('textAlign=right', 150, 100) + +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/set-text-align.png) +* +* 最低基础库: `1.1.0` */ + setTextAlign( + /** 文字的对齐方式 + * + * 参数 align 可选值: + * - 'left': 左对齐; + * - 'center': 居中对齐; + * - 'right': 右对齐; */ + align: 'left' | 'center' | 'right' + ): void + /** [CanvasContext.setTextBaseline(string textBaseline)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setTextBaseline.html) +* +* 设置文字的竖直对齐 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.setStrokeStyle('red') +ctx.moveTo(5, 75) +ctx.lineTo(295, 75) +ctx.stroke() + +ctx.setFontSize(20) + +ctx.setTextBaseline('top') +ctx.fillText('top', 5, 75) + +ctx.setTextBaseline('middle') +ctx.fillText('middle', 50, 75) + +ctx.setTextBaseline('bottom') +ctx.fillText('bottom', 120, 75) + +ctx.setTextBaseline('normal') +ctx.fillText('normal', 200, 75) + +ctx.draw() +``` +* ![](@program/dev/image/canvas/set-text-baseline.png) +* +* 最低基础库: `1.4.0` */ + setTextBaseline( + /** 文字的竖直对齐方式 + * + * 参数 textBaseline 可选值: + * - 'top': 顶部对齐; + * - 'bottom': 底部对齐; + * - 'middle': 居中对齐; + * - 'normal': ; */ + textBaseline: 'top' | 'bottom' | 'middle' | 'normal' + ): void + /** [CanvasContext.setTransform(number scaleX, number skewX, number skewY, number scaleY, number translateX, number translateY)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setTransform.html) + * + * 使用矩阵重新设置(覆盖)当前变换的方法 + * + * 最低基础库: `1.9.90` */ + setTransform( + /** 水平缩放 */ + scaleX: number, + /** 水平倾斜 */ + skewX: number, + /** 垂直倾斜 */ + skewY: number, + /** 垂直缩放 */ + scaleY: number, + /** 水平移动 */ + translateX: number, + /** 垂直移动 */ + translateY: number + ): void + /** [CanvasContext.stroke()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.stroke.html) +* +* 画出当前路径的边框。默认颜色色为黑色。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.moveTo(10, 10) +ctx.lineTo(100, 10) +ctx.lineTo(100, 100) +ctx.stroke() +ctx.draw() +``` +* ![](@program/dev/image/canvas/stroke-line.png) +* +* stroke() 描绘的的路径是从 beginPath() 开始计算,但是不会将 strokeRect() 包含进去。 +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +// begin path +ctx.rect(10, 10, 100, 30) +ctx.setStrokeStyle('yellow') +ctx.stroke() + +// begin another path +ctx.beginPath() +ctx.rect(10, 40, 100, 30) + +// only stoke this rect, not in current path +ctx.setStrokeStyle('blue') +ctx.strokeRect(10, 70, 100, 30) + +ctx.rect(10, 100, 100, 30) + +// it will stroke current path +ctx.setStrokeStyle('red') +ctx.stroke() +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/stroke-path.png) */ + stroke(): void + /** [CanvasContext.strokeRect(number x, number y, number width, number height)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.strokeRect.html) +* +* 画一个矩形(非填充)。 用 [`setStrokeStyle`](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.setStrokeStyle.html) 设置矩形线条的颜色,如果没设置默认是黑色。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') +ctx.setStrokeStyle('red') +ctx.strokeRect(10, 10, 150, 75) +ctx.draw() +``` +* ![](@program/dev/image/canvas/stroke-rect.png) */ + strokeRect( + /** 矩形路径左上角的横坐标 */ + x: number, + /** 矩形路径左上角的纵坐标 */ + y: number, + /** 矩形路径的宽度 */ + width: number, + /** 矩形路径的高度 */ + height: number + ): void + /** [CanvasContext.strokeText(string text, number x, number y, number maxWidth)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.strokeText.html) + * + * 给定的 (x, y) 位置绘制文本描边的方法 + * + * 最低基础库: `1.9.90` */ + strokeText( + /** 要绘制的文本 */ + text: string, + /** 文本起始点的 x 轴坐标 */ + x: number, + /** 文本起始点的 y 轴坐标 */ + y: number, + /** 需要绘制的最大宽度,可选 */ + maxWidth?: number + ): void + /** [CanvasContext.transform(number scaleX, number skewX, number skewY, number scaleY, number translateX, number translateY)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.transform.html) + * + * 使用矩阵多次叠加当前变换的方法 + * + * 最低基础库: `1.9.90` */ + transform( + /** 水平缩放 */ + scaleX: number, + /** 水平倾斜 */ + skewX: number, + /** 垂直倾斜 */ + skewY: number, + /** 垂直缩放 */ + scaleY: number, + /** 水平移动 */ + translateX: number, + /** 垂直移动 */ + translateY: number + ): void + /** [CanvasContext.translate(number x, number y)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.translate.html) +* +* 对当前坐标系的原点 (0, 0) 进行变换。默认的坐标系原点为页面左上角。 +* +* **示例代码** +* +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +ctx.strokeRect(10, 10, 150, 100) +ctx.translate(20, 20) +ctx.strokeRect(10, 10, 150, 100) +ctx.translate(20, 20) +ctx.strokeRect(10, 10, 150, 100) + +ctx.draw() +``` +* +* ![](@program/dev/image/canvas/translate.png) */ + translate( + /** 水平坐标平移量 */ + x: number, + /** 竖直坐标平移量 */ + y: number + ): void + /** [Object CanvasContext.measureText(string text)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.measureText.html) + * + * 测量文本尺寸信息。目前仅返回文本宽度。同步接口。 + * + * 最低基础库: `1.9.90` */ + measureText( + /** 要测量的文本 */ + text: string + ): TextMetrics + /** [[CanvasGradient](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasGradient.html) CanvasContext.createCircularGradient(number x, number y, number r)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.createCircularGradient.html) +* +* 创建一个圆形的渐变颜色。起点在圆心,终点在圆环。返回的`CanvasGradient`对象需要使用 [CanvasGradient.addColorStop()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasGradient.addColorStop.html) 来指定渐变点,至少要两个。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +// Create circular gradient +const grd = ctx.createCircularGradient(75, 50, 50) +grd.addColorStop(0, 'red') +grd.addColorStop(1, 'white') + +// Fill with gradient +ctx.setFillStyle(grd) +ctx.fillRect(10, 10, 150, 80) +ctx.draw() +``` +* ![](@program/dev/image/canvas/circular-gradient.png) */ + createCircularGradient( + /** 圆心的 x 坐标 */ + x: number, + /** 圆心的 y 坐标 */ + y: number, + /** 圆的半径 */ + r: number + ): CanvasGradient + /** [[CanvasGradient](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasGradient.html) CanvasContext.createLinearGradient(number x0, number y0, number x1, number y1)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.createLinearGradient.html) +* +* 创建一个线性的渐变颜色。返回的`CanvasGradient`对象需要使用 [CanvasGradient.addColorStop()](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasGradient.addColorStop.html) 来指定渐变点,至少要两个。 +* +* **示例代码** +* +* +* ```javascript +const ctx = wx.createCanvasContext('myCanvas') + +// Create linear gradient +const grd = ctx.createLinearGradient(0, 0, 200, 0) +grd.addColorStop(0, 'red') +grd.addColorStop(1, 'white') + +// Fill with gradient +ctx.setFillStyle(grd) +ctx.fillRect(10, 10, 150, 80) +ctx.draw() +``` +* ![](@program/dev/image/canvas/linear-gradient.png) */ + createLinearGradient( + /** 起点的 x 坐标 */ + x0: number, + /** 起点的 y 坐标 */ + y0: number, + /** 终点的 x 坐标 */ + x1: number, + /** 终点的 y 坐标 */ + y1: number + ): CanvasGradient + } + interface CanvasGradient { + /** [CanvasGradient.addColorStop(number stop, string color)](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasGradient.addColorStop.html) +* +* 添加颜色的渐变点。小于最小 stop 的部分会按最小 stop 的 color 来渲染,大于最大 stop 的部分会按最大 stop 的 color 来渲染 +* +* **示例代码** +* +* +* ```js +const ctx = wx.createCanvasContext('myCanvas') + +// Create circular gradient +const grd = ctx.createLinearGradient(30, 10, 120, 10) +grd.addColorStop(0, 'red') +grd.addColorStop(0.16, 'orange') +grd.addColorStop(0.33, 'yellow') +grd.addColorStop(0.5, 'green') +grd.addColorStop(0.66, 'cyan') +grd.addColorStop(0.83, 'blue') +grd.addColorStop(1, 'purple') + +// Fill with gradient +ctx.setFillStyle(grd) +ctx.fillRect(10, 10, 150, 80) +ctx.draw() +``` +* ![](@program/dev/image/canvas/color-stop.png) */ + addColorStop( + /** 表示渐变中开始与结束之间的位置,范围 0-1。 */ + stop: number, + /** 渐变点的颜色。 */ + color: string + ): void + } + interface Console { + /** [console.debug()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.debug.html) + * + * 向调试面板中打印 debug 日志 */ + debug( + /** 日志内容,可以有任意多个。 */ + ...args: any[] + ): void + /** [console.error()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.error.html) + * + * 向调试面板中打印 error 日志 */ + error( + /** 日志内容,可以有任意多个。 */ + ...args: any[] + ): void + /** [console.group(string label)](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.group.html) + * + * 在调试面板中创建一个新的分组。随后输出的内容都会被添加一个缩进,表示该内容属于当前分组。调用 [console.groupEnd](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.groupEnd.html)之后分组结束。 + * + * **注意** + * + * + * 仅在工具中有效,在 vConsole 中为空函数实现。 */ + group( + /** 分组标记,可选。 */ + label?: string + ): void + /** [console.groupEnd()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.groupEnd.html) + * + * 结束由 [console.group](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.group.html) 创建的分组 + * + * **注意** + * + * + * 仅在工具中有效,在 vConsole 中为空函数实现。 */ + groupEnd(): void + /** [console.info()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.info.html) + * + * 向调试面板中打印 info 日志 */ + info( + /** 日志内容,可以有任意多个。 */ + ...args: any[] + ): void + /** [console.log()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.log.html) + * + * 向调试面板中打印 log 日志 */ + log( + /** 日志内容,可以有任意多个。 */ + ...args: any[] + ): void + /** [console.warn()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/console.warn.html) + * + * 向调试面板中打印 warn 日志 */ + warn( + /** 日志内容,可以有任意多个。 */ + ...args: any[] + ): void + } + interface DownloadTask { + /** [DownloadTask.abort()](https://developers.weixin.qq.com/miniprogram/dev/api/network/download/DownloadTask.abort.html) + * + * 中断下载任务 + * + * 最低基础库: `1.4.0` */ + abort(): void + /** [DownloadTask.offHeadersReceived(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/network/download/DownloadTask.offHeadersReceived.html) + * + * 取消监听 HTTP Response Header 事件 + * + * 最低基础库: `2.1.0` */ + offHeadersReceived( + /** HTTP Response Header 事件的回调函数 */ + callback?: OffHeadersReceivedCallback + ): void + /** [DownloadTask.offProgressUpdate(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/network/download/DownloadTask.offProgressUpdate.html) + * + * 取消监听下载进度变化事件 + * + * 最低基础库: `2.1.0` */ + offProgressUpdate( + /** 下载进度变化事件的回调函数 */ + callback?: DownloadTaskOffProgressUpdateCallback + ): void + /** [DownloadTask.onHeadersReceived(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/network/download/DownloadTask.onHeadersReceived.html) + * + * 监听 HTTP Response Header 事件。会比请求完成事件更早 + * + * 最低基础库: `2.1.0` */ + onHeadersReceived( + /** HTTP Response Header 事件的回调函数 */ + callback: OnHeadersReceivedCallback + ): void + /** [DownloadTask.onProgressUpdate(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/network/download/DownloadTask.onProgressUpdate.html) + * + * 监听下载进度变化事件 + * + * 最低基础库: `1.4.0` */ + onProgressUpdate( + /** 下载进度变化事件的回调函数 */ + callback: DownloadTaskOnProgressUpdateCallback + ): void + } + interface EditorContext { + /** [EditorContext.blur(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.blur.html) + * + * 编辑器失焦,同时收起键盘。 + * + * 最低基础库: `2.8.3` */ + blur(option?: BlurOption): void + /** [EditorContext.clear(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.clear.html) + * + * 清空编辑器内容 + * + * 最低基础库: `2.7.0` */ + clear(option?: ClearOption): void + /** [EditorContext.format(string name, string value)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.format.html) + * + * 修改样式 + * + * **** + * + * ## 支持设置的样式列表 + * | name | value | verson | + * | --------------------------------------------------------- | ------------------------------- | ------ | + * | bold | | 2.7.0 | + * | italic | | 2.7.0 | + * | underline | | 2.7.0 | + * | strike | | 2.7.0 | + * | ins | | 2.7.0 | + * | script | sub / super | 2.7.0 | + * | header | H1 / H2 / h3 / H4 / h5 / H6 | 2.7.0 | + * | align | left / center / right / justify | 2.7.0 | + * | direction | rtl | 2.7.0 | + * | indent | -1 / +1 | 2.7.0 | + * | list | ordered / bullet / check | 2.7.0 | + * | color | hex color | 2.7.0 | + * | backgroundColor | hex color | 2.7.0 | + * | margin/marginTop/marginBottom/marginLeft/marginRight | css style | 2.7.0 | + * | padding/paddingTop/paddingBottom/paddingLeft/paddingRight | css style | 2.7.0 | + * | font/fontSize/fontStyle/fontVariant/fontWeight/fontFamily | css style | 2.7.0 | + * | lineHeight | css style | 2.7.0 | + * | letterSpacing | css style | 2.7.0 | + * | textDecoration | css style | 2.7.0 | + * | textIndent | css style | 2.8.0 | + * | wordWrap | css style | 2.10.2 | + * | wordBreak | css style | 2.10.2 | + * | whiteSpace | css style | 2.10.2 | + * + * 对已经应用样式的选区设置会取消样式。css style 表示 css 中规定的允许值。 + * + * 最低基础库: `2.7.0` */ + format( + /** 属性 */ + name: string, + /** 值 */ + value?: string + ): void + /** [EditorContext.getContents(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.getContents.html) + * + * 获取编辑器内容 + * + * 最低基础库: `2.7.0` */ + getContents(option?: GetContentsOption): void + /** [EditorContext.getSelectionText(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.getSelectionText.html) + * + * 获取编辑器已选区域内的纯文本内容。当编辑器失焦或未选中一段区间时,返回内容为空。 + * + * 最低基础库: `2.10.2` */ + getSelectionText(option?: GetSelectionTextOption): void + /** [EditorContext.insertDivider(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.insertDivider.html) + * + * 插入分割线 + * + * 最低基础库: `2.7.0` */ + insertDivider(option?: InsertDividerOption): void + /** [EditorContext.insertImage(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.insertImage.html) +* +* 插入图片。 +* +* 地址为临时文件时,获取的编辑器html格式内容中 标签增加属性 data-local,delta 格式内容中图片 attributes 属性增加 data-local 字段,该值为传入的临时文件地址。 +* +* 开发者可选择在提交阶段上传图片到服务器,获取到网络地址后进行替换。替换时对于html内容应替换掉 的 src 值,对于 delta 内容应替换掉 `insert { image: abc }` 值。 +* +* **示例代码** +* +* +* ```javascript +this.editorCtx.insertImage({ + src: 'xx', + width: '100px', + height: '50px', + extClass: className +}) +``` +* +* 最低基础库: `2.7.0` */ + insertImage(option: InsertImageOption): void + /** [EditorContext.insertText(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.insertText.html) + * + * 覆盖当前选区,设置一段文本 + * + * 最低基础库: `2.7.0` */ + insertText(option: InsertTextOption): void + /** [EditorContext.redo(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.redo.html) + * + * 恢复 + * + * 最低基础库: `2.7.0` */ + redo(option?: RedoOption): void + /** [EditorContext.removeFormat(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.removeFormat.html) + * + * 清除当前选区的样式 + * + * 最低基础库: `2.7.0` */ + removeFormat(option?: RemoveFormatOption): void + /** [EditorContext.scrollIntoView()](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.scrollIntoView.html) + * + * 使得编辑器光标处滚动到窗口可视区域内。 + * + * 最低基础库: `2.8.3` */ + scrollIntoView(): void + /** [EditorContext.setContents(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.setContents.html) + * + * 初始化编辑器内容,html和delta同时存在时仅delta生效 + * + * 最低基础库: `2.7.0` */ + setContents(option: SetContentsOption): void + /** [EditorContext.undo(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.undo.html) + * + * 撤销 + * + * 最低基础库: `2.7.0` */ + undo(option?: UndoOption): void + } + interface EntryList { + /** [Array EntryList.getEntries()](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/performance/EntryList.getEntries.html) + * + * 该方法返回当前列表中的所有性能数据 + * + * 最低基础库: `2.11.0` */ + getEntries(): any[] + /** [Array EntryList.getEntriesByName(string name, string entryType)](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/performance/EntryList.getEntriesByName.html) + * + * 获取当前列表中所有名称为 [name] 且类型为 [entryType] 的性能数据 + * + * 最低基础库: `2.11.0` */ + getEntriesByName(name: string, entryType?: string): any[] + /** [Array EntryList.getEntriesByType(string entryType)](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/performance/EntryList.getEntriesByType.html) + * + * 获取当前列表中所有类型为 [entryType] 的性能数据 + * + * 最低基础库: `2.11.0` */ + getEntriesByType(entryType: string): any[] + } + interface EventChannel { + /** [EventChannel.emit(string eventName, any args)](https://developers.weixin.qq.com/miniprogram/dev/api/route/EventChannel.emit.html) + * + * 触发一个事件 + * + * 最低基础库: `2.7.3` */ + emit( + /** 事件名称 */ + eventName: string, + /** 事件参数 */ + ...args: any + ): void + /** [EventChannel.off(string eventName, function fn)](https://developers.weixin.qq.com/miniprogram/dev/api/route/EventChannel.off.html) + * + * 取消监听一个事件。给出第二个参数时,只取消给出的监听函数,否则取消所有监听函数 + * + * 最低基础库: `2.7.3` */ + off( + /** 事件名称 */ + eventName: string, + /** 事件监听函数 */ + fn: EventCallback + ): void + /** [EventChannel.on(string eventName, function fn)](https://developers.weixin.qq.com/miniprogram/dev/api/route/EventChannel.on.html) + * + * 持续监听一个事件 + * + * 最低基础库: `2.7.3` */ + on( + /** 事件名称 */ + eventName: string, + /** 事件监听函数 */ + fn: EventCallback + ): void + /** [EventChannel.once(string eventName, function fn)](https://developers.weixin.qq.com/miniprogram/dev/api/route/EventChannel.once.html) + * + * 监听一个事件一次,触发后失效 + * + * 最低基础库: `2.7.3` */ + once( + /** 事件名称 */ + eventName: string, + /** 事件监听函数 */ + fn: EventCallback + ): void + } + interface FileSystemManager { + /** [Array.<string> FileSystemManager.readdirSync(string dirPath)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.readdirSync.html) + * + * [FileSystemManager.readdir](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.readdir.html) 的同步版本 */ + readdirSync( + /** 要读取的目录路径 (本地路径) */ + dirPath: string + ): string[] + /** [FileSystemManager.access(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.access.html) + * + * 判断文件/目录是否存在 */ + access(option: AccessOption): void + /** [FileSystemManager.accessSync(string path)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.accessSync.html) + * + * [FileSystemManager.access](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.access.html) 的同步版本 */ + accessSync( + /** 要判断是否存在的文件/目录路径 (本地路径) */ + path: string + ): void + /** [FileSystemManager.appendFile(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.appendFile.html) + * + * 在文件结尾追加内容 + * + * 最低基础库: `2.1.0` */ + appendFile(option: AppendFileOption): void + /** [FileSystemManager.appendFileSync(string filePath, string|ArrayBuffer data, string encoding)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.appendFileSync.html) + * + * [FileSystemManager.appendFile](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.appendFile.html) 的同步版本 + * + * 最低基础库: `2.1.0` */ + appendFileSync( + /** 要追加内容的文件路径 (本地路径) */ + filePath: string, + /** 要追加的文本或二进制数据 */ + data: string | ArrayBuffer, + /** 指定写入文件的字符编码 + * + * 参数 encoding 可选值: + * - 'ascii': ; + * - 'base64': ; + * - 'binary': ; + * - 'hex': ; + * - 'ucs2': 以小端序读取; + * - 'ucs-2': 以小端序读取; + * - 'utf16le': 以小端序读取; + * - 'utf-16le': 以小端序读取; + * - 'utf-8': ; + * - 'utf8': ; + * - 'latin1': ; */ + encoding?: + | 'ascii' + | 'base64' + | 'binary' + | 'hex' + | 'ucs2' + | 'ucs-2' + | 'utf16le' + | 'utf-16le' + | 'utf-8' + | 'utf8' + | 'latin1' + ): void + /** [FileSystemManager.copyFile(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.copyFile.html) + * + * 复制文件 */ + copyFile(option: CopyFileOption): void + /** [FileSystemManager.copyFileSync(string srcPath, string destPath)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.copyFileSync.html) + * + * [FileSystemManager.copyFile](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.copyFile.html) 的同步版本 */ + copyFileSync( + /** 源文件路径,支持本地路径 */ + srcPath: string, + /** 目标文件路径,支持本地路径 */ + destPath: string + ): void + /** [FileSystemManager.getFileInfo(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.getFileInfo.html) + * + * 获取该小程序下的 本地临时文件 或 本地缓存文件 信息 */ + getFileInfo(option: FileSystemManagerGetFileInfoOption): void + /** [FileSystemManager.getSavedFileList(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.getSavedFileList.html) + * + * 获取该小程序下已保存的本地缓存文件列表 */ + getSavedFileList(option?: FileSystemManagerGetSavedFileListOption): void + /** [FileSystemManager.mkdir(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.mkdir.html) + * + * 创建目录 */ + mkdir(option: MkdirOption): void + /** [FileSystemManager.mkdirSync(string dirPath, boolean recursive)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.mkdirSync.html) + * + * [FileSystemManager.mkdir](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.mkdir.html) 的同步版本 */ + mkdirSync( + /** 创建的目录路径 (本地路径) */ + dirPath: string, + /** 是否在递归创建该目录的上级目录后再创建该目录。如果对应的上级目录已经存在,则不创建该上级目录。如 dirPath 为 a/b/c/d 且 recursive 为 true,将创建 a 目录,再在 a 目录下创建 b 目录,以此类推直至创建 a/b/c 目录下的 d 目录。 + * + * 最低基础库: `2.3.0` */ + recursive?: boolean + ): void + /** [FileSystemManager.readFile(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.readFile.html) + * + * 读取本地文件内容 */ + readFile(option: ReadFileOption): void + /** [FileSystemManager.readdir(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.readdir.html) + * + * 读取目录内文件列表 */ + readdir(option: ReaddirOption): void + /** [FileSystemManager.removeSavedFile(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.removeSavedFile.html) + * + * 删除该小程序下已保存的本地缓存文件 */ + removeSavedFile(option: FileSystemManagerRemoveSavedFileOption): void + /** [FileSystemManager.rename(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.rename.html) + * + * 重命名文件。可以把文件从 oldPath 移动到 newPath */ + rename(option: RenameOption): void + /** [FileSystemManager.renameSync(string oldPath, string newPath)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.renameSync.html) + * + * [FileSystemManager.rename](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.rename.html) 的同步版本 */ + renameSync( + /** 源文件路径,支持本地路径 */ + oldPath: string, + /** 新文件路径,支持本地路径 */ + newPath: string + ): void + /** [FileSystemManager.rmdir(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.rmdir.html) + * + * 删除目录 */ + rmdir(option: RmdirOption): void + /** [FileSystemManager.rmdirSync(string dirPath, boolean recursive)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.rmdirSync.html) + * + * [FileSystemManager.rmdir](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.rmdir.html) 的同步版本 */ + rmdirSync( + /** 要删除的目录路径 (本地路径) */ + dirPath: string, + /** 是否递归删除目录。如果为 true,则删除该目录和该目录下的所有子目录以及文件。 + * + * 最低基础库: `2.3.0` */ + recursive?: boolean + ): void + /** [FileSystemManager.saveFile(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.saveFile.html) + * + * 保存临时文件到本地。此接口会移动临时文件,因此调用成功后,tempFilePath 将不可用。 */ + saveFile(option: FileSystemManagerSaveFileOption): void + /** [FileSystemManager.stat(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.stat.html) + * + * 获取文件 Stats 对象 */ + stat(option: StatOption): void + /** [FileSystemManager.unlink(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.unlink.html) + * + * 删除文件 */ + unlink(option: UnlinkOption): void + /** [FileSystemManager.unlinkSync(string filePath)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.unlinkSync.html) + * + * [FileSystemManager.unlink](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.unlink.html) 的同步版本 */ + unlinkSync( + /** 要删除的文件路径 (本地路径) */ + filePath: string + ): void + /** [FileSystemManager.unzip(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.unzip.html) + * + * 解压文件 */ + unzip(option: UnzipOption): void + /** [FileSystemManager.writeFile(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.writeFile.html) + * + * 写文件 */ + writeFile(option: WriteFileOption): void + /** [FileSystemManager.writeFileSync(string filePath, string|ArrayBuffer data, string encoding)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.writeFileSync.html) + * + * [FileSystemManager.writeFile](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.writeFile.html) 的同步版本 */ + writeFileSync( + /** 要写入的文件路径 (本地路径) */ + filePath: string, + /** 要写入的文本或二进制数据 */ + data: string | ArrayBuffer, + /** 指定写入文件的字符编码 + * + * 参数 encoding 可选值: + * - 'ascii': ; + * - 'base64': ; + * - 'binary': ; + * - 'hex': ; + * - 'ucs2': 以小端序读取; + * - 'ucs-2': 以小端序读取; + * - 'utf16le': 以小端序读取; + * - 'utf-16le': 以小端序读取; + * - 'utf-8': ; + * - 'utf8': ; + * - 'latin1': ; */ + encoding?: + | 'ascii' + | 'base64' + | 'binary' + | 'hex' + | 'ucs2' + | 'ucs-2' + | 'utf16le' + | 'utf-16le' + | 'utf-8' + | 'utf8' + | 'latin1' + ): void + /** [[Stats](https://developers.weixin.qq.com/miniprogram/dev/api/file/Stats.html)|Object FileSystemManager.statSync(string path, boolean recursive)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.statSync.html) + * + * [FileSystemManager.stat](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.stat.html) 的同步版本 */ + statSync( + /** 文件/目录路径 (本地路径) */ + path: string, + /** 是否递归获取目录下的每个文件的 Stats 信息 + * + * 最低基础库: `2.3.0` */ + recursive?: boolean + ): Stats | IAnyObject + /** [string FileSystemManager.saveFileSync(string tempFilePath, string filePath)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.saveFileSync.html) + * + * [FileSystemManager.saveFile](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.saveFile.html) 的同步版本 */ + saveFileSync( + /** 临时存储文件路径 (本地路径) */ + tempFilePath: string, + /** 要存储的文件路径 (本地路径) */ + filePath?: string + ): string + /** [string|ArrayBuffer FileSystemManager.readFileSync(string filePath, string encoding, number position, number length)](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.readFileSync.html) + * + * [FileSystemManager.readFile](https://developers.weixin.qq.com/miniprogram/dev/api/file/FileSystemManager.readFile.html) 的同步版本 */ + readFileSync( + /** 要读取的文件的路径 (本地路径) */ + filePath: string, + /** 指定读取文件的字符编码,如果不传 encoding,则以 ArrayBuffer 格式读取文件的二进制内容 + * + * 参数 encoding 可选值: + * - 'ascii': ; + * - 'base64': ; + * - 'binary': ; + * - 'hex': ; + * - 'ucs2': 以小端序读取; + * - 'ucs-2': 以小端序读取; + * - 'utf16le': 以小端序读取; + * - 'utf-16le': 以小端序读取; + * - 'utf-8': ; + * - 'utf8': ; + * - 'latin1': ; */ + encoding?: + | 'ascii' + | 'base64' + | 'binary' + | 'hex' + | 'ucs2' + | 'ucs-2' + | 'utf16le' + | 'utf-16le' + | 'utf-8' + | 'utf8' + | 'latin1', + /** 从文件指定位置开始读,如果不指定,则从文件头开始读。读取的范围应该是左闭右开区间 [position, position+length)。有效范围:[0, fileLength - 1]。单位:byte + * + * 最低基础库: `2.10.0` */ + position?: number, + /** 指定文件的长度,如果不指定,则读到文件末尾。有效范围:[1, fileLength]。单位:byte + * + * 最低基础库: `2.10.0` */ + length?: number + ): string | ArrayBuffer + } + interface GeneralCallbackResult { + errMsg: string + } + interface IBeaconError { + /** 错误信息 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 0 | ok | 正常 | + * | 11000 | unsupport | 系统或设备不支持 | + * | 11001 | bluetooth service unavailable | 蓝牙服务不可用 | + * | 11002 | location service unavailable | 位置服务不可用 | + * | 11003 | already start | 已经开始搜索 | + * | 11004 | not startBeaconDiscovery | 还未开始搜索 | + * | 11005 | system error | 系统错误 | + * | 11006 | invalid data | 参数不正确 | */ errMsg: string + /** 错误码 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 0 | ok | 正常 | + * | 11000 | unsupport | 系统或设备不支持 | + * | 11001 | bluetooth service unavailable | 蓝牙服务不可用 | + * | 11002 | location service unavailable | 位置服务不可用 | + * | 11003 | already start | 已经开始搜索 | + * | 11004 | not startBeaconDiscovery | 还未开始搜索 | + * | 11005 | system error | 系统错误 | + * | 11006 | invalid data | 参数不正确 | */ errCode: number + } + interface InnerAudioContext { + /** [InnerAudioContext.destroy()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.destroy.html) + * + * 销毁当前实例 */ + destroy(): void + /** [InnerAudioContext.offCanplay(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offCanplay.html) + * + * 取消监听音频进入可以播放状态的事件 + * + * 最低基础库: `1.9.0` */ + offCanplay( + /** 音频进入可以播放状态的事件的回调函数 */ + callback?: OffCanplayCallback + ): void + /** [InnerAudioContext.offEnded(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offEnded.html) + * + * 取消监听音频自然播放至结束的事件 + * + * 最低基础库: `1.9.0` */ + offEnded( + /** 音频自然播放至结束的事件的回调函数 */ + callback?: OffEndedCallback + ): void + /** [InnerAudioContext.offError(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offError.html) + * + * 取消监听音频播放错误事件 + * + * 最低基础库: `1.9.0` */ + offError( + /** 音频播放错误事件的回调函数 */ + callback?: InnerAudioContextOffErrorCallback + ): void + /** [InnerAudioContext.offPause(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offPause.html) + * + * 取消监听音频暂停事件 + * + * 最低基础库: `1.9.0` */ + offPause( + /** 音频暂停事件的回调函数 */ + callback?: OffPauseCallback + ): void + /** [InnerAudioContext.offPlay(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offPlay.html) + * + * 取消监听音频播放事件 + * + * 最低基础库: `1.9.0` */ + offPlay( + /** 音频播放事件的回调函数 */ + callback?: OffPlayCallback + ): void + /** [InnerAudioContext.offSeeked(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offSeeked.html) + * + * 取消监听音频完成跳转操作的事件 + * + * 最低基础库: `1.9.0` */ + offSeeked( + /** 音频完成跳转操作的事件的回调函数 */ + callback?: OffSeekedCallback + ): void + /** [InnerAudioContext.offSeeking(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offSeeking.html) + * + * 取消监听音频进行跳转操作的事件 + * + * 最低基础库: `1.9.0` */ + offSeeking( + /** 音频进行跳转操作的事件的回调函数 */ + callback?: OffSeekingCallback + ): void + /** [InnerAudioContext.offStop(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offStop.html) + * + * 取消监听音频停止事件 + * + * 最低基础库: `1.9.0` */ + offStop( + /** 音频停止事件的回调函数 */ + callback?: OffStopCallback + ): void + /** [InnerAudioContext.offTimeUpdate(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offTimeUpdate.html) + * + * 取消监听音频播放进度更新事件 + * + * 最低基础库: `1.9.0` */ + offTimeUpdate( + /** 音频播放进度更新事件的回调函数 */ + callback?: OffTimeUpdateCallback + ): void + /** [InnerAudioContext.offWaiting(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.offWaiting.html) + * + * 取消监听音频加载中事件 + * + * 最低基础库: `1.9.0` */ + offWaiting( + /** 音频加载中事件的回调函数 */ + callback?: OffWaitingCallback + ): void + /** [InnerAudioContext.onCanplay(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onCanplay.html) + * + * 监听音频进入可以播放状态的事件。但不保证后面可以流畅播放 */ + onCanplay( + /** 音频进入可以播放状态的事件的回调函数 */ + callback: OnCanplayCallback + ): void + /** [InnerAudioContext.onEnded(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onEnded.html) + * + * 监听音频自然播放至结束的事件 */ + onEnded( + /** 音频自然播放至结束的事件的回调函数 */ + callback: OnEndedCallback + ): void + /** [InnerAudioContext.onError(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onError.html) + * + * 监听音频播放错误事件 + * + * **Tips** + * + * + * 1. errCode=100001 时,如若 errMsg 中有 INNERCODE -11828 ,请先检查 response header 是否缺少 Content-Length + * 2. errCode=100001 时,如若 errMsg 中有 systemErrCode:200333420,请检查文件编码格式和 fileExtension 是否一致 */ + onError( + /** 音频播放错误事件的回调函数 */ + callback: InnerAudioContextOnErrorCallback + ): void + /** [InnerAudioContext.onPause(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onPause.html) + * + * 监听音频暂停事件 */ + onPause( + /** 音频暂停事件的回调函数 */ + callback: OnPauseCallback + ): void + /** [InnerAudioContext.onPlay(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onPlay.html) + * + * 监听音频播放事件 */ + onPlay( + /** 音频播放事件的回调函数 */ + callback: OnPlayCallback + ): void + /** [InnerAudioContext.onSeeked(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onSeeked.html) + * + * 监听音频完成跳转操作的事件 */ + onSeeked( + /** 音频完成跳转操作的事件的回调函数 */ + callback: OnSeekedCallback + ): void + /** [InnerAudioContext.onSeeking(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onSeeking.html) + * + * 监听音频进行跳转操作的事件 */ + onSeeking( + /** 音频进行跳转操作的事件的回调函数 */ + callback: OnSeekingCallback + ): void + /** [InnerAudioContext.onStop(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onStop.html) + * + * 监听音频停止事件 */ + onStop( + /** 音频停止事件的回调函数 */ + callback: InnerAudioContextOnStopCallback + ): void + /** [InnerAudioContext.onTimeUpdate(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onTimeUpdate.html) + * + * 监听音频播放进度更新事件 */ + onTimeUpdate( + /** 音频播放进度更新事件的回调函数 */ + callback: OnTimeUpdateCallback + ): void + /** [InnerAudioContext.onWaiting(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.onWaiting.html) + * + * 监听音频加载中事件。当音频因为数据不足,需要停下来加载时会触发 */ + onWaiting( + /** 音频加载中事件的回调函数 */ + callback: OnWaitingCallback + ): void + /** [InnerAudioContext.pause()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.pause.html) + * + * 暂停。暂停后的音频再播放会从暂停处开始播放 */ + pause(): void + /** [InnerAudioContext.play()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.play.html) + * + * 播放 */ + play(): void + /** [InnerAudioContext.seek(number position)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.seek.html) + * + * 跳转到指定位置 */ + seek( + /** 跳转的时间,单位 s。精确到小数点后 3 位,即支持 ms 级别精确度 */ + position: number + ): void + /** [InnerAudioContext.stop()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.stop.html) + * + * 停止。停止后的音频再播放会从头开始播放。 */ + stop(): void + } + interface IntersectionObserver { + /** [IntersectionObserver.disconnect()](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.disconnect.html) + * + * 停止监听。回调函数将不再触发 */ + disconnect(): void + /** [IntersectionObserver.observe(string targetSelector, function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.observe.html) + * + * 指定目标节点并开始监听相交状态变化情况 */ + observe( + /** 选择器 */ + targetSelector: string, + /** 监听相交状态变化的回调函数 */ + callback: IntersectionObserverObserveCallback + ): void + /** [[IntersectionObserver](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.html) IntersectionObserver.relativeTo(string selector, Object margins)](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.relativeTo.html) + * + * 使用选择器指定一个节点,作为参照区域之一。 */ + relativeTo( + /** 选择器 */ + selector: string, + /** 用来扩展(或收缩)参照节点布局区域的边界 */ + margins?: Margins + ): IntersectionObserver + /** [[IntersectionObserver](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.html) IntersectionObserver.relativeToViewport(Object margins)](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.relativeToViewport.html) +* +* 指定页面显示区域作为参照区域之一 +* +* **示例代码** +* +* +* 下面的示例代码中,如果目标节点(用选择器 .target-class 指定)进入显示区域以下 100px 时,就会触发回调函数。 +* ```javascript +Page({ + onLoad: function(){ + wx.createIntersectionObserver().relativeToViewport({bottom: 100}).observe('.target-class', (res) => { + res.intersectionRatio // 相交区域占目标节点的布局区域的比例 + res.intersectionRect // 相交区域 + res.intersectionRect.left // 相交区域的左边界坐标 + res.intersectionRect.top // 相交区域的上边界坐标 + res.intersectionRect.width // 相交区域的宽度 + res.intersectionRect.height // 相交区域的高度 + }) + } +}) +``` */ + relativeToViewport( + /** 用来扩展(或收缩)参照节点布局区域的边界 */ + margins?: Margins + ): IntersectionObserver + } + interface InterstitialAd { + /** [InterstitialAd.destroy()](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.destroy.html) + * + * 销毁插屏广告实例。 + * + * 最低基础库: `2.8.0` */ + destroy(): void + /** [InterstitialAd.offClose(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.offClose.html) + * + * 取消监听插屏广告关闭事件 */ + offClose( + /** 插屏广告关闭事件的回调函数 */ + callback?: UDPSocketOffCloseCallback + ): void + /** [InterstitialAd.offError(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.offError.html) + * + * 取消监听插屏错误事件 */ + offError( + /** 插屏错误事件的回调函数 */ + callback?: InterstitialAdOffErrorCallback + ): void + /** [InterstitialAd.offLoad(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.offLoad.html) + * + * 取消监听插屏广告加载事件 */ + offLoad( + /** 插屏广告加载事件的回调函数 */ + callback?: OffLoadCallback + ): void + /** [InterstitialAd.onClose(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.onClose.html) + * + * 监听插屏广告关闭事件。 */ + onClose( + /** 插屏广告关闭事件的回调函数 */ + callback: UDPSocketOnCloseCallback + ): void + /** [InterstitialAd.onError(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.onError.html) + * + * 监听插屏错误事件。 + * + * **错误码信息与解决方案表** + * + * + * 错误码是通过onError获取到的错误信息。调试期间,可以通过异常返回来捕获信息。 + * 在小程序发布上线之后,如果遇到异常问题,可以在[“运维中心“](https://mp.weixin.qq.com/)里面搜寻错误日志,还可以针对异常返回加上适当的监控信息。 + * + * | 代码 | 异常情况 | 理由 | 解决方案 | + * | ------ | -------------- | --------------- | -------------------------- | + * | 1000 | 后端错误调用失败 | 该项错误不是开发者的异常情况 | 一般情况下忽略一段时间即可恢复。 | + * | 1001 | 参数错误 | 使用方法错误 | 可以前往developers.weixin.qq.com确认具体教程(小程序和小游戏分别有各自的教程,可以在顶部选项中,“设计”一栏的右侧进行切换。| + * | 1002 | 广告单元无效 | 可能是拼写错误、或者误用了其他APP的广告ID | 请重新前往mp.weixin.qq.com确认广告位ID。 | + * | 1003 | 内部错误 | 该项错误不是开发者的异常情况 | 一般情况下忽略一段时间即可恢复。| + * | 1004 | 无适合的广告 | 广告不是每一次都会出现,这次没有出现可能是由于该用户不适合浏览广告 | 属于正常情况,且开发者需要针对这种情况做形态上的兼容。 | + * | 1005 | 广告组件审核中 | 你的广告正在被审核,无法展现广告 | 请前往mp.weixin.qq.com确认审核状态,且开发者需要针对这种情况做形态上的兼容。| + * | 1006 | 广告组件被驳回 | 你的广告审核失败,无法展现广告 | 请前往mp.weixin.qq.com确认审核状态,且开发者需要针对这种情况做形态上的兼容。| + * | 1007 | 广告组件被驳回 | 你的广告能力已经被封禁,封禁期间无法展现广告 | 请前往mp.weixin.qq.com确认小程序广告封禁状态。 | + * | 1008 | 广告单元已关闭 | 该广告位的广告能力已经被关闭 | 请前往mp.weixin.qq.com重新打开对应广告位的展现。| */ + onError( + /** 插屏错误事件的回调函数 */ + callback: InterstitialAdOnErrorCallback + ): void + /** [InterstitialAd.onLoad(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.onLoad.html) + * + * 监听插屏广告加载事件。 */ + onLoad( + /** 插屏广告加载事件的回调函数 */ + callback: OnLoadCallback + ): void + /** [Promise InterstitialAd.load()](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.load.html) + * + * 加载插屏广告。 + * + * 最低基础库: `2.8.0` */ + load(): Promise + /** [Promise InterstitialAd.show()](https://developers.weixin.qq.com/miniprogram/dev/api/ad/InterstitialAd.show.html) + * + * 显示插屏广告。 + * + * **错误码信息表** + * + * + * 如果插屏广告显示失败,InterstitialAd.show() 方法会返回一个rejected Promise,开发者可以获取到错误码及对应的错误信息。 + * + * | 代码 | 异常情况 | 理由 | + * | ------ | -------------- | -------------------------- | + * | 2001 | 触发频率限制 | 小程序启动一定时间内不允许展示插屏广告 | + * | 2002 | 触发频率限制 | 距离小程序插屏广告或者激励视频广告上次播放时间间隔不足,不允许展示插屏广告 | + * | 2003 | 触发频率限制 | 当前正在播放激励视频广告或者插屏广告,不允许再次展示插屏广告 | + * | 2004 | 广告渲染失败 | 该项错误不是开发者的异常情况,或因小程序页面切换导致广告渲染失败 | + * | 2005 | 广告调用异常 | 插屏广告实例不允许跨页面调用 | */ + show(): Promise + } + interface IsoDep { + /** [IsoDep.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/IsoDep.close.html) + * + * 断开连接 + * + * 最低基础库: `2.11.2` */ + close(option?: NdefCloseOption): void + /** [IsoDep.connect(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/IsoDep.connect.html) + * + * 连接 NFC 标签 + * + * 最低基础库: `2.11.2` */ + connect(option?: ConnectOption): void + /** [IsoDep.getHistoricalBytes(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/IsoDep.getHistoricalBytes.html) + * + * 获取复位信息 + * + * 最低基础库: `2.11.2` */ + getHistoricalBytes(option?: GetHistoricalBytesOption): void + /** [IsoDep.getMaxTransceiveLength(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/IsoDep.getMaxTransceiveLength.html) + * + * 获取最大传输长度 + * + * 最低基础库: `2.11.2` */ + getMaxTransceiveLength(option?: GetMaxTransceiveLengthOption): void + /** [IsoDep.isConnected(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/IsoDep.isConnected.html) + * + * 检查是否已连接 + * + * 最低基础库: `2.11.2` */ + isConnected(option?: IsConnectedOption): void + /** [IsoDep.setTimeout(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/IsoDep.setTimeout.html) + * + * 设置超时时间 + * + * 最低基础库: `2.11.2` */ + setTimeout(option: SetTimeoutOption): void + /** [IsoDep.transceive(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/IsoDep.transceive.html) + * + * 发送数据 + * + * 最低基础库: `2.11.2` */ + transceive(option: TransceiveOption): void + } + interface JoinVoIPChatError { + /** 错误信息 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | -1 | 当前已在房间内 | | + * | -2 | 录音设备被占用,可能是当前正在使用微信内语音通话或系统通话 | | + * | -3 | 加入会话期间退出(可能是用户主动退出,或者退后台、来电等原因),因此加入失败 | | + * | -1000 | 系统错误 | | */ errMsg: string + /** 错误码 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | -1 | 当前已在房间内 | | + * | -2 | 录音设备被占用,可能是当前正在使用微信内语音通话或系统通话 | | + * | -3 | 加入会话期间退出(可能是用户主动退出,或者退后台、来电等原因),因此加入失败 | | + * | -1000 | 系统错误 | | */ errCode: number + } + interface LivePlayerContext { + /** [LivePlayerContext.exitFullScreen(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.exitFullScreen.html) + * + * 退出全屏 */ + exitFullScreen(option?: ExitFullScreenOption): void + /** [LivePlayerContext.exitPictureInPicture(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.exitPictureInPicture.html) + * + * 退出小窗,该方法可在任意页面调用 */ + exitPictureInPicture(option?: ExitPictureInPictureOption): void + /** [LivePlayerContext.mute(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.mute.html) + * + * 静音 */ + mute(option?: MuteOption): void + /** [LivePlayerContext.pause(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.pause.html) + * + * 暂停 + * + * 最低基础库: `1.9.90` */ + pause(option?: PauseOption): void + /** [LivePlayerContext.play(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.play.html) + * + * 播放 */ + play(option?: PlayOption): void + /** [LivePlayerContext.requestFullScreen(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.requestFullScreen.html) + * + * 进入全屏 */ + requestFullScreen( + option: LivePlayerContextRequestFullScreenOption + ): void + /** [LivePlayerContext.requestPictureInPicture(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.requestPictureInPicture.html) + * + * 进入小窗 + * + * 最低基础库: `2.15.0` */ + requestPictureInPicture(option?: RequestPictureInPictureOption): void + /** [LivePlayerContext.resume(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.resume.html) + * + * 恢复 + * + * 最低基础库: `1.9.90` */ + resume(option?: ResumeOption): void + /** [LivePlayerContext.snapshot(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.snapshot.html) + * + * 截图 + * + * 最低基础库: `2.7.1` */ + snapshot(option: LivePlayerContextSnapshotOption): void + /** [LivePlayerContext.stop(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.stop.html) + * + * 停止 */ + stop(option?: StopOption): void + } + interface LivePusherContext { + /** [LivePusherContext.pause(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.pause.html) + * + * 暂停推流 */ + pause(option?: PauseOption): void + /** [LivePusherContext.pauseBGM(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.pauseBGM.html) + * + * 暂停背景音 + * + * 最低基础库: `2.4.0` */ + pauseBGM(option?: PauseBGMOption): void + /** [LivePusherContext.playBGM(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.playBGM.html) + * + * 播放背景音 + * + * 最低基础库: `2.4.0` */ + playBGM(option: PlayBGMOption): void + /** [LivePusherContext.resume(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.resume.html) + * + * 恢复推流 */ + resume(option?: ResumeOption): void + /** [LivePusherContext.resumeBGM(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.resumeBGM.html) + * + * 恢复背景音 + * + * 最低基础库: `2.4.0` */ + resumeBGM(option?: ResumeBGMOption): void + /** [LivePusherContext.sendMessage(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.sendMessage.html) + * + * 发送SEI消息 + * + * 最低基础库: `2.10.0` */ + sendMessage(option?: SendMessageOption): void + /** [LivePusherContext.setBGMVolume(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.setBGMVolume.html) + * + * 设置背景音音量 + * + * 最低基础库: `2.4.0` */ + setBGMVolume(option: SetBGMVolumeOption): void + /** [LivePusherContext.setMICVolume(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.setMICVolume.html) + * + * 设置麦克风音量 + * + * 最低基础库: `2.10.0` */ + setMICVolume(option: SetMICVolumeOption): void + /** [LivePusherContext.snapshot(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.snapshot.html) + * + * 快照 + * + * 最低基础库: `1.9.90` */ + snapshot(option: LivePusherContextSnapshotOption): void + /** [LivePusherContext.start(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.start.html) + * + * 开始推流,同时开启摄像头预览 */ + start(option?: CameraFrameListenerStartOption): void + /** [LivePusherContext.startPreview(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.startPreview.html) + * + * 开启摄像头预览 + * + * 最低基础库: `2.7.0` */ + startPreview(option?: StartPreviewOption): void + /** [LivePusherContext.stop(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.stop.html) + * + * 停止推流,同时停止摄像头预览 */ + stop(option?: StopOption): void + /** [LivePusherContext.stopBGM(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.stopBGM.html) + * + * 停止背景音 + * + * 最低基础库: `2.4.0` */ + stopBGM(option?: StopBGMOption): void + /** [LivePusherContext.stopPreview(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.stopPreview.html) + * + * 关闭摄像头预览 + * + * 最低基础库: `2.7.0` */ + stopPreview(option?: StopPreviewOption): void + /** [LivePusherContext.switchCamera(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.switchCamera.html) + * + * 切换前后摄像头 */ + switchCamera(option?: SwitchCameraOption): void + /** [LivePusherContext.toggleTorch(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePusherContext.toggleTorch.html) + * + * 切换手电筒 + * + * 最低基础库: `2.1.0` */ + toggleTorch(option?: ToggleTorchOption): void + } + interface LogManager { + /** [LogManager.debug()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/LogManager.debug.html) + * + * 写 debug 日志 */ + debug( + /** 日志内容,可以有任意多个。每次调用的参数的总大小不超过100Kb */ + ...args: any[] + ): void + /** [LogManager.info()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/LogManager.info.html) + * + * 写 info 日志 */ + info( + /** 日志内容,可以有任意多个。每次调用的参数的总大小不超过100Kb */ + ...args: any[] + ): void + /** [LogManager.log()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/LogManager.log.html) + * + * 写 log 日志 */ + log( + /** 日志内容,可以有任意多个。每次调用的参数的总大小不超过100Kb */ + ...args: any[] + ): void + /** [LogManager.warn()](https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/LogManager.warn.html) + * + * 写 warn 日志 */ + warn( + /** 日志内容,可以有任意多个。每次调用的参数的总大小不超过100Kb */ + ...args: any[] + ): void + } + interface MapContext { + /** [MapContext.addCustomLayer(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.addCustomLayer.html) + * + * 添加个性化图层。 + * + * 最低基础库: `2.12.0` */ + addCustomLayer(option: AddCustomLayerOption): void + /** [MapContext.addGroundOverlay(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.addGroundOverlay.html) + * + * 创建自定义图片图层,图片会随着地图缩放而缩放。 + * + * 最低基础库: `2.14.0` */ + addGroundOverlay(option: AddGroundOverlayOption): void + /** [MapContext.addMarkers(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.addMarkers.html) + * + * 添加 marker。 + * + * 最低基础库: `2.13.0` */ + addMarkers(option: AddMarkersOption): void + /** [MapContext.fromScreenLocation(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.fromScreenLocation.html) + * + * 获取屏幕上的点对应的经纬度,坐标原点为地图左上角。 + * + * 最低基础库: `2.14.0` */ + fromScreenLocation(option: FromScreenLocationOption): void + /** [MapContext.getCenterLocation(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.getCenterLocation.html) + * + * 获取当前地图中心的经纬度。返回的是 gcj02 坐标系,可以用于 [wx.openLocation()](https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.openLocation.html) */ + getCenterLocation(option?: GetCenterLocationOption): void + /** [MapContext.getRegion(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.getRegion.html) + * + * 获取当前地图的视野范围 + * + * 最低基础库: `1.4.0` */ + getRegion(option?: GetRegionOption): void + /** [MapContext.getRotate(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.getRotate.html) + * + * 获取当前地图的旋转角 + * + * 最低基础库: `2.8.0` */ + getRotate(option?: GetRotateOption): void + /** [MapContext.getScale(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.getScale.html) + * + * 获取当前地图的缩放级别 + * + * 最低基础库: `1.4.0` */ + getScale(option?: GetScaleOption): void + /** [MapContext.getSkew(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.getSkew.html) + * + * 获取当前地图的倾斜角 + * + * 最低基础库: `2.8.0` */ + getSkew(option?: GetSkewOption): void + /** [MapContext.includePoints(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.includePoints.html) + * + * 缩放视野展示所有经纬度 + * + * 最低基础库: `1.2.0` */ + includePoints(option: IncludePointsOption): void + /** [MapContext.initMarkerCluster(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.initMarkerCluster.html) + * + * 初始化点聚合的配置,未调用时采用默认配置。 + * + * 最低基础库: `2.13.0` */ + initMarkerCluster(option: InitMarkerClusterOption): void + /** [MapContext.moveAlong(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.moveAlong.html) + * + * 沿指定路径移动 `marker`,用于轨迹回放等场景。动画完成时触发回调事件,若动画进行中,对同一 `marker` 再次调用 `moveAlong` 方法,前一次的动画将被打断。 + * + * 最低基础库: `2.13.0` */ + moveAlong(option: MoveAlongOption): void + /** [MapContext.moveToLocation(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.moveToLocation.html) + * + * 将地图中心移置当前定位点,此时需设置地图组件 show-location 为true。[2.8.0](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 起支持将地图中心移动到指定位置。 + * + * 最低基础库: `1.2.0` */ + moveToLocation(option?: MoveToLocationOption): void + /** [MapContext.on(string event, function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.on.html) +* +* 监听地图事件。 +* +* ### markerClusterCreate +* +* 缩放或拖动导致新的聚合簇产生时触发,仅返回新创建的聚合簇信息。 +* +* #### 返回参数 +* +* | 参数 | 类型 | 说明 | +* | --------- | ------ | --------- | +* | clusters | `Array<ClusterInfo>` | 聚合簇数据 | +* +* ### markerClusterClick +* +* 聚合簇的点击事件。 +* +* #### 返回参数 +* +* | 参数 | 类型 | 说明 | +* | --------- | ------------- | --------- | +* | cluster | ClusterInfo | 聚合簇 | +* +* +* #### ClusterInfo 结构 +* +* | 参数 | 类型 | 说明 | +* | ---------- | -------------------- | -------------------------- | +* | clusterId | Number | 聚合簇的 id | +* | center | LatLng | 聚合簇的坐标 | +* | markerIds | `Array<Number>` | 该聚合簇内的点标记数据数组 | +* +* **示例代码** +* +* +* +* ```js + MapContext.on('markerClusterCreate', (res) => {}) + MapContext.on('markerClusterClick', (res) => {}) +``` +* +* 最低基础库: `2.13.0` */ + on( + /** 事件名 + * + * 参数 event 可选值: + * - 'markerClusterCreate': ; + * - 'markerClusterClick': ; */ + event: 'markerClusterCreate' | 'markerClusterClick', + /** 事件的回调函数 */ + callback: (...args: any[]) => any + ): void + /** [MapContext.openMapApp(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.openMapApp.html) + * + * 拉起地图APP选择导航。 + * + * 最低基础库: `2.14.0` */ + openMapApp(option: OpenMapAppOption): void + /** [MapContext.removeCustomLayer(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.removeCustomLayer.html) + * + * 移除个性化图层。 + * + * 最低基础库: `2.12.0` */ + removeCustomLayer(option: RemoveCustomLayerOption): void + /** [MapContext.removeGroundOverlay(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.removeGroundOverlay.html) + * + * 移除自定义图片图层。 + * + * 最低基础库: `2.14.0` */ + removeGroundOverlay(option: RemoveGroundOverlayOption): void + /** [MapContext.removeMarkers(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.removeMarkers.html) + * + * 移除 marker。 + * + * 最低基础库: `2.13.0` */ + removeMarkers(option: RemoveMarkersOption): void + /** [MapContext.setCenterOffset(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.setCenterOffset.html) + * + * 设置地图中心点偏移,向后向下为增长,屏幕比例范围(0.25~0.75),默认偏移为[0.5, 0.5] + * + * 最低基础库: `2.10.0` */ + setCenterOffset(option: SetCenterOffsetOption): void + /** [MapContext.toScreenLocation(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.toScreenLocation.html) + * + * 获取经纬度对应的屏幕坐标,坐标原点为地图左上角。 + * + * 最低基础库: `2.14.0` */ + toScreenLocation(option: ToScreenLocationOption): void + /** [MapContext.translateMarker(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.translateMarker.html) + * + * 平移marker,带动画。 + * + * 最低基础库: `1.2.0` */ + translateMarker(option: TranslateMarkerOption): void + /** [MapContext.updateGroundOverlay(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.updateGroundOverlay.html) + * + * 更新自定义图片图层。 + * + * 最低基础库: `2.14.0` */ + updateGroundOverlay(option: UpdateGroundOverlayOption): void + } + interface MediaAudioPlayer { + /** [Promise MediaAudioPlayer.addAudioSource([VideoDecoder](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-decoder/VideoDecoder.html) source)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.addAudioSource.html) + * + * 添加音频源 */ + addAudioSource( + /** [VideoDecoder](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-decoder/VideoDecoder.html) + * + * 视频解码器实例。作为音频源添加到音频播放器中 */ + source: VideoDecoder + ): Promise + /** [Promise MediaAudioPlayer.destroy()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.destroy.html) + * + * 销毁播放器 */ + destroy(): Promise + /** [Promise MediaAudioPlayer.removeAudioSource([VideoDecoder](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-decoder/VideoDecoder.html) source)](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.removeAudioSource.html) + * + * 移除音频源 */ + removeAudioSource( + /** [VideoDecoder](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-decoder/VideoDecoder.html) + * + * 视频解码器实例 */ + source: VideoDecoder + ): Promise + /** [Promise MediaAudioPlayer.start()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.start.html) + * + * 启动播放器 */ + start(): Promise + /** [Promise MediaAudioPlayer.stop()](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.stop.html) + * + * 停止播放器 */ + stop(): Promise + } + interface MediaContainer { + /** [MediaContainer.addTrack([MediaTrack](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaTrack.html) track)](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaContainer.addTrack.html) + * + * 将音频或视频轨道添加到容器 + * + * 最低基础库: `2.9.0` */ + addTrack( + /** [MediaTrack](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaTrack.html) + * + * 要添加的音频或视频轨道 */ + track: MediaTrack + ): void + /** [MediaContainer.destroy()](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaContainer.destroy.html) + * + * 将容器销毁,释放资源 + * + * 最低基础库: `2.9.0` */ + destroy(): void + /** [MediaContainer.export()](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaContainer.export.html) + * + * 将容器内的轨道合并并导出视频文件 + * + * 最低基础库: `2.9.0` */ + export(): void + /** [MediaContainer.extractDataSource(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaContainer.extractDataSource.html) + * + * 将传入的视频源分离轨道。不会自动将轨道添加到待合成的容器里。 + * + * 最低基础库: `2.9.0` */ + extractDataSource(option: ExtractDataSourceOption): void + /** [MediaContainer.removeTrack([MediaTrack](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaTrack.html) track)](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaContainer.removeTrack.html) + * + * 将音频或视频轨道从容器中移除 + * + * 最低基础库: `2.9.0` */ + removeTrack( + /** [MediaTrack](https://developers.weixin.qq.com/miniprogram/dev/api/media/video-processing/MediaTrack.html) + * + * 要移除的音频或视频轨道 */ + track: MediaTrack + ): void + } + interface MediaQueryObserver { + /** [MediaQueryObserver.disconnect()](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/MediaQueryObserver.disconnect.html) + * + * 停止监听。回调函数将不再触发 */ + disconnect(): void + /** [MediaQueryObserver.observe(Object descriptor, function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/MediaQueryObserver.observe.html) + * + * 开始监听页面 media query 变化情况 */ + observe( + /** media query 描述符 */ + descriptor: ObserveDescriptor, + /** 监听 media query 状态变化的回调函数 */ + callback: MediaQueryObserverObserveCallback + ): void + } + interface MediaRecorder { + /** [MediaRecorder.destroy()](https://developers.weixin.qq.com/miniprogram/dev/api/media/media-recorder/MediaRecorder.destroy.html) + * + * 销毁录制器 + * + * 最低基础库: `2.11.0` */ + destroy(): void + /** [MediaRecorder.off(string eventName, function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/media-recorder/MediaRecorder.off.html) + * + * 取消监听录制事件。当对应事件触发时,该回调函数不再执行。 + * + * 最低基础库: `2.11.0` */ + off( + /** 事件名 */ + eventName: string, + /** 事件触发时执行的回调函数 */ + callback: (...args: any[]) => any + ): void + /** [MediaRecorder.on(string eventName, function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/media-recorder/MediaRecorder.on.html) + * + * 注册监听录制事件的回调函数。当对应事件触发时,回调函数会被执行。 + * + * 最低基础库: `2.11.0` */ + on( + /** 事件名 + * + * 参数 eventName 可选值: + * - 'start': 录制开始事件。; + * - 'stop': 录制结束事件。返回 {tempFilePath, duration, fileSize}; */ + eventName: 'start' | 'stop', + /** 事件触发时执行的回调函数 */ + callback: (...args: any[]) => any + ): void + /** [MediaRecorder.pause()](https://developers.weixin.qq.com/miniprogram/dev/api/media/media-recorder/MediaRecorder.pause.html) + * + * 暂停录制 + * + * 最低基础库: `2.11.0` */ + pause(): void + /** [MediaRecorder.requestFrame(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/media/media-recorder/MediaRecorder.requestFrame.html) + * + * 请求下一帧录制,在 callback 里完成一帧渲染后开始录制当前帧 + * + * 最低基础库: `2.11.0` */ + requestFrame(callback: (...args: any[]) => any): void + /** [MediaRecorder.resume()](https://developers.weixin.qq.com/miniprogram/dev/api/media/media-recorder/MediaRecorder.resume.html) + * + * 恢复录制 + * + * 最低基础库: `2.11.0` */ + resume(): void + /** [MediaRecorder.start()](https://developers.weixin.qq.com/miniprogram/dev/api/media/media-recorder/MediaRecorder.start.html) + * + * 开始录制 + * + * 最低基础库: `2.11.0` */ + start(): void + /** [MediaRecorder.stop()](https://developers.weixin.qq.com/miniprogram/dev/api/media/media-recorder/MediaRecorder.stop.html) + * + * 结束录制 + * + * 最低基础库: `2.11.0` */ + stop(): void + } + interface MifareClassic { + /** [MifareClassic.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareClassic.close.html) + * + * 断开连接 + * + * 最低基础库: `2.11.2` */ + close(option?: NdefCloseOption): void + /** [MifareClassic.connect(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareClassic.connect.html) + * + * 连接 NFC 标签 + * + * 最低基础库: `2.11.2` */ + connect(option?: ConnectOption): void + /** [MifareClassic.getMaxTransceiveLength(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareClassic.getMaxTransceiveLength.html) + * + * 获取最大传输长度 + * + * 最低基础库: `2.11.2` */ + getMaxTransceiveLength(option?: GetMaxTransceiveLengthOption): void + /** [MifareClassic.isConnected(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareClassic.isConnected.html) + * + * 检查是否已连接 + * + * 最低基础库: `2.11.2` */ + isConnected(option?: IsConnectedOption): void + /** [MifareClassic.setTimeout(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareClassic.setTimeout.html) + * + * 设置超时时间 + * + * 最低基础库: `2.11.2` */ + setTimeout(option: SetTimeoutOption): void + /** [MifareClassic.transceive(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareClassic.transceive.html) + * + * 发送数据 + * + * 最低基础库: `2.11.2` */ + transceive(option: TransceiveOption): void + } + interface MifareUltralight { + /** [MifareUltralight.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareUltralight.close.html) + * + * 断开连接 + * + * 最低基础库: `2.11.2` */ + close(option?: NdefCloseOption): void + /** [MifareUltralight.connect(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareUltralight.connect.html) + * + * 连接 NFC 标签 + * + * 最低基础库: `2.11.2` */ + connect(option?: ConnectOption): void + /** [MifareUltralight.getMaxTransceiveLength(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareUltralight.getMaxTransceiveLength.html) + * + * 获取最大传输长度 + * + * 最低基础库: `2.11.2` */ + getMaxTransceiveLength(option?: GetMaxTransceiveLengthOption): void + /** [MifareUltralight.isConnected(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareUltralight.isConnected.html) + * + * 检查是否已连接 + * + * 最低基础库: `2.11.2` */ + isConnected(option?: IsConnectedOption): void + /** [MifareUltralight.setTimeout(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareUltralight.setTimeout.html) + * + * 设置超时时间 + * + * 最低基础库: `2.11.2` */ + setTimeout(option: SetTimeoutOption): void + /** [MifareUltralight.transceive(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareUltralight.transceive.html) + * + * 发送数据 + * + * 最低基础库: `2.11.2` */ + transceive(option: TransceiveOption): void + } + interface NFCAdapter { + /** [NFCAdapter.offDiscovered(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.offDiscovered.html) + * + * 取消监听 NFC Tag + * + * 最低基础库: `2.11.2` */ + offDiscovered( + /** 的回调函数 */ + callback?: OffDiscoveredCallback + ): void + /** [NFCAdapter.onDiscovered(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.onDiscovered.html) + * + * 监听 NFC Tag + * + * 最低基础库: `2.11.2` */ + onDiscovered( + /** 的回调函数 */ + callback: OnDiscoveredCallback + ): void + /** [NFCAdapter.startDiscovery(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.startDiscovery.html) + * + * + * + * 最低基础库: `2.11.2` */ + startDiscovery(option?: StartDiscoveryOption): void + /** [NFCAdapter.stopDiscovery(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.stopDiscovery.html) + * + * + * + * 最低基础库: `2.11.2` */ + stopDiscovery(option?: StopDiscoveryOption): void + /** [[IsoDep](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/IsoDep.html) NFCAdapter.getIsoDep()](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.getIsoDep.html) + * + * 获取IsoDep实例,实例支持ISO-DEP (ISO 14443-4)标准的读写 + * + * 最低基础库: `2.11.2` */ + getIsoDep(): IsoDep + /** [[MifareClassic](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareClassic.html) NFCAdapter.getMifareClassic()](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.getMifareClassic.html) + * + * 获取MifareClassic实例,实例支持MIFARE Classic标签的读写 + * + * 最低基础库: `2.11.2` */ + getMifareClassic(): MifareClassic + /** [[MifareUltralight](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/MifareUltralight.html) NFCAdapter.getMifareUltralight()](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.getMifareUltralight.html) + * + * 获取MifareUltralight实例,实例支持MIFARE Ultralight标签的读写 + * + * 最低基础库: `2.11.2` */ + getMifareUltralight(): MifareUltralight + /** [[Ndef](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/Ndef.html) NFCAdapter.getNdef()](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.getNdef.html) + * + * 获取Ndef实例,实例支持对NDEF格式的NFC标签上的NDEF数据的读写 + * + * 最低基础库: `2.11.2` */ + getNdef(): Ndef + /** [[NfcA](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.html) NFCAdapter.getNfcA()](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.getNfcA.html) + * + * 获取NfcA实例,实例支持NFC-A (ISO 14443-3A)标准的读写 + * + * 最低基础库: `2.11.2` */ + getNfcA(): NfcA + /** [[NfcB](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcB.html) NFCAdapter.getNfcB()](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.getNfcB.html) + * + * 获取NfcB实例,实例支持NFC-B (ISO 14443-3B)标准的读写 + * + * 最低基础库: `2.11.2` */ + getNfcB(): NfcB + /** [[NfcF](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcF.html) NFCAdapter.getNfcF()](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.getNfcF.html) + * + * 获取NfcF实例,实例支持NFC-F (JIS 6319-4)标准的读写 + * + * 最低基础库: `2.11.2` */ + getNfcF(): NfcF + /** [[NfcV](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcV.html) NFCAdapter.getNfcV()](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NFCAdapter.getNfcV.html) + * + * 获取NfcV实例,实例支持NFC-V (ISO 15693)标准的读写 + * + * 最低基础库: `2.11.2` */ + getNfcV(): NfcV + } + interface NFCError { + /** 错误信息 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 0 | ok | 正常 | + * | 13000 | | 当前设备不支持NFC | + * | 13001 | | 当前设备支持NFC,但系统NFC开关未开启 | + * | 13002 | | 当前设备支持NFC,但不支持HCE | + * | 13003 | | AID列表参数格式错误 | + * | 13004 | | 未设置微信为默认NFC支付应用 | + * | 13005 | | 返回的指令不合法 | + * | 13006 | | 注册AID失败 | */ errMsg: string + /** 错误码 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 0 | ok | 正常 | + * | 13000 | | 当前设备不支持NFC | + * | 13001 | | 当前设备支持NFC,但系统NFC开关未开启 | + * | 13002 | | 当前设备支持NFC,但不支持HCE | + * | 13003 | | AID列表参数格式错误 | + * | 13004 | | 未设置微信为默认NFC支付应用 | + * | 13005 | | 返回的指令不合法 | + * | 13006 | | 注册AID失败 | */ errCode: number + } + interface Ndef { + /** [Ndef.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/Ndef.close.html) + * + * 断开连接 + * + * 最低基础库: `2.11.2` */ + close(option?: NdefCloseOption): void + /** [Ndef.connect(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/Ndef.connect.html) + * + * 连接 NFC 标签 + * + * 最低基础库: `2.11.2` */ + connect(option?: ConnectOption): void + /** [Ndef.isConnected(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/Ndef.isConnected.html) + * + * 检查是否已连接 + * + * 最低基础库: `2.11.2` */ + isConnected(option?: IsConnectedOption): void + /** [Ndef.offNdefMessage(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/Ndef.offNdefMessage.html) + * + * 取消监听 Ndef 消息 + * + * 最低基础库: `2.11.2` */ + offNdefMessage(callback: (...args: any[]) => any): void + /** [Ndef.onNdefMessage(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/Ndef.onNdefMessage.html) + * + * 监听 Ndef 消息 + * + * 最低基础库: `2.11.2` */ + onNdefMessage(callback: (...args: any[]) => any): void + /** [Ndef.setTimeout(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/Ndef.setTimeout.html) + * + * 设置超时时间 + * + * 最低基础库: `2.11.2` */ + setTimeout(option: SetTimeoutOption): void + /** [Ndef.writeNdefMessage(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/Ndef.writeNdefMessage.html) + * + * 重写 Ndef 标签内容 + * + * 最低基础库: `2.11.2` */ + writeNdefMessage(option: WriteNdefMessageOption): void + } + interface NfcA { + /** [NfcA.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.close.html) + * + * 断开连接 + * + * 最低基础库: `2.11.2` */ + close(option?: NdefCloseOption): void + /** [NfcA.connect(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.connect.html) + * + * 连接 NFC 标签 + * + * 最低基础库: `2.11.2` */ + connect(option?: ConnectOption): void + /** [NfcA.getAtqa(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.getAtqa.html) + * + * 获取ATQA信息 + * + * 最低基础库: `2.11.2` */ + getAtqa(option?: GetAtqaOption): void + /** [NfcA.getMaxTransceiveLength(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.getMaxTransceiveLength.html) + * + * 获取最大传输长度 + * + * 最低基础库: `2.11.2` */ + getMaxTransceiveLength(option?: GetMaxTransceiveLengthOption): void + /** [NfcA.getSak(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.getSak.html) + * + * 获取SAK信息 + * + * 最低基础库: `2.11.2` */ + getSak(option?: GetSakOption): void + /** [NfcA.isConnected(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.isConnected.html) + * + * 检查是否已连接 + * + * 最低基础库: `2.11.2` */ + isConnected(option?: IsConnectedOption): void + /** [NfcA.setTimeout(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.setTimeout.html) + * + * 设置超时时间 + * + * 最低基础库: `2.11.2` */ + setTimeout(option: SetTimeoutOption): void + /** [NfcA.transceive(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcA.transceive.html) + * + * 发送数据 + * + * 最低基础库: `2.11.2` */ + transceive(option: TransceiveOption): void + } + interface NfcB { + /** [NfcB.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcB.close.html) + * + * 断开连接 + * + * 最低基础库: `2.11.2` */ + close(option?: NdefCloseOption): void + /** [NfcB.connect(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcB.connect.html) + * + * 连接 NFC 标签 + * + * 最低基础库: `2.11.2` */ + connect(option?: ConnectOption): void + /** [NfcB.getMaxTransceiveLength(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcB.getMaxTransceiveLength.html) + * + * 获取最大传输长度 + * + * 最低基础库: `2.11.2` */ + getMaxTransceiveLength(option?: GetMaxTransceiveLengthOption): void + /** [NfcB.isConnected(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcB.isConnected.html) + * + * 检查是否已连接 + * + * 最低基础库: `2.11.2` */ + isConnected(option?: IsConnectedOption): void + /** [NfcB.setTimeout(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcB.setTimeout.html) + * + * 设置超时时间 + * + * 最低基础库: `2.11.2` */ + setTimeout(option: SetTimeoutOption): void + /** [NfcB.transceive(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcB.transceive.html) + * + * 发送数据 + * + * 最低基础库: `2.11.2` */ + transceive(option: TransceiveOption): void + } + interface NfcF { + /** [NfcF.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcF.close.html) + * + * 断开连接 + * + * 最低基础库: `2.11.2` */ + close(option?: NdefCloseOption): void + /** [NfcF.connect(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcF.connect.html) + * + * 连接 NFC 标签 + * + * 最低基础库: `2.11.2` */ + connect(option?: ConnectOption): void + /** [NfcF.getMaxTransceiveLength(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcF.getMaxTransceiveLength.html) + * + * 获取最大传输长度 + * + * 最低基础库: `2.11.2` */ + getMaxTransceiveLength(option?: GetMaxTransceiveLengthOption): void + /** [NfcF.isConnected(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcF.isConnected.html) + * + * 检查是否已连接 + * + * 最低基础库: `2.11.2` */ + isConnected(option?: IsConnectedOption): void + /** [NfcF.setTimeout(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcF.setTimeout.html) + * + * 设置超时时间 + * + * 最低基础库: `2.11.2` */ + setTimeout(option: SetTimeoutOption): void + /** [NfcF.transceive(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcF.transceive.html) + * + * 发送数据 + * + * 最低基础库: `2.11.2` */ + transceive(option: TransceiveOption): void + } + interface NfcV { + /** [NfcV.close(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcV.close.html) + * + * 断开连接 + * + * 最低基础库: `2.11.2` */ + close(option?: NdefCloseOption): void + /** [NfcV.connect(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcV.connect.html) + * + * 连接 NFC 标签 + * + * 最低基础库: `2.11.2` */ + connect(option?: ConnectOption): void + /** [NfcV.getMaxTransceiveLength(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcV.getMaxTransceiveLength.html) + * + * 获取最大传输长度 + * + * 最低基础库: `2.11.2` */ + getMaxTransceiveLength(option?: GetMaxTransceiveLengthOption): void + /** [NfcV.isConnected(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcV.isConnected.html) + * + * 检查是否已连接 + * + * 最低基础库: `2.11.2` */ + isConnected(option?: IsConnectedOption): void + /** [NfcV.setTimeout(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcV.setTimeout.html) + * + * 设置超时时间 + * + * 最低基础库: `2.11.2` */ + setTimeout(option: SetTimeoutOption): void + /** [NfcV.transceive(Object object)](https://developers.weixin.qq.com/miniprogram/dev/api/device/nfc/NfcV.transceive.html) + * + * 发送数据 + * + * 最低基础库: `2.11.2` */ + transceive(option: TransceiveOption): void + } + interface Nfcrwerror { + /** 错误信息 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 13000 | 设备不支持NFC | | + * | 13001 | 系统NFC开关未打开 | | + * | 13010 | 未知错误 | | + * | 13019 | user is not authorized | 用户未授权 | + * | 13011 | invalid parameter | 参数无效 | + * | 13012 | parse NdefMessage failed | 将参数解析为NdefMessage失败 | + * | 13021 | NFC discovery already started | 已经开始NFC扫描 | + * | 13018 | NFC discovery has not started | 尝试在未开始NFC扫描时停止NFC扫描 | + * | 13022 | Tech already connected | 标签已经连接 | + * | 13023 | Tech has not connected | 尝试在未连接标签时断开连接 | + * | 13013 | NFC tag has not been discovered | 未扫描到NFC标签 | + * | 13014 | invalid tech | 无效的标签技术 | + * | 13015 | unavailable tech | 从标签上获取对应技术失败 | + * | 13024 | function not support | 当前标签技术不支持该功能 | + * | 13017 | system internal error | 相关读写操作失败 | + * | 13016 | connect fail | 连接失败 | */ errMsg: string + /** 错误码 + * + * | 错误码 | 错误信息 | 说明 | + * | - | - | - | + * | 13000 | 设备不支持NFC | | + * | 13001 | 系统NFC开关未打开 | | + * | 13010 | 未知错误 | | + * | 13019 | user is not authorized | 用户未授权 | + * | 13011 | invalid parameter | 参数无效 | + * | 13012 | parse NdefMessage failed | 将参数解析为NdefMessage失败 | + * | 13021 | NFC discovery already started | 已经开始NFC扫描 | + * | 13018 | NFC discovery has not started | 尝试在未开始NFC扫描时停止NFC扫描 | + * | 13022 | Tech already connected | 标签已经连接 | + * | 13023 | Tech has not connected | 尝试在未连接标签时断开连接 | + * | 13013 | NFC tag has not been discovered | 未扫描到NFC标签 | + * | 13014 | invalid tech | 无效的标签技术 | + * | 13015 | unavailable tech | 从标签上获取对应技术失败 | + * | 13024 | function not support | 当前标签技术不支持该功能 | + * | 13017 | system internal error | 相关读写操作失败 | + * | 13016 | connect fail | 连接失败 | */ errCode: number + } + interface NodesRef { + /** [[SelectorQuery](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/SelectorQuery.html) NodesRef.boundingClientRect(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/NodesRef.boundingClientRect.html) +* +* 添加节点的布局位置的查询请求。相对于显示区域,以像素为单位。其功能类似于 DOM 的 `getBoundingClientRect`。返回 `NodesRef` 对应的 `SelectorQuery`。 +* +* **示例代码** +* +* +* ```js +Page({ + getRect () { + wx.createSelectorQuery().select('#the-id').boundingClientRect(function(rect){ + rect.id // 节点的ID + rect.dataset // 节点的dataset + rect.left // 节点的左边界坐标 + rect.right // 节点的右边界坐标 + rect.top // 节点的上边界坐标 + rect.bottom // 节点的下边界坐标 + rect.width // 节点的宽度 + rect.height // 节点的高度 + }).exec() + }, + getAllRects () { + wx.createSelectorQuery().selectAll('.a-class').boundingClientRect(function(rects){ + rects.forEach(function(rect){ + rect.id // 节点的ID + rect.dataset // 节点的dataset + rect.left // 节点的左边界坐标 + rect.right // 节点的右边界坐标 + rect.top // 节点的上边界坐标 + rect.bottom // 节点的下边界坐标 + rect.width // 节点的宽度 + rect.height // 节点的高度 + }) + }).exec() + } +}) +``` */ + boundingClientRect( + /** 回调函数,在执行 `SelectorQuery.exec` 方法后,节点信息会在 `callback` 中返回。 */ + callback?: BoundingClientRectCallback + ): SelectorQuery + /** [[SelectorQuery](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/SelectorQuery.html) NodesRef.context(function callback)](https://developers.weixin.qq.com/miniprogram/dev/api/wxml/NodesRef.context.html) +* +* 添加节点的 Context 对象查询请求。目前支持 [VideoContext](https://developers.weixin.qq.com/miniprogram/dev/api/media/video/VideoContext.html)、[CanvasContext](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html)、[LivePlayerContext](https://developers.weixin.qq.com/miniprogram/dev/api/media/live/LivePlayerContext.html)、[EditorContext](https://developers.weixin.qq.com/miniprogram/dev/api/media/editor/EditorContext.html)和 [MapContext](https://developers.weixin.qq.com/miniprogram/dev/api/media/map/MapContext.html) 的获取。 +* +* **示例代码** +* +* +* ```js +Page({ + getContext () { + wx.createSelectorQuery().select('.the-video-class').context(function(res){ + console.log(res.context) // 节点对应的 Context 对象。如:选中的节点是