commit 5a041ecaf5637b0ef7b87dd77694a1001660c61e Author: dzc <2388224167@qq.com> Date: Tue Nov 25 11:50:02 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09144bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/.idea/ +/box-im.iml +/im-server/im-server.iml +/im-platform/im-platform.iml +/im-client/im-client.iml +/im-common/im-common.iml +/im-server/target/ +/im-platform/target/ +/im-client/target/ +/im-common/target/ +/im-web/node_modules/ +/im-web/dist/ +/im-uniapp/node_modules/ +/im-uniapp/package-lock.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..edc4cb5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 blue + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..96a5f4c --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ + +### **盒子IM** +![MIT协议](https://img.shields.io/badge/license-MIT-red) +[![star](https://gitee.com/bluexsx/box-im/badge/star.svg)](https://gitee.com/bluexsx/box-im) +[![star](https://img.shields.io/github/stars/bluexsx/box-im.svg?style=flat&logo=GitHub)](https://github.com/bluexsx/box-im) + + +1. 盒子IM是一个仿微信实现的网页版聊天软件,不依赖任何第三方收费组件。 +1. 支持私聊、群聊、离线消息、发送语音、图片、文件、已读未读、群@等功能 +1. 支持音视频通话(基于原生webrtc实现,需要ssl证书) +1. uniapp端兼容app、h5、微信小程序,可与web端同时在线,并保持消息同步 +1. 后端采用springboot+netty实现,网页端使用vue,移动端使用uniapp +1. 服务器支持集群化部署,具有良好的横向扩展能力 + + +详细文档:https://www.yuque.com/u1475064/mufu2a +官方论坛: https://bbs.boxim.online + +#### 近期更新 +发布3.0版本: + +- 后台管理端上线,后台管理代码仓库地址:https://gitee.com/bluexsx/box-im-admin +- 框架和组件版本全面升级: jdk17、springboot3.3、node18等 +- 部分界面,功能、性能优化 + + +#### 在线体验 + +网页端:https://www.boxim.online + +移动安卓端:已上架至各大主流手机应用市场以及腾讯应用宝,搜索"盒子IM",下载安装即可 + +移动ios端: 已上架至app store,搜索"盒子IM",下载安装即可 + +小程序段: 已上架至微信小程序,搜索“盒子IM”进入即可 + +移动H5端: https://www.boxim.online/h5/ ,或扫码: + +![输入图片说明](%E6%88%AA%E5%9B%BE/h5%E4%BA%8C%E7%BB%B4%E7%A0%81.png) + +测试账号:张三/Aa123123 李四/Aa123123,也可以自行注册 + +说明: +1.**请勿利用测试账号辱骂他人、发布低俗内容,否则将直接对您的IP进行封禁** +2.由于部分厂商上架审核要求实名制,app端隐藏了"用户名注册"注册通道,可通过长按注册页面蓝色文字标题解除限制 +3.由于微信小程序每次发布审核过于严苛和繁琐,暂时不再提供体验环境,但uniapp端依然会继续兼容小程序 +4.体验环境部署的是商业版本,与开源版本功能存在一定差异,具体请参考: +https://www.yuque.com/u1475064/imk5n2/qtezcg32q1d0dr29#SbvXq + + +#### 付费服务 +商业版源码: https://www.yuque.com/u1475064/imk5n2/qtezcg32q1d0dr29 +远程协助: https://www.yuque.com/u1475064/imk5n2/fettd57rvzc29s5r +环境搭建: https://www.yuque.com/u1475064/imk5n2/qgq5cvgmavallqnl + + +#### 项目结构 +| 模块 | 功能 | +|-------------|----------------------------------| +| im-platform | 业务平台服务,负责处理来自用户的业务请求(http) | +| im-server | 消息推送服务,不依赖业务,负责将消息推送给用户(ws) | +| im-client | 消息推送sdk, 其他服务可集成此sdk与im-server通信 | +| im-common | 公共包,后端服务均依赖此包 | +| im-web | web页面 | +| im-uniapp | uniapp页面,可打包成app、h5、微信小程序 | + +#### 消息推送方案 +当消息的发送者和接收者连的不是同一个server时,消息是无法直接推送的,所以我们设计出了能够支持跨节点推送的方案: +![输入图片说明](%E6%88%AA%E5%9B%BE/%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81%E9%9B%86%E7%BE%A4%E5%8C%96.jpg) + +- 利用了redis的list数据实现消息推送,其中key为im:unread:${serverid},每个key的数据可以看做一个queue,每个im-server根据自身的id只消费属于自己的queue +- redis记录了每个用户的websocket连接的是哪个im-server,当用户发送消息时,im-platform将根据所连接的im-server的id,决定将消息推向哪个queue + + +#### 本地启动 +1.安装运行环境 +- 安装node:v18.19.0 +- 安装jdk:17 +- 安装maven:3.9.6 +- 安装mysql:8.0,账号密码分别为root/root,创建名为im_platform的数据库,运行db/im_platfrom.sql脚本 +- 安装redis:6.2 +- 安装minio:RELEASE.2024-xx,使用默认账号、密码、端口 + +2.启动后端服务 +``` +mvn clean package +java -jar ./im-platform/target/im-platform.jar +java -jar ./im-server/target/im-server.jar +``` + +3.启动前端web +``` +cd im-web +npm install +npm run serve +``` +访问 http://localhost:8080 + +4.启动uniapp-h5 +将im-uniapp目录导入HBuilderX,点击菜单"运行"->"开发环境-h5" +访问 http://localhost:5173 + + +#### 界面截图 +私聊: +![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%A7%81%E8%81%8A.jpg) + +群聊: +![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%BE%A4%E8%81%8A.jpg) + +群通话(商业版): +![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E5%A4%9A%E4%BA%BA%E9%80%9A%E8%AF%9D.jpg) + +好友列表: +![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E5%A5%BD%E5%8F%8B.jpg) + +群列表: +![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%BE%A4%E5%88%97%E8%A1%A8.jpg) + +移动端APP: +![输入图片说明](%E6%88%AA%E5%9B%BE/app/1.png) + + +![输入图片说明](%E6%88%AA%E5%9B%BE/app/2.png) + +#### 加入交流群 +群1: 741174521(已满) +群2: 937470451(已满) +群3: 1012017031(已满) +群4: 765849133 + +欢迎进群与小伙们一起交流, **申请加群前请务必先star哦** + + +#### 点下star吧 +如果项目对您有帮助,请点亮右上方的star,支持一下作者吧! + + +#### 说明几点 + +1. 本系统允许用于商业用途,且不收费,**但切记不要用于任何非法用途** ,本软件作者不会为此承担任何责任 +1. 基于本系统二次开发后再次开源的项目,请注明引用出处,以避免引发不必要的误会 +1. 为方便管理,要pr的同学请将代码提交到v_3.0.0分支,作者会在功能上线时合并到master分支 +1. 允许商用不代表可以随意倒卖开源代码,尤其是代码、名字、截图一点都不改就拿去卖钱的,我劝你们善良一点! + diff --git a/db/im-platform.sql b/db/im-platform.sql new file mode 100644 index 0000000..20d1c71 --- /dev/null +++ b/db/im-platform.sql @@ -0,0 +1,112 @@ +create table `im_user`( + `id` bigint not null auto_increment primary key comment 'id', + `user_name` varchar(255) not null comment '用户名', + `nick_name` varchar(255) not null comment '用户昵称', + `head_image` varchar(255) default '' comment '用户头像', + `head_image_thumb` varchar(255) default '' comment '用户头像缩略图', + `password` varchar(255) not null comment '密码', + `sex` tinyint(1) default 0 comment '性别 0:男 1:女', + `is_banned` tinyint(1) default 0 comment '是否被封禁 0:否 1:是', + `reason` varchar(255) default '' comment '被封禁原因', + `type` smallint default 1 comment '用户类型 1:普通用户 2:审核账户', + `signature` varchar(1024) default '' comment '个性签名', + `last_login_time` datetime DEFAULT null comment '最后登录时间', + `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', + unique key `idx_user_name`(user_name), + key `idx_nick_name`(nick_name) +) ENGINE=InnoDB CHARSET=utf8mb4 comment '用户'; + +create table `im_friend`( + `id` bigint not null auto_increment primary key comment 'id', + `user_id` bigint not null comment '用户id', + `friend_id` bigint not null comment '好友id', + `friend_nick_name` varchar(255) not null comment '好友昵称', + `friend_head_image` varchar(255) default '' comment '好友头像', + `is_dnd` tinyint comment '免打扰标识(Do Not Disturb) 0:关闭 1:开启', + `deleted` tinyint comment '删除标识 0:正常 1:已删除', + `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', + key `idx_user_id` (`user_id`), + key `idx_friend_id` (`friend_id`) +) ENGINE=InnoDB CHARSET=utf8mb4 comment '好友'; + +create table `im_private_message`( + `id` bigint not null auto_increment primary key comment 'id', + `tmp_id` varchar(32) comment '临时id,由前端生成', + `send_id` bigint not null comment '发送用户id', + `recv_id` bigint not null comment '接收用户id', + `content` text comment '发送内容', + `type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 4:视频 21:提示', + `status` tinyint(1) NOT NULL comment '状态 0:未读 1:已读 2:撤回 3:已读', + `send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间', + key `idx_send_id` (`send_id`), + key `idx_recv_id` (`recv_id`) +)ENGINE=InnoDB CHARSET=utf8mb4 comment '私聊消息'; + + +create table `im_group`( + `id` bigint not null auto_increment primary key comment 'id', + `name` varchar(255) not null comment '群名字', + `owner_id` bigint not null comment '群主id', + `head_image` varchar(255) default '' comment '群头像', + `head_image_thumb` varchar(255) default '' comment '群头像缩略图', + `notice` varchar(1024) default '' comment '群公告', + `is_banned` tinyint(1) default 0 comment '是否被封禁 0:否 1:是', + `reason` varchar(255) default '' comment '被封禁原因', + `dissolve` tinyint(1) default 0 comment '是否已解散', + `created_time` datetime default CURRENT_TIMESTAMP comment '创建时间' +)ENGINE=InnoDB CHARSET=utf8mb4 comment '群'; + +create table `im_group_member`( + `id` bigint not null auto_increment primary key comment 'id', + `group_id` bigint not null comment '群id', + `user_id` bigint not null comment '用户id', + `user_nick_name` varchar(255) DEFAULT '' comment '用户昵称', + `remark_nick_name` varchar(255) DEFAULT '' comment '显示昵称备注', + `head_image` varchar(255) DEFAULT '' comment '用户头像', + `remark_group_name` varchar(255) DEFAULT '' comment '显示群名备注', + `is_dnd` tinyint comment '免打扰标识(Do Not Disturb) 0:关闭 1:开启', + `quit` tinyint(1) DEFAULT 0 comment '是否已退出', + `quit_time` datetime DEFAULT NULL comment '退出时间', + `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', + key `idx_group_id`(`group_id`), + key `idx_user_id`(`user_id`) +)ENGINE=InnoDB CHARSET=utf8mb4 comment '群成员'; + +create table `im_group_message`( + `id` bigint not null auto_increment primary key comment 'id', + `tmp_id` varchar(32) comment '临时id,由前端生成', + `group_id` bigint not null comment '群id', + `send_id` bigint not null comment '发送用户id', + `send_nick_name` varchar(255) DEFAULT '' comment '发送用户昵称', + `recv_ids` varchar(1024) DEFAULT '' comment '接收用户id,逗号分隔,为空表示发给所有成员', + `content` text comment '发送内容', + `at_user_ids` varchar(1024) comment '被@的用户id列表,逗号分隔', + `receipt` tinyint DEFAULT 0 comment '是否回执消息', + `receipt_ok` tinyint DEFAULT 0 comment '回执消息是否完成', + `type` tinyint(1) NOT NULL comment '消息类型 0:文字 1:图片 2:文件 3:语音 4:视频 21:提示' , + `status` tinyint(1) DEFAULT 0 comment '状态 0:未发出 2:撤回 ', + `send_time` datetime DEFAULT CURRENT_TIMESTAMP comment '发送时间', + key `idx_group_id` (group_id) +)ENGINE=InnoDB CHARSET=utf8mb4 comment '群消息'; + +create table `im_sensitive_word`( + `id` bigint not null auto_increment primary key comment 'id', + `content` varchar(64) not null comment '敏感词内容', + `enabled` tinyint DEFAULT 0 COMMENT '是否启用 0:未启用 1:启用', + `creator` bigint DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' +)ENGINE=InnoDB CHARSET=utf8mb4 comment '敏感词'; + +CREATE TABLE `im_file_info` ( + `id` BIGINT NOT NULL auto_increment PRIMARY key comment 'id', + `file_name` VARCHAR(255) NOT NULL comment '文件名', + `file_path` VARCHAR(255) NOT NULL comment '文件地址', + `file_size` INTEGER NOT NULL comment '文件大小', + `file_type` tinyint NOT NULL comment '0:普通文件 1:图片 2:视频', + `compressed_path` VARCHAR(255) DEFAULT NULL comment '压缩文件路径', + `cover_path` VARCHAR(255) DEFAULT NULL comment '封面文件路径,仅视频文件有效', + `upload_time` datetime DEFAULT CURRENT_TIMESTAMP comment '上传时间', + `is_permanent` tinyint DEFAULT 0 comment '是否永久文件', + `md5` VARCHAR(64) NOT NULL comment '文件md5', + KEY `idx_md5` (md5) +) ENGINE = InnoDB CHARSET = utf8mb4 comment '文件'; \ No newline at end of file diff --git a/db/敏感词库初始化.sql b/db/敏感词库初始化.sql new file mode 100644 index 0000000..f6e46aa --- /dev/null +++ b/db/敏感词库初始化.sql @@ -0,0 +1,470 @@ +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('爱液', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('按摩棒', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('拔出来', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('爆草', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('包二奶', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('暴干', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('暴奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('暴乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('爆乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('暴淫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('屄', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('被操', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('被插', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('被干', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('逼奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('仓井空', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('插暴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操黑', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操烂', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肏你', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肏死', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操死', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操我', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('厕奴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('插比', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('插逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('插进', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('插你', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('插我', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('插阴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('潮吹', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('潮喷', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人电影', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人论坛', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人小说', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人电', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人电影', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人卡通', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人聊', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人片', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人视', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人图', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人小', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人电影', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人论坛', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人色情', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人网站', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人小说', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('艳情小说', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人游戏', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('吃精', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('赤裸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('抽插', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('扌由插', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('抽一插', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('春药', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('大波', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('大力抽送', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('大乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('荡妇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('荡女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('盗撮', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('多人轮', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('发浪', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('放尿', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肥逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('粉穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('封面女郎', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('风月大陆', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('干死你', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('干穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肛交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肛门', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('龟头', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裹本', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('好嫩', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('豪乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('黑逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('后庭', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('后穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('虎骑', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('花花公子', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('换妻俱乐部', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('黄片', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('几吧', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('鸡吧', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('鸡巴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('鸡奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('寂寞男', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('寂寞女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('妓女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('激情', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('集体淫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('奸情', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('叫床', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('脚交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('精液', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('就去日', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('巨屌', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('菊花洞', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('菊门', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('巨奶', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('巨乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('菊穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('开苞', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('口爆', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('口活', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('口交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('口射', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('口淫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裤袜', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('狂操', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('狂插', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('浪逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('浪妇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('浪叫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('浪女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('狼友', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('聊性', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('流淫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('铃木麻', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('凌辱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('漏乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乱交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乱伦', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('轮暴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('轮操', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('轮奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裸陪', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('买春', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('美逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('美少妇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('美乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('美腿', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('美穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('美幼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('秘唇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('迷奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('密穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('蜜穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('蜜液', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('摸奶', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('摸胸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('母奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('奈美', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('奶子', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('男奴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('内射', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('嫩逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('嫩女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('嫩穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('捏弄', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('女优', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('炮友', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('砲友', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('喷精', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('屁眼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('品香堂', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('前凸后翘', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('强暴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('强奸处女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('情趣用品', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('情色', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('拳交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('全裸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('群交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('惹火身材', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人妻', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人兽', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('日逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('日烂', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉棒', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉唇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉洞', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉缝', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉棍', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉茎', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉具', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('揉乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('肉欲', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乳爆', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乳房', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乳沟', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乳交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乳头', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('三级片', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('骚逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('骚比', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('骚女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('骚水', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('骚穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('色逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('色界', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('色猫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('色盟', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('色情网站', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('色区', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('色诱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('色欲', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('射爽', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('射颜', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('食精', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('释欲', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('兽奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('兽交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('手淫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('兽欲', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('熟妇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('熟母', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('熟女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('爽片', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('爽死我了', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('双臀', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('死逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('丝袜', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('丝诱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('松岛枫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('酥痒', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('汤加丽', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('套弄', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('体奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('体位', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('舔脚', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('舔阴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('调教', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('偷欢', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('偷拍', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('推油', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('脱内裤', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('文做', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('我就色', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('无码', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('舞女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('无修正', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('吸精', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('夏川纯', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('相奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('小逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('校鸡', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('小穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('写真', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性感妖娆', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性感诱惑', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性虎', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性饥渴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性技巧', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性奴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性虐', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性息', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性欲', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('胸推', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('穴口', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('学生妹', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('穴图', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('亚情', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('颜射', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阳具', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('杨思敏', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('要射了', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('夜勤病栋', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('一本道', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('一夜欢', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('一夜情', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴部', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫虫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴唇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫荡', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴道', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫电影', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴阜', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫妇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫河', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴核', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴户', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫贱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫叫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫教师', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴茎', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴精', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫浪', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫媚', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫糜', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫魔', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫母', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫虐', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫妻', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫情', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫色', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫声浪语', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫兽学园', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫书', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫术炼金士', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫水', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫娃', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫威', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫亵', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫样', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫液', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫照', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('应召', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('幼交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('幼男', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('幼女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('欲火', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('欲女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('玉女心经', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('玉蒲团', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('玉乳', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('欲仙欲死', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('玉穴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('援交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('原味内衣', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('援助交际', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('张筱雨', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('招鸡', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('招妓', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('中年美妇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('抓胸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('自拍', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('自慰', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('作爱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('贱人', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('傻逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('煞逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('煞笔', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('刹笔', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('傻比', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('沙比', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('欠干', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('婊子养的', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('我日你', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('我操', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('我草', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('卧艹', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('卧槽', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('爆你菊', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('艹你', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('你他妈', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('真他妈', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('别他吗', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('草你吗', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('草你丫', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操你妈', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('擦你妈', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操你娘', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操他妈', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('日你妈', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('干你妈', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('干你娘', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('娘西皮', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('狗操', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('狗草', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('狗杂种', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('狗日的', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操你祖宗', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操你全家', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('操你大爷', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('妈逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('你麻痹', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('麻痹的', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('妈了个逼', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('马勒', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('狗娘养', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('贱比', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('下贱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('死全家', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('全家死光', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('全家不得好死', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('全家死绝', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('白痴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('无耻', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('你妈的', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('婊子', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('贱货', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('人渣', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('混蛋', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('媚外', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('和弦', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('兼职', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('限量', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('铃声', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性伴侣', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('男公关', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('火辣', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('精子', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('射精', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('诱奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('强奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('做爱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性爱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('发生关系', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('按摩', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('快感', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('处男', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('猛男', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('少妇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('屌', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('屁股', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('下体', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('内裤', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('浑圆', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('咪咪', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('发情', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('刺激', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('白嫩', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('粉嫩', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('兽性', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('风骚', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('呻吟', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阉割', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('高潮', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裸露', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('不穿', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('一丝不挂', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('脱光', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('干你', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('干死', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('我干', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裙中性运动', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乱奸', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乱伦', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乱伦类', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('乱伦小', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('伦理大', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('伦理电影', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('伦理毛', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('伦理片', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裸聊', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裸聊网', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裸体写真', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裸舞视', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('裸照', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('美女裸体', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性交', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性交视频', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性交图片', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性奴', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性奴集中营', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('性虐', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴唇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴道', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴蒂', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴户', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴间来电', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴茎', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴茎增大', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴茎助勃', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('阴毛', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('陰唇', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('陰道', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('陰戶', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫荡', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫荡美女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫荡视频', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫荡照片', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫乱', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫靡', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫魔', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫魔舞', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫情女', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫肉', 1, 10000); +INSERT INTO `im_sensitive_word` (`content`, `enabled`, `creator`) VALUES ('淫騷妹', 1, 10000); + + diff --git a/im-client/pom.xml b/im-client/pom.xml new file mode 100644 index 0000000..dbe3d30 --- /dev/null +++ b/im-client/pom.xml @@ -0,0 +1,21 @@ + + + + box-im + com.bx + 3.0.0 + + 4.0.0 + + im-client + + + + com.bx + im-common + 3.0.0 + + + \ No newline at end of file diff --git a/im-client/src/main/java/com/bx/imclient/IMAutoConfiguration.java b/im-client/src/main/java/com/bx/imclient/IMAutoConfiguration.java new file mode 100644 index 0000000..c172099 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/IMAutoConfiguration.java @@ -0,0 +1,13 @@ +package com.bx.imclient; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + + +@Slf4j +@Configuration +@ComponentScan(basePackages = {"com.bx.imclient", "com.bx.imcommon"}) +public class IMAutoConfiguration { + +} diff --git a/im-client/src/main/java/com/bx/imclient/IMClient.java b/im-client/src/main/java/com/bx/imclient/IMClient.java new file mode 100644 index 0000000..1630d64 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/IMClient.java @@ -0,0 +1,92 @@ +package com.bx.imclient; + +import com.bx.imclient.sender.IMSender; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.model.IMGroupMessage; +import com.bx.imcommon.model.IMPrivateMessage; +import com.bx.imcommon.model.IMSystemMessage; +import lombok.AllArgsConstructor; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.Map; + +@Configuration +@AllArgsConstructor +public class IMClient { + + private final IMSender imSender; + + /** + * 判断用户是否在线 + * + * @param userId 用户id + */ + public Boolean isOnline(Long userId){ + return imSender.isOnline(userId); + } + + /** + * 判断用户是否在线 + * + * @param userId 用户id + * @param terminal 终端可惜 + */ + public Boolean isOnline(Long userId,IMTerminalType terminal){ + return imSender.isOnline(userId,terminal); + } + + + /** + * 判断多个用户是否在线 + * + * @param userIds 用户id列表 + * @return 在线的用户列表 + */ + public List getOnlineUser(List userIds){ + return imSender.getOnlineUser(userIds); + } + + + /** + * 判断多个用户是否在线 + * + * @param userIds 用户id列表 + * @return 在线的用户终端 + */ + public Map> getOnlineTerminal(List userIds){ + return imSender.getOnlineTerminal(userIds); + } + + /** + * 发送系统消息(发送结果通过MessageListener接收) + * + * @param message 私有消息 + */ + public void sendSystemMessage(IMSystemMessage message){ + imSender.sendSystemMessage(message); + } + + + /** + * 发送私聊消息(发送结果通过MessageListener接收) + * + * @param message 私有消息 + */ + public void sendPrivateMessage(IMPrivateMessage message){ + imSender.sendPrivateMessage(message); + } + + /** + * 发送群聊消息(发送结果通过MessageListener接收) + * + * @param message 群聊消息 + */ + public void sendGroupMessage(IMGroupMessage message){ + imSender.sendGroupMessage(message); + } + + + + +} diff --git a/im-client/src/main/java/com/bx/imclient/annotation/IMListener.java b/im-client/src/main/java/com/bx/imclient/annotation/IMListener.java new file mode 100644 index 0000000..4f54bf9 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/annotation/IMListener.java @@ -0,0 +1,18 @@ +package com.bx.imclient.annotation; + +import com.bx.imcommon.enums.IMListenerType; +import org.springframework.stereotype.Component; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE,ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface IMListener { + + IMListenerType type(); + +} diff --git a/im-client/src/main/java/com/bx/imclient/listener/MessageListener.java b/im-client/src/main/java/com/bx/imclient/listener/MessageListener.java new file mode 100644 index 0000000..0ee2af3 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/listener/MessageListener.java @@ -0,0 +1,12 @@ +package com.bx.imclient.listener; + + +import com.bx.imcommon.model.IMSendResult; + +import java.util.List; + +public interface MessageListener { + + void process(List> result); + +} diff --git a/im-client/src/main/java/com/bx/imclient/listener/MessageListenerMulticaster.java b/im-client/src/main/java/com/bx/imclient/listener/MessageListenerMulticaster.java new file mode 100644 index 0000000..bd95284 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/listener/MessageListenerMulticaster.java @@ -0,0 +1,46 @@ +package com.bx.imclient.listener; + + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONObject; +import com.bx.imclient.annotation.IMListener; +import com.bx.imcommon.enums.IMListenerType; +import com.bx.imcommon.model.IMSendResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; + +@Component +public class MessageListenerMulticaster { + + @Autowired(required = false) + private List messageListeners = Collections.emptyList(); + + public void multicast(IMListenerType listenerType, List results){ + if(CollUtil.isEmpty(results)){ + return; + } + for(MessageListener listener:messageListeners){ + IMListener annotation = listener.getClass().getAnnotation(IMListener.class); + if(annotation!=null && (annotation.type().equals(IMListenerType.ALL) || annotation.type().equals(listenerType))){ + results.forEach(result->{ + // 将data转回对象类型 + if(result.getData() instanceof JSONObject){ + Type superClass = listener.getClass().getGenericInterfaces()[0]; + Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + JSONObject data = (JSONObject)result.getData(); + result.setData(data.toJavaObject(type)); + } + }); + // 回调到调用方处理 + listener.process(results); + } + } + } + + +} diff --git a/im-client/src/main/java/com/bx/imclient/sender/IMSender.java b/im-client/src/main/java/com/bx/imclient/sender/IMSender.java new file mode 100644 index 0000000..1ced811 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/sender/IMSender.java @@ -0,0 +1,262 @@ +package com.bx.imclient.sender; + +import cn.hutool.core.collection.CollUtil; +import com.bx.imclient.listener.MessageListenerMulticaster; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.enums.IMListenerType; +import com.bx.imcommon.enums.IMSendCode; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.model.*; +import com.bx.imcommon.mq.RedisMQTemplate; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +@RequiredArgsConstructor +public class IMSender { + + @Autowired + private RedisMQTemplate redisMQTemplate; + + @Value("${spring.application.name}") + private String appName; + + private final MessageListenerMulticaster listenerMulticaster; + + + public void sendSystemMessage(IMSystemMessage message){ + // 根据群聊每个成员所连的IM-server,进行分组 + Map sendMap = new HashMap<>(); + for (Integer terminal : message.getRecvTerminals()) { + message.getRecvIds().forEach(id -> { + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, id.toString(), terminal.toString()); + sendMap.put(key,new IMUserInfo(id, terminal)); + }); + } + // 批量拉取 + List serverIds = redisMQTemplate.opsForValue().multiGet(sendMap.keySet()); + // 格式:map<服务器id,list<接收方>> + Map> serverMap = new HashMap<>(); + List offLineUsers = new LinkedList<>(); + int idx = 0; + for (Map.Entry entry : sendMap.entrySet()) { + Integer serverId = (Integer)serverIds.get(idx++); + if (!Objects.isNull(serverId)) { + List list = serverMap.computeIfAbsent(serverId, o -> new LinkedList<>()); + list.add(entry.getValue()); + } else { + // 加入离线列表 + offLineUsers.add(entry.getValue()); + } + } + // 逐个server发送 + for (Map.Entry> entry : serverMap.entrySet()) { + IMRecvInfo recvInfo = new IMRecvInfo(); + recvInfo.setCmd(IMCmdType.SYSTEM_MESSAGE.code()); + recvInfo.setReceivers(new LinkedList<>(entry.getValue())); + recvInfo.setServiceName(appName); + recvInfo.setSendResult(message.getSendResult()); + recvInfo.setData(message.getData()); + // 推送至队列 + String key = String.join(":", IMRedisKey.IM_MESSAGE_SYSTEM_QUEUE, entry.getKey().toString()); + redisMQTemplate.opsForList().rightPush(key, recvInfo); + } + // 对离线用户回复消息状态 + if(message.getSendResult() && !offLineUsers.isEmpty()){ + List results = new LinkedList<>(); + for (IMUserInfo offLineUser : offLineUsers) { + IMSendResult result = new IMSendResult(); + result.setReceiver(offLineUser); + result.setCode(IMSendCode.NOT_ONLINE.code()); + result.setData(message.getData()); + results.add(result); + } + listenerMulticaster.multicast(IMListenerType.SYSTEM_MESSAGE, results); + } + } + + + public void sendPrivateMessage(IMPrivateMessage message) { + List results = new LinkedList<>(); + if(!Objects.isNull(message.getRecvId())){ + for (Integer terminal : message.getRecvTerminals()) { + // 获取对方连接的channelId + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, message.getRecvId().toString(), terminal.toString()); + Integer serverId = (Integer)redisMQTemplate.opsForValue().get(key); + // 如果对方在线,将数据存储至redis,等待拉取推送 + if (serverId != null) { + String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE, serverId.toString()); + IMRecvInfo recvInfo = new IMRecvInfo(); + recvInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code()); + recvInfo.setSendResult(message.getSendResult()); + recvInfo.setServiceName(appName); + recvInfo.setSender(message.getSender()); + recvInfo.setReceivers(Collections.singletonList(new IMUserInfo(message.getRecvId(), terminal))); + recvInfo.setData(message.getData()); + redisMQTemplate.opsForList().rightPush(sendKey, recvInfo); + } else { + IMSendResult result = new IMSendResult(); + result.setSender(message.getSender()); + result.setReceiver(new IMUserInfo(message.getRecvId(), terminal)); + result.setCode(IMSendCode.NOT_ONLINE.code()); + result.setData(message.getData()); + results.add(result); + } + } + } + + // 推送给自己的其他终端 + if(message.getSendToSelf()){ + for (Integer terminal : IMTerminalType.codes()) { + if (message.getSender().getTerminal().equals(terminal)) { + continue; + } + // 获取终端连接的channelId + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, message.getSender().getId().toString(), terminal.toString()); + Integer serverId = (Integer)redisMQTemplate.opsForValue().get(key); + // 如果终端在线,将数据存储至redis,等待拉取推送 + if (serverId != null) { + String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE, serverId.toString()); + IMRecvInfo recvInfo = new IMRecvInfo(); + // 自己的消息不需要回推消息结果 + recvInfo.setSendResult(false); + recvInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code()); + recvInfo.setSender(message.getSender()); + recvInfo.setReceivers(Collections.singletonList(new IMUserInfo(message.getSender().getId(), terminal))); + recvInfo.setData(message.getData()); + redisMQTemplate.opsForList().rightPush(sendKey, recvInfo); + } + } + } + // 对离线用户回复消息状态 + if(message.getSendResult() && !results.isEmpty()){ + listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, results); + } + + } + + public void sendGroupMessage(IMGroupMessage message) { + // 根据群聊每个成员所连的IM-server,进行分组 + Map sendMap = new HashMap<>(); + for (Integer terminal : message.getRecvTerminals()) { + message.getRecvIds().forEach(id -> { + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, id.toString(), terminal.toString()); + sendMap.put(key,new IMUserInfo(id, terminal)); + }); + } + // 批量拉取 + List serverIds = redisMQTemplate.opsForValue().multiGet(sendMap.keySet()); + // 格式:map<服务器id,list<接收方>> + Map> serverMap = new HashMap<>(); + List offLineUsers = new LinkedList<>(); + int idx = 0; + for (Map.Entry entry : sendMap.entrySet()) { + Integer serverId = (Integer)serverIds.get(idx++); + if (!Objects.isNull(serverId)) { + List list = serverMap.computeIfAbsent(serverId, o -> new LinkedList<>()); + list.add(entry.getValue()); + } else { + // 加入离线列表 + offLineUsers.add(entry.getValue()); + } + } + // 逐个server发送 + for (Map.Entry> entry : serverMap.entrySet()) { + IMRecvInfo recvInfo = new IMRecvInfo(); + recvInfo.setCmd(IMCmdType.GROUP_MESSAGE.code()); + recvInfo.setReceivers(new LinkedList<>(entry.getValue())); + recvInfo.setSender(message.getSender()); + recvInfo.setServiceName(appName); + recvInfo.setSendResult(message.getSendResult()); + recvInfo.setData(message.getData()); + // 推送至队列 + String key = String.join(":", IMRedisKey.IM_MESSAGE_GROUP_QUEUE, entry.getKey().toString()); + redisMQTemplate.opsForList().rightPush(key, recvInfo); + } + + // 推送给自己的其他终端 + if (message.getSendToSelf()) { + for (Integer terminal : IMTerminalType.codes()) { + if (terminal.equals(message.getSender().getTerminal())) { + continue; + } + // 获取终端连接的channelId + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, message.getSender().getId().toString(), terminal.toString()); + Integer serverId = (Integer)redisMQTemplate.opsForValue().get(key); + // 如果终端在线,将数据存储至redis,等待拉取推送 + if (!Objects.isNull(serverId)) { + IMRecvInfo recvInfo = new IMRecvInfo(); + recvInfo.setCmd(IMCmdType.GROUP_MESSAGE.code()); + recvInfo.setSender(message.getSender()); + recvInfo.setReceivers(Collections.singletonList(new IMUserInfo(message.getSender().getId(), terminal))); + // 自己的消息不需要回推消息结果 + recvInfo.setSendResult(false); + recvInfo.setData(message.getData()); + String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_GROUP_QUEUE, serverId.toString()); + redisMQTemplate.opsForList().rightPush(sendKey, recvInfo); + } + } + } + // 对离线用户回复消息状态 + if(message.getSendResult() && !offLineUsers.isEmpty()){ + List results = new LinkedList<>(); + for (IMUserInfo offLineUser : offLineUsers) { + IMSendResult result = new IMSendResult(); + result.setSender(message.getSender()); + result.setReceiver(offLineUser); + result.setCode(IMSendCode.NOT_ONLINE.code()); + result.setData(message.getData()); + results.add(result); + } + listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE, results); + } + + } + + public Map> getOnlineTerminal(List userIds){ + if(CollUtil.isEmpty(userIds)){ + return Collections.emptyMap(); + } + // 把所有用户的key都存起来 + Map userMap = new HashMap<>(); + for(Long id:userIds){ + for (Integer terminal : IMTerminalType.codes()) { + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, id.toString(), terminal.toString()); + userMap.put(key,new IMUserInfo(id,terminal)); + } + } + // 批量拉取 + List serverIds = redisMQTemplate.opsForValue().multiGet(userMap.keySet()); + int idx = 0; + Map> onlineMap = new HashMap<>(); + for (Map.Entry entry : userMap.entrySet()) { + // serverid有值表示用户在线 + if(serverIds.get(idx++) != null){ + IMUserInfo userInfo = entry.getValue(); + List terminals = onlineMap.computeIfAbsent(userInfo.getId(), o -> new LinkedList<>()); + terminals.add(IMTerminalType.fromCode(userInfo.getTerminal())); + } + } + // 去重并返回 + return onlineMap; + } + + public Boolean isOnline(Long userId, IMTerminalType terminal) { + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), terminal.code().toString()); + return redisMQTemplate.hasKey(key); + } + + public Boolean isOnline(Long userId) { + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), "*"); + return !Objects.requireNonNull(redisMQTemplate.keys(key)).isEmpty(); + } + + public List getOnlineUser(List userIds){ + return new LinkedList<>(getOnlineTerminal(userIds).keySet()); + } +} diff --git a/im-client/src/main/java/com/bx/imclient/task/AbstractMessageResultTask.java b/im-client/src/main/java/com/bx/imclient/task/AbstractMessageResultTask.java new file mode 100644 index 0000000..346cfab --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/task/AbstractMessageResultTask.java @@ -0,0 +1,21 @@ +package com.bx.imclient.task; + +import cn.hutool.core.util.StrUtil; +import com.bx.imcommon.mq.RedisMQConsumer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; + +@Slf4j +public abstract class AbstractMessageResultTask extends RedisMQConsumer { + + @Value("${spring.application.name}") + private String appName; + + @Override + public String generateKey() { + return StrUtil.join(":", super.generateKey(), appName); + } + + + +} diff --git a/im-client/src/main/java/com/bx/imclient/task/GroupMessageResultResultTask.java b/im-client/src/main/java/com/bx/imclient/task/GroupMessageResultResultTask.java new file mode 100644 index 0000000..c8be7e2 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/task/GroupMessageResultResultTask.java @@ -0,0 +1,27 @@ +package com.bx.imclient.task; + +import com.bx.imclient.listener.MessageListenerMulticaster; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMListenerType; +import com.bx.imcommon.model.IMSendResult; +import com.bx.imcommon.mq.RedisMQListener; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + + +@Component +@RequiredArgsConstructor +@RedisMQListener(queue = IMRedisKey.IM_RESULT_GROUP_QUEUE, batchSize = 100) +public class GroupMessageResultResultTask extends AbstractMessageResultTask { + + private final MessageListenerMulticaster listenerMulticaster; + + @Override + public void onMessage(List results) { + listenerMulticaster.multicast(IMListenerType.GROUP_MESSAGE, results); + } + + +} diff --git a/im-client/src/main/java/com/bx/imclient/task/PrivateMessageResultResultTask.java b/im-client/src/main/java/com/bx/imclient/task/PrivateMessageResultResultTask.java new file mode 100644 index 0000000..38e5ac1 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/task/PrivateMessageResultResultTask.java @@ -0,0 +1,26 @@ +package com.bx.imclient.task; + +import com.bx.imclient.listener.MessageListenerMulticaster; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMListenerType; +import com.bx.imcommon.model.IMSendResult; +import com.bx.imcommon.mq.RedisMQListener; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + + +@Component +@RequiredArgsConstructor +@RedisMQListener(queue = IMRedisKey.IM_RESULT_PRIVATE_QUEUE, batchSize = 100) +public class PrivateMessageResultResultTask extends AbstractMessageResultTask { + + private final MessageListenerMulticaster listenerMulticaster; + + @Override + public void onMessage(List results) { + listenerMulticaster.multicast(IMListenerType.PRIVATE_MESSAGE, results); + } + +} diff --git a/im-client/src/main/java/com/bx/imclient/task/SystemMessageResultResultTask.java b/im-client/src/main/java/com/bx/imclient/task/SystemMessageResultResultTask.java new file mode 100644 index 0000000..a42ca94 --- /dev/null +++ b/im-client/src/main/java/com/bx/imclient/task/SystemMessageResultResultTask.java @@ -0,0 +1,25 @@ +package com.bx.imclient.task; + +import com.bx.imclient.listener.MessageListenerMulticaster; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMListenerType; +import com.bx.imcommon.model.IMSendResult; +import com.bx.imcommon.mq.RedisMQListener; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +@RedisMQListener(queue = IMRedisKey.IM_RESULT_SYSTEM_QUEUE, batchSize = 100) +public class SystemMessageResultResultTask extends AbstractMessageResultTask { + + private final MessageListenerMulticaster listenerMulticaster; + + @Override + public void onMessage(List results) { + listenerMulticaster.multicast(IMListenerType.SYSTEM_MESSAGE, results); + } + +} diff --git a/im-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/im-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..fe31de9 --- /dev/null +++ b/im-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.bx.imclient.IMAutoConfiguration \ No newline at end of file diff --git a/im-common/pom.xml b/im-common/pom.xml new file mode 100644 index 0000000..3e87e6f --- /dev/null +++ b/im-common/pom.xml @@ -0,0 +1,62 @@ + + + + box-im + com.bx + 3.0.0 + + 4.0.0 + im-common + jar + + + org.projectlombok + lombok + ${lombok.version} + + + cn.hutool + hutool-all + + + + com.alibaba + fastjson + + + org.apache.commons + commons-lang3 + + + + org.springframework + spring-beans + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + + + org.springframework + spring-context + + + + com.auth0 + java-jwt + 3.11.0 + + + + org.slf4j + slf4j-api + + + + org.springframework.boot + spring-boot-starter-data-redis + + + \ No newline at end of file diff --git a/im-common/src/main/java/com/bx/imcommon/contant/IMConstant.java b/im-common/src/main/java/com/bx/imcommon/contant/IMConstant.java new file mode 100644 index 0000000..40f3128 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/contant/IMConstant.java @@ -0,0 +1,18 @@ +package com.bx.imcommon.contant; + +public final class IMConstant { + + private IMConstant() { + } + + /** + * 在线状态过期时间 600s + */ + public static final long ONLINE_TIMEOUT_SECOND = 600; + /** + * 消息允许撤回时间 300s + */ + public static final long ALLOW_RECALL_SECOND = 300; + + +} diff --git a/im-common/src/main/java/com/bx/imcommon/contant/IMRedisKey.java b/im-common/src/main/java/com/bx/imcommon/contant/IMRedisKey.java new file mode 100644 index 0000000..a7a8bf3 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/contant/IMRedisKey.java @@ -0,0 +1,40 @@ +package com.bx.imcommon.contant; + +public final class IMRedisKey { + + + /** + * im-server最大id,从0开始递增 + */ + public static final String IM_MAX_SERVER_ID = "im:max_server_id"; + /** + * 用户ID所连接的IM-server的ID + */ + public static final String IM_USER_SERVER_ID = "im:user:server_id"; + /** + * 系统消息队列 + */ + public static final String IM_MESSAGE_SYSTEM_QUEUE = "im:message:system"; + /** + * 私聊消息队列 + */ + public static final String IM_MESSAGE_PRIVATE_QUEUE = "im:message:private"; + /** + * 群聊消息队列 + */ + public static final String IM_MESSAGE_GROUP_QUEUE = "im:message:group"; + + /** + * 系统消息发送结果队列 + */ + public static final String IM_RESULT_SYSTEM_QUEUE = "im:result:system"; + /** + * 私聊消息发送结果队列 + */ + public static final String IM_RESULT_PRIVATE_QUEUE = "im:result:private"; + /** + * 群聊消息发送结果队列 + */ + public static final String IM_RESULT_GROUP_QUEUE = "im:result:group"; + +} diff --git a/im-common/src/main/java/com/bx/imcommon/enums/IMCmdType.java b/im-common/src/main/java/com/bx/imcommon/enums/IMCmdType.java new file mode 100644 index 0000000..74793ec --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/enums/IMCmdType.java @@ -0,0 +1,55 @@ +package com.bx.imcommon.enums; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum IMCmdType { + + /** + * 登录 + */ + LOGIN(0, "登录"), + /** + * 心跳 + */ + HEART_BEAT(1, "心跳"), + /** + * 强制下线 + */ + FORCE_LOGUT(2, "强制下线"), + /** + * 私聊消息 + */ + PRIVATE_MESSAGE(3, "私聊消息"), + /** + * 群发消息 + */ + GROUP_MESSAGE(4, "群发消息"), + /** + * 系统消息 + */ + SYSTEM_MESSAGE(5,"系统消息"); + + + private final Integer code; + + private final String desc; + + + public static IMCmdType fromCode(Integer code) { + for (IMCmdType typeEnum : values()) { + if (typeEnum.code.equals(code)) { + return typeEnum; + } + } + return null; + } + + + public Integer code() { + return this.code; + } + + +} + diff --git a/im-common/src/main/java/com/bx/imcommon/enums/IMListenerType.java b/im-common/src/main/java/com/bx/imcommon/enums/IMListenerType.java new file mode 100644 index 0000000..5dbb1b2 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/enums/IMListenerType.java @@ -0,0 +1,34 @@ +package com.bx.imcommon.enums; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum IMListenerType { + /** + * 全部消息 + */ + ALL(0, "全部消息"), + /** + * 私聊消息 + */ + PRIVATE_MESSAGE(1, "私聊消息"), + /** + * 群聊消息 + */ + GROUP_MESSAGE(2, "群聊消息"), + /** + * 系统消息 + */ + SYSTEM_MESSAGE(3, "群聊消息"); + + + + private final Integer code; + + private final String desc; + + public Integer code() { + return this.code; + } + +} diff --git a/im-common/src/main/java/com/bx/imcommon/enums/IMSendCode.java b/im-common/src/main/java/com/bx/imcommon/enums/IMSendCode.java new file mode 100644 index 0000000..1babdc9 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/enums/IMSendCode.java @@ -0,0 +1,32 @@ +package com.bx.imcommon.enums; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum IMSendCode { + + /** + * 发送成功 + */ + SUCCESS(0, "发送成功"), + /** + * 对方当前不在线 + */ + NOT_ONLINE(1, "对方当前不在线"), + /** + * 未找到对方的channel + */ + NOT_FIND_CHANNEL(2, "未找到对方的channel"), + /** + * 未知异常 + */ + UNKONW_ERROR(9999, "未知异常"); + + private final Integer code; + private final String desc; + + public Integer code() { + return this.code; + } + +} diff --git a/im-common/src/main/java/com/bx/imcommon/enums/IMTerminalType.java b/im-common/src/main/java/com/bx/imcommon/enums/IMTerminalType.java new file mode 100644 index 0000000..d5f5fac --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/enums/IMTerminalType.java @@ -0,0 +1,51 @@ +package com.bx.imcommon.enums; + +import lombok.AllArgsConstructor; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@AllArgsConstructor +public enum IMTerminalType { + + /** + * web + */ + WEB(0, "web"), + /** + * app + */ + APP(1, "app"), + /** + * pc + */ + PC(2, "pc"), + /** + * 未知 + */ + UNKNOW(-1, "未知"); + + private final Integer code; + + private final String desc; + + + public static IMTerminalType fromCode(Integer code) { + for (IMTerminalType typeEnum : values()) { + if (typeEnum.code.equals(code)) { + return typeEnum; + } + } + return null; + } + + public static List codes() { + return Arrays.stream(values()).map(IMTerminalType::code).collect(Collectors.toList()); + } + + public Integer code() { + return this.code; + } + +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMGroupMessage.java b/im-common/src/main/java/com/bx/imcommon/model/IMGroupMessage.java new file mode 100644 index 0000000..05b3333 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMGroupMessage.java @@ -0,0 +1,44 @@ +package com.bx.imcommon.model; + +import com.bx.imcommon.enums.IMTerminalType; +import lombok.Data; + +import java.util.LinkedList; +import java.util.List; + +@Data +public class IMGroupMessage { + + /** + * 发送方 + */ + private IMUserInfo sender; + + /** + * 接收者id列表(群成员列表,为空则不会推送) + */ + private List recvIds = new LinkedList<>(); + + + /** + * 接收者终端类型,默认全部 + */ + private List recvTerminals = IMTerminalType.codes(); + + /** + * 是否发送给自己的其他终端,默认true + */ + private Boolean sendToSelf = true; + + /** + * 是否需要回推发送结果,默认true + */ + private Boolean sendResult = true; + + /** + * 消息内容 + */ + private T data; + + +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMHeartbeatInfo.java b/im-common/src/main/java/com/bx/imcommon/model/IMHeartbeatInfo.java new file mode 100644 index 0000000..4b50271 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMHeartbeatInfo.java @@ -0,0 +1,7 @@ +package com.bx.imcommon.model; + +import lombok.Data; + +@Data +public class IMHeartbeatInfo { +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMLoginInfo.java b/im-common/src/main/java/com/bx/imcommon/model/IMLoginInfo.java new file mode 100644 index 0000000..afccfca --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMLoginInfo.java @@ -0,0 +1,9 @@ +package com.bx.imcommon.model; + +import lombok.Data; + +@Data +public class IMLoginInfo { + + private String accessToken; +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMPrivateMessage.java b/im-common/src/main/java/com/bx/imcommon/model/IMPrivateMessage.java new file mode 100644 index 0000000..e33b540 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMPrivateMessage.java @@ -0,0 +1,44 @@ +package com.bx.imcommon.model; + +import com.bx.imcommon.enums.IMTerminalType; +import lombok.Data; + +import java.util.List; + + +@Data +public class IMPrivateMessage { + + /** + * 发送方 + */ + private IMUserInfo sender; + + /** + * 接收者id + */ + private Long recvId; + + + /** + * 接收者终端类型,默认全部 + */ + private List recvTerminals = IMTerminalType.codes(); + + /** + * 是否同步消息给自己的其他终端,默认true + */ + private Boolean sendToSelf = true; + + /** + * 是否需要回推发送结果,默认true + */ + private Boolean sendResult = true; + + /** + * 消息内容 + */ + private T data; + + +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMRecvInfo.java b/im-common/src/main/java/com/bx/imcommon/model/IMRecvInfo.java new file mode 100644 index 0000000..52c74e6 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMRecvInfo.java @@ -0,0 +1,40 @@ +package com.bx.imcommon.model; + +import lombok.Data; + +import java.util.List; + +@Data +public class IMRecvInfo { + + /** + * 命令类型 IMCmdType + */ + private Integer cmd; + + /** + * 发送方 + */ + private IMUserInfo sender; + + /** + * 接收方用户列表 + */ + List receivers; + + /** + * 是否需要回调发送结果 + */ + private Boolean sendResult; + + /** + * 当前服务名(回调发送结果使用) + */ + private String serviceName; + /** + * 推送消息体 + */ + private Object data; +} + + diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMSendInfo.java b/im-common/src/main/java/com/bx/imcommon/model/IMSendInfo.java new file mode 100644 index 0000000..90d8522 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMSendInfo.java @@ -0,0 +1,18 @@ +package com.bx.imcommon.model; + +import lombok.Data; + +@Data +public class IMSendInfo { + + /** + * 命令 + */ + private Integer cmd; + + /** + * 推送消息体 + */ + private T data; + +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMSendResult.java b/im-common/src/main/java/com/bx/imcommon/model/IMSendResult.java new file mode 100644 index 0000000..9f4688c --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMSendResult.java @@ -0,0 +1,28 @@ +package com.bx.imcommon.model; + +import lombok.Data; + +@Data +public class IMSendResult { + + /** + * 发送方 + */ + private IMUserInfo sender; + + /** + * 接收方 + */ + private IMUserInfo receiver; + + /** + * 发送状态编码 IMSendCode + */ + private Integer code; + + /** + * 消息内容 + */ + private T data; + +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMSessionInfo.java b/im-common/src/main/java/com/bx/imcommon/model/IMSessionInfo.java new file mode 100644 index 0000000..ee7122e --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMSessionInfo.java @@ -0,0 +1,17 @@ +package com.bx.imcommon.model; + +import lombok.Data; + +@Data +public class IMSessionInfo { + /** + * 用户id + */ + private Long userId; + + /** + * 终端类型 + */ + private Integer terminal; + +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMSystemMessage.java b/im-common/src/main/java/com/bx/imcommon/model/IMSystemMessage.java new file mode 100644 index 0000000..2241cb5 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMSystemMessage.java @@ -0,0 +1,34 @@ +package com.bx.imcommon.model; + +import com.bx.imcommon.enums.IMTerminalType; +import lombok.Data; + +import java.util.LinkedList; +import java.util.List; + +@Data +public class IMSystemMessage { + + + /** + * 接收者id列表,为空表示向所有在线用户广播 + */ + private List recvIds = new LinkedList<>(); + + /** + * 接收者终端类型,默认全部 + */ + private List recvTerminals = IMTerminalType.codes(); + + /** + * 是否需要回推发送结果,默认true + */ + private Boolean sendResult = true; + + /** + * 消息内容 + */ + private T data; + + +} diff --git a/im-common/src/main/java/com/bx/imcommon/model/IMUserInfo.java b/im-common/src/main/java/com/bx/imcommon/model/IMUserInfo.java new file mode 100644 index 0000000..680cda6 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/model/IMUserInfo.java @@ -0,0 +1,28 @@ +package com.bx.imcommon.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: Blue + * @date: 2023-09-24 09:23:11 + * @version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IMUserInfo { + + /** + * 用户id + */ + private Long id; + + /** + * 用户终端类型 IMTerminalType + */ + private Integer terminal; + + +} diff --git a/im-common/src/main/java/com/bx/imcommon/mq/RedisMQConfig.java b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQConfig.java new file mode 100644 index 0000000..99c5074 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQConfig.java @@ -0,0 +1,32 @@ +package com.bx.imcommon.mq; + +import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.StringRedisSerializer; + + +@Configuration +public class RedisMQConfig { + + @Bean + public RedisMQTemplate redisMQTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisMQTemplate redisMQTemplate = new RedisMQTemplate(); + redisMQTemplate.setConnectionFactory(redisConnectionFactory); + // 设置值(value)的序列化采用FastJsonRedisSerializer + redisMQTemplate.setValueSerializer(fastJsonRedisSerializer()); + redisMQTemplate.setHashValueSerializer(fastJsonRedisSerializer()); + // 设置键(key)的序列化采用StringRedisSerializer。 + redisMQTemplate.setKeySerializer(new StringRedisSerializer()); + redisMQTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisMQTemplate.afterPropertiesSet(); + return redisMQTemplate; + } + + @Bean + public FastJsonRedisSerializer fastJsonRedisSerializer(){ + return new FastJsonRedisSerializer<>(Object.class); + } + +} diff --git a/im-common/src/main/java/com/bx/imcommon/mq/RedisMQConsumer.java b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQConsumer.java new file mode 100644 index 0000000..cd997fd --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQConsumer.java @@ -0,0 +1,35 @@ +package com.bx.imcommon.mq; + +import java.util.List; + +/** + * redis 队列消费者抽象类 + */ +public abstract class RedisMQConsumer { + + /** + * 消费消息回调(单条) + */ + public void onMessage(T data){} + + /** + * 消费消息回调(批量) + */ + public void onMessage(List datas){} + + /** + * 生成redis队列完整key + */ + public String generateKey(){ + // 默认队列名就是redis的key + RedisMQListener annotation = this.getClass().getAnnotation(RedisMQListener.class); + return annotation.queue(); + } + + /** + * 队列是否就绪,返回true才会开始消费 + */ + public Boolean isReady(){ + return true; + } +} diff --git a/im-common/src/main/java/com/bx/imcommon/mq/RedisMQListener.java b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQListener.java new file mode 100644 index 0000000..eb5fd1e --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQListener.java @@ -0,0 +1,29 @@ +package com.bx.imcommon.mq; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * redis 队列消费监听注解 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface RedisMQListener { + + /** + * 队列,也是redis的key + */ + String queue(); + + /** + * 一次性拉取的数据数量 + */ + int batchSize() default 1; + + /** + * 拉取间隔周期,单位:ms + */ + int period() default 100; +} diff --git a/im-common/src/main/java/com/bx/imcommon/mq/RedisMQPullTask.java b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQPullTask.java new file mode 100644 index 0000000..a2ecded --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQPullTask.java @@ -0,0 +1,116 @@ +package com.bx.imcommon.mq; + +import com.alibaba.fastjson.JSONObject; +import com.bx.imcommon.util.ThreadPoolExecutorFactory; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * reids 队列拉取定时任务 + * + * @author: Blue + * @date: 2024-07-15 + * @version: 1.0 + */ +@Slf4j +@Component +public class RedisMQPullTask implements CommandLineRunner { + + private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor(); + + @Autowired(required = false) + private List consumers = Collections.emptyList(); + + @Autowired + private RedisMQTemplate redisMQTemplate; + + @Override + public void run(String... args) { + consumers.forEach((consumer -> { + // 注解参数 + RedisMQListener annotation = consumer.getClass().getAnnotation(RedisMQListener.class); + String queue = annotation.queue(); + int batchSize = annotation.batchSize(); + int period = annotation.period(); + // 获取泛型类型 + Type superClass = consumer.getClass().getGenericSuperclass(); + Type type = ((ParameterizedType)superClass).getActualTypeArguments()[0]; + EXECUTOR.execute(new Runnable() { + @Override + public void run() { + List datas = new LinkedList<>(); + try { + if (consumer.isReady()) { + String key = consumer.generateKey(); + // 拉取一个批次的数据 + List objects = pullBatch(key, batchSize); + for (Object obj : objects) { + if (obj instanceof JSONObject) { + JSONObject jsonObject = (JSONObject)obj; + Object data = jsonObject.toJavaObject(type); + consumer.onMessage(data); + datas.add(data); + } + } + if (!datas.isEmpty()) { + consumer.onMessage(datas); + } + } + } catch (Exception e) { + log.error("数据消费异常,队列:{}", queue, e); + // 出现异常,10s后再重新尝试消费 + EXECUTOR.schedule(this, 10, TimeUnit.SECONDS); + return; + } + // 继续消费数据 + if (!EXECUTOR.isShutdown()) { + if (datas.size() < batchSize) { + // 数据已经消费完,等待下一个周期继续拉取 + EXECUTOR.schedule(this, period, TimeUnit.MILLISECONDS); + } else { + // 数据没有消费完,直接开启下一个消费周期 + EXECUTOR.execute(this); + } + } + } + }); + })); + } + + private List pullBatch(String key, Integer batchSize) { + List objects = new LinkedList<>(); + if (redisMQTemplate.isSupportBatchPull()) { + // 版本大于6.2,支持批量拉取 + objects = redisMQTemplate.opsForList().leftPop(key, batchSize); + } else { + // 版本小于6.2,只能逐条拉取 + Object obj = redisMQTemplate.opsForList().leftPop(key); + while (!Objects.isNull(obj) && objects.size() < batchSize) { + objects.add(obj); + obj = redisMQTemplate.opsForList().leftPop(key); + } + if (!Objects.isNull(obj)){ + objects.add(obj); + } + } + return objects; + } + + @PreDestroy + public void destory() { + log.info("消费线程停止..."); + ThreadPoolExecutorFactory.shutDown(); + } +} diff --git a/im-common/src/main/java/com/bx/imcommon/mq/RedisMQTemplate.java b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQTemplate.java new file mode 100644 index 0000000..dbb927b --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/mq/RedisMQTemplate.java @@ -0,0 +1,44 @@ +package com.bx.imcommon.mq; + +import org.apache.logging.log4j.util.Strings; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisConnectionUtils; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Properties; + +/** + * @author: Blue + * @date: 2024-07-16 + * @version: 1.0 + */ +public class RedisMQTemplate extends RedisTemplate { + + private String version = Strings.EMPTY; + + public String getVersion() { + if (version.isEmpty()) { + RedisConnection connection = RedisConnectionUtils.getConnection(getConnectionFactory()); + Properties properties = connection.info(); + version = properties.getProperty("redis_version"); + RedisConnectionUtils.releaseConnection(connection,getConnectionFactory()); + } + return version; + } + + /** + * 是否支持批量拉取,redis版本大于6.2支持批量拉取 + * @return + */ + Boolean isSupportBatchPull() { + String version = getVersion(); + String[] arr = version.split("\\."); + if (arr.length < 2) { + return false; + } + Integer firVersion = Integer.valueOf(arr[0]); + Integer secVersion = Integer.valueOf(arr[1]); + return firVersion > 6 || (firVersion == 6 && secVersion >= 2); + } + +} diff --git a/im-common/src/main/java/com/bx/imcommon/serializer/DateToLongSerializer.java b/im-common/src/main/java/com/bx/imcommon/serializer/DateToLongSerializer.java new file mode 100644 index 0000000..6aa3421 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/serializer/DateToLongSerializer.java @@ -0,0 +1,28 @@ +package com.bx.imcommon.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.WritableTypeId; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; + +import java.io.IOException; +import java.util.Date; + +public class DateToLongSerializer extends JsonSerializer { + + @Override + public void serialize(Date date, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeNumber(date.getTime()); + } + + @Override + public void serializeWithType(Date value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException { + WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen, + typeSer.typeId(value, JsonToken.VALUE_STRING)); + serialize(value, gen, serializers); + typeSer.writeTypeSuffix(gen, typeIdDef); + } +} diff --git a/im-common/src/main/java/com/bx/imcommon/util/CommaTextUtils.java b/im-common/src/main/java/com/bx/imcommon/util/CommaTextUtils.java new file mode 100644 index 0000000..f1c3354 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/util/CommaTextUtils.java @@ -0,0 +1,89 @@ +package com.bx.imcommon.util; + + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 逗号分格文本处理工具类 + * + * @author: blue + * @date: 2023-11-09 09:52:49 + * @version: 1.0 + */ +public class CommaTextUtils { + + /** + * 文本转列表 + * + * @param strText 文件 + * @return 列表 + */ + public static List asList(String strText) { + if (StrUtil.isEmpty(strText)) { + return new LinkedList<>(); + } + return new LinkedList<>(Arrays.asList(strText.split(","))); + + } + + /** + * 列表转字符串,并且自动清空、去重、排序 + * + * @param texts 列表 + * @return 文本 + */ + public static String asText(Collection texts) { + if (CollUtil.isEmpty(texts)) { + return StrUtil.EMPTY; + } + return texts.stream().map(text -> StrUtil.toString(text)).filter(StrUtil::isNotEmpty).distinct().sorted().collect(Collectors.joining(",")); + } + + /** + * 追加一个单词 + * + * @param strText 文本 + * @param word 单词 + * @return 文本 + */ + public static String appendWord(String strText, T word) { + List texts = asList(strText); + texts.add(StrUtil.toString(word)); + return asText(texts); + } + + /** + * 删除一个单词 + * + * @param strText 文本 + * @param word 单词 + * @return 文本 + */ + public static String removeWord(String strText, T word) { + List texts = asList(strText); + texts.remove(StrUtil.toString(word)); + return asText(texts); + } + + /** + * 合并 + * + * @param strText1 文本1 + * @param strText2 文本2 + * @return 文本 + */ + public static String merge(String strText1, String strText2) { + List texts = asList(strText1); + texts.addAll(asList(strText2)); + return asText(texts); + } + +} diff --git a/im-common/src/main/java/com/bx/imcommon/util/JwtUtil.java b/im-common/src/main/java/com/bx/imcommon/util/JwtUtil.java new file mode 100644 index 0000000..6645f28 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/util/JwtUtil.java @@ -0,0 +1,89 @@ +package com.bx.imcommon.util; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.JWTVerificationException; + +import java.util.Date; + +public final class JwtUtil { + + private JwtUtil() { + } + + /** + * 生成jwt字符串 JWT(json web token) + * + * @param userId 用户id + * @param info 用户细腻系 + * @param expireIn 过期时间 + * @param secret 秘钥 + * @return token + */ + public static String sign(Long userId, String info, long expireIn, String secret) { + try { + Date date = new Date(System.currentTimeMillis() + expireIn * 1000); + Algorithm algorithm = Algorithm.HMAC256(secret); + return JWT.create() + //将userId保存到token里面 + .withAudience(userId.toString()) + //存放自定义数据 + .withClaim("info", info) + //过期时间 + .withExpiresAt(date) + //token的密钥 + .sign(algorithm); + } catch (Exception e) { + return null; + } + } + + /** + * 根据token获取userId + * + * @param token 登录token + * @return 用户id + */ + public static Long getUserId(String token) { + try { + String userId = JWT.decode(token).getAudience().get(0); + return Long.parseLong(userId); + } catch (JWTDecodeException e) { + return null; + } + } + + /** + * 根据token获取用户数据 + * + * @param token 用户登录token + * @return 用户数据 + */ + public static String getInfo(String token) { + try { + return JWT.decode(token).getClaim("info").asString(); + } catch (JWTDecodeException e) { + return null; + } + } + + /** + * 校验token + * + * @param token 用户登录token + * @param secret 秘钥 + * @return true/false + */ + public static Boolean checkSign(String token, String secret) { + try { + Algorithm algorithm = Algorithm.HMAC256(secret); + JWTVerifier verifier = JWT.require(algorithm).build(); + verifier.verify(token); + return true; + } catch (JWTVerificationException e) { + return false; + } + } +} diff --git a/im-common/src/main/java/com/bx/imcommon/util/ThreadPoolExecutorFactory.java b/im-common/src/main/java/com/bx/imcommon/util/ThreadPoolExecutorFactory.java new file mode 100644 index 0000000..46d77b3 --- /dev/null +++ b/im-common/src/main/java/com/bx/imcommon/util/ThreadPoolExecutorFactory.java @@ -0,0 +1,87 @@ +package com.bx.imcommon.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 创建单例线程池 + * + * @author Andrews + * @date 2023/11/30 11:12 + */ +@Slf4j +public final class ThreadPoolExecutorFactory { + /** + * 机器的CPU核数:Runtime.getRuntime().availableProcessors() + * corePoolSize 池中所保存的线程数,包括空闲线程。 + * CPU 密集型:核心线程数 = CPU核数 + 1 + * IO 密集型:核心线程数 = CPU核数 * 2 + */ + private static final int CORE_POOL_SIZE = + Math.min(ThreadPoolExecutorFactory.MAX_IMUM_POOL_SIZE, Runtime.getRuntime().availableProcessors() * 2); + /** + * maximumPoolSize - 池中允许的最大线程数(采用LinkedBlockingQueue时没有作用)。 + */ + private static final int MAX_IMUM_POOL_SIZE = 100; + /** + * keepAliveTime -当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间,线程池维护线程所允许的空闲时间 + */ + private static final int KEEP_ALIVE_TIME = 1000; + /** + * 等待队列的大小。默认是无界的,性能损耗的关键 + */ + private static final int QUEUE_SIZE = 200; + + /** + * 线程池对象 + */ + private static volatile ScheduledThreadPoolExecutor threadPoolExecutor = null; + + /** + * 构造方法私有化 + */ + private ThreadPoolExecutorFactory() { + if (null == threadPoolExecutor) { + threadPoolExecutor = ThreadPoolExecutorFactory.getThreadPoolExecutor(); + } + } + + + /** + * 双检锁创建线程安全的单例 + */ + public static ScheduledThreadPoolExecutor getThreadPoolExecutor() { + if (null == threadPoolExecutor) { + synchronized (ThreadPoolExecutorFactory.class) { + if (null == threadPoolExecutor) { + threadPoolExecutor = new ScheduledThreadPoolExecutor( + //核心线程数 + CORE_POOL_SIZE, + //拒绝策略 + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + } + } + return threadPoolExecutor; + } + + /** + * 关闭线程池 + */ + public static void shutDown() { + if (threadPoolExecutor != null) { + threadPoolExecutor.shutdown(); + } + } + + public static void execute(Runnable runnable) { + if (runnable == null) { + return; + } + threadPoolExecutor.execute(runnable); + } + +} diff --git a/im-platform/pom.xml b/im-platform/pom.xml new file mode 100644 index 0000000..65ef8ab --- /dev/null +++ b/im-platform/pom.xml @@ -0,0 +1,111 @@ + + + + box-im + com.bx + 3.0.0 + + 4.0.0 + im-platform + + + + com.bx + im-client + 3.0.0 + + + org.springframework.boot + spring-boot-starter-web + + + com.baomidou + mybatis-plus-boot-starter + + + org.mybatis + mybatis-spring + + + + + org.mybatis + mybatis-spring + ${mybatis.spring.version} + + + com.alibaba + druid + + + com.mysql + mysql-connector-j + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.aspectj + aspectjweaver + + + org.springframework.security + spring-security-crypto + 6.3.1 + + + org.springframework.session + spring-session-data-redis + + + org.springframework.boot + spring-boot-starter-validation + + + + io.minio + minio + ${minio.version} + + + + net.coobird + thumbnailator + 0.4.8 + + + com.baomidou + mybatis-plus-generator + ${mybatisplus.version} + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/IMPlatformApp.java b/im-platform/src/main/java/com/bx/implatform/IMPlatformApp.java new file mode 100644 index 0000000..73dab5d --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/IMPlatformApp.java @@ -0,0 +1,23 @@ +package com.bx.implatform; + +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + + + +@Slf4j +@EnableAspectJAutoProxy(exposeProxy = true) +@ComponentScan(basePackages = {"com.bx"}) +@MapperScan(basePackages = {"com.bx.implatform.mapper"}) +@SpringBootApplication +public class IMPlatformApp { + + public static void main(String[] args) { + SpringApplication.run(IMPlatformApp.class, args); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/annotation/OnlineCheck.java b/im-platform/src/main/java/com/bx/implatform/annotation/OnlineCheck.java new file mode 100644 index 0000000..4baec54 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/annotation/OnlineCheck.java @@ -0,0 +1,15 @@ +package com.bx.implatform.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 在线校验,标注此注解的接口用户必须保持长连接,否则将抛异常 + */ +@Retention(RetentionPolicy.RUNTIME)//运行时生效 +@Target(ElementType.METHOD)//作用在方法上 +public @interface OnlineCheck { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java b/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java new file mode 100644 index 0000000..fca9a98 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/annotation/RedisLock.java @@ -0,0 +1,37 @@ +package com.bx.implatform.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * 分布式锁注解 + */ +@Retention(RetentionPolicy.RUNTIME)//运行时生效 +@Target(ElementType.METHOD)//作用在方法上 +public @interface RedisLock { + + /** + * key的前缀,prefixKey+key就是redis的key + */ + String prefixKey() ; + + /** + * spel 表达式 + */ + String key() default ""; + + /** + * 等待锁的时间,默认-1,不等待直接失败,redisson默认也是-1 + */ + int waitTime() default -1; + + /** + * 等待锁的时间单位,默认毫秒 + * + */ + TimeUnit unit() default TimeUnit.MILLISECONDS; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/annotation/RepeatSubmit.java b/im-platform/src/main/java/com/bx/implatform/annotation/RepeatSubmit.java new file mode 100644 index 0000000..7bd6704 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/annotation/RepeatSubmit.java @@ -0,0 +1,31 @@ +package com.bx.implatform.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * 防止表单重复提交注解 + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit { + + + /** + * 间隔时间,小于此时间视为重复提交 + */ + int interval() default 5000; + + /** + * 间隔时间单位 + */ + TimeUnit timeUnit() default TimeUnit.MILLISECONDS; + + /** + * 提示消息 + */ + String message() default "请勿提交重复请求"; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/aspect/OnlineCheckAspect.java b/im-platform/src/main/java/com/bx/implatform/aspect/OnlineCheckAspect.java new file mode 100644 index 0000000..a9be97e --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/aspect/OnlineCheckAspect.java @@ -0,0 +1,36 @@ +package com.bx.implatform.aspect; + +import com.bx.imclient.IMClient; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.session.UserSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +/** + * @author: blue + * @date: 2024-06-16 + * @version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class OnlineCheckAspect { + + private final IMClient imClient; + + @Around("@annotation(com.bx.implatform.annotation.OnlineCheck)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + UserSession session = SessionContext.getSession(); + if(!imClient.isOnline(session.getUserId())){ + throw new GlobalException("您当前的网络连接已断开,请稍后重试"); + } + return joinPoint.proceed(); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java b/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java new file mode 100644 index 0000000..2c0d26a --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/aspect/RedisLockAspect.java @@ -0,0 +1,85 @@ +package com.bx.implatform.aspect; + +import cn.hutool.core.util.StrUtil; +import com.bx.implatform.annotation.RedisLock; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.annotation.Order; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * @author: blue + * @date: 2024-06-09 + * @version: 1.0 + */ + +@Slf4j +@Aspect +@Order(0) +@Component +@RequiredArgsConstructor +public class RedisLockAspect { + + private ExpressionParser parser = new SpelExpressionParser(); + private DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + private final RedissonClient redissonClient; + + @Around("@annotation(com.bx.implatform.annotation.RedisLock)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + RedisLock annotation = method.getAnnotation(RedisLock.class); + // 解析表达式中的key + String key = parseKey(joinPoint); + String lockKey = StrUtil.join(":",annotation.prefixKey(),key); + // 上锁 + RLock lock = redissonClient.getLock(lockKey); + lock.lock(annotation.waitTime(),annotation.unit()); + try { + // 执行方法 + return joinPoint.proceed(); + }finally { + lock.unlock(); + } + } + + private String parseKey(ProceedingJoinPoint joinPoint){ + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + RedisLock annotation = method.getAnnotation(RedisLock.class); + String key = annotation.key(); + if(StrUtil.isEmpty(key)){ + return Strings.EMPTY; + } + // el解析需要的上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + // 参数名 + String[] params = parameterNameDiscoverer.getParameterNames(method); + if(Objects.isNull(params)){ + return key; + } + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < params.length; i++) { + context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去 + } + Expression expression = parser.parseExpression(key); + return expression.getValue(context, String.class); + } + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/aspect/RepeatSubmitAspect.java b/im-platform/src/main/java/com/bx/implatform/aspect/RepeatSubmitAspect.java new file mode 100644 index 0000000..cdd566f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/aspect/RepeatSubmitAspect.java @@ -0,0 +1,100 @@ +package com.bx.implatform.aspect; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import com.alibaba.fastjson.JSON; +import com.bx.implatform.annotation.RepeatSubmit; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.session.SessionContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Collection; +import java.util.Map; +import java.util.StringJoiner; + +/** + * @author: blue + * @date: 2024-12-08 + * @version: 1.0 + */ + +@Aspect +@Component +@AllArgsConstructor +public class RepeatSubmitAspect { + + private final RedisTemplate redisTemplate; + + @Before("@annotation(repeatSubmit)") + public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable { + // 如果注解不为0 则使用注解数值 + long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval()); + HttpServletRequest request = + ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); + String url = request.getRequestURL().toString(); + Long userId = SessionContext.getSession().getUserId(); + String reqParams = argsArrayToString(point.getArgs()); + String md5 = SecureUtil.md5(StrUtil.join(":", userId, url, reqParams)); + // 唯一标识 + String key = String.join(":",RedisKey.IM_REPEAT_SUBMIT,md5) ; + if(redisTemplate.hasKey(key)){ + throw new GlobalException(repeatSubmit.message()); + } + redisTemplate.opsForValue().set(key,1,repeatSubmit.interval(),repeatSubmit.timeUnit()); + } + + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray) { + StringJoiner params = new StringJoiner(" "); + if (ArrayUtil.isEmpty(paramsArray)) { + return params.toString(); + } + for (Object o : paramsArray) { + if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) { + params.add(JSON.toJSONString(o)); + } + } + return params.toString(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) { + Class clazz = o.getClass(); + if (clazz.isArray()) { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection collection = (Collection)o; + for (Object value : collection) { + return value instanceof MultipartFile; + } + } else if (Map.class.isAssignableFrom(clazz)) { + Map map = (Map)o; + for (Object value : map.values()) { + return value instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse; + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/ICEServer.java b/im-platform/src/main/java/com/bx/implatform/config/ICEServer.java new file mode 100644 index 0000000..4aa6113 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/ICEServer.java @@ -0,0 +1,11 @@ +package com.bx.implatform.config; + +import lombok.Data; + +@Data +public class ICEServer { + private String urls; + private String username; + private String credential; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java b/im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java new file mode 100644 index 0000000..ca661b8 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/MinIoClientConfig.java @@ -0,0 +1,19 @@ +package com.bx.implatform.config; + +import com.bx.implatform.config.props.MinioProperties; +import io.minio.MinioClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MinIoClientConfig { + + @Bean + public MinioClient minioClient(MinioProperties minioProps) { + // 注入minio 客户端 + return MinioClient.builder() + .endpoint(minioProps.getEndpoint()) + .credentials(minioProps.getAccessKey(), minioProps.getSecretKey()) + .build(); + } +} \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/config/MvcConfig.java b/im-platform/src/main/java/com/bx/implatform/config/MvcConfig.java new file mode 100644 index 0000000..2dee3c9 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/MvcConfig.java @@ -0,0 +1,34 @@ +package com.bx.implatform.config; + +import com.bx.implatform.interceptor.AuthInterceptor; +import com.bx.implatform.interceptor.XssInterceptor; +import lombok.AllArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@AllArgsConstructor +public class MvcConfig implements WebMvcConfigurer { + + private final AuthInterceptor authInterceptor; + private final XssInterceptor xssInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(xssInterceptor).addPathPatterns("/**").excludePathPatterns("/error"); + registry.addInterceptor(authInterceptor).addPathPatterns("/**") + .excludePathPatterns("/login", "/logout", "/register", "/refreshToken", "/swagger/**", "/v3/api-docs/**", + "/swagger-resources/**", "/swagger-ui.html", "/swagger-ui/**", "/doc.html"); + } + + @Bean + public PasswordEncoder passwordEncoder() { + // 使用BCrypt加密密码 + return new BCryptPasswordEncoder(); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/RedisConfig.java b/im-platform/src/main/java/com/bx/implatform/config/RedisConfig.java new file mode 100644 index 0000000..d74e656 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/RedisConfig.java @@ -0,0 +1,74 @@ +package com.bx.implatform.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.annotation.Resource; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class RedisConfig extends CachingConfigurerSupport { + + @Resource + private RedisConnectionFactory factory; + + @Primary + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + // 设置值(value)的序列化采用jackson2JsonRedisSerializer + redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); + redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); + // 设置键(key)的序列化采用StringRedisSerializer。 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } + + + + @Bean + public CacheManager cacheManager() { + // 设置redis缓存管理器 + RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(600)) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer())); + return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build(); + } + + + @Bean + public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer() { + ObjectMapper om = new ObjectMapper(); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + // 解决jackson2无法反序列化LocalDateTime的问题 + om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + om.registerModule(new JavaTimeModule()); + om.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略空值 + om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); + //忽略无效字段 + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return new Jackson2JsonRedisSerializer<>(om, Object.class); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/SwaggerConfig.java b/im-platform/src/main/java/com/bx/implatform/config/SwaggerConfig.java new file mode 100644 index 0000000..3b2ae2a --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/SwaggerConfig.java @@ -0,0 +1,38 @@ +package com.bx.implatform.config; + + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public GroupedOpenApi userApi() { + String[] paths = {"/**"}; + String[] packagedToMatch = {"com.bx"}; + return GroupedOpenApi.builder().group("IM-Platform") + .pathsToMatch(paths) + .packagesToScan(packagedToMatch).build(); + } + + @Bean + public OpenAPI customOpenAPI() { + Contact contact = new Contact(); + contact.setName("Blue"); + return new OpenAPI().info(new Info() + .title("盒子IM接口文档") + .description("盒子IM业务平台服务") + .contact(contact) + .version("3.0") + .termsOfService("https://www.boxim.online") + .license(new License().name("MIT") + .url("https://www.boxim.online"))); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/TaskSchedulerConfig.java b/im-platform/src/main/java/com/bx/implatform/config/TaskSchedulerConfig.java new file mode 100644 index 0000000..ed14e09 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/TaskSchedulerConfig.java @@ -0,0 +1,27 @@ +package com.bx.implatform.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +/** + * @author: Blue + * @date: 2024-09-01 + * @version: 1.0 + */ + +@EnableScheduling +@Configuration +public class TaskSchedulerConfig { + + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setPoolSize(10); // 设置线程池大小 + taskScheduler.setThreadNamePrefix("scheduled-task-"); + taskScheduler.initialize(); + return taskScheduler; + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/WebrtcConfig.java b/im-platform/src/main/java/com/bx/implatform/config/WebrtcConfig.java new file mode 100644 index 0000000..633d640 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/WebrtcConfig.java @@ -0,0 +1,19 @@ +package com.bx.implatform.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Component +@ConfigurationProperties(prefix = "webrtc") +public class WebrtcConfig { + + private Integer maxChannel = 9; + + private List iceServers = new ArrayList<>(); + +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/props/JwtProperties.java b/im-platform/src/main/java/com/bx/implatform/config/props/JwtProperties.java new file mode 100644 index 0000000..4642b8d --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/props/JwtProperties.java @@ -0,0 +1,22 @@ +package com.bx.implatform.config.props; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Data +@Component +public class JwtProperties { + + @Value("${jwt.accessToken.expireIn}") + private Integer accessTokenExpireIn; + + @Value("${jwt.accessToken.secret}") + private String accessTokenSecret; + + @Value("${jwt.refreshToken.expireIn}") + private Integer refreshTokenExpireIn; + + @Value("${jwt.refreshToken.secret}") + private String refreshTokenSecret; +} diff --git a/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java b/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java new file mode 100644 index 0000000..5a64db3 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/config/props/MinioProperties.java @@ -0,0 +1,34 @@ +package com.bx.implatform.config.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author: Blue + * @date: 2024-09-28 + * @version: 1.0 + */ +@Data +@Component +@ConfigurationProperties(prefix = "minio") +public class MinioProperties { + + private String endpoint; + + private String accessKey; + + private String secretKey; + + private String domain; + + private String bucketName; + + private String imagePath; + + private String filePath; + + private String videoPath; + + private Integer expireIn; +} diff --git a/im-platform/src/main/java/com/bx/implatform/contant/Constant.java b/im-platform/src/main/java/com/bx/implatform/contant/Constant.java new file mode 100644 index 0000000..d255ba3 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/contant/Constant.java @@ -0,0 +1,33 @@ +package com.bx.implatform.contant; + +public final class Constant { + + /** + * 系统用户id + */ + public static final Long SYS_USER_ID = 0L; + /** + * 最大图片上传大小 + */ + public static final Long MAX_IMAGE_SIZE = 20 * 1024 * 1024L; + /** + * 最大上传文件大小 + */ + public static final Long MAX_FILE_SIZE = 20 * 1024 * 1024L; + + /** + * 最大文件名长度 + */ + public static final Long MAX_FILE_NAME_LENGTH = 128L; + + /** + * 大群人数上限 + */ + public static final Long MAX_LARGE_GROUP_MEMBER = 3000L; + + /** + * 普通群人数上限 + */ + public static final Long MAX_NORMAL_GROUP_MEMBER = 500L; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java new file mode 100644 index 0000000..e19c8aa --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java @@ -0,0 +1,72 @@ +package com.bx.implatform.contant; + +public final class RedisKey { + + /** + * 用户状态 无值:空闲 1:正在忙 + */ + public static final String IM_USER_STATE = "im:user:state"; + /** + * 已读群聊消息位置(已读最大id) + */ + public static final String IM_GROUP_READED_POSITION = "im:readed:group:position"; + /** + * webrtc 单人通话 + */ + public static final String IM_WEBRTC_PRIVATE_SESSION = "im:webrtc:private:session"; + /** + * webrtc 群通话 + */ + public static final String IM_WEBRTC_GROUP_SESSION = "im:webrtc:group:session"; + + /** + * 用户被封禁消息队列 + */ + public static final String IM_QUEUE_USER_BANNED = "im:queue:user:banned"; + + /** + * 群聊被封禁消息队列 + */ + public static final String IM_QUEUE_GROUP_BANNED = "im:queue:group:banned"; + + /** + * 群聊解封消息队列 + */ + public static final String IM_QUEUE_GROUP_UNBAN = "im:queue:group:unban"; + + + /** + * 缓存是否好友:bool + */ + public static final String IM_CACHE_FRIEND = "im:cache:friend"; + /** + * 缓存群聊信息 + */ + public static final String IM_CACHE_GROUP = "im:cache:group"; + /** + * 缓存群聊成员id + */ + public static final String IM_CACHE_GROUP_MEMBER_ID = "im:cache:group_member_ids"; + + /** + * 重复提交 + */ + public static final String IM_REPEAT_SUBMIT = "im:repeat:submit"; + + /** + * 分布式锁-添加好友 + */ + public static final String IM_LOCK_FRIEND_ADD = "im:lock:friend:add"; + + /** + * 分布式锁-进入群聊 + */ + public static final String IM_LOCK_GROUP_ENTER = "im:lock:group:enter"; + + + /** + * 分布式锁-清理过期文件 + */ + public static final String IM_LOCK_FILE_TASK = "im:lock:task:file"; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/controller/FileController.java b/im-platform/src/main/java/com/bx/implatform/controller/FileController.java new file mode 100644 index 0000000..e9c0856 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/FileController.java @@ -0,0 +1,38 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.FileService; +import com.bx.implatform.vo.UploadImageVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@RestController +@Tag(name = "文件上传") +@RequiredArgsConstructor +public class FileController { + + private final FileService fileService; + + @Operation(summary = "上传图片", description = "上传图片,上传后返回原图和缩略图的url") + @PostMapping("/image/upload") + public Result uploadImage(@RequestParam("file") MultipartFile file, + @RequestParam(defaultValue = "true") Boolean isPermanent, @RequestParam(defaultValue = "50") Long thumbSize) { + return ResultUtils.success(fileService.uploadImage(file, isPermanent,thumbSize)); + } + + @Operation(summary = "上传文件", description = "上传文件,上传后返回文件url") + @PostMapping("/file/upload") + public Result uploadFile(@RequestParam("file") MultipartFile file) { + return ResultUtils.success(fileService.uploadFile(file), Strings.EMPTY); + } + +} \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/controller/FriendController.java b/im-platform/src/main/java/com/bx/implatform/controller/FriendController.java new file mode 100644 index 0000000..76e6e5f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/FriendController.java @@ -0,0 +1,64 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.annotation.RepeatSubmit; +import com.bx.implatform.dto.FriendDndDTO; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.FriendService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.vo.FriendVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "好友") +@RestController +@RequestMapping("/friend") +@RequiredArgsConstructor +public class FriendController { + + private final FriendService friendService; + + @GetMapping("/list") + @Operation(summary = "好友列表", description = "获取好友列表") + public Result> findFriends() { + return ResultUtils.success(friendService.findFriends()); + } + + + @RepeatSubmit + @PostMapping("/add") + @Operation(summary = "添加好友", description = "双方建立好友关系") + public Result addFriend(@NotNull(message = "好友id不可为空") @RequestParam Long friendId) { + friendService.addFriend(SessionContext.getSession().getUserId(),friendId); + return ResultUtils.success(); + } + + @GetMapping("/find/{friendId}") + @Operation(summary = "查找好友信息", description = "查找好友信息") + public Result findFriend(@NotNull(message = "好友id不可为空") @PathVariable Long friendId) { + return ResultUtils.success(friendService.findFriend(friendId)); + } + + + @DeleteMapping("/delete/{friendId}") + @Operation(summary = "删除好友", description = "解除好友关系") + public Result delFriend(@NotNull(message = "好友id不可为空") @PathVariable Long friendId) { + friendService.delFriend(friendId); + return ResultUtils.success(); + } + + @PutMapping("/dnd") + @Operation(summary = "开启/关闭免打扰状态", description = "开启/关闭免打扰状态") + public Result setFriendDnd(@Valid @RequestBody FriendDndDTO dto) { + friendService.setDnd(dto); + return ResultUtils.success(); + } + +} + diff --git a/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java b/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java new file mode 100644 index 0000000..af25a5a --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java @@ -0,0 +1,113 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.annotation.RepeatSubmit; +import com.bx.implatform.dto.GroupDndDTO; +import com.bx.implatform.dto.GroupInviteDTO; +import com.bx.implatform.dto.GroupMemberRemoveDTO; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.GroupService; +import com.bx.implatform.vo.GroupMemberVO; +import com.bx.implatform.vo.GroupVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "群聊") +@RestController +@RequestMapping("/group") +@RequiredArgsConstructor +public class GroupController { + + private final GroupService groupService; + + @RepeatSubmit + @Operation(summary = "创建群聊", description = "创建群聊") + @PostMapping("/create") + public Result createGroup(@Valid @RequestBody GroupVO vo) { + return ResultUtils.success(groupService.createGroup(vo)); + } + + @RepeatSubmit + @Operation(summary = "修改群聊信息", description = "修改群聊信息") + @PutMapping("/modify") + public Result modifyGroup(@Valid @RequestBody GroupVO vo) { + return ResultUtils.success(groupService.modifyGroup(vo)); + } + + @RepeatSubmit + @Operation(summary = "解散群聊", description = "解散群聊") + @DeleteMapping("/delete/{groupId}") + public Result deleteGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) { + groupService.deleteGroup(groupId); + return ResultUtils.success(); + } + + + @Operation(summary = "查询群聊", description = "查询单个群聊信息") + @GetMapping("/find/{groupId}") + public Result findGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) { + return ResultUtils.success(groupService.findById(groupId)); + } + + @Operation(summary = "查询群聊列表", description = "查询群聊列表") + @GetMapping("/list") + public Result> findGroups() { + return ResultUtils.success(groupService.findGroups()); + } + + @RepeatSubmit + @Operation(summary = "邀请进群", description = "邀请好友进群") + @PostMapping("/invite") + public Result invite(@Valid @RequestBody GroupInviteDTO dto) { + groupService.invite(dto); + return ResultUtils.success(); + } + + @Operation(summary = "查询群聊成员", description = "查询群聊成员") + @GetMapping("/members/{groupId}") + public Result> findGroupMembers( + @NotNull(message = "群聊id不能为空") @PathVariable Long groupId) { + return ResultUtils.success(groupService.findGroupMembers(groupId)); + } + + @RepeatSubmit + @Operation(summary = "将成员移出群聊", description = "将成员移出群聊") + @DeleteMapping("/members/remove") + public Result removeMembers(@Valid @RequestBody GroupMemberRemoveDTO dto) { + groupService.removeGroupMembers(dto); + return ResultUtils.success(); + } + + + @RepeatSubmit + @Operation(summary = "退出群聊", description = "退出群聊") + @DeleteMapping("/quit/{groupId}") + public Result quitGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId) { + groupService.quitGroup(groupId); + return ResultUtils.success(); + } + + @RepeatSubmit + @Operation(summary = "踢出群聊(已废弃)", description = "将用户踢出群聊") + @DeleteMapping("/kick/{groupId}") + public Result kickGroup(@NotNull(message = "群聊id不能为空") @PathVariable Long groupId, + @NotNull(message = "用户id不能为空") @RequestParam Long userId) { + groupService.kickGroup(groupId, userId); + return ResultUtils.success(); + } + + @Operation(summary = "开启/关闭免打扰", description = "开启/关闭免打扰") + @PutMapping("/dnd") + public Result setGroupDnd(@Valid @RequestBody GroupDndDTO dto) { + groupService.setDnd(dto); + return ResultUtils.success(); + } + +} + diff --git a/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java b/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java new file mode 100644 index 0000000..108bb68 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java @@ -0,0 +1,73 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.dto.GroupMessageDTO; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.GroupMessageService; +import com.bx.implatform.vo.GroupMessageVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "群聊消息") +@RestController +@RequestMapping("/message/group") +@RequiredArgsConstructor +public class GroupMessageController { + + private final GroupMessageService groupMessageService; + + @PostMapping("/send") + @Operation(summary = "发送群聊消息", description = "发送群聊消息") + public Result sendMessage(@Valid @RequestBody GroupMessageDTO vo) { + return ResultUtils.success(groupMessageService.sendMessage(vo)); + } + + @DeleteMapping("/recall/{id}") + @Operation(summary = "撤回消息", description = "撤回群聊消息") + public Result recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) { + return ResultUtils.success(groupMessageService.recallMessage(id)); + } + + @GetMapping("/pullOfflineMessage") + @Operation(summary = "拉取离线消息(已废弃)", description = "拉取离线消息,消息将通过webscoket异步推送") + public Result pullOfflineMessage(@RequestParam Long minId) { + groupMessageService.pullOfflineMessage(minId); + return ResultUtils.success(); + } + + @GetMapping(value = "/loadOfflineMessage") + @Operation(summary = "拉取离线消息", description = "拉取离线消息") + public Result> loadOfflineMessage(@RequestParam Long minId) { + return ResultUtils.success(groupMessageService.loadOffineMessage(minId)); + } + + + @PutMapping("/readed") + @Operation(summary = "消息已读", description = "将群聊中的消息状态置为已读") + public Result readedMessage(@RequestParam Long groupId) { + groupMessageService.readedMessage(groupId); + return ResultUtils.success(); + } + + @GetMapping("/findReadedUsers") + @Operation(summary = "获取已读用户id", description = "获取消息已读用户列表") + public Result> findReadedUsers(@RequestParam Long groupId, + @RequestParam Long messageId) { + return ResultUtils.success(groupMessageService.findReadedUsers(groupId, messageId)); + } + + @GetMapping("/history") + @Operation(summary = "查询聊天记录", description = "查询聊天记录") + public Result> recallMessage(@NotNull(message = "群聊id不能为空") @RequestParam Long groupId, + @NotNull(message = "页码不能为空") @RequestParam Long page, + @NotNull(message = "size不能为空") @RequestParam Long size) { + return ResultUtils.success(groupMessageService.findHistoryMessage(groupId, page, size)); + } +} + diff --git a/im-platform/src/main/java/com/bx/implatform/controller/LoginController.java b/im-platform/src/main/java/com/bx/implatform/controller/LoginController.java new file mode 100644 index 0000000..36a7a43 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/LoginController.java @@ -0,0 +1,51 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.dto.LoginDTO; +import com.bx.implatform.dto.ModifyPwdDTO; +import com.bx.implatform.dto.RegisterDTO; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.UserService; +import com.bx.implatform.vo.LoginVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "注册登录") +@RestController +@RequiredArgsConstructor +public class LoginController { + + private final UserService userService; + + @PostMapping("/login") + @Operation(summary = "用户登录", description = "用户登录") + public Result login(@Valid @RequestBody LoginDTO dto) { + LoginVO vo = userService.login(dto); + return ResultUtils.success(vo); + } + + @PutMapping("/refreshToken") + @Operation(summary = "刷新token", description = "用refreshtoken换取新的token") + public Result refreshToken(@RequestHeader("refreshToken") String refreshToken) { + LoginVO vo = userService.refreshToken(refreshToken); + return ResultUtils.success(vo); + } + + @PostMapping("/register") + @Operation(summary = "用户注册", description = "用户注册") + public Result register(@Valid @RequestBody RegisterDTO dto) { + userService.register(dto); + return ResultUtils.success(); + } + + @PutMapping("/modifyPwd") + @Operation(summary = "修改密码", description = "修改用户密码") + public Result modifyPassword(@Valid @RequestBody ModifyPwdDTO dto) { + userService.modifyPassword(dto); + return ResultUtils.success(); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java b/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java new file mode 100644 index 0000000..4fea71d --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java @@ -0,0 +1,74 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.dto.PrivateMessageDTO; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.PrivateMessageService; +import com.bx.implatform.vo.PrivateMessageVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "私聊消息") +@RestController +@RequestMapping("/message/private") +@RequiredArgsConstructor +public class PrivateMessageController { + + private final PrivateMessageService privateMessageService; + + @PostMapping("/send") + @Operation(summary = "发送消息", description = "发送私聊消息") + public Result sendMessage(@Valid @RequestBody PrivateMessageDTO vo) { + return ResultUtils.success(privateMessageService.sendMessage(vo)); + } + + @DeleteMapping("/recall/{id}") + @Operation(summary = "撤回消息", description = "撤回私聊消息") + public Result recallMessage(@NotNull(message = "消息id不能为空") @PathVariable Long id) { + return ResultUtils.success( privateMessageService.recallMessage(id)); + } + + @GetMapping("/pullOfflineMessage") + @Operation(summary = "拉取离线消息(已废弃)", description = "拉取离线消息,消息将通过webscoket异步推送") + public Result pullOfflineMessage(@RequestParam Long minId) { + privateMessageService.pullOfflineMessage(minId); + return ResultUtils.success(); + } + + @GetMapping(value = "/loadOfflineMessage") + @Operation(summary = "拉取离线消息", description = "拉取离线消息") + public Result> loadOfflineMessage(@RequestParam Long minId) { + return ResultUtils.success(privateMessageService.loadOfflineMessage(minId)); + } + + + @PutMapping("/readed") + @Operation(summary = "消息已读", description = "将会话中接收的消息状态置为已读") + public Result readedMessage(@RequestParam Long friendId) { + privateMessageService.readedMessage(friendId); + return ResultUtils.success(); + } + + @GetMapping("/maxReadedId") + @Operation(summary = "获取最大已读消息的id", description = "获取某个会话中已读消息的最大id") + public Result getMaxReadedId(@RequestParam Long friendId) { + return ResultUtils.success(privateMessageService.getMaxReadedId(friendId)); + } + + @GetMapping("/history") + @Operation(summary = "查询聊天记录", description = "查询聊天记录") + public Result> recallMessage( + @NotNull(message = "好友id不能为空") @RequestParam Long friendId, + @NotNull(message = "页码不能为空") @RequestParam Long page, + @NotNull(message = "size不能为空") @RequestParam Long size) { + return ResultUtils.success(privateMessageService.findHistoryMessage(friendId, page, size)); + } + +} + diff --git a/im-platform/src/main/java/com/bx/implatform/controller/SystemController.java b/im-platform/src/main/java/com/bx/implatform/controller/SystemController.java new file mode 100644 index 0000000..fd6de4d --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/SystemController.java @@ -0,0 +1,36 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.config.WebrtcConfig; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.vo.SystemConfigVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + + +/** + * @author: blue + * @date: 2024-06-10 + * @version: 1.0 + */ +@Tag(name = "系统相关") +@RestController +@RequestMapping("/system") +@RequiredArgsConstructor +public class SystemController { + + private final WebrtcConfig webrtcConfig; + + @GetMapping("/config") + @Operation(summary = "加载系统配置", description = "加载系统配置") + public Result loadConfig() { + return ResultUtils.success(new SystemConfigVO(webrtcConfig)); + } + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/controller/UserController.java b/im-platform/src/main/java/com/bx/implatform/controller/UserController.java new file mode 100644 index 0000000..9c5af89 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/UserController.java @@ -0,0 +1,65 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.entity.User; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.UserService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.session.UserSession; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.vo.OnlineTerminalVO; +import com.bx.implatform.vo.UserVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "用户相关") +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @GetMapping("/terminal/online") + @Operation(summary = "判断用户哪个终端在线", description = "返回在线的用户id的终端集合") + public Result> getOnlineTerminal(@NotNull @RequestParam("userIds") String userIds) { + return ResultUtils.success(userService.getOnlineTerminals(userIds)); + } + + + @GetMapping("/self") + @Operation(summary = "获取当前用户信息", description = "获取当前用户信息") + public Result findSelfInfo() { + UserSession session = SessionContext.getSession(); + User user = userService.getById(session.getUserId()); + UserVO userVO = BeanUtils.copyProperties(user, UserVO.class); + return ResultUtils.success(userVO); + } + + + @GetMapping("/find/{id}") + @Operation(summary = "查找用户", description = "根据id查找用户") + public Result findById(@NotNull @PathVariable("id") Long id) { + return ResultUtils.success(userService.findUserById(id)); + } + + @PutMapping("/update") + @Operation(summary = "修改用户信息", description = "修改用户信息,仅允许修改登录用户信息") + public Result update(@Valid @RequestBody UserVO vo) { + userService.update(vo); + return ResultUtils.success(); + } + + @GetMapping("/findByName") + @Operation(summary = "查找用户", description = "根据用户名或昵称查找用户") + public Result> findByName(@RequestParam String name) { + return ResultUtils.success(userService.findUserByName(name)); + } +} + diff --git a/im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java new file mode 100644 index 0000000..2ce13b4 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/controller/WebrtcPrivateController.java @@ -0,0 +1,76 @@ +package com.bx.implatform.controller; + +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import com.bx.implatform.service.WebrtcPrivateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "单人通话") +@RestController +@RequestMapping("/webrtc/private") +@RequiredArgsConstructor +public class WebrtcPrivateController { + + private final WebrtcPrivateService webrtcPrivateService; + + + @Operation(summary = "呼叫视频通话") + @PostMapping("/call") + public Result call(@RequestParam Long uid, @RequestParam(defaultValue = "video") String mode, + @RequestBody String offer) { + webrtcPrivateService.call(uid, mode, offer); + return ResultUtils.success(); + } + + @Operation(summary = "接受视频通话") + @PostMapping("/accept") + public Result accept(@RequestParam Long uid, @RequestBody String answer) { + webrtcPrivateService.accept(uid, answer); + return ResultUtils.success(); + } + + @Operation(summary = "拒绝视频通话") + @PostMapping("/reject") + public Result reject(@RequestParam Long uid) { + webrtcPrivateService.reject(uid); + return ResultUtils.success(); + } + + @Operation(summary = "取消呼叫") + @PostMapping("/cancel") + public Result cancel(@RequestParam Long uid) { + webrtcPrivateService.cancel(uid); + return ResultUtils.success(); + } + + @Operation(summary = "呼叫失败") + @PostMapping("/failed") + public Result failed(@RequestParam Long uid, @RequestParam String reason) { + webrtcPrivateService.failed(uid, reason); + return ResultUtils.success(); + } + + @Operation(summary = "挂断") + @PostMapping("/handup") + public Result handup(@RequestParam Long uid) { + webrtcPrivateService.handup(uid); + return ResultUtils.success(); + } + + @PostMapping("/candidate") + @Operation(summary = "同步candidate") + public Result candidate(@RequestParam Long uid, @RequestBody String candidate) { + webrtcPrivateService.candidate(uid, candidate); + return ResultUtils.success(); + } + + @Operation(summary = "心跳") + @PostMapping("/heartbeat") + public Result heartbeat(@RequestParam Long uid) { + webrtcPrivateService.heartbeat(uid); + return ResultUtils.success(); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/FriendDndDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/FriendDndDTO.java new file mode 100644 index 0000000..12d1721 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/FriendDndDTO.java @@ -0,0 +1,23 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author Blue + * @version 1.0 + */ +@Data +@Schema(description = "好友免打扰") +public class FriendDndDTO { + + @NotNull(message = "好友id不可为空") + @Schema(description = "好友用户id") + private Long friendId; + + @NotNull(message = "消息免打扰状态不可为空") + @Schema(description = "消息免打扰状态") + private Boolean isDnd; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupBanDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupBanDTO.java new file mode 100644 index 0000000..cbe673d --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupBanDTO.java @@ -0,0 +1,21 @@ +package com.bx.implatform.dto; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-07-14 + * @version: 1.0 + */ +@Data +@Schema(description = "群组封禁") +public class GroupBanDTO { + + @Schema(description = "群组id") + private Long id; + + @Schema(description = "封禁原因") + private String reason; +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupDndDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupDndDTO.java new file mode 100644 index 0000000..1358b82 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupDndDTO.java @@ -0,0 +1,23 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author Blue + * @version 1.0 + * @date 2025-02-23 + */ +@Data +@Schema(description = "群聊免打扰") +public class GroupDndDTO { + + @NotNull(message = "群id不可为空") + @Schema(description = "群组id") + private Long groupId; + + @NotNull(message = "免打扰状态不可为空") + @Schema(description = "免打扰状态") + private Boolean isDnd; +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupInviteDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupInviteDTO.java new file mode 100644 index 0000000..efeb5fb --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupInviteDTO.java @@ -0,0 +1,23 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "邀请好友进群请求DTO") +public class GroupInviteDTO { + + @NotNull(message = "群id不可为空") + @Schema(description = "群id") + private Long groupId; + + @Size(max = 50, message = "一次最多只能邀请50位用户") + @NotEmpty(message = "群id不可为空") + @Schema(description = "好友id列表不可为空") + private List friendIds; +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupMemberRemoveDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupMemberRemoveDTO.java new file mode 100644 index 0000000..5d98e71 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupMemberRemoveDTO.java @@ -0,0 +1,28 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +/** + * @author Blue + * @version 1.0 + * @date 2025-02-23 + */ +@Data +@Schema(description = "移除群聊成员") +public class GroupMemberRemoveDTO { + + @NotNull(message = "群id不可为空") + @Schema(description = "群组id") + private Long groupId; + + @Size(max = 50, message = "一次最多只能选择50位用户") + @NotEmpty(message = "成员用户id不可为空") + @Schema(description = "成员用户id") + private List userIds; +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java new file mode 100644 index 0000000..8b4f257 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupMessageDTO.java @@ -0,0 +1,38 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.util.List; + +@Data +@Schema(description = "群聊消息DTO") +public class GroupMessageDTO { + + @Schema(description = "临时id") + private String tmpId; + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + + @Length(max = 1024, message = "发送内容长度不得大于1024") + @NotEmpty(message = "发送内容不可为空") + @Schema(description = "发送内容") + private String content; + + @NotNull(message = "消息类型不可为空") + @Schema(description = "消息类型 0:文字 1:图片 2:文件 3:语音 4:视频") + private Integer type; + + @Schema(description = "是否回执消息") + private Boolean receipt = false; + + @Size(max = 20, message = "一次最多只能@20个小伙伴哦") + @Schema(description = "被@用户列表") + private List atUserIds; +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupUnbanDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupUnbanDTO.java new file mode 100644 index 0000000..2ccffa6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupUnbanDTO.java @@ -0,0 +1,18 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-07-14 + * @version: 1.0 + */ +@Data +@Schema(description = "群组解锁") +public class GroupUnbanDTO { + + @Schema(description = "群组id") + private Long id; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java new file mode 100644 index 0000000..30b240e --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java @@ -0,0 +1,28 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "用户登录DTO") +public class LoginDTO { + + @Max(value = 2, message = "登录终端类型取值范围:0,2") + @Min(value = 0, message = "登录终端类型取值范围:0,2") + @NotNull(message = "登录终端类型不可为空") + @Schema(description = "登录终端 0:web 1:app 2:pc") + private Integer terminal; + + @NotEmpty(message = "用户名不可为空") + @Schema(description = "用户名") + private String userName; + + @NotEmpty(message = "用户密码不可为空") + @Schema(description = "用户密码") + private String password; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/ModifyPwdDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/ModifyPwdDTO.java new file mode 100644 index 0000000..c733493 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/ModifyPwdDTO.java @@ -0,0 +1,19 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Data +@Schema(description = "修改密码DTO") +public class ModifyPwdDTO { + + @NotEmpty(message = "旧用户密码不可为空") + @Schema(description = "旧用户密码") + private String oldPassword; + + @NotEmpty(message = "新用户密码不可为空") + @Schema(description = "新用户密码") + private String newPassword; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java new file mode 100644 index 0000000..639325c --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java @@ -0,0 +1,29 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Data +@Schema(description = "私聊消息DTO") +public class PrivateMessageDTO { + + @Schema(description = "临时id") + private String tmpId; + + @NotNull(message = "接收用户id不可为空") + @Schema(description = "接收用户id") + private Long recvId; + + @Length(max = 1024, message = "内容长度不得大于1024") + @NotEmpty(message = "发送内容不可为空") + @Schema(description = "发送内容") + private String content; + + @NotNull(message = "消息类型不可为空") + @Schema(description = "消息类型 0:文字 1:图片 2:文件 3:语音 4:视频") + private Integer type; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/RegisterDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/RegisterDTO.java new file mode 100644 index 0000000..37a0c51 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/RegisterDTO.java @@ -0,0 +1,27 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Data +@Schema(description = "用户注册DTO") +public class RegisterDTO { + + @Length(max = 20, message = "用户名不能大于20字符") + @NotEmpty(message = "用户名不可为空") + @Schema(description = "用户名") + private String userName; + + @Length(min = 5, max = 20, message = "密码长度必须在5-20个字符之间") + @NotEmpty(message = "用户密码不可为空") + @Schema(description = "用户密码") + private String password; + + @Length(max = 20, message = "昵称不能大于20字符") + @Schema(description = "用户昵称") + private String nickName; + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/UserBanDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/UserBanDTO.java new file mode 100644 index 0000000..f8953c6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/UserBanDTO.java @@ -0,0 +1,21 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-07-14 + * @version: 1.0 + */ +@Data +@Schema(description = "用户锁定DTO") +public class UserBanDTO { + + @Schema(description = "用户id") + private Long id; + + @Schema(description = "锁定原因") + private String reason; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupAnswerDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupAnswerDTO.java new file mode 100644 index 0000000..6e96bc0 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupAnswerDTO.java @@ -0,0 +1,29 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@Schema(description = "回复用户连接请求DTO") +public class WebrtcGroupAnswerDTO { + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + + @NotNull(message = "用户id不可为空") + @Schema(description = "用户id,代表回复谁的连接请求") + private Long userId; + + @NotEmpty(message = "anwer不可为空") + @Schema(description = "用户本地anwer信息") + private String answer; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupCandidateDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupCandidateDTO.java new file mode 100644 index 0000000..69d054e --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupCandidateDTO.java @@ -0,0 +1,29 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@Schema(description = "发起群视频通话DTO") +public class WebrtcGroupCandidateDTO { + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + + @NotNull(message = "用户id不可为空") + @Schema(description = "用户id") + private Long userId; + + @NotEmpty(message = "candidate信息不可为空") + @Schema(description = "candidate信息") + private String candidate; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java new file mode 100644 index 0000000..b7e2073 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupDeviceDTO.java @@ -0,0 +1,26 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@Schema(description = "用户设备操作DTO") +public class WebrtcGroupDeviceDTO { + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + + @Schema(description = "是否开启摄像头") + private Boolean isCamera; + + @Schema(description = "是否开启麦克风") + private Boolean isMicroPhone; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupFailedDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupFailedDTO.java new file mode 100644 index 0000000..fa2d5ba --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupFailedDTO.java @@ -0,0 +1,23 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@Schema(description = "用户通话失败DTO") +public class WebrtcGroupFailedDTO { + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + + @Schema(description = "失败原因") + private String reason; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupInviteDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupInviteDTO.java new file mode 100644 index 0000000..1c0f894 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupInviteDTO.java @@ -0,0 +1,28 @@ +package com.bx.implatform.dto; + +import com.bx.implatform.session.WebrtcUserInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@Schema(description = "邀请用户进入群视频通话DTO") +public class WebrtcGroupInviteDTO { + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + + @NotEmpty(message = "参与用户信息不可为空") + @Schema(description = "参与用户信息") + private List userInfos; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupJoinDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupJoinDTO.java new file mode 100644 index 0000000..b59adb0 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupJoinDTO.java @@ -0,0 +1,20 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@Schema(description = "进入群视频通话DTO") +public class WebrtcGroupJoinDTO { + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupOfferDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupOfferDTO.java new file mode 100644 index 0000000..dbe0048 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupOfferDTO.java @@ -0,0 +1,29 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@Schema(description = "回复用户连接请求DTO") +public class WebrtcGroupOfferDTO { + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + + @NotNull(message = "用户id不可为空") + @Schema(description = "用户id,代表回复谁的连接请求") + private Long userId; + + @NotEmpty(message = "offer不可为空") + @Schema(description = "用户offer信息") + private String offer; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupSetupDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupSetupDTO.java new file mode 100644 index 0000000..724ceec --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/WebrtcGroupSetupDTO.java @@ -0,0 +1,28 @@ +package com.bx.implatform.dto; + +import com.bx.implatform.session.WebrtcUserInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +@Schema(description = "发起群视频通话DTO") +public class WebrtcGroupSetupDTO { + + @NotNull(message = "群聊id不可为空") + @Schema(description = "群聊id") + private Long groupId; + + @NotEmpty(message = "参与用户信息不可为空") + @Schema(description = "参与用户信息") + private List userInfos; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java b/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java new file mode 100644 index 0000000..9a847f5 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/FileInfo.java @@ -0,0 +1,67 @@ +package com.bx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * @author Blue + * @version 1.0 + */ +@Data +@TableName("im_file_info") +public class FileInfo { + + /** + * 文件ID + */ + @TableId + private Long id; + + /** + * 文件名 + */ + private String fileName; + + /** + * 原始文件存储路径 + */ + private String filePath; + + /** + * 压缩文件存储路径 + */ + private String compressedPath; + + /** + * 封面文件路径 + */ + private String coverPath; + + /** + * 原始文件大小(字节) + */ + private Long fileSize; + + /** + * 上传时间 + */ + private Date uploadTime; + + /** + * 文件类型,枚举: FileType + */ + private Integer fileType; + + /** + * 是否永久存储 + */ + private Boolean isPermanent; + + /** + * 文件MD5哈希值 + */ + private String md5; +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/Friend.java b/im-platform/src/main/java/com/bx/implatform/entity/Friend.java new file mode 100644 index 0000000..e3f96f7 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/Friend.java @@ -0,0 +1,63 @@ +package com.bx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + *

+ * 好友 + *

+ * + * @author blue + * @since 2022-10-22 + */ +@Data +@TableName("im_friend") +public class Friend{ + + + /** + * id + */ + @TableId + private Long id; + + /** + * 用户id + */ + private Long userId; + + /** + * 好友id + */ + private Long friendId; + + /** + * 用户昵称 + */ + private String friendNickName; + + /** + * 用户头像 + */ + private String friendHeadImage; + + /** + * 是否开启免打扰 + */ + private Boolean isDnd; + + /** + * 是否已删除 + */ + private Boolean deleted; + + /** + * 创建时间 + */ + private Date createdTime; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/Group.java b/im-platform/src/main/java/com/bx/implatform/entity/Group.java new file mode 100644 index 0000000..06ba349 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/Group.java @@ -0,0 +1,72 @@ +package com.bx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + * 群 + * + * @author blue + * @since 2022-10-31 + */ +@Data +@TableName("im_group") +public class Group { + + /** + * id + */ + @TableId + private Long id; + + /** + * 群名字 + */ + private String name; + + /** + * 群主id + */ + private Long ownerId; + + /** + * 群头像 + */ + private String headImage; + + /** + * 群头像缩略图 + */ + private String headImageThumb; + + /** + * 群公告 + */ + private String notice; + + /** + * 是否被封禁 + */ + private Boolean isBanned; + + /** + * 被封禁原因 + */ + private String reason; + + /** + * 创建时间 + */ + private Date createdTime; + + /** + * 是否已删除 + */ + private Boolean dissolve; + + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java b/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java new file mode 100644 index 0000000..8aec5cc --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java @@ -0,0 +1,86 @@ +package com.bx.implatform.entity; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + *

+ * 群成员 + *

+ * + * @author blue + * @since 2022-10-31 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("im_group_member") +public class GroupMember extends Model { + + /** + * id + */ + @TableId + private Long id; + + /** + * 群id + */ + private Long groupId; + + /** + * 用户id + */ + private Long userId; + + /** + * 用户昵称 + */ + private String userNickName; + + /** + * 显示昵称备注 + */ + private String remarkNickName; + + /** + * 用户头像 + */ + private String headImage; + + /** + * 显示群名备注 + */ + private String remarkGroupName; + + /** + * 是否免打扰 + */ + private Boolean isDnd; + + + /** + * 是否已退出 + */ + private Boolean quit; + + /** + * 创建时间 + */ + private Date createdTime; + + /** + * 退出时间 + */ + private Date quitTime; + + public String getShowNickName() { + return StrUtil.blankToDefault(remarkNickName, userNickName); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java b/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java new file mode 100644 index 0000000..77a038a --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/GroupMessage.java @@ -0,0 +1,88 @@ +package com.bx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + *

+ * 群消息 + *

+ * + * @author blue + * @since 2022-10-31 + */ +@Data + +@TableName("im_group_message") +public class GroupMessage { + + /** + * id + */ + @TableId + private Long id; + + /** + * 临时id,由前端生成 + * 作用:如果用户正在发送消息时掉线了,可以通过此字段获取该消息的实际发送状态 + */ + private String tmpId; + + /** + * 群id + */ + private Long groupId; + + /** + * 发送用户id + */ + private Long sendId; + + /** + * 发送用户昵称 + */ + private String sendNickName; + + /** + * 接受用户id,为空表示全体发送 + */ + private String recvIds; + + /** + * @用户列表 + */ + private String atUserIds; + /** + * 发送内容 + */ + private String content; + + /** + * 消息类型 MessageType + */ + private Integer type; + + /** + * 是否回执消息 + */ + private Boolean receipt; + + /** + * 回执消息是否完成 + */ + private Boolean receiptOk; + + /** + * 状态 MessageStatus + */ + private Integer status; + + /** + * 发送时间 + */ + private Date sendTime; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java b/im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java new file mode 100644 index 0000000..8c1dd52 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/PrivateMessage.java @@ -0,0 +1,63 @@ +package com.bx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + *

+ * + *

+ * + * @author blue + * @since 2022-10-01 + */ +@Data +@TableName("im_private_message") +public class PrivateMessage { + + /** + * id + */ + private Long id; + + /** + * 临时id,由前端生成 + * 作用:如果用户正在发送消息时掉线了,可以通过此字段获取该消息的实际发送状态 + */ + private String tmpId; + + /** + * 发送用户id + */ + private Long sendId; + + /** + * 接收用户id + */ + private Long recvId; + + /** + * 发送内容 + */ + private String content; + + /** + * 消息类型 MessageType + */ + private Integer type; + + /** + * 状态 + */ + private Integer status; + + + /** + * 发送时间 + */ + private Date sendTime; + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/SensitiveWord.java b/im-platform/src/main/java/com/bx/implatform/entity/SensitiveWord.java new file mode 100644 index 0000000..8ced7c7 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/SensitiveWord.java @@ -0,0 +1,50 @@ +package com.bx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 敏感词 + * + * @author Blue + * @since 1.0.0 2024-07-20 + */ + +@Data +@TableName("im_sensitive_word") +public class SensitiveWord { + /** + * id + */ + @TableId + private Long id; + + /** + * 敏感词内容 + */ + private String content; + + /** + * 是否启用 + */ + private Boolean enabled; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private Long creator; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + +} \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/entity/User.java b/im-platform/src/main/java/com/bx/implatform/entity/User.java new file mode 100644 index 0000000..ca292e6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/entity/User.java @@ -0,0 +1,87 @@ +package com.bx.implatform.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.Date; + +/** + *

+ * 用户 + *

+ * + * @author blue + * @since 2022-10-01 + */ +@Data +@TableName("im_user") +public class User { + + /** + * id + */ + @TableId + private Long id; + + /** + * 用户名 + */ + private String userName; + + /** + * 用户昵称 + */ + private String nickName; + + /** + * 用户头像 + */ + private String headImage; + + /** + * 用户头像缩略图 + */ + private String headImageThumb; + + /** + * 密码(明文) + */ + private String password; + + /** + * 性别 0:男 1::女 + */ + private Integer sex; + + /** + * 个性签名 + */ + private String signature; + + /** + * 账号是否被封禁 + */ + private Boolean isBanned; + + /** + * 账号被封禁原因 + */ + private String reason; + + /** + * 最后登录时间 + */ + private Date lastLoginTime; + + /** + * 创建时间(注册时间) + */ + private Date createdTime; + + /** + * 账号类型 1:普通用户 2:wx小程序审核账户 + */ + private Integer type; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/enums/FileType.java b/im-platform/src/main/java/com/bx/implatform/enums/FileType.java new file mode 100644 index 0000000..2237f48 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/enums/FileType.java @@ -0,0 +1,32 @@ +package com.bx.implatform.enums; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum FileType { + + /** + * 文件 + */ + FILE(0, "文件"), + /** + * 图片 + */ + IMAGE(1, "图片"), + /** + * 视频 + */ + VIDEO(2, "视频"); + + private final Integer code; + + private final String desc; + + + public Integer code() { + return this.code; + } + + +} + diff --git a/im-platform/src/main/java/com/bx/implatform/enums/MessageStatus.java b/im-platform/src/main/java/com/bx/implatform/enums/MessageStatus.java new file mode 100644 index 0000000..73197c1 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/enums/MessageStatus.java @@ -0,0 +1,33 @@ +package com.bx.implatform.enums; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum MessageStatus { + + /** + * 等待推送(未送达) + */ + PENDING(0, "等待推送"), + /** + * 已送达(未读) + */ + DELIVERED(1, "已送达"), + /** + * 撤回 + */ + RECALL(2, "撤回"), + /** + * 已读 + */ + READED(3, "已读"); + + private final Integer code; + + private final String desc; + + + public Integer code() { + return this.code; + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java b/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java new file mode 100644 index 0000000..b044a44 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java @@ -0,0 +1,74 @@ +package com.bx.implatform.enums; + +import lombok.AllArgsConstructor; + + +/** + * 0-9: 真正的消息,需要存储到数据库 + * 10-19: 状态类消息: 撤回、已读、回执 + * 20-29: 提示类消息: 在会话中间显示的提示 + * 30-39: UI交互类消息: 显示加载状态等 + * 40-49: 操作交互类消息: 语音通话、视频通话消息等 + * 50-60: 后台操作类消息: 用户封禁、群组封禁等 + * 80-89: 好友变化消息 + * 90-99: 群聊变化消息 + * 100-199: 单人语音通话rtc信令 + * 200-299: 多人语音通话rtc信令 + * + */ +@AllArgsConstructor +public enum MessageType { + + TEXT(0, "文字消息"), + IMAGE(1, "图片消息"), + FILE(2, "文件消息"), + AUDIO(3, "语音消息"), + VIDEO(4, "视频消息"), + RECALL(10, "撤回"), + READED(11, "已读"), + RECEIPT(12, "消息已读回执"), + TIP_TIME(20,"时间提示"), + TIP_TEXT(21,"文字提示"), + LOADING(30,"加载中标记"), + ACT_RT_VOICE(40,"语音通话"), + ACT_RT_VIDEO(41,"视频通话"), + USER_BANNED(50,"用户封禁"), + GROUP_BANNED(51,"群聊封禁"), + GROUP_UNBAN(52,"群聊解封"), + FRIEND_NEW(80, "新增好友"), + FRIEND_DEL(81, "删除好友"), + FRIEND_DND(82, "好友免打扰"), + GROUP_NEW(90, "新增群聊"), + GROUP_DEL(91, "删除群聊"), + GROUP_DND(92, "群聊免打扰"), + RTC_CALL_VOICE(100, "语音呼叫"), + RTC_CALL_VIDEO(101, "视频呼叫"), + RTC_ACCEPT(102, "接受"), + RTC_REJECT(103, "拒绝"), + RTC_CANCEL(104, "取消呼叫"), + RTC_FAILED(105, "呼叫失败"), + RTC_HANDUP(106, "挂断"), + RTC_CANDIDATE(107, "同步candidate"), + RTC_GROUP_SETUP(200,"发起群视频通话"), + RTC_GROUP_ACCEPT(201,"接受通话呼叫"), + RTC_GROUP_REJECT(202,"拒绝通话呼叫"), + RTC_GROUP_FAILED(203,"拒绝通话呼叫"), + RTC_GROUP_CANCEL(204,"取消通话呼叫"), + RTC_GROUP_QUIT(205,"退出通话"), + RTC_GROUP_INVITE(206,"邀请进入通话"), + RTC_GROUP_JOIN(207,"主动进入通话"), + RTC_GROUP_OFFER(208,"推送offer信息"), + RTC_GROUP_ANSWER(209,"推送answer信息"), + RTC_GROUP_CANDIDATE(210,"同步candidate"), + RTC_GROUP_DEVICE(211,"设备操作"), + ; + + private final Integer code; + + private final String desc; + + + public Integer code() { + return this.code; + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/enums/ResultCode.java b/im-platform/src/main/java/com/bx/implatform/enums/ResultCode.java new file mode 100644 index 0000000..fd1a871 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/enums/ResultCode.java @@ -0,0 +1,50 @@ +package com.bx.implatform.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 响应码枚举 + * + * @author Blue + * @date 2020/10/19 + **/ +@Getter +@AllArgsConstructor +public enum ResultCode { + /** + * 成功 + */ + SUCCESS(200, "成功"), + /** + * 未登录 + */ + NO_LOGIN(400, "未登录"), + /** + * token无效或已过期 + */ + INVALID_TOKEN(401, "token无效或已过期"), + /** + * 系统繁忙,请稍后再试 + */ + PROGRAM_ERROR(500, "系统繁忙,请稍后再试"), + /** + * 密码不正确 + */ + PASSWOR_ERROR(10001, "密码不正确"), + /** + * 该用户名已注册 + */ + USERNAME_ALREADY_REGISTER(10003, "该用户名已注册"), + /** + * 请不要输入非法内容 + */ + XSS_PARAM_ERROR(10004, "请不要输入非法内容"); + + + private final int code; + private final String msg; + + +} + diff --git a/im-platform/src/main/java/com/bx/implatform/enums/WebrtcMode.java b/im-platform/src/main/java/com/bx/implatform/enums/WebrtcMode.java new file mode 100644 index 0000000..ebaf099 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/enums/WebrtcMode.java @@ -0,0 +1,27 @@ +package com.bx.implatform.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Getter +@AllArgsConstructor +public enum WebrtcMode { + + /** + * 视频通话 + */ + VIDEO( "video"), + + /** + * 语音通话 + */ + VOICE( "voice"); + + private final String value; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/exception/GlobalException.java b/im-platform/src/main/java/com/bx/implatform/exception/GlobalException.java new file mode 100644 index 0000000..049498f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/exception/GlobalException.java @@ -0,0 +1,34 @@ +package com.bx.implatform.exception; + +import com.bx.implatform.enums.ResultCode; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class GlobalException extends RuntimeException implements Serializable { + private static final long serialVersionUID = 8134030011662574394L; + private Integer code; + private String message; + + public GlobalException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public GlobalException(ResultCode resultCode, String message) { + this.code = resultCode.getCode(); + this.message = message; + } + + public GlobalException(ResultCode resultCode) { + this.code = resultCode.getCode(); + this.message = resultCode.getMsg(); + } + + public GlobalException(String message) { + this.code = ResultCode.PROGRAM_ERROR.getCode(); + this.message = message; + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/exception/GlobalExceptionHandler.java b/im-platform/src/main/java/com/bx/implatform/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f703960 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/exception/GlobalExceptionHandler.java @@ -0,0 +1,100 @@ +package com.bx.implatform.exception; + +import cn.hutool.json.JSONException; +import com.bx.implatform.enums.ResultCode; +import com.bx.implatform.result.Result; +import com.bx.implatform.result.ResultUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.lang.reflect.UndeclaredThrowableException; +import java.time.format.DateTimeParseException; +import java.util.List; + +@ControllerAdvice +@ResponseBody +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(value = Exception.class) + public Result handleException(Exception e) { + if (e instanceof GlobalException) { + GlobalException ex = (GlobalException) e; + // token过期是正常情况,不打印 + if(!ex.getCode().equals(ResultCode.INVALID_TOKEN.getCode())){ + log.error("全局异常捕获:msg:{},log:{},{}", ex.getMessage(), e); + } + return ResultUtils.error(ex.getCode(), ex.getMessage()); + } else if (e instanceof UndeclaredThrowableException) { + GlobalException ex = (GlobalException) e.getCause(); + log.error("全局异常捕获:msg:{},log:{},{}", ex.getMessage(), e); + return ResultUtils.error(ex.getCode(), ex.getMessage()); + } else { + log.error("全局异常捕获:msg:{},{}", e.getMessage(), e); + return ResultUtils.error(ResultCode.PROGRAM_ERROR); + } + } + + + /** + * 数据解析错误 + **/ + @ExceptionHandler(value = HttpMessageNotReadableException.class) + public Result handleMessageNotReadableException(HttpMessageNotReadableException e) { + log.error("全局异常捕获:msg:{}", e.getMessage()); + Throwable t = e.getCause(); + if (t instanceof JSONException) { + t = t.getCause(); + if (t instanceof DateTimeParseException) { + return ResultUtils.error(ResultCode.PROGRAM_ERROR, "日期格式不正确"); + } + return ResultUtils.error(ResultCode.PROGRAM_ERROR, "数据格式不正确"); + } + return ResultUtils.error(ResultCode.PROGRAM_ERROR); + } + + /** + * 处理请求参数格式错误 @RequestBody上validate失败后抛出的异常 + * + * @param exception + * @return + */ + @ExceptionHandler(value = {MethodArgumentNotValidException.class}) + @ResponseStatus(HttpStatus.OK) + public Result handleValidationExceptionHandler(MethodArgumentNotValidException exception) { + BindingResult bindResult = exception.getBindingResult(); + String msg; + if (bindResult.hasErrors()) { + msg = bindResult.getAllErrors().get(0).getDefaultMessage(); + if (msg.contains("NumberFormatException")) { + msg = "参数类型错误!"; + } + } else { + msg = "系统繁忙,请稍后重试..."; + } + return ResultUtils.error(ResultCode.PROGRAM_ERROR, msg); + } + + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.OK) + public Result handleBindException(BindException e) { + //抛出异常可能不止一个 输出为一个List集合 + List errors = e.getAllErrors(); + //取第一个异常 + ObjectError error = errors.get(0); + //获取异常信息 + String errorMsg = error.getDefaultMessage(); + return ResultUtils.error(ResultCode.PROGRAM_ERROR, errorMsg); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/filter/CacheFilter.java b/im-platform/src/main/java/com/bx/implatform/filter/CacheFilter.java new file mode 100644 index 0000000..6872796 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/filter/CacheFilter.java @@ -0,0 +1,29 @@ +package com.bx.implatform.filter; + +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@ServletComponentScan +@WebFilter(urlPatterns = "/*", filterName = "xssFilter") +public class CacheFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + chain.doFilter(new CacheHttpServletRequestWrapper((HttpServletRequest) request), response); + } + +} \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/filter/CacheHttpServletRequestWrapper.java b/im-platform/src/main/java/com/bx/implatform/filter/CacheHttpServletRequestWrapper.java new file mode 100644 index 0000000..1e301a6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/filter/CacheHttpServletRequestWrapper.java @@ -0,0 +1,57 @@ +package com.bx.implatform.filter; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.apache.commons.compress.utils.IOUtils; + +import java.io.*; + +public class CacheHttpServletRequestWrapper extends HttpServletRequestWrapper { + private byte[] requestBody; + private final HttpServletRequest request; + + public CacheHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + this.request = request; + } + + + @Override + public ServletInputStream getInputStream() throws IOException { + if (null == this.requestBody) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IOUtils.copy(request.getInputStream(), baos); + this.requestBody = baos.toByteArray(); + } + ByteArrayInputStream bais = new ByteArrayInputStream(requestBody); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener listener) { + } + + @Override + public int read() { + return bais.read(); + } + }; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java b/im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java new file mode 100644 index 0000000..8cc0bf1 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java @@ -0,0 +1,50 @@ +package com.bx.implatform.interceptor; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.bx.imcommon.util.JwtUtil; +import com.bx.implatform.config.props.JwtProperties; +import com.bx.implatform.enums.ResultCode; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.session.UserSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Slf4j +@Component +@AllArgsConstructor +public class AuthInterceptor implements HandlerInterceptor { + + private final JwtProperties jwtProperties; + + @Override + public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception { + //如果不是映射到方法直接通过 + if (!(handler instanceof HandlerMethod)) { + return true; + } + //从 http 请求头中取出 token + String token = request.getHeader("accessToken"); + if (StrUtil.isEmpty(token)) { + log.error("未登录,url:{}", request.getRequestURI()); + throw new GlobalException(ResultCode.NO_LOGIN); + } + String strJson = JwtUtil.getInfo(token); + UserSession userSession = JSON.parseObject(strJson, UserSession.class); + // 验证 token + if (!JwtUtil.checkSign(token, jwtProperties.getAccessTokenSecret())) { + log.error("token已失效,用户:{}", userSession.getUserName()); + log.error("token:{}", token); + throw new GlobalException(ResultCode.INVALID_TOKEN); + } + // 存放session + request.setAttribute("session", userSession); + return true; + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/interceptor/XssInterceptor.java b/im-platform/src/main/java/com/bx/implatform/interceptor/XssInterceptor.java new file mode 100644 index 0000000..073b2c4 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/interceptor/XssInterceptor.java @@ -0,0 +1,50 @@ +package com.bx.implatform.interceptor; + +import com.bx.implatform.enums.ResultCode; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.util.XssUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.io.BufferedReader; +import java.util.Map; + +@Slf4j +@Component +public class XssInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) { + // 检查参数 + Map paramMap = request.getParameterMap(); + for (String[] values : paramMap.values()) { + for (String value : values) { + if (XssUtil.checkXss(value)) { + throw new GlobalException(ResultCode.XSS_PARAM_ERROR); + } + } + } + // 检查body + String body = getBody(request); + if (XssUtil.checkXss(body)) { + throw new GlobalException(ResultCode.XSS_PARAM_ERROR); + } + return true; + } + + @SneakyThrows + private String getBody(HttpServletRequest request) { + BufferedReader reader = request.getReader(); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + return stringBuilder.toString(); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java b/im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java new file mode 100644 index 0000000..25a31f6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java @@ -0,0 +1,32 @@ +package com.bx.implatform.listener; + +import com.bx.imclient.annotation.IMListener; +import com.bx.imclient.listener.MessageListener; +import com.bx.imcommon.enums.IMListenerType; +import com.bx.imcommon.enums.IMSendCode; +import com.bx.imcommon.model.IMSendResult; +import com.bx.implatform.vo.GroupMessageVO; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.List; + +@Slf4j +@IMListener(type = IMListenerType.GROUP_MESSAGE) +@AllArgsConstructor +public class GroupMessageListener implements MessageListener { + + private final RedisTemplate redisTemplate; + + @Override + public void process(List> results) { + for(IMSendResult result:results){ + GroupMessageVO messageInfo = result.getData(); + if (result.getCode().equals(IMSendCode.SUCCESS.code())) { + log.info("消息送达,消息id:{},发送者:{},接收者:{},终端:{}", messageInfo.getId(), result.getSender().getId(), result.getReceiver().getId(), result.getReceiver().getTerminal()); + } + } + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java b/im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java new file mode 100644 index 0000000..ba61cd2 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java @@ -0,0 +1,53 @@ +package com.bx.implatform.listener; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.bx.imclient.annotation.IMListener; +import com.bx.imclient.listener.MessageListener; +import com.bx.imcommon.enums.IMListenerType; +import com.bx.imcommon.enums.IMSendCode; +import com.bx.imcommon.model.IMSendResult; +import com.bx.implatform.entity.PrivateMessage; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.service.PrivateMessageService; +import com.bx.implatform.vo.PrivateMessageVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@Slf4j +@IMListener(type = IMListenerType.PRIVATE_MESSAGE) +public class PrivateMessageListener implements MessageListener { + @Lazy + @Autowired + private PrivateMessageService privateMessageService; + + @Override + public void process(List> results) { + Set messageIds = new HashSet<>(); + for (IMSendResult result : results) { + PrivateMessageVO messageInfo = result.getData(); + // 更新消息状态,这里只处理成功消息,失败的消息继续保持未读状态 + if (result.getCode().equals(IMSendCode.SUCCESS.code()) && !Objects.isNull(messageInfo.getId())) { + if (result.getReceiver().getId().equals(messageInfo.getRecvId())) { + messageIds.add(messageInfo.getId()); + log.info("消息送达,消息id:{},发送者:{},接收者:{},终端:{}", messageInfo.getId(), + result.getSender().getId(), result.getReceiver().getId(), result.getReceiver().getTerminal()); + } + } + } + // 批量修改状态 + if (CollUtil.isNotEmpty(messageIds)) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda().in(PrivateMessage::getId, messageIds) + .eq(PrivateMessage::getStatus, MessageStatus.PENDING.code()) + .set(PrivateMessage::getStatus, MessageStatus.DELIVERED.code()); + privateMessageService.update(updateWrapper); + } + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/listener/SystemMessageListener.java b/im-platform/src/main/java/com/bx/implatform/listener/SystemMessageListener.java new file mode 100644 index 0000000..e97474d --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/listener/SystemMessageListener.java @@ -0,0 +1,26 @@ +package com.bx.implatform.listener; + +import com.bx.imclient.annotation.IMListener; +import com.bx.imclient.listener.MessageListener; +import com.bx.imcommon.enums.IMListenerType; +import com.bx.imcommon.enums.IMSendCode; +import com.bx.imcommon.model.IMSendResult; +import com.bx.implatform.vo.SystemMessageVO; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +@IMListener(type = IMListenerType.SYSTEM_MESSAGE) +public class SystemMessageListener implements MessageListener { + + @Override + public void process(List> results) { + for(IMSendResult result : results){ + SystemMessageVO messageInfo = result.getData(); + if (result.getCode().equals(IMSendCode.SUCCESS.code())) { + log.info("消息送达,消息id:{},接收者:{},终端:{}", messageInfo.getId(), result.getReceiver().getId(), result.getReceiver().getTerminal()); + } + } + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java new file mode 100644 index 0000000..37ebf8c --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/FileInfoMapper.java @@ -0,0 +1,8 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.FileInfo; + +public interface FileInfoMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/FriendMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/FriendMapper.java new file mode 100644 index 0000000..afde0ac --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/FriendMapper.java @@ -0,0 +1,8 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.Friend; + +public interface FriendMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/GroupMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/GroupMapper.java new file mode 100644 index 0000000..a1cbfee --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/GroupMapper.java @@ -0,0 +1,8 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.Group; + +public interface GroupMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/GroupMemberMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/GroupMemberMapper.java new file mode 100644 index 0000000..84afbfb --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/GroupMemberMapper.java @@ -0,0 +1,8 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.GroupMember; + +public interface GroupMemberMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/GroupMessageMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/GroupMessageMapper.java new file mode 100644 index 0000000..518092f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/GroupMessageMapper.java @@ -0,0 +1,8 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.GroupMessage; + +public interface GroupMessageMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/PrivateMessageMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/PrivateMessageMapper.java new file mode 100644 index 0000000..665e331 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/PrivateMessageMapper.java @@ -0,0 +1,8 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.PrivateMessage; + +public interface PrivateMessageMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/SensitiveWordMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/SensitiveWordMapper.java new file mode 100644 index 0000000..d989801 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/SensitiveWordMapper.java @@ -0,0 +1,16 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.SensitiveWord; +import org.apache.ibatis.annotations.Mapper; + +/** +* 敏感词 +* +* @author Blue +* @since 1.0.0 2024-07-20 +*/ +@Mapper +public interface SensitiveWordMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/mapper/UserMapper.java b/im-platform/src/main/java/com/bx/implatform/mapper/UserMapper.java new file mode 100644 index 0000000..b512f4f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/mapper/UserMapper.java @@ -0,0 +1,8 @@ +package com.bx.implatform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bx.implatform.entity.User; + +public interface UserMapper extends BaseMapper { + +} diff --git a/im-platform/src/main/java/com/bx/implatform/result/Result.java b/im-platform/src/main/java/com/bx/implatform/result/Result.java new file mode 100644 index 0000000..3ad52cc --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/result/Result.java @@ -0,0 +1,14 @@ +package com.bx.implatform.result; + +import lombok.Data; + +@Data +public class Result { + + private int code; + + private String message; + + private T data; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/result/ResultUtils.java b/im-platform/src/main/java/com/bx/implatform/result/ResultUtils.java new file mode 100644 index 0000000..447fcd0 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/result/ResultUtils.java @@ -0,0 +1,63 @@ +package com.bx.implatform.result; + +import com.bx.implatform.enums.ResultCode; + +public final class ResultUtils { + + private ResultUtils() { + } + + public static Result success() { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(ResultCode.SUCCESS.getMsg()); + return result; + } + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(ResultCode.SUCCESS.getMsg()); + result.setData(data); + return result; + } + + public static Result success(T data, String messsage) { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(messsage); + result.setData(data); + return result; + } + + public static Result success(String messsage) { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(messsage); + return result; + } + + public static Result error(Integer code, String messsage) { + Result result = new Result<>(); + result.setCode(code); + result.setMessage(messsage); + return result; + } + + + public static Result error(ResultCode resultCode, String messsage) { + Result result = new Result<>(); + result.setCode(resultCode.getCode()); + result.setMessage(messsage); + return result; + } + + public static Result error(ResultCode resultCode) { + Result result = new Result<>(); + result.setCode(resultCode.getCode()); + result.setMessage(resultCode.getMsg()); + return result; + } + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/FileService.java b/im-platform/src/main/java/com/bx/implatform/service/FileService.java new file mode 100644 index 0000000..2540b9e --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/FileService.java @@ -0,0 +1,15 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.entity.FileInfo; +import com.bx.implatform.vo.UploadImageVO; +import org.springframework.web.multipart.MultipartFile; + +public interface FileService extends IService { + + String uploadFile(MultipartFile file); + + UploadImageVO uploadImage(MultipartFile file,Boolean isPermanent,Long thumbSize); + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/FriendService.java b/im-platform/src/main/java/com/bx/implatform/service/FriendService.java new file mode 100644 index 0000000..d25a850 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/FriendService.java @@ -0,0 +1,81 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.FriendDndDTO; +import com.bx.implatform.entity.Friend; +import com.bx.implatform.vo.FriendVO; + +import java.util.List; + +public interface FriendService extends IService { + + /** + * 判断用户2是否用户1的好友 + * + * @param userId1 用户1的id + * @param userId2 用户2的id + * @return true/false + */ + Boolean isFriend(Long userId1, Long userId2); + + /** + * 查询用户的所有好友,包括已删除的 + * + * @return 好友列表 + */ + List findAllFriends(); + + /** + * 查询用户的所有好友 + * + * @param friendIds 好友id + * @return 好友列表 + */ + List findByFriendIds(List friendIds); + + /** + * 查询当前用户的所有好友 + * + * @return 好友列表 + */ + List findFriends(); + + /** + * 添加好友,互相建立好友关系 + * + * @param userId 用户id + * @param friendId 好友的用户id + */ + void addFriend(Long userId,Long friendId); + + /** + * 删除好友,双方都会解除好友关系 + * + * @param friendId 好友的用户id + */ + void delFriend(Long friendId); + + /** + * 查询指定的某个好友信息 + * + * @param friendId 好友的用户id + * @return 好友信息 + */ + FriendVO findFriend(Long friendId); + + /** + * 绑定好友关系 + * + * @param userId 好友的id + * @param friendId 好友的用户id + * @return 好友信息 + */ + void bindFriend(Long userId, Long friendId); + + /** + * 设置好友免打扰状态 + * @param dto + */ + void setDnd(FriendDndDTO dto); + +} \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java b/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java new file mode 100644 index 0000000..770943b --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java @@ -0,0 +1,102 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.GroupDndDTO; +import com.bx.implatform.entity.GroupMember; + +import java.util.List; + +public interface GroupMemberService extends IService { + + /** + * 根据群聊id和用户id查询群聊成员 + * + * @param groupId 群聊id + * @param userId 用户id + * @return 群聊成员信息 + */ + GroupMember findByGroupAndUserId(Long groupId, Long userId); + + + /** + * 根据用户id查询群聊成员 + * + * @param userId 用户id + * @return 成员列表 + */ + List findByUserId(Long userId); + + /** + * 根据用户id查询一个月内退的群 + * + * @param userId 用户id + * @return 成员列表 + */ + List findQuitInMonth(Long userId); + + /** + * 根据群聊id查询群聊成员(包括已退出) + * + * @param groupId 群聊id + * @return 群聊成员列表 + */ + List findByGroupId(Long groupId); + + + + /** + * 根据群聊id查询没有退出的群聊成员id + * + * @param groupId 群聊id + * @return 群聊成员id列表 + */ + List findUserIdsByGroupId(Long groupId); + + /** + * 批量添加成员 + * + * @param groupId 群聊id + * @param members 成员列表 + * @return 成功或失败 + */ + boolean saveOrUpdateBatch(Long groupId, List members); + + /** + * 根据群聊id删除移除成员 + * + * @param groupId 群聊id + */ + void removeByGroupId(Long groupId); + + /** + * 根据群聊id和用户id移除成员 + * + * @param groupId 群聊id + * @param userId 用户id + */ + void removeByGroupAndUserId(Long groupId, Long userId); + + /** + * 根据群聊id和用户id移除成员 + * + * @param groupId 群聊id + * @param userIds 用户id + */ + void removeByGroupAndUserIds(Long groupId, List userIds); + + /** + * 用户用户是否在群中 + * + * @param groupId 群聊id + * @param userIds 用户id + */ + Boolean isInGroup(Long groupId,List userIds); + + /** + * 设置免打扰状态 + * @param groupId 群id + * @param userId 用户id + * @param isDnd 是否开启免打扰 + */ + void setDnd(Long groupId, Long userId, Boolean isDnd); +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java b/im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java new file mode 100644 index 0000000..5b526b6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java @@ -0,0 +1,65 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.GroupMessageDTO; +import com.bx.implatform.entity.GroupMessage; +import com.bx.implatform.vo.GroupMessageVO; + +import java.util.List; + +public interface GroupMessageService extends IService { + + /** + * 发送群聊消息(高并发接口,查询mysql接口都要进行缓存) + * + * @param dto 群聊消息 + * @return 群聊id + */ + GroupMessageVO sendMessage(GroupMessageDTO dto); + + /** + * 撤回消息 + * + * @param id 消息id + */ + GroupMessageVO recallMessage(Long id); + + /** + * 拉取离线消息,只能拉取最近1个月的消息,最多拉取1000条 + * + * @param minId 消息起始id + */ + void pullOfflineMessage(Long minId); + + /** + * 拉取离线消息,只能拉取最近1个月的消息 + * + * @param minId 消息起始id + */ + List loadOffineMessage(Long minId); + + + /** + * 消息已读,同步其他终端,清空未读数量 + * + * @param groupId 群聊 + */ + void readedMessage(Long groupId); + + /** + * 查询群里消息已读用户id列表 + * @param groupId 群里id + * @param messageId 消息id + * @return 已读用户id集合 + */ + List findReadedUsers(Long groupId,Long messageId); + /** + * 拉取历史聊天记录 + * + * @param groupId 群聊id + * @param page 页码 + * @param size 页码大小 + * @return 聊天记录列表 + */ + List findHistoryMessage(Long groupId, Long page, Long size); +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/GroupService.java b/im-platform/src/main/java/com/bx/implatform/service/GroupService.java new file mode 100644 index 0000000..a43320a --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/GroupService.java @@ -0,0 +1,102 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.GroupDndDTO; +import com.bx.implatform.dto.GroupInviteDTO; +import com.bx.implatform.dto.GroupMemberRemoveDTO; +import com.bx.implatform.entity.Group; +import com.bx.implatform.vo.GroupMemberVO; +import com.bx.implatform.vo.GroupVO; + +import java.util.List; + +public interface GroupService extends IService { + + /** + * 创建新群聊 + * + * @param vo 群聊信息 + * @return 群聊信息 + **/ + GroupVO createGroup(GroupVO vo); + + /** + * 修改群聊信息 + * + * @param vo 群聊信息 + * @return 群聊信息 + **/ + GroupVO modifyGroup(GroupVO vo); + + /** + * 删除群聊 + * + * @param groupId 群聊id + **/ + void deleteGroup(Long groupId); + + /** + * 退出群聊 + * + * @param groupId 群聊id + */ + void quitGroup(Long groupId); + + /** + * 将用户踢出群聊 + * + * @param groupId 群聊id + * @param userId 用户id + */ + void kickGroup(Long groupId, Long userId); + + /** + * 将用户移出群聊 + * @param dto dto + */ + void removeGroupMembers(GroupMemberRemoveDTO dto); + + /** + * 查询当前用户的所有群聊 + * + * @return 群聊信息列表 + **/ + List findGroups(); + + /** + * 邀请好友进群 + * + * @param dto 群id、好友id列表 + **/ + void invite(GroupInviteDTO dto); + + /** + * 根据id查找群聊,并进行缓存 + * + * @param groupId 群聊id + * @return 群聊实体 + */ + Group getAndCheckById(Long groupId); + + /** + * 根据id查找群聊 + * + * @param groupId 群聊id + * @return 群聊vo + */ + GroupVO findById(Long groupId); + + /** + * 查询群成员 + * + * @param groupId 群聊id + * @return List + **/ + List findGroupMembers(Long groupId); + + /** + * 开启/关闭免打扰 + * @param dto + */ + void setDnd(GroupDndDTO dto); +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java b/im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java new file mode 100644 index 0000000..ed2a269 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java @@ -0,0 +1,67 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.PrivateMessageDTO; +import com.bx.implatform.entity.PrivateMessage; +import com.bx.implatform.vo.PrivateMessageVO; + +import java.util.List; + +public interface PrivateMessageService extends IService { + + /** + * 发送私聊消息(高并发接口,查询mysql接口都要进行缓存) + * + * @param dto 私聊消息 + * @return 消息id + */ + PrivateMessageVO sendMessage(PrivateMessageDTO dto); + + + /** + * 撤回消息 + * + * @param id 消息id + */ + PrivateMessageVO recallMessage(Long id); + + /** + * 拉取历史聊天记录 + * + * @param friendId 好友id + * @param page 页码 + * @param size 页码大小 + * @return 聊天记录列表 + */ + List findHistoryMessage(Long friendId, Long page, Long size); + + + /** + * 拉取离线消息,只能拉取最近1个月的消息,最多拉取1000条 + * + * @param minId 消息起始id + */ + void pullOfflineMessage(Long minId); + + /** + * 拉取离线消息,只能拉取最近1个月的消息 + * + * @param minId 消息起始id + */ + List loadOfflineMessage(Long minId); + + + /** + * 消息已读,将整个会话的消息都置为已读状态 + * + * @param friendId 好友id + */ + void readedMessage(Long friendId); + + /** + * 获取某个会话中已读消息的最大id + * + * @param friendId 好友id + */ + Long getMaxReadedId(Long friendId); +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/SensitiveWordService.java b/im-platform/src/main/java/com/bx/implatform/service/SensitiveWordService.java new file mode 100644 index 0000000..2a4f099 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/SensitiveWordService.java @@ -0,0 +1,15 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.entity.SensitiveWord; + +import java.util.List; + +public interface SensitiveWordService extends IService { + + /** + * 查询所有开启的敏感词 + * @return + */ + List findAllEnabledWords(); +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/UserService.java b/im-platform/src/main/java/com/bx/implatform/service/UserService.java new file mode 100644 index 0000000..37bcc8b --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/UserService.java @@ -0,0 +1,86 @@ +package com.bx.implatform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.LoginDTO; +import com.bx.implatform.dto.ModifyPwdDTO; +import com.bx.implatform.dto.RegisterDTO; +import com.bx.implatform.entity.User; +import com.bx.implatform.vo.LoginVO; +import com.bx.implatform.vo.OnlineTerminalVO; +import com.bx.implatform.vo.UserVO; + +import java.util.List; + +public interface UserService extends IService { + + /** + * 用户登录 + * + * @param dto 登录dto + * @return 登录token + */ + LoginVO login(LoginDTO dto); + + /** + * 修改用户密码 + * + * @param dto 修改密码dto + */ + void modifyPassword(ModifyPwdDTO dto); + + /** + * 用refreshToken换取新 token + * + * @param refreshToken 刷新token + * @return 登录token + */ + LoginVO refreshToken(String refreshToken); + + /** + * 用户注册 + * + * @param dto 注册dto + */ + void register(RegisterDTO dto); + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return 用户信息 + */ + User findUserByUserName(String username); + + /** + * 更新用户信息,好友昵称和群聊昵称等冗余信息也会更新 + * + * @param vo 用户信息vo + */ + void update(UserVO vo); + + /** + * 根据用户昵id查询用户以及在线状态 + * + * @param id 用户id + * @return 用户信息 + */ + UserVO findUserById(Long id); + + /** + * 根据用户昵称查询用户,最多返回20条数据 + * + * @param name 用户名或昵称 + * @return 用户列表 + */ + List findUserByName(String name); + + /** + * 获取用户在线的终端类型 + * + * @param userIds 用户id,多个用‘,’分割 + * @return 在线用户终端 + */ + List getOnlineTerminals(String userIds); + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/WebrtcPrivateService.java b/im-platform/src/main/java/com/bx/implatform/service/WebrtcPrivateService.java new file mode 100644 index 0000000..c2f9c3f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/WebrtcPrivateService.java @@ -0,0 +1,26 @@ +package com.bx.implatform.service; + +/** + * webrtc 通信服务 + * + * @author + */ +public interface WebrtcPrivateService { + + void call(Long uid, String mode,String offer); + + void accept(Long uid, String answer); + + void reject(Long uid); + + void cancel(Long uid); + + void failed(Long uid, String reason); + + void handup(Long uid); + + void candidate(Long uid, String candidate); + + void heartbeat(Long uid); + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/FileServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..9eb4ce5 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/FileServiceImpl.java @@ -0,0 +1,212 @@ +package com.bx.implatform.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.implatform.config.props.MinioProperties; +import com.bx.implatform.contant.Constant; +import com.bx.implatform.entity.FileInfo; +import com.bx.implatform.enums.FileType; +import com.bx.implatform.enums.ResultCode; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.mapper.FileInfoMapper; +import com.bx.implatform.service.FileService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.thirdparty.MinioService; +import com.bx.implatform.util.FileUtil; +import com.bx.implatform.util.ImageUtil; +import com.bx.implatform.vo.UploadImageVO; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.DigestUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Date; +import java.util.Objects; + +/** + * 文件上传服务 + * + * author: Blue date: 2024-09-28 version: 1.0 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FileServiceImpl extends ServiceImpl implements FileService { + + private final MinioService minioSerivce; + + private final MinioProperties minioProps; + + @PostConstruct + public void init() { + if (!minioSerivce.bucketExists(minioProps.getBucketName())) { + // 创建bucket + minioSerivce.makeBucket(minioProps.getBucketName()); + // 公开bucket + minioSerivce.setBucketPublic(minioProps.getBucketName()); + } + } + + @Override + public String uploadFile(MultipartFile file) { + try { + Long userId = SessionContext.getSession().getUserId(); + // 文件名长度校验 + checkFileNameLength(file); + // 大小校验 + if (file.getSize() > Constant.MAX_FILE_SIZE) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件大小不能超过20M"); + } + // 如果文件已存在,直接复用 + String md5 = DigestUtils.md5DigestAsHex(file.getInputStream()); + FileInfo fileInfo = findByMd5(md5, FileType.FILE.code()); + if (!Objects.isNull(fileInfo)) { + // 更新上传时间 + fileInfo.setUploadTime(new Date()); + this.updateById(fileInfo); + // 返回 + return fileInfo.getFilePath(); + } + // 上传 + String fileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getFilePath(), file); + if (StringUtils.isEmpty(fileName)) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "文件上传失败"); + } + String url = generUrl(FileType.FILE, fileName); + // 保存文件 + saveFileInfo(file, md5, url); + log.info("文件文件成功,用户id:{},url:{}", userId, url); + return url; + } catch (IOException e) { + log.error("上传图片失败,{}", e.getMessage(), e); + throw new GlobalException(ResultCode.PROGRAM_ERROR, "上传图片失败"); + } + } + + @Transactional + @Override + public UploadImageVO uploadImage(MultipartFile file, Boolean isPermanent,Long thumbSize) { + try { + Long userId = SessionContext.getSession().getUserId(); + // 文件名长度校验 + checkFileNameLength(file); + // 大小校验 + if (file.getSize() > Constant.MAX_IMAGE_SIZE) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片大小不能超过20M"); + } + // 图片格式校验 + if (!FileUtil.isImage(file.getOriginalFilename())) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片格式不合法"); + } + UploadImageVO vo = new UploadImageVO(); + // 获取图片长度和宽度 + BufferedImage bufferedImage = ImageIO.read(file.getInputStream()); + if(!Objects.isNull(bufferedImage)){ + vo.setWidth(bufferedImage.getWidth()); + vo.setHeight(bufferedImage.getHeight()); + } + // 如果文件已存在,直接复用 + String md5 = DigestUtils.md5DigestAsHex(file.getInputStream()); + FileInfo fileInfo = findByMd5(md5, FileType.IMAGE.code()); + if (!Objects.isNull(fileInfo)) { + // 更新上传时间和持久化标记 + fileInfo.setIsPermanent(isPermanent || fileInfo.getIsPermanent()); + fileInfo.setUploadTime(new Date()); + this.updateById(fileInfo); + // 返回 + vo.setOriginUrl(fileInfo.getFilePath()); + vo.setThumbUrl(fileInfo.getCompressedPath()); + return vo; + } + // 上传原图 + String fileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(), file); + if (StringUtils.isEmpty(fileName)) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); + } + vo.setOriginUrl(generUrl(FileType.IMAGE, fileName)); + if (file.getSize() > thumbSize * 1024) { + // 大于50K的文件需上传缩略图 + byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), thumbSize); + String thumbFileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(), + file.getOriginalFilename(), imageByte, file.getContentType()); + if (StringUtils.isEmpty(thumbFileName)) { + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); + } + vo.setThumbUrl(generUrl(FileType.IMAGE, thumbFileName)); + // 保存文件信息 + saveImageFileInfo(file, md5, vo.getOriginUrl(), vo.getThumbUrl(), isPermanent); + } else { + // 小于50k,用原图充当缩略图 + vo.setThumbUrl(generUrl(FileType.IMAGE, fileName)); + // 保存文件信息,由于缩略图不允许删除,此时原图也不允许删除 + saveImageFileInfo(file, md5, vo.getOriginUrl(), vo.getThumbUrl(), true); + } + log.info("文件图片成功,用户id:{},url:{}", userId, vo.getOriginUrl()); + return vo; + } catch (IOException e) { + log.error("上传图片失败,{}", e.getMessage(), e); + throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); + } + } + + private String generUrl(FileType fileType, String fileName) { + return StrUtil.join("/", minioProps.getDomain(), minioProps.getBucketName(), getBucketPath(fileType), fileName); + } + + private String getBucketPath(FileType fileType) { + return switch (fileType) { + case FILE -> minioProps.getFilePath(); + case IMAGE -> minioProps.getImagePath(); + case VIDEO -> minioProps.getVideoPath(); + }; + } + + private FileInfo findByMd5(String md5, Integer fileType) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FileInfo::getMd5, md5); + wrapper.eq(FileInfo::getFileType, fileType); + wrapper.last("limit 1"); + return getOne(wrapper); + } + + private void saveImageFileInfo(MultipartFile file, String md5, String filePath, String compressedPath, + Boolean isPermanent) throws IOException { + FileInfo fileInfo = new FileInfo(); + fileInfo.setFileName(file.getOriginalFilename()); + fileInfo.setFileSize(file.getSize()); + fileInfo.setFileType(FileType.IMAGE.code()); + fileInfo.setFilePath(filePath); + fileInfo.setCompressedPath(compressedPath); + fileInfo.setMd5(md5); + fileInfo.setIsPermanent(isPermanent); + fileInfo.setUploadTime(new Date()); + this.save(fileInfo); + } + + private void saveFileInfo(MultipartFile file, String md5, String filePath) throws IOException { + FileInfo fileInfo = new FileInfo(); + fileInfo.setFileName(file.getOriginalFilename()); + fileInfo.setFileSize(file.getSize()); + fileInfo.setFileType(FileType.FILE.code()); + fileInfo.setFilePath(filePath); + fileInfo.setMd5(md5); + fileInfo.setIsPermanent(false); + fileInfo.setUploadTime(new Date()); + this.save(fileInfo); + } + + private void checkFileNameLength(MultipartFile file){ + if(file.getOriginalFilename().length() > Constant.MAX_FILE_NAME_LENGTH){ + throw new GlobalException("文件名长度不能超过" + Constant.MAX_FILE_NAME_LENGTH); + } + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java new file mode 100644 index 0000000..82fc1df --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java @@ -0,0 +1,289 @@ +package com.bx.implatform.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.imclient.IMClient; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.model.IMPrivateMessage; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.implatform.annotation.RedisLock; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.FriendDndDTO; +import com.bx.implatform.entity.Friend; +import com.bx.implatform.entity.PrivateMessage; +import com.bx.implatform.entity.User; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.mapper.FriendMapper; +import com.bx.implatform.mapper.PrivateMessageMapper; +import com.bx.implatform.mapper.UserMapper; +import com.bx.implatform.service.FriendService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.session.UserSession; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.vo.FriendVO; +import com.bx.implatform.vo.PrivateMessageVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.framework.AopContext; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = RedisKey.IM_CACHE_FRIEND) +public class FriendServiceImpl extends ServiceImpl implements FriendService { + + private final PrivateMessageMapper privateMessageMapper; + private final UserMapper userMapper; + private final IMClient imClient; + + @Override + public List findAllFriends() { + Long userId = SessionContext.getSession().getUserId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(Friend::getUserId, userId); + return this.list(wrapper); + } + + @Override + public List findByFriendIds(List friendIds) { + Long userId = SessionContext.getSession().getUserId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(Friend::getUserId, userId); + wrapper.in(Friend::getFriendId, friendIds); + wrapper.eq(Friend::getDeleted,false); + return this.list(wrapper); + } + + @Override + public List findFriends() { + List friends = this.findAllFriends(); + return friends.stream().map(this::conver).collect(Collectors.toList()); + } + + @RedisLock(prefixKey = RedisKey.IM_LOCK_FRIEND_ADD, key = "#userId+':'+#friendId") + @Transactional(rollbackFor = Exception.class) + @Override + public void addFriend(Long userId, Long friendId) { + if (friendId.equals(userId)) { + throw new GlobalException("不允许添加自己为好友"); + } + // 互相绑定好友关系 + FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy(); + proxy.bindFriend(userId, friendId); + proxy.bindFriend(friendId, userId); + // 推送添加好友提示 + sendAddTipMessage(friendId); + log.info("添加好友,用户id:{},好友id:{}", userId, friendId); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void delFriend(Long friendId) { + Long userId = SessionContext.getSession().getUserId(); + // 互相解除好友关系,走代理清理缓存 + FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy(); + proxy.unbindFriend(userId, friendId); + proxy.unbindFriend(friendId, userId); + // 推送解除好友提示 + sendDelTipMessage(friendId); + log.info("删除好友,用户id:{},好友id:{}", userId, friendId); + } + + @Cacheable(key = "#userId1+':'+#userId2") + @Override + public Boolean isFriend(Long userId1, Long userId2) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(Friend::getUserId, userId1); + wrapper.eq(Friend::getFriendId, userId2); + wrapper.eq(Friend::getDeleted,false); + return this.exists(wrapper); + } + + /** + * 单向绑定好友关系 + * + * @param userId 用户id + * @param friendId 好友的用户id + */ + @CacheEvict(key = "#userId+':'+#friendId") + public void bindFriend(Long userId, Long friendId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(Friend::getUserId, userId).eq(Friend::getFriendId, friendId); + Friend friend = this.getOne(wrapper); + if (Objects.isNull(friend)) { + friend = new Friend(); + } + friend.setUserId(userId); + friend.setFriendId(friendId); + User friendInfo = userMapper.selectById(friendId); + friend.setFriendHeadImage(friendInfo.getHeadImageThumb()); + friend.setFriendNickName(friendInfo.getNickName()); + friend.setDeleted(false); + this.saveOrUpdate(friend); + // 推送好友变化信息s + sendAddFriendMessage(userId, friendId, friend); + } + + @Override + public void setDnd(FriendDndDTO dto) { + UserSession session = SessionContext.getSession(); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(Friend::getUserId, session.getUserId()); + wrapper.eq(Friend::getFriendId, dto.getFriendId()); + wrapper.set(Friend::getIsDnd, dto.getIsDnd()); + this.update(wrapper); + // 推送同步消息 + sendSyncDndMessage(dto.getFriendId(), dto.getIsDnd()); + } + + /** + * 单向解除好友关系 + * + * @param userId 用户id + * @param friendId 好友的用户id + */ + @CacheEvict(key = "#userId+':'+#friendId") + public void unbindFriend(Long userId, Long friendId) { + // 逻辑删除 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(Friend::getUserId, userId); + wrapper.eq(Friend::getFriendId, friendId); + wrapper.set(Friend::getDeleted,true); + this.update(wrapper); + // 推送好友变化信息 + sendDelFriendMessage(userId, friendId); + } + + @Override + public FriendVO findFriend(Long friendId) { + UserSession session = SessionContext.getSession(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(Friend::getUserId, session.getUserId()); + wrapper.eq(Friend::getFriendId, friendId); + Friend friend = this.getOne(wrapper); + if (Objects.isNull(friend)) { + throw new GlobalException("对方不是您的好友"); + } + return conver(friend); + } + + private FriendVO conver(Friend f) { + FriendVO vo = new FriendVO(); + vo.setId(f.getFriendId()); + vo.setHeadImage(f.getFriendHeadImage()); + vo.setNickName(f.getFriendNickName()); + vo.setDeleted(f.getDeleted()); + vo.setIsDnd(f.getIsDnd()); + return vo; + } + + void sendAddFriendMessage(Long userId, Long friendId, Friend friend) { + // 推送好友状态信息 + PrivateMessageVO msgInfo = new PrivateMessageVO(); + msgInfo.setSendId(friendId); + msgInfo.setRecvId(userId); + msgInfo.setSendTime(new Date()); + msgInfo.setType(MessageType.FRIEND_NEW.code()); + FriendVO vo = conver(friend); + msgInfo.setContent(JSON.toJSONString(vo)); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(friendId, IMTerminalType.UNKNOW.code())); + sendMessage.setRecvId(userId); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + imClient.sendPrivateMessage(sendMessage); + } + + void sendDelFriendMessage(Long userId, Long friendId) { + // 推送好友状态信息 + PrivateMessageVO msgInfo = new PrivateMessageVO(); + msgInfo.setSendId(friendId); + msgInfo.setRecvId(userId); + msgInfo.setSendTime(new Date()); + msgInfo.setType(MessageType.FRIEND_DEL.code()); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(friendId, IMTerminalType.UNKNOW.code())); + sendMessage.setRecvId(userId); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + imClient.sendPrivateMessage(sendMessage); + } + + void sendAddTipMessage(Long friendId) { + UserSession session = SessionContext.getSession(); + PrivateMessage msg = new PrivateMessage(); + msg.setSendId(session.getUserId()); + msg.setRecvId(friendId); + msg.setContent("你们已成为好友,现在可以开始聊天了"); + msg.setSendTime(new Date()); + msg.setStatus(MessageStatus.PENDING.code()); + msg.setType(MessageType.TIP_TEXT.code()); + privateMessageMapper.insert(msg); + // 推给对方 + PrivateMessageVO messageInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(friendId); + sendMessage.setSendToSelf(false); + sendMessage.setData(messageInfo); + imClient.sendPrivateMessage(sendMessage); + // 推给自己 + sendMessage.setRecvId(session.getUserId()); + imClient.sendPrivateMessage(sendMessage); + } + + void sendDelTipMessage(Long friendId){ + UserSession session = SessionContext.getSession(); + // 推送好友状态信息 + PrivateMessage msg = new PrivateMessage(); + msg.setSendId(session.getUserId()); + msg.setRecvId(friendId); + msg.setSendTime(new Date()); + msg.setType(MessageType.TIP_TEXT.code()); + msg.setStatus(MessageStatus.PENDING.code()); + msg.setContent("你们的好友关系已被解除"); + privateMessageMapper.insert(msg); + // 推送 + PrivateMessageVO messageInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(friendId, IMTerminalType.UNKNOW.code())); + sendMessage.setRecvId(friendId); + sendMessage.setData(messageInfo); + imClient.sendPrivateMessage(sendMessage); + } + + void sendSyncDndMessage(Long friendId, Boolean isDnd) { + // 同步免打扰状态到其他终端 + UserSession session = SessionContext.getSession(); + PrivateMessageVO msgInfo = new PrivateMessageVO(); + msgInfo.setSendId(session.getUserId()); + msgInfo.setRecvId(friendId); + msgInfo.setSendTime(new Date()); + msgInfo.setType(MessageType.FRIEND_DND.code()); + msgInfo.setContent(isDnd.toString()); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(true); + imClient.sendPrivateMessage(sendMessage); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java new file mode 100644 index 0000000..9302c61 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java @@ -0,0 +1,131 @@ +package com.bx.implatform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.entity.GroupMember; +import com.bx.implatform.mapper.GroupMemberMapper; +import com.bx.implatform.service.GroupMemberService; +import com.bx.implatform.util.DateTimeUtils; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@CacheConfig(cacheNames = RedisKey.IM_CACHE_GROUP_MEMBER_ID) +public class GroupMemberServiceImpl extends ServiceImpl implements GroupMemberService { + @CacheEvict(key = "#member.getGroupId()") + @Override + public boolean save(GroupMember member) { + return super.save(member); + } + + @CacheEvict(key = "#groupId") + @Override + public boolean saveOrUpdateBatch(Long groupId, List members) { + return super.saveOrUpdateBatch(members); + } + + @Override + public GroupMember findByGroupAndUserId(Long groupId, Long userId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.eq(GroupMember::getUserId, userId); + return this.getOne(wrapper); + } + + + @Override + public List findByUserId(Long userId) { + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(GroupMember::getUserId, userId).eq(GroupMember::getQuit, false); + return this.list(memberWrapper); + } + + @Override + public List findQuitInMonth(Long userId) { + Date monthTime = DateTimeUtils.addMonths(new Date(), -1); + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(GroupMember::getUserId, userId).eq(GroupMember::getQuit, true) + .ge(GroupMember::getQuitTime, monthTime); + return this.list(memberWrapper); + } + + @Override + public List findByGroupId(Long groupId) { + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(GroupMember::getGroupId, groupId); + return this.list(memberWrapper); + } + + @Cacheable(key = "#groupId") + @Override + public List findUserIdsByGroupId(Long groupId) { + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(GroupMember::getGroupId, groupId).eq(GroupMember::getQuit, false) + .select(GroupMember::getUserId); + List members = this.list(memberWrapper); + return members.stream().map(GroupMember::getUserId).collect(Collectors.toList()); + } + + @CacheEvict(key = "#groupId") + @Override + public void removeByGroupId(Long groupId) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(GroupMember::getGroupId, groupId).set(GroupMember::getQuit, true) + .set(GroupMember::getQuitTime, new Date()); + this.update(wrapper); + } + + @CacheEvict(key = "#groupId") + @Override + public void removeByGroupAndUserId(Long groupId, Long userId) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.eq(GroupMember::getUserId, userId); + wrapper.set(GroupMember::getQuit, true); + wrapper.set(GroupMember::getQuitTime, new Date()); + this.update(wrapper); + } + + @CacheEvict(key = "#groupId") + @Override + public void removeByGroupAndUserIds(Long groupId, List userId) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.in(GroupMember::getUserId, userId); + wrapper.set(GroupMember::getQuit, true); + wrapper.set(GroupMember::getQuitTime, new Date()); + this.update(wrapper); + } + + + @Override + public Boolean isInGroup(Long groupId, List userIds) { + if (CollectionUtils.isEmpty(userIds)) { + return true; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.eq(GroupMember::getQuit, false); + wrapper.in(GroupMember::getUserId, userIds); + return userIds.size() == this.count(wrapper); + } + + @Override + public void setDnd(Long groupId, Long userId, Boolean isDnd) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.eq(GroupMember::getUserId, userId); + wrapper.set(GroupMember::getIsDnd, isDnd); + this.update(wrapper); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java new file mode 100644 index 0000000..81c91d5 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java @@ -0,0 +1,470 @@ +package com.bx.implatform.service.impl; + +import cn.hutool.core.collection.CollStreamUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.imclient.IMClient; +import com.bx.imcommon.contant.IMConstant; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.model.IMGroupMessage; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.util.CommaTextUtils; +import com.bx.imcommon.util.ThreadPoolExecutorFactory; +import com.bx.implatform.contant.Constant; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.GroupMessageDTO; +import com.bx.implatform.entity.Group; +import com.bx.implatform.entity.GroupMember; +import com.bx.implatform.entity.GroupMessage; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.mapper.GroupMessageMapper; +import com.bx.implatform.service.GroupMemberService; +import com.bx.implatform.service.GroupMessageService; +import com.bx.implatform.service.GroupService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.session.UserSession; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.util.SensitiveFilterUtil; +import com.bx.implatform.vo.GroupMessageVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GroupMessageServiceImpl extends ServiceImpl + implements GroupMessageService { + private final GroupService groupService; + private final GroupMemberService groupMemberService; + private final RedisTemplate redisTemplate; + private final IMClient imClient; + private final SensitiveFilterUtil sensitiveFilterUtil; + private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor(); + + @Override + public GroupMessageVO sendMessage(GroupMessageDTO dto) { + UserSession session = SessionContext.getSession(); + Group group = groupService.getAndCheckById(dto.getGroupId()); + // 是否在群聊里面 + GroupMember member = groupMemberService.findByGroupAndUserId(dto.getGroupId(), session.getUserId()); + if (Objects.isNull(member) || member.getQuit()) { + throw new GlobalException("您已不在群聊里面,无法发送消息"); + } + // 群聊成员列表 + List userIds = groupMemberService.findUserIdsByGroupId(group.getId()); + if (dto.getReceipt() && userIds.size() > Constant.MAX_NORMAL_GROUP_MEMBER) { + // 大群的回执消息过于消耗资源,不允许发送 + throw new GlobalException( + String.format("当前群聊大于%s人,不支持发送回执消息", Constant.MAX_NORMAL_GROUP_MEMBER)); + } + // 不用发给自己 + userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList()); + // 保存消息 + GroupMessage msg = BeanUtils.copyProperties(dto, GroupMessage.class); + msg.setSendId(session.getUserId()); + msg.setSendTime(new Date()); + msg.setSendNickName(member.getShowNickName()); + msg.setAtUserIds(CommaTextUtils.asText(dto.getAtUserIds())); + msg.setStatus(MessageStatus.PENDING.code()); + // 过滤内容中的敏感词 + if (MessageType.TEXT.code().equals(dto.getType())) { + msg.setContent(sensitiveFilterUtil.filter(dto.getContent())); + } + this.save(msg); + // 群发 + GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class); + msgInfo.setAtUserIds(dto.getAtUserIds()); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvIds(userIds); + sendMessage.setSendResult(false); + sendMessage.setData(msgInfo); + imClient.sendGroupMessage(sendMessage); + log.info("发送群聊消息,发送id:{},群聊id:{},内容:{}", session.getUserId(), dto.getGroupId(), dto.getContent()); + return msgInfo; + } + + @Transactional + @Override + public GroupMessageVO recallMessage(Long id) { + UserSession session = SessionContext.getSession(); + GroupMessage msg = this.getById(id); + if (Objects.isNull(msg)) { + throw new GlobalException("消息不存在"); + } + if (!msg.getSendId().equals(session.getUserId())) { + throw new GlobalException("这条消息不是由您发送,无法撤回"); + } + if (System.currentTimeMillis() - msg.getSendTime().getTime() > IMConstant.ALLOW_RECALL_SECOND * 1000) { + throw new GlobalException("消息已发送超过5分钟,无法撤回"); + } + // 判断是否在群里 + GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(), session.getUserId()); + if (Objects.isNull(member) || Boolean.TRUE.equals(member.getQuit())) { + throw new GlobalException("您已不在群聊里面,无法撤回消息"); + } + // 修改数据库 + msg.setStatus(MessageStatus.RECALL.code()); + this.updateById(msg); + // 生成一条撤回消息 + GroupMessage recallMsg = new GroupMessage(); + recallMsg.setStatus(MessageStatus.PENDING.code()); + recallMsg.setType(MessageType.RECALL.code()); + recallMsg.setGroupId(msg.getGroupId()); + recallMsg.setSendId(session.getUserId()); + recallMsg.setSendNickName(member.getShowNickName()); + recallMsg.setContent(id.toString()); + recallMsg.setSendTime(new Date()); + this.save(recallMsg); + // 群发 + List userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId()); + GroupMessageVO msgInfo = BeanUtils.copyProperties(recallMsg, GroupMessageVO.class); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvIds(userIds); + sendMessage.setData(msgInfo); + imClient.sendGroupMessage(sendMessage); + log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}", session.getUserId(), msg.getGroupId(), msg.getContent()); + return msgInfo; + } + + @Override + public void pullOfflineMessage(Long minId) { + UserSession session = SessionContext.getSession(); + if (!imClient.isOnline(session.getUserId())) { + throw new GlobalException("网络连接失败,无法拉取离线消息"); + } + // 查询用户加入的群组 + List members = groupMemberService.findByUserId(session.getUserId()); + Map groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId); + Set groupIds = groupMemberMap.keySet(); + if (CollectionUtil.isEmpty(groupIds)) { + // 关闭加载中标志 + this.sendLoadingMessage(false, session); + return; + } + // 只拉最近一个月 + Date minDate = DateUtils.addMonths(new Date(), -1); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.gt(GroupMessage::getId, minId); + wrapper.gt(GroupMessage::getSendTime, minDate); + wrapper.in(GroupMessage::getGroupId, groupIds); + wrapper.orderByDesc(GroupMessage::getId); + wrapper.last("limit 50000"); + List messages = this.list(wrapper); + // 通过群聊对消息进行分组 + Map> messageGroupMap = + messages.stream().collect(Collectors.groupingBy(GroupMessage::getGroupId)); + // 退群前的消息 + List quitMembers = groupMemberService.findQuitInMonth(session.getUserId()); + for (GroupMember quitMember : quitMembers) { + wrapper = Wrappers.lambdaQuery(); + wrapper.gt(GroupMessage::getId, minId); + wrapper.between(GroupMessage::getSendTime, minDate, quitMember.getQuitTime()); + wrapper.eq(GroupMessage::getGroupId, quitMember.getGroupId()); + wrapper.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()); + wrapper.orderByDesc(GroupMessage::getId); + List groupMessages = this.list(wrapper); + messageGroupMap.put(quitMember.getGroupId(), groupMessages); + groupMemberMap.put(quitMember.getGroupId(), quitMember); + } + EXECUTOR.execute(() -> { + // 开启加载中标志 + this.sendLoadingMessage(true, session); + // 推送消息 + int sendCount = 0; + for (Map.Entry> entry : messageGroupMap.entrySet()) { + Long groupId = entry.getKey(); + List groupMessages = entry.getValue(); + // 第一次拉取时,一个群最多推送3000条消息,防止前端接收能力溢出导致卡顿 + List sendMessages = groupMessages; + if (minId <= 0 && groupMessages.size() > 3000) { + sendMessages = groupMessages.subList(0, 3000); + } + // id从小到大排序 + CollectionUtil.reverse(sendMessages); + // 填充消息状态 + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); + Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString()); + long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString()); + Map maxIdMap = null; + for (GroupMessage m : sendMessages) { + // 推送过程如果用户下线了,则不再推送 + if (!imClient.isOnline(session.getUserId(), IMTerminalType.fromCode(session.getTerminal()))) { + log.info("用户已下线,停止推送离线群聊消息,用户id:{}", session.getUserId()); + return; + } + // 排除加群之前的消息 + GroupMember member = groupMemberMap.get(m.getGroupId()); + if (DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0) { + continue; + } + // 排除不需要接收的消息 + List recvIds = CommaTextUtils.asList(m.getRecvIds()); + if (!recvIds.isEmpty() && !recvIds.contains(session.getUserId().toString())) { + continue; + } + // 组装vo + GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class); + // 被@用户列表 + List atIds = CommaTextUtils.asList(m.getAtUserIds()); + vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList())); + // 填充状态 + vo.setStatus(readedMaxId >= m.getId() ? MessageStatus.READED.code() : MessageStatus.PENDING.code()); + // 针对回执消息填充已读人数 + if (m.getReceipt()) { + if (Objects.isNull(maxIdMap)) { + maxIdMap = redisTemplate.opsForHash().entries(key); + } + int count = getReadedUserIds(maxIdMap, m.getId(), m.getSendId()).size(); + vo.setReadedCount(count); + } + // 推送 + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code())); + sendMessage.setRecvIds(Arrays.asList(session.getUserId())); + sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal())); + sendMessage.setSendResult(false); + sendMessage.setSendToSelf(false); + sendMessage.setData(vo); + imClient.sendGroupMessage(sendMessage); + sendCount++; + } + } + // 关闭加载中标志 + this.sendLoadingMessage(false, session); + log.info("拉取离线群聊消息,用户id:{},数量:{}", session.getUserId(), sendCount++); + }); + } + + @Override + public List loadOffineMessage(Long minId) { + UserSession session = SessionContext.getSession(); + // 查询用户加入的群组 + List members = groupMemberService.findByUserId(session.getUserId()); + Map groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId); + Set groupIds = groupMemberMap.keySet(); + if (groupIds.isEmpty()) { + return Collections.EMPTY_LIST; + } + // 只能拉取最近1个月的消息 + Date minDate = DateUtils.addMonths(new Date(), -1); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.gt(GroupMessage::getId, minId); + wrapper.gt(GroupMessage::getSendTime, minDate); + wrapper.in(GroupMessage::getGroupId, groupIds); + wrapper.orderByDesc(GroupMessage::getId); + wrapper.last("limit 50000"); + List messages = this.list(wrapper); + // 退群前的消息 + List quitMembers = groupMemberService.findQuitInMonth(session.getUserId()); + for (GroupMember quitMember : quitMembers) { + wrapper = Wrappers.lambdaQuery(); + wrapper.gt(GroupMessage::getId, minId); + wrapper.between(GroupMessage::getSendTime, minDate, quitMember.getQuitTime()); + wrapper.eq(GroupMessage::getGroupId, quitMember.getGroupId()); + wrapper.orderByDesc(GroupMessage::getId); + wrapper.last("limit 1000"); + List groupMessages = this.list(wrapper); + if (!groupMessages.isEmpty()) { + messages.addAll(groupMessages); + groupMemberMap.put(quitMember.getGroupId(), quitMember); + } + } + // 通过群聊对消息进行分组 + Map> messageGroupMap = + messages.stream().collect(Collectors.groupingBy(GroupMessage::getGroupId)); + List vos = new LinkedList<>(); + for (Map.Entry> entry : messageGroupMap.entrySet()) { + Long groupId = entry.getKey(); + List groupMessages = entry.getValue(); + // 填充消息状态 + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); + Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString()); + long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString()); + Map maxIdMap = null; + for (GroupMessage m : groupMessages) { + // 排除加群之前的消息 + GroupMember member = groupMemberMap.get(m.getGroupId()); + if (DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0) { + continue; + } + // 排除不需要接收的消息 + List recvIds = CommaTextUtils.asList(m.getRecvIds()); + if (!recvIds.isEmpty() && !recvIds.contains(session.getUserId().toString())) { + continue; + } + // 组装vo + GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class); + // 被@用户列表 + List atIds = CommaTextUtils.asList(m.getAtUserIds()); + vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList())); + // 填充状态 + vo.setStatus(readedMaxId >= m.getId() ? MessageStatus.READED.code() : MessageStatus.PENDING.code()); + // 针对回执消息填充已读人数 + if (m.getReceipt()) { + if (Objects.isNull(maxIdMap)) { + maxIdMap = redisTemplate.opsForHash().entries(key); + } + int count = getReadedUserIds(maxIdMap, m.getId(), m.getSendId()).size(); + vo.setReadedCount(count); + } + vos.add(vo); + } + } + // 排序 + return vos.stream().sorted(Comparator.comparing(GroupMessageVO::getId)).collect(Collectors.toList()); + } + + + @Override + public void readedMessage(Long groupId) { + UserSession session = SessionContext.getSession(); + // 取出最后的消息id + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(GroupMessage::getGroupId, groupId).orderByDesc(GroupMessage::getId).last("limit 1") + .select(GroupMessage::getId); + GroupMessage message = this.getOne(wrapper); + if (Objects.isNull(message)) { + return; + } + // 推送消息给自己的其他终端,同步清空会话列表中的未读数量 + GroupMessageVO msgInfo = new GroupMessageVO(); + msgInfo.setType(MessageType.READED.code()); + msgInfo.setSendTime(new Date()); + msgInfo.setSendId(session.getUserId()); + msgInfo.setGroupId(groupId); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setSendToSelf(true); + sendMessage.setData(msgInfo); + sendMessage.setSendResult(true); + imClient.sendGroupMessage(sendMessage); + // 已读消息key + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); + // 原来的已读消息位置 + Object maxReadedId = redisTemplate.opsForHash().get(key, session.getUserId().toString()); + // 记录已读消息位置 + redisTemplate.opsForHash().put(key, session.getUserId().toString(), message.getId()); + // 推送消息回执,刷新已读人数显示 + wrapper = Wrappers.lambdaQuery(); + wrapper.eq(GroupMessage::getGroupId, groupId); + wrapper.gt(!Objects.isNull(maxReadedId), GroupMessage::getId, maxReadedId); + wrapper.le(!Objects.isNull(maxReadedId), GroupMessage::getId, message.getId()); + wrapper.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()); + wrapper.eq(GroupMessage::getReceipt, true); + List receiptMessages = this.list(wrapper); + if (CollectionUtil.isNotEmpty(receiptMessages)) { + List userIds = groupMemberService.findUserIdsByGroupId(groupId); + Map maxIdMap = redisTemplate.opsForHash().entries(key); + for (GroupMessage receiptMessage : receiptMessages) { + Integer readedCount = + getReadedUserIds(maxIdMap, receiptMessage.getId(), receiptMessage.getSendId()).size(); + // 如果所有人都已读,记录回执消息完成标记 + if (readedCount >= userIds.size() - 1) { + receiptMessage.setReceiptOk(true); + this.updateById(receiptMessage); + } + msgInfo = new GroupMessageVO(); + msgInfo.setId(receiptMessage.getId()); + msgInfo.setGroupId(groupId); + msgInfo.setReadedCount(readedCount); + msgInfo.setReceiptOk(receiptMessage.getReceiptOk()); + msgInfo.setType(MessageType.RECEIPT.code()); + sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvIds(userIds); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + imClient.sendGroupMessage(sendMessage); + } + } + } + + @Override + public List findReadedUsers(Long groupId, Long messageId) { + UserSession session = SessionContext.getSession(); + GroupMessage message = this.getById(messageId); + if (Objects.isNull(message)) { + throw new GlobalException("消息不存在"); + } + // 是否在群聊里面 + GroupMember member = groupMemberService.findByGroupAndUserId(groupId, session.getUserId()); + if (Objects.isNull(member) || member.getQuit()) { + throw new GlobalException("您已不在群聊里面"); + } + // 已读位置key + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); + // 一次获取所有用户的已读位置 + Map maxIdMap = redisTemplate.opsForHash().entries(key); + // 返回已读用户的id集合 + return getReadedUserIds(maxIdMap, message.getId(), message.getSendId()); + } + + @Override + public List findHistoryMessage(Long groupId, Long page, Long size) { + page = page > 0 ? page : 1; + size = size > 0 ? size : 10; + Long userId = SessionContext.getSession().getUserId(); + long stIdx = (page - 1) * size; + // 群聊成员信息 + GroupMember member = groupMemberService.findByGroupAndUserId(groupId, userId); + if (Objects.isNull(member) || member.getQuit()) { + throw new GlobalException("您已不在群聊中"); + } + // 查询聊天记录,只查询加入群聊时间之后的消息 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(GroupMessage::getGroupId, groupId).gt(GroupMessage::getSendTime, member.getCreatedTime()) + .ne(GroupMessage::getStatus, MessageStatus.RECALL.code()).orderByDesc(GroupMessage::getId) + .last("limit " + stIdx + "," + size); + List messages = this.list(wrapper); + List messageInfos = + messages.stream().map(m -> BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList()); + log.info("拉取群聊记录,用户id:{},群聊id:{},数量:{}", userId, groupId, messageInfos.size()); + return messageInfos; + } + + private List getReadedUserIds(Map maxIdMap, Long messageId, Long sendId) { + List userIds = new LinkedList<>(); + maxIdMap.forEach((k, v) -> { + Long userId = Long.valueOf(k.toString()); + long maxId = Long.parseLong(v.toString()); + // 发送者不计入已读人数 + if (!sendId.equals(userId) && maxId >= messageId) { + userIds.add(userId); + } + }); + return userIds; + } + + private void sendLoadingMessage(Boolean isLoadding, UserSession session) { + GroupMessageVO msgInfo = new GroupMessageVO(); + msgInfo.setType(MessageType.LOADING.code()); + msgInfo.setContent(isLoadding.toString()); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvIds(Arrays.asList(session.getUserId())); + sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal())); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + imClient.sendGroupMessage(sendMessage); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java new file mode 100644 index 0000000..34daed1 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java @@ -0,0 +1,418 @@ +package com.bx.implatform.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.imclient.IMClient; +import com.bx.imcommon.model.IMGroupMessage; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.util.CommaTextUtils; +import com.bx.implatform.annotation.RedisLock; +import com.bx.implatform.contant.Constant; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.GroupDndDTO; +import com.bx.implatform.dto.GroupInviteDTO; +import com.bx.implatform.dto.GroupMemberRemoveDTO; +import com.bx.implatform.entity.*; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.mapper.GroupMapper; +import com.bx.implatform.mapper.GroupMessageMapper; +import com.bx.implatform.service.FriendService; +import com.bx.implatform.service.GroupMemberService; +import com.bx.implatform.service.GroupService; +import com.bx.implatform.service.UserService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.session.UserSession; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.vo.GroupMemberVO; +import com.bx.implatform.vo.GroupMessageVO; +import com.bx.implatform.vo.GroupVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.Lists; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = RedisKey.IM_CACHE_GROUP) +public class GroupServiceImpl extends ServiceImpl implements GroupService { + private final UserService userService; + private final GroupMemberService groupMemberService; + private final GroupMessageMapper groupMessageMapper; + private final FriendService friendsService; + private final IMClient imClient; + private final RedisTemplate redisTemplate; + + @Override + public GroupVO createGroup(GroupVO vo) { + UserSession session = SessionContext.getSession(); + User user = userService.getById(session.getUserId()); + // 保存群组数据 + Group group = BeanUtils.copyProperties(vo, Group.class); + group.setOwnerId(user.getId()); + this.save(group); + // 把群主加入群 + GroupMember member = new GroupMember(); + member.setGroupId(group.getId()); + member.setUserId(user.getId()); + member.setHeadImage(user.getHeadImageThumb()); + member.setUserNickName(user.getNickName()); + member.setRemarkNickName(vo.getRemarkNickName()); + member.setRemarkGroupName(vo.getRemarkGroupName()); + groupMemberService.save(member); + GroupVO groupVo = findById(group.getId()); + // 推送同步消息给自己的其他终端 + sendAddGroupMessage(groupVo, Lists.newArrayList(), true); + // 返回 + log.info("创建群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); + return groupVo; + } + + @CacheEvict(key = "#vo.getId()") + @Transactional(rollbackFor = Exception.class) + @Override + public GroupVO modifyGroup(GroupVO vo) { + UserSession session = SessionContext.getSession(); + // 校验是不是群主,只有群主能改信息 + Group group = this.getAndCheckById(vo.getId()); + // 更新成员信息 + GroupMember member = groupMemberService.findByGroupAndUserId(vo.getId(), session.getUserId()); + if (Objects.isNull(member) || member.getQuit()) { + throw new GlobalException("您不是群聊的成员"); + } + member.setRemarkNickName(vo.getRemarkNickName()); + member.setRemarkGroupName(vo.getRemarkGroupName()); + groupMemberService.updateById(member); + // 群主有权修改群基本信息 + if (group.getOwnerId().equals(session.getUserId())) { + group = BeanUtils.copyProperties(vo, Group.class); + this.updateById(group); + } + log.info("修改群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); + return convert(group, member); + } + + @Transactional(rollbackFor = Exception.class) + @CacheEvict(key = "#groupId") + @Override + public void deleteGroup(Long groupId) { + UserSession session = SessionContext.getSession(); + Group group = this.getById(groupId); + if (!group.getOwnerId().equals(session.getUserId())) { + throw new GlobalException("只有群主才有权限解除群聊"); + } + // 群聊用户id + List userIds = groupMemberService.findUserIdsByGroupId(groupId); + // 逻辑删除群数据 + group.setDissolve(true); + this.updateById(group); + // 删除成员数据 + groupMemberService.removeByGroupId(groupId); + // 清理已读缓存 + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); + redisTemplate.delete(key); + // 推送解散群聊提示 + String content = String.format("'%s'解散了群聊", session.getNickName()); + this.sendTipMessage(groupId, userIds, content, true); + // 推送同步消息 + this.sendDelGroupMessage(groupId, userIds); + log.info("删除群聊,群聊id:{},群聊名称:{}", group.getId(), group.getName()); + } + + @Override + public void quitGroup(Long groupId) { + Long userId = SessionContext.getSession().getUserId(); + Group group = this.getById(groupId); + if (group.getOwnerId().equals(userId)) { + throw new GlobalException("您是群主,不可退出群聊"); + } + // 删除群聊成员 + groupMemberService.removeByGroupAndUserId(groupId, userId); + // 清理已读缓存 + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); + redisTemplate.opsForHash().delete(key, userId.toString()); + // 推送退出群聊提示 + this.sendTipMessage(groupId, List.of(userId), "您已退出群聊", false); + // 推送同步消息 + this.sendDelGroupMessage(groupId, List.of(userId)); + log.info("退出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); + } + + @Override + public void kickGroup(Long groupId, Long userId) { + UserSession session = SessionContext.getSession(); + Group group = this.getAndCheckById(groupId); + if (!group.getOwnerId().equals(session.getUserId())) { + throw new GlobalException("您不是群主,没有权限踢人"); + } + if (userId.equals(session.getUserId())) { + throw new GlobalException("亲,不能移除自己哟"); + } + // 删除群聊成员 + groupMemberService.removeByGroupAndUserId(groupId, userId); + // 清理已读缓存 + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); + redisTemplate.opsForHash().delete(key, userId.toString()); + // 推送踢出群聊提示 + this.sendTipMessage(groupId, List.of(userId), "您已被移出群聊", false); + // 推送同步消息 + this.sendDelGroupMessage(groupId, List.of(userId)); + log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), userId); + } + + @Override + public void removeGroupMembers(GroupMemberRemoveDTO dto) { + UserSession session = SessionContext.getSession(); + Group group = this.getAndCheckById(dto.getGroupId()); + if (!group.getOwnerId().equals(session.getUserId())) { + throw new GlobalException("您没有权限"); + } + if (dto.getUserIds().contains(group.getOwnerId())) { + throw new GlobalException("不允许移除群主"); + } + if (dto.getUserIds().contains(session.getUserId())) { + throw new GlobalException("不允许移除自己"); + } + // 删除群聊成员 + groupMemberService.removeByGroupAndUserIds(dto.getGroupId(), dto.getUserIds()); + // 清理已读缓存 + String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, dto.getGroupId()); + dto.getUserIds().forEach(id -> redisTemplate.opsForHash().delete(key, id.toString())); + // 推送踢出群聊提示 + this.sendTipMessage(dto.getGroupId(), dto.getUserIds(), "您已被移出群聊", false); + // 推送同步消息 + this.sendDelGroupMessage(dto.getGroupId(), dto.getUserIds()); + log.info("踢出群聊,群聊id:{},群聊名称:{},用户id:{}", group.getId(), group.getName(), dto.getUserIds()); + } + + @Override + public GroupVO findById(Long groupId) { + UserSession session = SessionContext.getSession(); + Group group = super.getById(groupId); + if (Objects.isNull(group)) { + throw new GlobalException("群组不存在"); + } + GroupMember member = groupMemberService.findByGroupAndUserId(groupId, session.getUserId()); + if (Objects.isNull(member)) { + throw new GlobalException("您未加入群聊"); + } + return convert(group, member); + } + + @Cacheable(key = "#groupId") + @Override + public Group getAndCheckById(Long groupId) { + Group group = super.getById(groupId); + if (Objects.isNull(group)) { + throw new GlobalException("群组不存在"); + } + if (group.getDissolve()) { + throw new GlobalException("群组'" + group.getName() + "'已解散"); + } + if (group.getIsBanned()) { + throw new GlobalException("群组'" + group.getName() + "'已被封禁,原因:" + group.getReason()); + } + return group; + } + + @Override + public List findGroups() { + UserSession session = SessionContext.getSession(); + // 查询当前用户的群id列表 + List groupMembers = groupMemberService.findByUserId(session.getUserId()); + // 一个月内退的群可能存在退群前的离线消息,一并返回作为前端缓存 + groupMembers.addAll(groupMemberService.findQuitInMonth(session.getUserId())); + if (groupMembers.isEmpty()) { + return new LinkedList<>(); + } + // 拉取群列表 + List ids = groupMembers.stream().map((GroupMember::getGroupId)).collect(Collectors.toList()); + LambdaQueryWrapper groupWrapper = Wrappers.lambdaQuery(); + groupWrapper.in(Group::getId, ids); + List groups = this.list(groupWrapper); + // 转vo + return groups.stream().map(group -> { + GroupMember member = + groupMembers.stream().filter(m -> group.getId().equals(m.getGroupId())).findFirst().get(); + return convert(group, member); + }).collect(Collectors.toList()); + } + + @RedisLock(prefixKey = RedisKey.IM_LOCK_GROUP_ENTER,key = "#dto.getGroupId()") + @Override + public void invite(GroupInviteDTO dto) { + UserSession session = SessionContext.getSession(); + Group group = this.getAndCheckById(dto.getGroupId()); + GroupMember member = groupMemberService.findByGroupAndUserId(dto.getGroupId(), session.getUserId()); + if (Objects.isNull(group) || member.getQuit()) { + throw new GlobalException("您不在群聊中,邀请失败"); + } + // 群聊人数校验 + List members = groupMemberService.findByGroupId(dto.getGroupId()); + long size = members.stream().filter(m -> !m.getQuit()).count(); + if (dto.getFriendIds().size() + size > Constant.MAX_LARGE_GROUP_MEMBER) { + throw new GlobalException("群聊人数不能大于" + Constant.MAX_LARGE_GROUP_MEMBER + "人"); + } + // 找出好友信息 + List friends = friendsService.findByFriendIds(dto.getFriendIds()); + if (dto.getFriendIds().size() != friends.size()) { + throw new GlobalException("部分用户不是您的好友,邀请失败"); + } + // 批量保存成员数据 + List groupMembers = friends.stream().map(f -> { + Optional optional = + members.stream().filter(m -> m.getUserId().equals(f.getFriendId())).findFirst(); + GroupMember groupMember = optional.orElseGet(GroupMember::new); + groupMember.setGroupId(dto.getGroupId()); + groupMember.setUserId(f.getFriendId()); + groupMember.setUserNickName(f.getFriendNickName()); + groupMember.setHeadImage(f.getFriendHeadImage()); + groupMember.setCreatedTime(new Date()); + groupMember.setQuit(false); + return groupMember; + }).collect(Collectors.toList()); + if (!groupMembers.isEmpty()) { + groupMemberService.saveOrUpdateBatch(group.getId(), groupMembers); + } + // 推送同步消息给被邀请人 + for (GroupMember m : groupMembers) { + GroupVO groupVo = convert(group, m); + sendAddGroupMessage(groupVo, List.of(m.getUserId()), false); + } + // 推送进入群聊消息 + List userIds = groupMemberService.findUserIdsByGroupId(dto.getGroupId()); + String memberNames = groupMembers.stream().map(GroupMember::getShowNickName).collect(Collectors.joining(",")); + String content = String.format("'%s'邀请'%s'加入了群聊", session.getNickName(), memberNames); + this.sendTipMessage(dto.getGroupId(), userIds, content, true); + log.info("邀请进入群聊,群聊id:{},群聊名称:{},被邀请用户id:{}", group.getId(), group.getName(), + dto.getFriendIds()); + } + + @Override + public List findGroupMembers(Long groupId) { + Group group = getAndCheckById(groupId); + List members = groupMemberService.findByGroupId(groupId); + List userIds = members.stream().map(GroupMember::getUserId).collect(Collectors.toList()); + List onlineUserIds = imClient.getOnlineUser(userIds); + return members.stream().map(m -> { + GroupMemberVO vo = BeanUtils.copyProperties(m, GroupMemberVO.class); + vo.setShowNickName(m.getShowNickName()); + vo.setShowGroupName(StrUtil.blankToDefault(m.getRemarkGroupName(), group.getName())); + vo.setOnline(onlineUserIds.contains(m.getUserId())); + return vo; + }).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList()); + } + + @Override + public void setDnd(GroupDndDTO dto) { + UserSession session = SessionContext.getSession(); + groupMemberService.setDnd(dto.getGroupId(), session.getUserId(), dto.getIsDnd()); + // 推送同步消息 + sendSyncDndMessage(dto.getGroupId(), dto.getIsDnd()); + } + + + private void sendTipMessage(Long groupId, List recvIds, String content, Boolean sendToAll) { + UserSession session = SessionContext.getSession(); + // 消息入库 + GroupMessage message = new GroupMessage(); + message.setContent(content); + message.setType(MessageType.TIP_TEXT.code()); + message.setStatus(MessageStatus.PENDING.code()); + message.setSendTime(new Date()); + message.setSendNickName(session.getNickName()); + message.setGroupId(groupId); + message.setSendId(session.getUserId()); + message.setRecvIds(sendToAll ? "" : CommaTextUtils.asText(recvIds)); + groupMessageMapper.insert(message); + // 推送 + GroupMessageVO msgInfo = BeanUtils.copyProperties(message, GroupMessageVO.class); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + if (CollUtil.isEmpty(recvIds)) { + // 为空表示向全体发送 + List userIds = groupMemberService.findUserIdsByGroupId(groupId); + sendMessage.setRecvIds(userIds); + } else { + sendMessage.setRecvIds(recvIds); + } + sendMessage.setData(msgInfo); + sendMessage.setSendResult(false); + sendMessage.setSendToSelf(false); + imClient.sendGroupMessage(sendMessage); + } + + private GroupVO convert(Group group, GroupMember member) { + GroupVO vo = BeanUtils.copyProperties(group, GroupVO.class); + vo.setRemarkGroupName(member.getRemarkGroupName()); + vo.setRemarkNickName(member.getRemarkNickName()); + vo.setShowNickName(member.getShowNickName()); + vo.setShowGroupName(StrUtil.blankToDefault(member.getRemarkGroupName(), group.getName())); + vo.setQuit(member.getQuit()); + vo.setIsDnd(member.getIsDnd()); + return vo; + } + + private void sendAddGroupMessage(GroupVO group, List recvIds, Boolean sendToSelf) { + UserSession session = SessionContext.getSession(); + GroupMessageVO msgInfo = new GroupMessageVO(); + msgInfo.setContent(JSON.toJSONString(group)); + msgInfo.setType(MessageType.GROUP_NEW.code()); + msgInfo.setSendTime(new Date()); + msgInfo.setGroupId(group.getId()); + msgInfo.setSendId(session.getUserId()); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvIds(recvIds); + sendMessage.setData(msgInfo); + sendMessage.setSendResult(false); + sendMessage.setSendToSelf(sendToSelf); + imClient.sendGroupMessage(sendMessage); + } + + private void sendDelGroupMessage(Long groupId, List recvIds) { + UserSession session = SessionContext.getSession(); + GroupMessageVO msgInfo = new GroupMessageVO(); + msgInfo.setType(MessageType.GROUP_DEL.code()); + msgInfo.setSendTime(new Date()); + msgInfo.setGroupId(groupId); + msgInfo.setSendId(session.getUserId()); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvIds(recvIds); + sendMessage.setData(msgInfo); + sendMessage.setSendResult(false); + sendMessage.setSendToSelf(false); + imClient.sendGroupMessage(sendMessage); + } + + private void sendSyncDndMessage(Long groupId, Boolean isDnd) { + UserSession session = SessionContext.getSession(); + GroupMessageVO msgInfo = new GroupMessageVO(); + msgInfo.setType(MessageType.GROUP_DND.code()); + msgInfo.setSendTime(new Date()); + msgInfo.setGroupId(groupId); + msgInfo.setSendId(session.getUserId()); + msgInfo.setContent(isDnd.toString()); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setData(msgInfo); + sendMessage.setSendResult(false); + imClient.sendGroupMessage(sendMessage); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java new file mode 100644 index 0000000..3401f8a --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java @@ -0,0 +1,271 @@ +package com.bx.implatform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.imclient.IMClient; +import com.bx.imcommon.contant.IMConstant; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.model.IMPrivateMessage; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.util.ThreadPoolExecutorFactory; +import com.bx.implatform.dto.PrivateMessageDTO; +import com.bx.implatform.entity.PrivateMessage; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.mapper.PrivateMessageMapper; +import com.bx.implatform.service.FriendService; +import com.bx.implatform.service.PrivateMessageService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.session.UserSession; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.util.SensitiveFilterUtil; +import com.bx.implatform.vo.PrivateMessageVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PrivateMessageServiceImpl extends ServiceImpl + implements PrivateMessageService { + + private final FriendService friendService; + private final IMClient imClient; + private final SensitiveFilterUtil sensitiveFilterUtil; + private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor(); + + @Override + public PrivateMessageVO sendMessage(PrivateMessageDTO dto) { + UserSession session = SessionContext.getSession(); + Boolean isFriends = friendService.isFriend(session.getUserId(), dto.getRecvId()); + if (Boolean.FALSE.equals(isFriends)) { + throw new GlobalException("您已不是对方好友,无法发送消息"); + } + // 保存消息 + PrivateMessage msg = BeanUtils.copyProperties(dto, PrivateMessage.class); + msg.setSendId(session.getUserId()); + msg.setStatus(MessageStatus.PENDING.code()); + msg.setSendTime(new Date()); + // 过滤内容中的敏感词 + if (MessageType.TEXT.code().equals(dto.getType())) { + msg.setContent(sensitiveFilterUtil.filter(dto.getContent())); + } + this.save(msg); + // 推送消息 + PrivateMessageVO msgInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(msgInfo.getRecvId()); + sendMessage.setSendToSelf(true); + sendMessage.setData(msgInfo); + sendMessage.setSendResult(true); + imClient.sendPrivateMessage(sendMessage); + log.info("发送私聊消息,发送id:{},接收id:{},内容:{}", session.getUserId(), dto.getRecvId(), dto.getContent()); + return msgInfo; + } + + @Transactional + @Override + public PrivateMessageVO recallMessage(Long id) { + UserSession session = SessionContext.getSession(); + PrivateMessage msg = this.getById(id); + if (Objects.isNull(msg)) { + throw new GlobalException("消息不存在"); + } + if (!msg.getSendId().equals(session.getUserId())) { + throw new GlobalException("这条消息不是由您发送,无法撤回"); + } + if (System.currentTimeMillis() - msg.getSendTime().getTime() > IMConstant.ALLOW_RECALL_SECOND * 1000) { + throw new GlobalException("消息已发送超过5分钟,无法撤回"); + } + // 修改消息状态 + msg.setStatus(MessageStatus.RECALL.code()); + this.updateById(msg); + // 生成一条撤回消息 + PrivateMessage recallMsg = new PrivateMessage(); + recallMsg.setSendId(session.getUserId()); + recallMsg.setStatus(MessageStatus.PENDING.code()); + recallMsg.setSendTime(new Date()); + recallMsg.setRecvId(msg.getRecvId()); + recallMsg.setType(MessageType.RECALL.code()); + recallMsg.setContent(id.toString()); + this.save(recallMsg); + // 推送消息 + PrivateMessageVO msgInfo = BeanUtils.copyProperties(recallMsg, PrivateMessageVO.class); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(msgInfo.getRecvId()); + sendMessage.setData(msgInfo); + imClient.sendPrivateMessage(sendMessage); + log.info("撤回私聊消息,发送id:{},接收id:{},内容:{}", msg.getSendId(), msg.getRecvId(), msg.getContent()); + return msgInfo; + } + + @Override + public List findHistoryMessage(Long friendId, Long page, Long size) { + page = page > 0 ? page : 1; + size = size > 0 ? size : 10; + Long userId = SessionContext.getSession().getUserId(); + long stIdx = (page - 1) * size; + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().and( + wrap -> wrap.and(wp -> wp.eq(PrivateMessage::getSendId, userId).eq(PrivateMessage::getRecvId, friendId)) + .or(wp -> wp.eq(PrivateMessage::getRecvId, userId).eq(PrivateMessage::getSendId, friendId))) + .ne(PrivateMessage::getStatus, MessageStatus.RECALL.code()).orderByDesc(PrivateMessage::getId) + .last("limit " + stIdx + "," + size); + + List messages = this.list(wrapper); + List messageInfos = + messages.stream().map(m -> BeanUtils.copyProperties(m, PrivateMessageVO.class)) + .collect(Collectors.toList()); + log.info("拉取聊天记录,用户id:{},好友id:{},数量:{}", userId, friendId, messageInfos.size()); + return messageInfos; + } + + @Override + public void pullOfflineMessage(Long minId) { + UserSession session = SessionContext.getSession(); + // 获取当前用户的消息 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // 只能拉取最近1个月的消息 + Date minDate = DateUtils.addMonths(new Date(), -1); + wrapper.gt(PrivateMessage::getId, minId); + wrapper.ge(PrivateMessage::getSendTime, minDate); + wrapper.and(wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).or() + .eq(PrivateMessage::getRecvId, session.getUserId())); + wrapper.orderByAsc(PrivateMessage::getId); + List messages = this.list(wrapper); + // 异步推送消息 + EXECUTOR.execute(() -> { + // 开启加载中标志 + this.sendLoadingMessage(true, session); + for (PrivateMessage m : messages) { + // 推送过程如果用户下线了,则不再推送 + if (!imClient.isOnline(session.getUserId(), IMTerminalType.fromCode(session.getTerminal()))) { + log.info("用户已下线,停止推送离线私聊消息,用户id:{}", session.getUserId()); + return; + } + PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code())); + sendMessage.setRecvId(session.getUserId()); + sendMessage.setRecvTerminals(List.of(session.getTerminal())); + sendMessage.setSendToSelf(false); + sendMessage.setData(vo); + sendMessage.setSendResult(true); + imClient.sendPrivateMessage(sendMessage); + } + // 关闭加载中标志 + this.sendLoadingMessage(false, session); + log.info("拉取私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size()); + }); + } + + @Override + public List loadOfflineMessage(Long minId) { + UserSession session = SessionContext.getSession(); + // 获取当前用户的消息 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + // 只能拉取最近1个月的消息 + Date minDate = DateUtils.addMonths(new Date(), -1); + wrapper.gt(PrivateMessage::getId, minId); + wrapper.ge(PrivateMessage::getSendTime, minDate); + wrapper.and(wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).or() + .eq(PrivateMessage::getRecvId, session.getUserId())); + wrapper.orderByAsc(PrivateMessage::getId); + List messages = this.list(wrapper); + // 更新消息为送达状态 + List messageIds = messages.stream().filter(m -> m.getRecvId().equals(session.getUserId())) + .filter(m -> m.getStatus().equals(MessageStatus.PENDING.code())).map(PrivateMessage::getId) + .collect(Collectors.toList()); + if (!messageIds.isEmpty()) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.in(PrivateMessage::getId, messageIds); + updateWrapper.set(PrivateMessage::getStatus, MessageStatus.DELIVERED.code()); + update(updateWrapper); + } + // 转换vo + List vos = messages.stream().map(m -> BeanUtils.copyProperties(m, PrivateMessageVO.class)) + .collect(Collectors.toList()); + log.info("拉取私聊消息,用户id:{},数量:{},minId:{}", session.getUserId(), messages.size(), minId); + return vos; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void readedMessage(Long friendId) { + UserSession session = SessionContext.getSession(); + // 推送消息给自己,清空会话列表上的已读数量 + PrivateMessageVO msgInfo = new PrivateMessageVO(); + msgInfo.setType(MessageType.READED.code()); + msgInfo.setSendId(session.getUserId()); + msgInfo.setRecvId(friendId); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setData(msgInfo); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setSendToSelf(true); + sendMessage.setSendResult(false); + imClient.sendPrivateMessage(sendMessage); + // 推送回执消息给对方,更新已读状态 + msgInfo = new PrivateMessageVO(); + msgInfo.setType(MessageType.RECEIPT.code()); + msgInfo.setSendId(session.getUserId()); + msgInfo.setRecvId(friendId); + sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(friendId); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + sendMessage.setData(msgInfo); + imClient.sendPrivateMessage(sendMessage); + // 修改消息状态为已读 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(PrivateMessage::getSendId, friendId).eq(PrivateMessage::getRecvId, session.getUserId()) + .eq(PrivateMessage::getStatus, MessageStatus.DELIVERED.code()) + .set(PrivateMessage::getStatus, MessageStatus.READED.code()); + this.update(updateWrapper); + log.info("消息已读,接收方id:{},发送方id:{}", session.getUserId(), friendId); + } + + @Override + public Long getMaxReadedId(Long friendId) { + UserSession session = SessionContext.getSession(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(PrivateMessage::getSendId, session.getUserId()).eq(PrivateMessage::getRecvId, friendId) + .eq(PrivateMessage::getStatus, MessageStatus.READED.code()).orderByDesc(PrivateMessage::getId) + .select(PrivateMessage::getId).last("limit 1"); + PrivateMessage message = this.getOne(wrapper); + if (Objects.isNull(message)) { + return -1L; + } + return message.getId(); + } + + private void sendLoadingMessage(Boolean isLoadding, UserSession session) { + PrivateMessageVO msgInfo = new PrivateMessageVO(); + msgInfo.setType(MessageType.LOADING.code()); + msgInfo.setContent(isLoadding.toString()); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(session.getUserId()); + sendMessage.setRecvTerminals(List.of(session.getTerminal())); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + imClient.sendPrivateMessage(sendMessage); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/SensitiveWordServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/SensitiveWordServiceImpl.java new file mode 100644 index 0000000..572e5bd --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/SensitiveWordServiceImpl.java @@ -0,0 +1,30 @@ +package com.bx.implatform.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.implatform.entity.SensitiveWord; +import com.bx.implatform.mapper.SensitiveWordMapper; +import com.bx.implatform.service.SensitiveWordService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SensitiveWordServiceImpl extends ServiceImpl implements + SensitiveWordService { + + @Override + public List findAllEnabledWords() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(SensitiveWord::getEnabled,true); + wrapper.select(SensitiveWord::getContent); + List words = this.list(wrapper); + return words.stream().map(SensitiveWord::getContent).collect(Collectors.toList()); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..938dd07 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java @@ -0,0 +1,225 @@ +package com.bx.implatform.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bx.imclient.IMClient; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.util.JwtUtil; +import com.bx.implatform.config.props.JwtProperties; +import com.bx.implatform.dto.LoginDTO; +import com.bx.implatform.dto.ModifyPwdDTO; +import com.bx.implatform.dto.RegisterDTO; +import com.bx.implatform.entity.Friend; +import com.bx.implatform.entity.GroupMember; +import com.bx.implatform.entity.User; +import com.bx.implatform.enums.ResultCode; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.mapper.UserMapper; +import com.bx.implatform.service.FriendService; +import com.bx.implatform.service.GroupMemberService; +import com.bx.implatform.service.UserService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.session.UserSession; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.util.SensitiveFilterUtil; +import com.bx.implatform.vo.LoginVO; +import com.bx.implatform.vo.OnlineTerminalVO; +import com.bx.implatform.vo.UserVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserServiceImpl extends ServiceImpl implements UserService { + + private final PasswordEncoder passwordEncoder; + private final GroupMemberService groupMemberService; + private final FriendService friendService; + private final JwtProperties jwtProperties; + private final IMClient imClient; + private final SensitiveFilterUtil sensitiveFilterUtil; + + @Override + public LoginVO login(LoginDTO dto) { + User user = this.findUserByUserName(dto.getUserName()); + if (Objects.isNull(user)) { + throw new GlobalException("用户不存在"); + } + if (user.getIsBanned()) { + String tip = String.format("您的账号因'%s'已被管理员封禁,请联系客服!",user.getReason()); + throw new GlobalException(tip); + } + if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { + throw new GlobalException(ResultCode.PASSWOR_ERROR); + } + // 生成token + UserSession session = BeanUtils.copyProperties(user, UserSession.class); + session.setUserId(user.getId()); + session.setTerminal(dto.getTerminal()); + String strJson = JSON.toJSONString(session); + String accessToken = JwtUtil.sign(user.getId(), strJson, jwtProperties.getAccessTokenExpireIn(), + jwtProperties.getAccessTokenSecret()); + String refreshToken = JwtUtil.sign(user.getId(), strJson, jwtProperties.getRefreshTokenExpireIn(), + jwtProperties.getRefreshTokenSecret()); + LoginVO vo = new LoginVO(); + vo.setAccessToken(accessToken); + vo.setAccessTokenExpiresIn(jwtProperties.getAccessTokenExpireIn()); + vo.setRefreshToken(refreshToken); + vo.setRefreshTokenExpiresIn(jwtProperties.getRefreshTokenExpireIn()); + return vo; + } + + @Override + public LoginVO refreshToken(String refreshToken) { + //验证 token + if (!JwtUtil.checkSign(refreshToken, jwtProperties.getRefreshTokenSecret())) { + throw new GlobalException("您的登录信息已过期,请重新登录"); + } + String strJson = JwtUtil.getInfo(refreshToken); + Long userId = JwtUtil.getUserId(refreshToken); + User user = this.getById(userId); + if (Objects.isNull(user)) { + throw new GlobalException("用户不存在"); + } + if (user.getIsBanned()) { + String tip = String.format("您的账号因'%s'被管理员封禁,请联系客服!",user.getReason()); + throw new GlobalException(tip); + } + String accessToken = + JwtUtil.sign(userId, strJson, jwtProperties.getAccessTokenExpireIn(), jwtProperties.getAccessTokenSecret()); + String newRefreshToken = JwtUtil.sign(userId, strJson, jwtProperties.getRefreshTokenExpireIn(), + jwtProperties.getRefreshTokenSecret()); + LoginVO vo = new LoginVO(); + vo.setAccessToken(accessToken); + vo.setAccessTokenExpiresIn(jwtProperties.getAccessTokenExpireIn()); + vo.setRefreshToken(newRefreshToken); + vo.setRefreshTokenExpiresIn(jwtProperties.getRefreshTokenExpireIn()); + return vo; + } + + @Override + public void register(RegisterDTO dto) { + // 昵称默认跟用户名保持一致 + if(StrUtil.isEmpty(dto.getNickName())){ + dto.setUserName(dto.getUserName()); + } + User user = this.findUserByUserName(dto.getUserName()); + if(!dto.getUserName().equals(sensitiveFilterUtil.filter(dto.getUserName()))){ + throw new GlobalException("用户名包含敏感字符"); + } + if(!dto.getNickName().equals(sensitiveFilterUtil.filter(dto.getNickName()))){ + throw new GlobalException("昵称包含敏感字符"); + } + if (!Objects.isNull(user)) { + throw new GlobalException(ResultCode.USERNAME_ALREADY_REGISTER); + } + user = BeanUtils.copyProperties(dto, User.class); + user.setPassword(passwordEncoder.encode(user.getPassword())); + this.save(user); + log.info("注册用户,用户id:{},用户名:{},昵称:{}", user.getId(), dto.getUserName(), dto.getNickName()); + } + + @Override + public void modifyPassword(ModifyPwdDTO dto) { + UserSession session = SessionContext.getSession(); + User user = this.getById(session.getUserId()); + if (!passwordEncoder.matches(dto.getOldPassword(), user.getPassword())) { + throw new GlobalException("旧密码不正确"); + } + user.setPassword(passwordEncoder.encode(dto.getNewPassword())); + this.updateById(user); + log.info("用户修改密码,用户id:{},用户名:{},昵称:{}", user.getId(), user.getUserName(), user.getNickName()); + } + + @Override + public User findUserByUserName(String username) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(User::getUserName, username); + return this.getOne(queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void update(UserVO vo) { + UserSession session = SessionContext.getSession(); + if(!vo.getNickName().equals(sensitiveFilterUtil.filter(vo.getNickName()))){ + throw new GlobalException("昵称包含敏感字符"); + } + if (!session.getUserId().equals(vo.getId())) { + throw new GlobalException("不允许修改其他用户的信息"); + } + User user = this.getById(vo.getId()); + if (Objects.isNull(user)) { + throw new GlobalException("用户不存在"); + } + if (!user.getNickName().equals(vo.getNickName()) || !user.getHeadImageThumb().equals(vo.getHeadImageThumb())) { + // 更新好友昵称和头像 + LambdaUpdateWrapper wrapper1 = Wrappers.lambdaUpdate(); + wrapper1.eq(Friend::getFriendId, session.getUserId()); + wrapper1.set(Friend::getFriendNickName,vo.getNickName()); + wrapper1.set(Friend::getFriendHeadImage,vo.getHeadImageThumb()); + friendService.update(wrapper1); + // 更新群聊中的昵称和头像 + LambdaUpdateWrapper wrapper2 = Wrappers.lambdaUpdate(); + wrapper2.eq(GroupMember::getUserId, session.getUserId()); + wrapper2.set(GroupMember::getHeadImage,vo.getHeadImageThumb()); + wrapper2.set(GroupMember::getUserNickName,vo.getNickName()); + groupMemberService.update(wrapper2); + } + // 更新用户信息 + user.setNickName(vo.getNickName()); + user.setSex(vo.getSex()); + user.setSignature(vo.getSignature()); + user.setHeadImage(vo.getHeadImage()); + user.setHeadImageThumb(vo.getHeadImageThumb()); + this.updateById(user); + log.info("用户信息更新,用户:{}}", user); + } + + @Override + public UserVO findUserById(Long id) { + User user = this.getById(id); + UserVO vo = BeanUtils.copyProperties(user, UserVO.class); + vo.setOnline(imClient.isOnline(id)); + return vo; + } + + @Override + public List findUserByName(String name) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.like(User::getUserName, name).or().like(User::getNickName, name).last("limit 20"); + List users = this.list(queryWrapper); + List userIds = users.stream().map(User::getId).collect(Collectors.toList()); + List onlineUserIds = imClient.getOnlineUser(userIds); + return users.stream().map(u -> { + UserVO vo = BeanUtils.copyProperties(u, UserVO.class); + vo.setOnline(onlineUserIds.contains(u.getId())); + return vo; + }).collect(Collectors.toList()); + } + + @Override + public List getOnlineTerminals(String userIds) { + List userIdList = Arrays.stream(userIds.split(",")).map(Long::parseLong).collect(Collectors.toList()); + // 查询在线的终端 + Map> terminalMap = imClient.getOnlineTerminal(userIdList); + // 组装vo + List vos = new LinkedList<>(); + terminalMap.forEach((userId, types) -> { + List terminals = types.stream().map(IMTerminalType::code).collect(Collectors.toList()); + vos.add(new OnlineTerminalVO(userId, terminals)); + }); + return vos; + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java new file mode 100644 index 0000000..374c740 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcPrivateServiceImpl.java @@ -0,0 +1,337 @@ +package com.bx.implatform.service.impl; + +import com.bx.imclient.IMClient; +import com.bx.imcommon.model.IMPrivateMessage; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.implatform.annotation.OnlineCheck; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.entity.PrivateMessage; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.enums.WebrtcMode; +import com.bx.implatform.exception.GlobalException; +import com.bx.implatform.service.PrivateMessageService; +import com.bx.implatform.service.WebrtcPrivateService; +import com.bx.implatform.session.SessionContext; +import com.bx.implatform.session.UserSession; +import com.bx.implatform.session.WebrtcPrivateSession; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.util.UserStateUtils; +import com.bx.implatform.vo.PrivateMessageVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class WebrtcPrivateServiceImpl implements WebrtcPrivateService { + + private final IMClient imClient; + private final RedisTemplate redisTemplate; + private final PrivateMessageService privateMessageService; + private final UserStateUtils userStateUtils; + + @OnlineCheck + @Override + public void call(Long uid, String mode, String offer) { + UserSession session = SessionContext.getSession(); + log.info("发起呼叫,sid:{},uid:{}", session.getUserId(), uid); + // 创建webrtc会话 + WebrtcPrivateSession webrtcSession = new WebrtcPrivateSession(); + webrtcSession.setCallerId(session.getUserId()); + webrtcSession.setCallerTerminal(session.getTerminal()); + webrtcSession.setAcceptorId(uid); + webrtcSession.setMode(mode); + // 校验 + if (!imClient.isOnline(uid)) { + this.sendActMessage(webrtcSession, MessageStatus.PENDING, "未接通"); + log.info("对方不在线,uid:{}", uid); + throw new GlobalException("对方目前不在线"); + } + if (userStateUtils.isBusy(uid)) { + this.sendActMessage(webrtcSession, MessageStatus.PENDING, "未接通"); + log.info("对方正忙,uid:{}", uid); + throw new GlobalException("对方正忙"); + } + // 保存rtc session + String key = getWebRtcSessionKey(session.getUserId(), uid); + redisTemplate.opsForValue().set(key, webrtcSession, 60, TimeUnit.SECONDS); + // 设置用户忙线状态 + userStateUtils.setBusy(uid); + userStateUtils.setBusy(session.getUserId()); + // 向对方所有终端发起呼叫 + PrivateMessageVO messageInfo = new PrivateMessageVO(); + MessageType messageType = + mode.equals(WebrtcMode.VIDEO.getValue()) ? MessageType.RTC_CALL_VIDEO : MessageType.RTC_CALL_VOICE; + messageInfo.setType(messageType.code()); + messageInfo.setRecvId(uid); + messageInfo.setSendId(session.getUserId()); + messageInfo.setContent(offer); + + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(uid); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + sendMessage.setData(messageInfo); + imClient.sendPrivateMessage(sendMessage); + + } + + @Override + public void accept(Long uid, @RequestBody String answer) { + UserSession session = SessionContext.getSession(); + log.info("接受通话,sid:{},uid:{}", session.getUserId(), uid); + // 查询webrtc会话 + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + // 更新接受者信息 + webrtcSession.setAcceptorId(session.getUserId()); + webrtcSession.setAcceptorTerminal(session.getTerminal()); + webrtcSession.setChatTimeStamp(System.currentTimeMillis()); + String key = getWebRtcSessionKey(session.getUserId(), uid); + redisTemplate.opsForValue().set(key, webrtcSession, 60, TimeUnit.SECONDS); + // 向发起人推送接受通话信令 + PrivateMessageVO messageInfo = new PrivateMessageVO(); + messageInfo.setType(MessageType.RTC_ACCEPT.code()); + messageInfo.setRecvId(uid); + messageInfo.setSendId(session.getUserId()); + messageInfo.setContent(answer); + + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(uid); + // 告知其他终端已经接受会话,中止呼叫 + sendMessage.setSendToSelf(true); + sendMessage.setSendResult(false); + sendMessage.setRecvTerminals((Collections.singletonList(webrtcSession.getCallerTerminal()))); + sendMessage.setData(messageInfo); + imClient.sendPrivateMessage(sendMessage); + } + + @Override + public void reject(Long uid) { + UserSession session = SessionContext.getSession(); + log.info("拒绝通话,sid:{},uid:{}", session.getUserId(), uid); + // 查询webrtc会话 + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + // 删除会话信息 + removeWebrtcSession(uid, session.getUserId()); + // 设置用户空闲状态 + userStateUtils.setFree(uid); + userStateUtils.setFree(session.getUserId()); + // 向发起人推送拒绝通话信令 + PrivateMessageVO messageInfo = new PrivateMessageVO(); + messageInfo.setType(MessageType.RTC_REJECT.code()); + messageInfo.setRecvId(uid); + messageInfo.setSendId(session.getUserId()); + + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(uid); + // 告知其他终端已经拒绝会话,中止呼叫 + sendMessage.setSendToSelf(true); + sendMessage.setSendResult(false); + sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal())); + sendMessage.setData(messageInfo); + imClient.sendPrivateMessage(sendMessage); + // 生成通话消息 + sendActMessage(webrtcSession, MessageStatus.READED, "已拒绝"); + } + + @Override + public void cancel(Long uid) { + UserSession session = SessionContext.getSession(); + log.info("取消通话,sid:{},uid:{}", session.getUserId(), uid); + // 查询webrtc会话 + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + // 删除会话信息 + removeWebrtcSession(session.getUserId(), uid); + // 设置用户空闲状态 + userStateUtils.setFree(uid); + userStateUtils.setFree(session.getUserId()); + // 向对方所有终端推送取消通话信令 + PrivateMessageVO messageInfo = new PrivateMessageVO(); + messageInfo.setType(MessageType.RTC_CANCEL.code()); + messageInfo.setRecvId(uid); + messageInfo.setSendId(session.getUserId()); + + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(uid); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + sendMessage.setData(messageInfo); + // 通知对方取消会话 + imClient.sendPrivateMessage(sendMessage); + // 生成通话消息 + sendActMessage(webrtcSession, MessageStatus.PENDING, "已取消"); + } + + @Override + public void failed(Long uid, String reason) { + UserSession session = SessionContext.getSession(); + log.info("通话失败,sid:{},uid:{},reason:{}", session.getUserId(), uid, reason); + // 查询webrtc会话 + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + // 删除会话信息 + removeWebrtcSession(uid, session.getUserId()); + // 设置用户空闲状态 + userStateUtils.setFree(uid); + userStateUtils.setFree(session.getUserId()); + // 向发起方推送通话失败信令 + PrivateMessageVO messageInfo = new PrivateMessageVO(); + messageInfo.setType(MessageType.RTC_FAILED.code()); + messageInfo.setRecvId(uid); + messageInfo.setSendId(session.getUserId()); + messageInfo.setContent(reason); + + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(uid); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal())); + sendMessage.setData(messageInfo); + // 通知对方取消会话 + imClient.sendPrivateMessage(sendMessage); + // 生成消息 + sendActMessage(webrtcSession, MessageStatus.READED, "未接通"); + } + + @Override + public void handup(Long uid) { + UserSession session = SessionContext.getSession(); + log.info("挂断通话,sid:{},uid:{}", session.getUserId(), uid); + // 查询webrtc会话 + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + // 删除会话信息 + removeWebrtcSession(uid, session.getUserId()); + // 设置用户空闲状态 + userStateUtils.setFree(uid); + userStateUtils.setFree(session.getUserId()); + // 向对方推送挂断通话信令 + PrivateMessageVO messageInfo = new PrivateMessageVO(); + messageInfo.setType(MessageType.RTC_HANDUP.code()); + messageInfo.setRecvId(uid); + messageInfo.setSendId(session.getUserId()); + + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(uid); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + Integer terminal = getTerminalType(uid, webrtcSession); + sendMessage.setRecvTerminals(Collections.singletonList(terminal)); + sendMessage.setData(messageInfo); + // 通知对方取消会话 + imClient.sendPrivateMessage(sendMessage); + // 生成通话消息 + sendActMessage(webrtcSession, MessageStatus.READED, "通话时长 " + chatTimeText(webrtcSession)); + } + + @Override + public void candidate(Long uid, String candidate) { + UserSession session = SessionContext.getSession(); + // 查询webrtc会话 + WebrtcPrivateSession webrtcSession = getWebrtcSession(session.getUserId(), uid); + // 向发起方推送同步candidate信令 + PrivateMessageVO messageInfo = new PrivateMessageVO(); + messageInfo.setType(MessageType.RTC_CANDIDATE.code()); + messageInfo.setRecvId(uid); + messageInfo.setSendId(session.getUserId()); + messageInfo.setContent(candidate); + + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setRecvId(uid); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(false); + Integer terminal = getTerminalType(uid, webrtcSession); + sendMessage.setRecvTerminals(Collections.singletonList(terminal)); + sendMessage.setData(messageInfo); + imClient.sendPrivateMessage(sendMessage); + } + + @Override + public void heartbeat(Long uid) { + UserSession session = SessionContext.getSession(); + // 会话续命 + String key = getWebRtcSessionKey(session.getUserId(), uid); + redisTemplate.expire(key, 60, TimeUnit.SECONDS); + // 用户状态续命 + userStateUtils.expire(session.getUserId()); + } + + private WebrtcPrivateSession getWebrtcSession(Long userId, Long uid) { + String key = getWebRtcSessionKey(userId, uid); + WebrtcPrivateSession webrtcSession = (WebrtcPrivateSession)redisTemplate.opsForValue().get(key); + if (webrtcSession == null) { + throw new GlobalException("通话已结束"); + } + return webrtcSession; + } + + private void removeWebrtcSession(Long userId, Long uid) { + String key = getWebRtcSessionKey(userId, uid); + redisTemplate.delete(key); + } + + private String getWebRtcSessionKey(Long id1, Long id2) { + Long minId = id1 > id2 ? id2 : id1; + Long maxId = id1 > id2 ? id1 : id2; + return String.join(":", RedisKey.IM_WEBRTC_PRIVATE_SESSION, minId.toString(), maxId.toString()); + } + + private Integer getTerminalType(Long uid, WebrtcPrivateSession webrtcSession) { + if (uid.equals(webrtcSession.getCallerId())) { + return webrtcSession.getCallerTerminal(); + } + return webrtcSession.getAcceptorTerminal(); + } + + private void sendActMessage(WebrtcPrivateSession rtcSession, MessageStatus status, String content) { + // 保存消息 + PrivateMessage msg = new PrivateMessage(); + msg.setSendId(rtcSession.getCallerId()); + msg.setRecvId(rtcSession.getAcceptorId()); + msg.setContent(content); + msg.setSendTime(new Date()); + msg.setStatus(status.code()); + MessageType type = rtcSession.getMode().equals(WebrtcMode.VIDEO.getValue()) ? MessageType.ACT_RT_VIDEO + : MessageType.ACT_RT_VOICE; + msg.setType(type.code()); + privateMessageService.save(msg); + // 推给发起人 + PrivateMessageVO messageInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(rtcSession.getCallerId(), rtcSession.getCallerTerminal())); + sendMessage.setRecvId(rtcSession.getCallerId()); + sendMessage.setSendToSelf(false); + sendMessage.setSendResult(true); + sendMessage.setData(messageInfo); + imClient.sendPrivateMessage(sendMessage); + // 推给接听方 + sendMessage.setRecvId(rtcSession.getAcceptorId()); + imClient.sendPrivateMessage(sendMessage); + } + + private String chatTimeText(WebrtcPrivateSession rtcSession) { + long chatTime = (System.currentTimeMillis() - rtcSession.getChatTimeStamp()) / 1000; + int min = Math.abs((int)chatTime / 60); + int sec = Math.abs((int)chatTime % 60); + String strTime = min < 10 ? "0" : ""; + strTime += min; + strTime += ":"; + strTime += sec < 10 ? "0" : ""; + strTime += sec; + return strTime; + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/session/SessionContext.java b/im-platform/src/main/java/com/bx/implatform/session/SessionContext.java new file mode 100644 index 0000000..ce61dbb --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/session/SessionContext.java @@ -0,0 +1,21 @@ +package com.bx.implatform.session; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/* + * @Description + * @Author Blue + * @Date 2022/10/21 + */ +public class SessionContext { + + public static UserSession getSession() { + // 从请求上下文里获取Request对象 + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = requestAttributes.getRequest(); + return (UserSession) request.getAttribute("session"); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/session/UserSession.java b/im-platform/src/main/java/com/bx/implatform/session/UserSession.java new file mode 100644 index 0000000..8b8815f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/session/UserSession.java @@ -0,0 +1,20 @@ +package com.bx.implatform.session; + +import com.bx.imcommon.model.IMSessionInfo; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class UserSession extends IMSessionInfo { + + /** + * 用户名称 + */ + private String userName; + + /** + * 用户昵称 + */ + private String nickName; +} diff --git a/im-platform/src/main/java/com/bx/implatform/session/WebrtcGroupSession.java b/im-platform/src/main/java/com/bx/implatform/session/WebrtcGroupSession.java new file mode 100644 index 0000000..098c724 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/session/WebrtcGroupSession.java @@ -0,0 +1,33 @@ +package com.bx.implatform.session; + +import com.bx.imcommon.model.IMUserInfo; +import lombok.Data; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author: Blue + * @date: 2024-06-01 + * @version: 1.0 + */ +@Data +public class WebrtcGroupSession { + + /** + * 通话发起者 + */ + private IMUserInfo host; + + /** + * 所有被邀请的用户列表 + */ + private List userInfos; + + /** + * 已经进入通话的用户列表 + */ + private List inChatUsers = new LinkedList<>(); + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/session/WebrtcPrivateSession.java b/im-platform/src/main/java/com/bx/implatform/session/WebrtcPrivateSession.java new file mode 100644 index 0000000..3d74cd1 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/session/WebrtcPrivateSession.java @@ -0,0 +1,39 @@ +package com.bx.implatform.session; + +import lombok.Data; + +/* + * webrtc 会话信息 + * @Author Blue + * @Date 2022/10/21 + */ +@Data +public class WebrtcPrivateSession { + /** + * 发起者id + */ + private Long callerId; + /** + * 发起者终端类型 + */ + private Integer callerTerminal; + + /** + * 接受者id + */ + private Long acceptorId; + + /** + * 接受者终端类型 + */ + private Integer acceptorTerminal; + + /** + * 通话模式 + */ + private String mode; + /** + * 开始聊天时间戳 + */ + private Long chatTimeStamp; +} diff --git a/im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java b/im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java new file mode 100644 index 0000000..03c6b2d --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/session/WebrtcUserInfo.java @@ -0,0 +1,28 @@ +package com.bx.implatform.session; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author: Blue + * @date: 2024-06-02 + * @version: 1.0 + */ +@Data +@Schema(description = "用户信息") +public class WebrtcUserInfo { + @Schema(description = "用户id") + private Long id; + + @Schema(description = "用户昵称") + private String nickName; + + @Schema(description = "用户头像") + private String headImage; + + @Schema(description = "是否开启摄像头") + private Boolean isCamera; + + @Schema(description = "是否开启麦克风") + private Boolean isMicroPhone; +} diff --git a/im-platform/src/main/java/com/bx/implatform/task/consumer/GroupBannedConsumerTask.java b/im-platform/src/main/java/com/bx/implatform/task/consumer/GroupBannedConsumerTask.java new file mode 100644 index 0000000..44712d9 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/task/consumer/GroupBannedConsumerTask.java @@ -0,0 +1,68 @@ +package com.bx.implatform.task.consumer; + +import com.bx.imclient.IMClient; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.model.IMGroupMessage; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.mq.RedisMQConsumer; +import com.bx.imcommon.mq.RedisMQListener; +import com.bx.implatform.contant.Constant; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.GroupBanDTO; +import com.bx.implatform.entity.GroupMessage; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.service.GroupMemberService; +import com.bx.implatform.service.GroupMessageService; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.vo.GroupMessageVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; + +/** + * @author: Blue + * @date: 2024-07-15 + * @version: 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@RedisMQListener(queue = RedisKey.IM_QUEUE_GROUP_BANNED) +public class GroupBannedConsumerTask extends RedisMQConsumer { + + private final IMClient imClient; + + private final GroupMessageService groupMessageService; + + private final GroupMemberService groupMemberService; + + @Override + public void onMessage(GroupBanDTO dto) { + log.info("群聊被封禁处理,群id:{},原因:{}", dto.getId(), dto.getReason()); + // 群聊成员列表 + List userIds = groupMemberService.findUserIdsByGroupId(dto.getId()); + // 保存消息 + GroupMessage msg = new GroupMessage(); + msg.setGroupId(dto.getId()); + String tip = "本群聊已被管理员封禁,原因:" + dto.getReason(); + msg.setContent(tip); + msg.setSendId(Constant.SYS_USER_ID); + msg.setSendTime(new Date()); + msg.setStatus(MessageStatus.PENDING.code()); + msg.setSendNickName("系统管理员"); + msg.setType(MessageType.TIP_TEXT.code()); + groupMessageService.save(msg); + // 推送提示语到群聊中 + GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(Constant.SYS_USER_ID, IMTerminalType.PC.code())); + sendMessage.setRecvIds(userIds); + sendMessage.setSendToSelf(false); + sendMessage.setData(msgInfo); + imClient.sendGroupMessage(sendMessage); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/task/consumer/GroupUnbanConsumerTask.java b/im-platform/src/main/java/com/bx/implatform/task/consumer/GroupUnbanConsumerTask.java new file mode 100644 index 0000000..ec7c2da --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/task/consumer/GroupUnbanConsumerTask.java @@ -0,0 +1,67 @@ +package com.bx.implatform.task.consumer; + +import com.bx.imclient.IMClient; +import com.bx.imcommon.enums.IMTerminalType; +import com.bx.imcommon.model.IMGroupMessage; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.mq.RedisMQConsumer; +import com.bx.imcommon.mq.RedisMQListener; +import com.bx.implatform.contant.Constant; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.GroupUnbanDTO; +import com.bx.implatform.entity.GroupMessage; +import com.bx.implatform.enums.MessageStatus; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.service.GroupMemberService; +import com.bx.implatform.service.GroupMessageService; +import com.bx.implatform.util.BeanUtils; +import com.bx.implatform.vo.GroupMessageVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; + +/** + * @author: Blue + * @date: 2024-07-15 + * @version: 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@RedisMQListener(queue = RedisKey.IM_QUEUE_GROUP_UNBAN) +public class GroupUnbanConsumerTask extends RedisMQConsumer { + + private final IMClient imClient; + + private final GroupMessageService groupMessageService; + + private final GroupMemberService groupMemberService; + + @Override + public void onMessage(GroupUnbanDTO dto) { + log.info("群聊解除封禁处理,群id:{}",dto.getId()); + // 群聊成员列表 + List userIds = groupMemberService.findUserIdsByGroupId(dto.getId()); + // 保存消息 + GroupMessage msg = new GroupMessage(); + msg.setGroupId(dto.getId()); + msg.setContent("已解除封禁"); + msg.setSendId(Constant.SYS_USER_ID); + msg.setSendTime(new Date()); + msg.setStatus(MessageStatus.PENDING.code()); + msg.setSendNickName("系统管理员"); + msg.setType(MessageType.TIP_TEXT.code()); + groupMessageService.save(msg); + // 推送提示语到群聊中 + GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(Constant.SYS_USER_ID, IMTerminalType.PC.code())); + sendMessage.setRecvIds(userIds); + sendMessage.setSendToSelf(false); + sendMessage.setData(msgInfo); + imClient.sendGroupMessage(sendMessage); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/task/consumer/UserBannedConsumerTask.java b/im-platform/src/main/java/com/bx/implatform/task/consumer/UserBannedConsumerTask.java new file mode 100644 index 0000000..c84b986 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/task/consumer/UserBannedConsumerTask.java @@ -0,0 +1,42 @@ +package com.bx.implatform.task.consumer; + +import com.bx.imclient.IMClient; +import com.bx.imcommon.model.IMSystemMessage; +import com.bx.imcommon.mq.RedisMQConsumer; +import com.bx.imcommon.mq.RedisMQListener; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.UserBanDTO; +import com.bx.implatform.enums.MessageType; +import com.bx.implatform.vo.SystemMessageVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Collections; + +/** + * @author: Blue + * @date: 2024-07-15 + * @version: 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@RedisMQListener(queue = RedisKey.IM_QUEUE_USER_BANNED) +public class UserBannedConsumerTask extends RedisMQConsumer { + + private final IMClient imClient; + @Override + public void onMessage(UserBanDTO dto) { + log.info("用户被封禁处理,userId:{},原因:{}",dto.getId(),dto.getReason()); + // 推送消息将用户赶下线 + SystemMessageVO msgInfo = new SystemMessageVO(); + msgInfo.setType(MessageType.USER_BANNED.code()); + msgInfo.setContent(dto.getReason()); + IMSystemMessage sendMessage = new IMSystemMessage<>(); + sendMessage.setRecvIds(Collections.singletonList(dto.getId())); + sendMessage.setData(msgInfo); + sendMessage.setSendResult(true); + imClient.sendSystemMessage(sendMessage); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java b/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java new file mode 100644 index 0000000..babcca4 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java @@ -0,0 +1,78 @@ +package com.bx.implatform.task.schedule; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bx.implatform.annotation.RedisLock; +import com.bx.implatform.config.props.MinioProperties; +import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.entity.FileInfo; +import com.bx.implatform.service.FileService; +import com.bx.implatform.thirdparty.MinioService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; + +/** + * 过期文件清理任务 + * + * @author Blue + * @version 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FileExpireTask { + + private final FileService fileService; + private final MinioService minioService; + private final MinioProperties minioProps; + + @RedisLock(prefixKey = RedisKey.IM_LOCK_FILE_TASK) + @Scheduled(cron = "0 0 3 * * ?") + public void run() { + log.info("【定时任务】过期文件处理..."); + int batchSize = 100; + List files = loadBatch(batchSize); + while (true) { + for (FileInfo fileInfo : files) { + String url = fileInfo.getFilePath(); + String relativePath = url.substring(fileInfo.getFilePath().indexOf(minioProps.getBucketName())); + String[] arr = relativePath.split("/"); + String bucket = minioProps.getBucketName(); + String path = arr[1]; + String fileNme = StrUtil.join("/", arr[2], arr[3]); + if (minioService.isExist(bucket, path, fileNme)) { + if (!minioService.remove(bucket, path, fileNme)) { + // 删除失败,不再往下执行 + log.error("删除过期文件异常, id:{},文件名:{}", fileInfo.getId(), fileInfo.getFileName()); + return; + } + // 删除文件信息 + fileService.removeById(fileInfo.getId()); + } + } + if (files.size() < batchSize) { + break; + } + // 下一批 + files = loadBatch(batchSize); + } + } + + List loadBatch(int size) { + Date minDate = DateUtils.addDays(new Date(), -minioProps.getExpireIn()); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FileInfo::getIsPermanent, false); + wrapper.le(FileInfo::getUploadTime, minDate); + wrapper.orderByAsc(FileInfo::getId); + wrapper.last("limit " + size); + return fileService.list(wrapper); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/task/schedule/ReloadSensitiveWordTask.java b/im-platform/src/main/java/com/bx/implatform/task/schedule/ReloadSensitiveWordTask.java new file mode 100644 index 0000000..2ff15ac --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/task/schedule/ReloadSensitiveWordTask.java @@ -0,0 +1,26 @@ +package com.bx.implatform.task.schedule; + +import com.bx.implatform.util.SensitiveFilterUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * @author: Blue + * @date: 2024-09-01 + * @version: 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ReloadSensitiveWordTask { + + private final SensitiveFilterUtil sensitiveFilterUtil; + + @Scheduled(fixedRate = 60000) + public void run() { + log.info("【定时任务】重新装载敏感词..."); + sensitiveFilterUtil.reload(); + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java b/im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java new file mode 100644 index 0000000..d5ad8da --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java @@ -0,0 +1,156 @@ +package com.bx.implatform.thirdparty; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import com.bx.implatform.util.DateTimeUtils; +import com.bx.implatform.util.FileUtil; +import io.minio.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Date; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MinioService { + + private final MinioClient minioClient; + + /** + * 查看存储bucket是否存在 + * + * @return boolean + */ + public Boolean bucketExists(String bucketName) { + try { + return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + } catch (Exception e) { + log.error("查询bucket失败", e); + return false; + } + } + + /** + * 创建存储bucket + */ + public void makeBucket(String bucketName) { + try { + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + } catch (Exception e) { + log.error("创建bucket失败,", e); + } + } + + /** + * 设置bucket权限为public + */ + public void setBucketPublic(String bucketName) { + try { + // 设置公开 + String sb = + "{\"Version\":\"2012-10-17\"," + "\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":" + "{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\"," + "\"s3:GetBucketLocation\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"; + minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(sb).build()); + } catch (Exception e) { + log.error("创建bucket失败,", e); + } + } + + /** + * 文件上传 + * + * @param bucketName bucket名称 + * @param path 路径 + * @param file 文件 + * @return Boolean + */ + public String upload(String bucketName, String path, MultipartFile file) { + String originalFilename = file.getOriginalFilename(); + if (StringUtils.isBlank(originalFilename)) { + throw new RuntimeException(); + } + // 文件名使用雪花算法生成,防止重名以及出现一些特殊字符 + String fileName = IdUtil.getSnowflakeNextIdStr() + "." + FileUtil.getFileExtension(originalFilename); + String objectName = DateTimeUtils.getFormatDate(new Date(), DateTimeUtils.PARTDATEFORMAT) + "/" + fileName; + try { + InputStream stream = new ByteArrayInputStream(file.getBytes()); + PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(path + "/" + objectName) + .stream(stream, file.getSize(), -1).contentType(file.getContentType()).build(); + //文件名称相同会覆盖 + minioClient.putObject(objectArgs); + } catch (Exception e) { + log.error("上传图片失败,", e); + return null; + } + return objectName; + } + + /** + * 文件上传 + * + * @param bucketName bucket名称 + * @param path 路径 + * @param name 文件名 + * @param fileByte 文件内容 + * @param contentType contentType + * @return objectName + */ + public String upload(String bucketName, String path, String name, byte[] fileByte, String contentType) { + // 文件名使用雪花算法生成,防止重名以及出现一些特殊字符 + String fileName = IdUtil.getSnowflakeNextIdStr() + "." + FileUtil.getFileExtension(name); + String objectName = DateTimeUtils.getFormatDate(new Date(), DateTimeUtils.PARTDATEFORMAT) + "/" + fileName; + try { + InputStream stream = new ByteArrayInputStream(fileByte); + PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(path + "/" + objectName) + .stream(stream, fileByte.length, -1).contentType(contentType).build(); + //文件名称相同会覆盖 + minioClient.putObject(objectArgs); + } catch (Exception e) { + log.error("上传文件失败,", e); + return null; + } + return objectName; + } + + /** + * 删除 + * + * @param bucketName bucket名称 + * @param path 路径 + * @param fileName 文件名 + * @return true/false + */ + public boolean remove(String bucketName, String path, String fileName) { + try { + minioClient.removeObject( + RemoveObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build()); + } catch (Exception e) { + log.error("删除文件失败,", e); + return false; + } + return true; + } + + /** + * 判断文件是否存在 + * + * @param bucketName bucket名称 + * @param path 路径 + * @param fileName 文件名 + * @return + */ + public Boolean isExist(String bucketName, String path, String fileName) { + try { + minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build()); + } catch (Exception e) { + return false; + } + return true; + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/util/BeanUtils.java b/im-platform/src/main/java/com/bx/implatform/util/BeanUtils.java new file mode 100644 index 0000000..f532292 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/BeanUtils.java @@ -0,0 +1,45 @@ +package com.bx.implatform.util; + +import org.springframework.util.ReflectionUtils; + +public final class BeanUtils { + + private BeanUtils() { + } + + private static void handleReflectionException(Exception e) { + ReflectionUtils.handleReflectionException(e); + } + + + /** + * 属性拷贝 + * + * @param orig 源对象 + * @param destClass 目标 + * @return T + */ + public static T copyProperties(Object orig, Class destClass) { + try { + T target = destClass.newInstance(); + if (orig == null) { + return null; + } + copyProperties(orig, target); + return target; + } catch (Exception e) { + handleReflectionException(e); + return null; + } + } + + + public static void copyProperties(Object orig, Object dest) { + try { + org.springframework.beans.BeanUtils.copyProperties(orig, dest); + } catch (Exception e) { + handleReflectionException(e); + } + } + +} \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/util/DateTimeUtils.java b/im-platform/src/main/java/com/bx/implatform/util/DateTimeUtils.java new file mode 100644 index 0000000..455ea23 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/DateTimeUtils.java @@ -0,0 +1,35 @@ +package com.bx.implatform.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 日期处理工具类 + * + * @version 1.0 + */ +public final class DateTimeUtils extends DateUtils { + + public static final String FULL_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String PARTDATEFORMAT = "yyyyMMdd"; + + + /** + * 将日期类型转换为字符串 + * + * @param date 日期 + * @param xFormat 格式 + * @return 日期 + */ + public static String getFormatDate(Date date, String xFormat) { + date = date == null ? new Date() : date; + xFormat = StringUtils.isNotEmpty(xFormat) ? xFormat : FULL_DATE_FORMAT; + SimpleDateFormat sdf = new SimpleDateFormat(xFormat); + return sdf.format(date); + } + + +} \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/util/FileUtil.java b/im-platform/src/main/java/com/bx/implatform/util/FileUtil.java new file mode 100644 index 0000000..89aeddd --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/FileUtil.java @@ -0,0 +1,43 @@ +package com.bx.implatform.util; + +public final class FileUtil { + + + /** + * 获取文件后缀 + * + * @param fileName 文件名 + * @return boolean + */ + public static String getFileExtension(String fileName) { + return fileName.substring(fileName.lastIndexOf(".") + 1); + } + + /** + * 去除文件扩展名 + * + * @param fileName 文件名 + * @return + */ + public static String excludeExtension(String fileName) { + return fileName.substring(0,fileName.lastIndexOf(".")); + } + + /** + * 判断文件是否图片类型 + * + * @param fileName 文件名 + * @return boolean + */ + public static boolean isImage(String fileName) { + String extension = getFileExtension(fileName); + String[] imageExtension = new String[]{"jpeg", "jpg", "bmp", "png", "webp", "gif"}; + for (String e : imageExtension) { + if (extension.toLowerCase().equals(e)) { + return true; + } + } + + return false; + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/util/ImageUtil.java b/im-platform/src/main/java/com/bx/implatform/util/ImageUtil.java new file mode 100644 index 0000000..5c08170 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/ImageUtil.java @@ -0,0 +1,78 @@ +package com.bx.implatform.util; + +import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +@Slf4j +public final class ImageUtil { + + //以下是常量,按照阿里代码开发规范,不允许代码中出现魔法值 + private static final Integer ZERO = 0; + private static final Integer ONE_ZERO_TWO_FOUR = 1024; + private static final Integer NINE_ZERO_ZERO = 900; + private static final Integer THREE_TWO_SEVEN_FIVE = 3275; + private static final Integer TWO_ZERO_FOUR_SEVEN = 2047; + private static final Double ZERO_EIGHT_FIVE = 0.85; + private static final Double ZERO_SIX = 0.6; + private static final Double ZERO_FOUR_FOUR = 0.44; + private static final Double ZERO_FOUR = 0.4; + + /** + * 根据指定大小压缩图片 + * + * @param imageBytes 源图片字节数组 + * @param desFileSize 指定图片大小,单位kb + * @return 压缩质量后的图片字节数组 + */ + public static byte[] compressForScale(byte[] imageBytes, long desFileSize) { + if (imageBytes == null || imageBytes.length <= ZERO || imageBytes.length < desFileSize * ONE_ZERO_TWO_FOUR) { + return imageBytes; + } + long srcSize = imageBytes.length; + double accuracy = getAccuracy(srcSize / ONE_ZERO_TWO_FOUR); + try { + while (imageBytes.length > desFileSize * ONE_ZERO_TWO_FOUR) { + ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(imageBytes.length); + Thumbnails.of(inputStream) + .scale(accuracy) + .outputQuality(accuracy) + .toOutputStream(outputStream); + imageBytes = outputStream.toByteArray(); + } + log.info("图片原大小={}kb | 压缩后大小={}kb", + srcSize / ONE_ZERO_TWO_FOUR, imageBytes.length / ONE_ZERO_TWO_FOUR); + } catch (Exception e) { + log.error("【图片压缩】msg=图片压缩失败!", e); + } + return imageBytes; + } + + + /** + * 自动调节精度(经验数值) + * + * @param size 源图片大小 + * @return 图片压缩质量比 + */ + private static double getAccuracy(long size) { + double accuracy; + if (size < NINE_ZERO_ZERO) { + accuracy = ZERO_EIGHT_FIVE; + } else if (size < TWO_ZERO_FOUR_SEVEN) { + accuracy = ZERO_SIX; + } else if (size < THREE_TWO_SEVEN_FIVE) { + accuracy = ZERO_FOUR_FOUR; + } else { + accuracy = ZERO_FOUR; + } + return accuracy; + } + +} + + + diff --git a/im-platform/src/main/java/com/bx/implatform/util/SensitiveFilterUtil.java b/im-platform/src/main/java/com/bx/implatform/util/SensitiveFilterUtil.java new file mode 100644 index 0000000..c3e83d7 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/SensitiveFilterUtil.java @@ -0,0 +1,207 @@ +package com.bx.implatform.util; + +import cn.hutool.core.util.StrUtil; +import com.bx.imcommon.util.ThreadPoolExecutorFactory; +import com.bx.implatform.service.SensitiveWordService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +/** + * 敏感词过滤器——SensitiveFilter + * + * @author Andrews + * @date 2023/12/4 11:12 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public final class SensitiveFilterUtil { + + /** + * 替换符 + */ + private static final String REPLACE_MENT = "***"; + + /** + * 根节点 + */ + private static TrieNode ROOT_NODE = new TrieNode(); + + /** + * 线程池 + */ + private static final ScheduledThreadPoolExecutor EXECUTOR_SERVICE = + ThreadPoolExecutorFactory.getThreadPoolExecutor(); + + private final SensitiveWordService sensitiveWordService; + + /** + * 1、 前缀树 前缀树某一个节点 + * + * @author NXY + * @date 2023/12/4 11:17 + */ + private static class TrieNode { + // 关键词结束标识 + private boolean isKeywordEnd = false; + + // 子节点(key是下级字符,value是下级节点) + // 当前节点的子节点 + private final Map subNodes = new HashMap<>(); + + public boolean isKeywordEnd() { + return isKeywordEnd; + } + + public void setKeywordEnd(boolean keywordEnd) { + isKeywordEnd = keywordEnd; + } + + // 添加子节点 + public void addSubNode(Character c, TrieNode node) { + subNodes.put(c, node); + } + + // 获取子节点 + public TrieNode getSubNode(Character c) { + return subNodes.get(c); + } + + } + + /** + * 2、初始化方法,服务器启动时初始化 + * + * @author NXY + * @date 2023/12/4 11:18 + */ + @PostConstruct + public void reload() { + // 使用copy on write的方式,防止出现并发问题 + TrieNode newNode = new TrieNode(); + List keywords = sensitiveWordService.findAllEnabledWords(); + keywords.forEach(keyword -> { + if (StrUtil.isNotEmpty(keyword)) { + // 添加到前缀树 + addKeyword(newNode,keyword); + } + }); + ROOT_NODE = newNode; + } + + /** + * 3、将一个敏感词添加到前缀树中 + * + * @param node + * @param keyword + * @author NXY + * @date 2023/12/4 11:15 + */ + private void addKeyword(TrieNode node, String keyword) { + for (int i = 0; i < keyword.length(); i++) { + char c = keyword.charAt(i); + TrieNode subNode = node.getSubNode(c); + if (subNode == null) { + // 初始化子节点 + subNode = new TrieNode(); + node.addSubNode(c, subNode); + } + // 指向子节点,进入下一轮循环 + node = subNode; + // 设置结束标识 + if (i == keyword.length() - 1) { + node.setKeywordEnd(true); + } + } + } + + /** + * 过滤敏感词 + * + * @param text 待过滤的文本 + * @return 过滤后的文本 + */ + public String filter(String text) { + if (StringUtils.isBlank(text)) { + return null; + } + // 结果 + StringBuilder sb = new StringBuilder(); + try { + // 指针1 + TrieNode tempNode = ROOT_NODE; + // 指针2 + int begin = 0; + // 指针3 + int position = 0; + while (begin < text.length()) { + if (position < text.length()) { + char c = text.charAt(position); + // 跳过符号 + if (isSymbol(c)) { + // 若指针1处于根节点,将此符号计入结果,让指针2向下走一步 + if (tempNode == ROOT_NODE) { + sb.append(c); + begin++; + } + // 无论符号在开头或中间,指针3都向下走一步 + position++; + continue; + } + // 检查下级节点 + tempNode = tempNode.getSubNode(c); + if (tempNode == null) { + // 以begin开头的字符串不是敏感词 + sb.append(text.charAt(begin)); + // 进入下一个位置 + position = ++begin; + // 重新指向根节点 + tempNode = ROOT_NODE; + } else if (tempNode.isKeywordEnd()) { + // 发现敏感词,将begin~position字符串替换掉 + sb.append(REPLACE_MENT); + // 进入下一个位置 + begin = ++position; + // 重新指向根节点 + tempNode = ROOT_NODE; + } else { + // 检查下一个字符 + position++; + } + } + // position遍历越界仍未匹配到敏感词 + else { + sb.append(text.charAt(begin)); + position = ++begin; + tempNode = ROOT_NODE; + } + } + } catch (Exception e) { + sb = new StringBuilder(text); + } + return sb.toString(); + } + + /** + * 判断是否为符号 ——特殊符号 + * + * @return boolean + * @author NXY + * @date 2023/12/4 11:17 + */ + private boolean isSymbol(Character c) { + // 0x2E80~0x9FFF 是东亚文字范围 + return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF); + } +} + + diff --git a/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java b/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java new file mode 100644 index 0000000..567737e --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/UserStateUtils.java @@ -0,0 +1,44 @@ +package com.bx.implatform.util; + +import cn.hutool.core.util.StrUtil; +import com.bx.implatform.contant.RedisKey; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * @author: Blue + * @date: 2024-06-10 + * @version: 1.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class UserStateUtils { + + private final RedisTemplate redisTemplate; + + public void setBusy(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.opsForValue().set(key,1,30, TimeUnit.SECONDS); + } + + public void expire(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.expire(key,30, TimeUnit.SECONDS); + } + + public void setFree(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + redisTemplate.delete(key); + } + + public Boolean isBusy(Long userId){ + String key = StrUtil.join(":", RedisKey.IM_USER_STATE,userId); + return redisTemplate.hasKey(key); + } + +} diff --git a/im-platform/src/main/java/com/bx/implatform/util/XssUtil.java b/im-platform/src/main/java/com/bx/implatform/util/XssUtil.java new file mode 100644 index 0000000..8178b09 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/util/XssUtil.java @@ -0,0 +1,21 @@ +package com.bx.implatform.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class XssUtil { + + private XssUtil() { + } + + private static final String XSS_PATTERN = "((?i))|((?i)alert\\s*\\()|((?i)prompt\\s*\\()|((?i)document\\.cookie)|((?i)location\\.href)|((?i)window\\.location)|((?i)onerror\\s*\\()|((?i)eval\\s*\\()|((?i)window\\.open\\s*\\()|((?i)innerHTML)|((?i)onclick\\s*\\()|((?i)onmouseover\\s*\\()|((?i)onsubmit\\s*\\()|((?i)onload\\s*\\()|((?i)onfocus\\s*\\()|((?i)onblur\\s*\\()|((?i)onkeyup\\s*\\()|((?i)onkeydown\\s*\\()|((?i)onkeypress\\s*\\()|((?i)onmouseout\\s*\\()|((?i)src=)|((?i)href=)|((?i)style=)|((?i)background=)|((?i)expression\\s*\\()|((?i)XMLHttpRequest\\s*\\()|((?i)ActiveXObject\\s*\\()|((?i)iframe)|((?i)document\\.write\\s*\\()|((?i)document\\.writeln\\s*\\()|((?i)setTimeout\\s*\\()|((?i)setInterval\\s*\\()|((?i)onreadystatechange\\s*\\()|((?i)appendChild\\s*\\()|((?i)createTextNode\\s*\\()|((?i)createElement\\s*\\()|((?i)getElementsByTagName\\s*\\()|((?i)getElementsByClassName\\s*\\()|((?i)querySelector\\s*\\()|((?i)querySelectorAll\\s*\\()|((?i)document\\.location)|((?i)document\\.body\\.innerHTML)|((?i)document\\.forms)|((?i)document\\.images)|((?i)document\\.links)|((?i)document\\.URL)|((?i)document\\.domain)|((?i)document\\.referrer)|((?i)history\\.back\\s*\\()"; + private static final Pattern PATTERN = Pattern.compile(XSS_PATTERN); + + public static boolean checkXss(String inputString) { + if (inputString != null) { + Matcher matcher = PATTERN.matcher(inputString); + return matcher.find(); + } + return false; + } +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java b/im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java new file mode 100644 index 0000000..cf65b05 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java @@ -0,0 +1,29 @@ +package com.bx.implatform.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "好友信息VO") +public class FriendVO { + + @NotNull(message = "好友id不可为空") + @Schema(description = "好友id") + private Long id; + + @NotNull(message = "好友昵称不可为空") + @Schema(description = "好友昵称") + private String nickName; + + + @Schema(description = "好友头像") + private String headImage; + + @Schema(description = "是否开启免打扰") + private Boolean isDnd; + + + @Schema(description = "是否已删除") + private Boolean deleted; +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/GroupMemberVO.java b/im-platform/src/main/java/com/bx/implatform/vo/GroupMemberVO.java new file mode 100644 index 0000000..fd2a876 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/GroupMemberVO.java @@ -0,0 +1,34 @@ +package com.bx.implatform.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "群成员信息VO") +public class GroupMemberVO { + + @Schema(description = "用户id") + private Long userId; + + @Schema(description = "群内显示名称") + private String showNickName; + + @Schema(description = "群内昵称备注") + private String remarkNickName; + + @Schema(description = "头像") + private String headImage; + + @Schema(description = "是否已退出") + private Boolean quit; + + @Schema(description = "是否在线") + private Boolean online; + + @Schema(description = "群名显示名称") + private String showGroupName; + + @Schema(description = "群名备注") + private String remarkGroupName; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java b/im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java new file mode 100644 index 0000000..9613a1f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java @@ -0,0 +1,53 @@ +package com.bx.implatform.vo; + +import com.bx.imcommon.serializer.DateToLongSerializer; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +@Data +public class GroupMessageVO { + + @Schema(description = "消息id") + private Long id; + + @Schema(description = "临时id") + private String tmpId; + + @Schema(description = "群聊id") + private Long groupId; + + @Schema(description = " 发送者id") + private Long sendId; + + @Schema(description = " 发送者昵称") + private String sendNickName; + + @Schema(description = "消息内容") + private String content; + + @Schema(description = "消息内容类型 具体枚举值由应用层定义") + private Integer type; + + @Schema(description = "是否回执消息") + private Boolean receipt; + + @Schema(description = "回执消息是否完成") + private Boolean receiptOk; + + @Schema(description = "已读消息数量") + private Integer readedCount = 0; + + @Schema(description = "@用户列表") + private List atUserIds; + + @Schema(description = " 状态") + private Integer status; + + @Schema(description = "发送时间") + @JsonSerialize(using = DateToLongSerializer.class) + private Date sendTime; +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java b/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java new file mode 100644 index 0000000..1673ee2 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java @@ -0,0 +1,63 @@ +package com.bx.implatform.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Data +@Schema(description = "群信息VO") +public class GroupVO { + + @Schema(description = "群id") + private Long id; + + @Length(max = 20, message = "群名称长度不能大于20") + @NotEmpty(message = "群名称不可为空") + @Schema(description = "群名称") + private String name; + + @Schema(description = "群主id") + private Long ownerId; + + @Schema(description = "头像") + private String headImage; + + @Schema(description = "头像缩略图") + private String headImageThumb; + + @Length(max = 1024, message = "群聊显示长度不能大于1024") + @Schema(description = "群公告") + private String notice; + + @Length(max = 20, message = "显示昵称长度不能大于20") + @Schema(description = "用户在群显示昵称") + private String remarkNickName; + + @Schema(description = "群内显示名称") + private String showNickName; + + @Schema(description = "群名显示名称") + private String showGroupName; + + @Length(max = 20, message = "群备注长度不能大于20") + @Schema(description = "群名备注") + private String remarkGroupName; + + @Schema(description = "是否已解散") + private Boolean dissolve; + + @Schema(description = "是否已退出") + private Boolean quit; + + @Schema(description = "账号是否被封禁") + private Boolean isBanned; + + @Schema(description = "被封禁原因") + private String reason; + + @Schema(description = "是否开启免打扰") + private Boolean isDnd; + + +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/LoginVO.java b/im-platform/src/main/java/com/bx/implatform/vo/LoginVO.java new file mode 100644 index 0000000..ba9f9a2 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/LoginVO.java @@ -0,0 +1,22 @@ +package com.bx.implatform.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "用户登录VO") +public class LoginVO { + + @Schema(description = "每次请求都必须在header中携带accessToken") + private String accessToken; + + @Schema(description = "accessToken过期时间(秒)") + private Integer accessTokenExpiresIn; + + @Schema(description = "accessToken过期后,通过refreshToken换取新的token") + private String refreshToken; + + @Schema(description = "refreshToken过期时间(秒)") + private Integer refreshTokenExpiresIn; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/OnlineTerminalVO.java b/im-platform/src/main/java/com/bx/implatform/vo/OnlineTerminalVO.java new file mode 100644 index 0000000..da40ec6 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/OnlineTerminalVO.java @@ -0,0 +1,24 @@ +package com.bx.implatform.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +/** + * @author: Blue + * @date: 2023-10-28 21:17:59 + * @version: 1.0 + */ +@Data +@AllArgsConstructor +public class OnlineTerminalVO { + + @Schema(description = "用户id") + private Long userId; + + @Schema(description = "在线终端类型") + private List terminals; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java b/im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java new file mode 100644 index 0000000..7777a5f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java @@ -0,0 +1,38 @@ +package com.bx.implatform.vo; + +import com.bx.imcommon.serializer.DateToLongSerializer; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Data +@Schema(description = "私聊消息VO") +public class PrivateMessageVO { + + @Schema(description = " 消息id") + private Long id; + + @Schema(description = "临时id") + private String tmpId; + + @Schema(description = " 发送者id") + private Long sendId; + + @Schema(description = " 接收者id") + private Long recvId; + + @Schema(description = " 发送内容") + private String content; + + @Schema(description = "消息内容类型 MessageType") + private Integer type; + + @Schema(description = " 状态") + private Integer status; + + @Schema(description = " 发送时间") + @JsonSerialize(using = DateToLongSerializer.class) + private Date sendTime; +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/SystemConfigVO.java b/im-platform/src/main/java/com/bx/implatform/vo/SystemConfigVO.java new file mode 100644 index 0000000..aa92709 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/SystemConfigVO.java @@ -0,0 +1,21 @@ +package com.bx.implatform.vo; + +import com.bx.implatform.config.WebrtcConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * @author: blue + * @date: 2024-06-10 + * @version: 1.0 + */ +@Data +@Schema(description = "系统配置VO") +@AllArgsConstructor +public class SystemConfigVO { + + @Schema(description = "webrtc配置") + private WebrtcConfig webrtc; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/SystemMessageVO.java b/im-platform/src/main/java/com/bx/implatform/vo/SystemMessageVO.java new file mode 100644 index 0000000..d1ee164 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/SystemMessageVO.java @@ -0,0 +1,26 @@ +package com.bx.implatform.vo; + +import com.bx.imcommon.serializer.DateToLongSerializer; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Data +@Schema(description = "系统消息VO") +public class SystemMessageVO { + + @Schema(description = " 消息id") + private Long id; + + @Schema(description = " 发送内容") + private String content; + + @Schema(description = "消息内容类型 MessageType") + private Integer type; + + @Schema(description = " 发送时间") + @JsonSerialize(using = DateToLongSerializer.class) + private Date sendTime; +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/UploadImageVO.java b/im-platform/src/main/java/com/bx/implatform/vo/UploadImageVO.java new file mode 100644 index 0000000..671fe9a --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/UploadImageVO.java @@ -0,0 +1,21 @@ +package com.bx.implatform.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "图片上传VO") +public class UploadImageVO { + + @Schema(description = "原图") + private String originUrl; + + @Schema(description = "缩略图") + private String thumbUrl; + + @Schema(description = "图片宽度") + private int width; + + @Schema(description = "图片高度") + private int height; +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/UserVO.java b/im-platform/src/main/java/com/bx/implatform/vo/UserVO.java new file mode 100644 index 0000000..64ccae5 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/UserVO.java @@ -0,0 +1,51 @@ +package com.bx.implatform.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Data +@Schema(description = "用户信息VO") +public class UserVO { + + @NotNull(message = "用户id不能为空") + @Schema(description = "id") + private Long id; + + @NotEmpty(message = "用户名不能为空") + @Length(max = 20, message = "用户名不能大于20字符") + @Schema(description = "用户名") + private String userName; + + @NotEmpty(message = "用户昵称不能为空") + @Length(max = 20, message = "昵称不能大于20字符") + @Schema(description = "用户昵称") + private String nickName; + + @Schema(description = "性别") + private Integer sex; + + @Schema(description = "用户类型 1:普通用户 2:审核账户") + private Integer type; + + @Length(max = 128, message = "个性签名不能大于128个字符") + @Schema(description = "个性签名") + private String signature; + + @Schema(description = "头像") + private String headImage; + + @Schema(description = "头像缩略图") + private String headImageThumb; + + @Schema(description = "是否在线") + private Boolean online; + + @Schema(description = "账号是否被封禁") + private Boolean isBanned; + + @Schema(description = "被封禁原因") + private String reason; +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupFailedVO.java b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupFailedVO.java new file mode 100644 index 0000000..7cd4482 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupFailedVO.java @@ -0,0 +1,22 @@ +package com.bx.implatform.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * @author: Blue + * @date: 2024-06-09 + * @version: 1.0 + */ +@Data +@Schema(description = "用户加入群通话失败VO") +public class WebrtcGroupFailedVO { + + @Schema(description = "失败用户列表") + private List userIds; + + @Schema(description = "失败原因") + private String reason; +} diff --git a/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java new file mode 100644 index 0000000..7bed11f --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/vo/WebrtcGroupInfoVO.java @@ -0,0 +1,27 @@ +package com.bx.implatform.vo; + +import com.bx.implatform.session.WebrtcUserInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * @author: Blue + * @date: 2024-06-09 + * @version: 1.0 + */ +@Data +@Schema(description = "群通话信息VO") +public class WebrtcGroupInfoVO { + + + @Schema(description = "是否在通话中") + private Boolean isChating; + + @Schema(description = "通话发起人") + WebrtcUserInfo host; + + @Schema(description = "通话用户列表") + private List userInfos; +} diff --git a/im-platform/src/main/resources/application-dev.yml b/im-platform/src/main/resources/application-dev.yml new file mode 100644 index 0000000..5fbc810 --- /dev/null +++ b/im-platform/src/main/resources/application-dev.yml @@ -0,0 +1,28 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://123.157.250.162:3306/im_platform?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true + username: im_platform + password: 3tKHbkSHfycbriN7 + data: + redis: + host: localhost + port: 6379 + +minio: + endpoint: https://minioapi.zianxin.com #内网地址 + domain: https://minioapi.zianxin.com #外网访问地址 + accessKey: EnBCxtxZ9j0tJJKqjRjG + secretKey: imxrsyk1hM3lZ8dSbzz2QQSrw43rH33uZvnHXFzb + bucketName: im-platform + imagePath: image + filePath: file + videoPath: video + expireIn: 180 # 文件过期时间,单位:天 + +webrtc: + max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 + iceServers: + - urls: stun:stun.l.google.com:19302 + + diff --git a/im-platform/src/main/resources/application-prod.yml b/im-platform/src/main/resources/application-prod.yml new file mode 100644 index 0000000..ee43fe1 --- /dev/null +++ b/im-platform/src/main/resources/application-prod.yml @@ -0,0 +1,33 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/im_platform?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true + username: im_platform + password: 3tKHbkSHfycbriN7 + data: + redis: + host: localhost + port: 6379 + password: 179965 + +minio: + endpoint: http://192.168.0.6:9000 #内网地址 + domain: https://minioapi.zianxin.com #外网访问地址 + accessKey: EnBCxtxZ9j0tJJKqjRjG + secretKey: imxrsyk1hM3lZ8dSbzz2QQSrw43rH33uZvnHXFzb + bucketName: im-platform + imagePath: image + filePath: file + videoPath: video + expireIn: 180 # 文件过期时间,单位:天 + +webrtc: + max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 + iceServers: #coturn配置 + - urls: stun:www.boxim.online:3478 + username: admin + credential: UrHHKNvE7nFvBTMV + - urls: turn:www.boxim.online:3478 + username: admin + credential: UrHHKNvE7nFvBTMV + diff --git a/im-platform/src/main/resources/application-test.yml b/im-platform/src/main/resources/application-test.yml new file mode 100644 index 0000000..8be6393 --- /dev/null +++ b/im-platform/src/main/resources/application-test.yml @@ -0,0 +1,32 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/im_platform?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true + username: root + password: naQRLdDPbGv2j7PX_ + data: + redis: + host: localhost + port: 6379 + password: PmEpfRjpBnTN6CgW + +minio: + endpoint: http://127.0.0.1:9001 #内网地址 + domain: https://www.boxim.online/file #外网访问地址 + accessKey: admin + secretKey: 3fBSt6AkgFuD77D6 + bucketName: box-im + imagePath: image + filePath: file + videoPath: video + expireIn: 180 # 文件过期时间,单位:天 + +webrtc: + max-channel: 9 # 多人通话最大通道数量,最大不能超过16,建议值:4,9,16 + iceServers: #coturn配置 + - urls: stun:www.boxim.online:3478 + username: admin + credential: UrHHKNvE7nFvBTMV + - urls: turn:www.boxim.online:3478 + username: admin + credential: UrHHKNvE7nFvBTMV diff --git a/im-platform/src/main/resources/application.yml b/im-platform/src/main/resources/application.yml new file mode 100644 index 0000000..fd187ff --- /dev/null +++ b/im-platform/src/main/resources/application.yml @@ -0,0 +1,33 @@ +server: + port: 8888 +spring: + profiles: + active: prod # 环境 dev|test|prod + application: + name: im-platform + mvc: + pathmatch: + matching-strategy: ant_path_matcher + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB + +mybatis-plus: + global-config: + db-config: + id-type: AUTO # ID自增 + configuration: + map-underscore-to-camel-case: true #开启自动驼峰命名规则 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +jwt: + accessToken: + expireIn: 1800 #半个小时 + secret: MIIBIjANBgkq + refreshToken: + expireIn: 604800 #7天 + secret: IKDiqVmn0VFU + + + diff --git a/im-platform/src/main/resources/logback.xml b/im-platform/src/main/resources/logback.xml new file mode 100644 index 0000000..a1084bb --- /dev/null +++ b/im-platform/src/main/resources/logback.xml @@ -0,0 +1,48 @@ + + + + + + + + info + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + ${LOG_PATH}/${APP_NAME}.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + ${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log + 100MB + 60 + 20GB + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/im-platform/src/main/resources/static/favicon.ico b/im-platform/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..d31f507 Binary files /dev/null and b/im-platform/src/main/resources/static/favicon.ico differ diff --git a/im-server/pom.xml b/im-server/pom.xml new file mode 100644 index 0000000..f53f68a --- /dev/null +++ b/im-server/pom.xml @@ -0,0 +1,43 @@ + + + + box-im + com.bx + 3.0.0 + + 4.0.0 + + im-server + + + + + com.bx + im-common + 3.0.0 + + + io.netty + netty-all + 4.1.42.Final + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/im-server/src/main/java/com/bx/imserver/IMServerApp.java b/im-server/src/main/java/com/bx/imserver/IMServerApp.java new file mode 100644 index 0000000..95796b2 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/IMServerApp.java @@ -0,0 +1,19 @@ +package com.bx.imserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableAsync +@EnableScheduling +@ComponentScan(basePackages = {"com.bx"}) +@SpringBootApplication +public class IMServerApp { + + public static void main(String[] args) { + SpringApplication.run(IMServerApp.class, args); + } + +} diff --git a/im-server/src/main/java/com/bx/imserver/constant/ChannelAttrKey.java b/im-server/src/main/java/com/bx/imserver/constant/ChannelAttrKey.java new file mode 100644 index 0000000..552aee5 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/constant/ChannelAttrKey.java @@ -0,0 +1,21 @@ +package com.bx.imserver.constant; + +public final class ChannelAttrKey { + + private ChannelAttrKey() { + } + + /** + * 用户ID + */ + public static final String USER_ID = "USER_ID"; + /** + * 终端类型 + */ + public static final String TERMINAL_TYPE = "TERMINAL_TYPE"; + /** + * 心跳次数 + */ + public static final String HEARTBEAT_TIMES = "HEARTBEAT_TIMES"; + +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/IMChannelHandler.java b/im-server/src/main/java/com/bx/imserver/netty/IMChannelHandler.java new file mode 100644 index 0000000..298f1c3 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/IMChannelHandler.java @@ -0,0 +1,99 @@ +package com.bx.imserver.netty; + +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.model.IMSendInfo; +import com.bx.imcommon.mq.RedisMQTemplate; +import com.bx.imserver.constant.ChannelAttrKey; +import com.bx.imserver.netty.processor.AbstractMessageProcessor; +import com.bx.imserver.netty.processor.ProcessorFactory; +import com.bx.imserver.util.SpringContextHolder; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.AttributeKey; +import lombok.extern.slf4j.Slf4j; + +/** + * WebSocket 长连接下 文本帧的处理器 + * 实现浏览器发送文本回写 + * 浏览器连接状态监控 + */ +@Slf4j +public class IMChannelHandler extends SimpleChannelInboundHandler { + + /** + * 读取到消息后进行处理 + * + * @param ctx channel上下文 + * @param sendInfo 发送消息 + */ + @Override + protected void channelRead0(ChannelHandlerContext ctx, IMSendInfo sendInfo) { + // 创建处理器进行处理 + AbstractMessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.fromCode(sendInfo.getCmd())); + processor.process(ctx, processor.transForm(sendInfo.getData())); + } + + /** + * 出现异常的处理 打印报错日志 + * + * @param ctx channel上下文 + * @param cause 异常信息 + */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error(cause.getMessage()); + //关闭上下文 + //ctx.close(); + } + + /** + * 监控浏览器上线 + * + * @param ctx channel上下文 + */ + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + log.info(ctx.channel().id().asLongText() + "连接"); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + AttributeKey userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID); + Long userId = ctx.channel().attr(userIdAttr).get(); + AttributeKey terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE); + Integer terminal = ctx.channel().attr(terminalAttr).get(); + ChannelHandlerContext context = UserChannelCtxMap.getChannelCtx(userId, terminal); + // 判断一下,避免异地登录导致的误删 + if (context != null && ctx.channel().id().equals(context.channel().id())) { + // 移除channel + UserChannelCtxMap.removeChannelCtx(userId, terminal); + // 用户下线 + RedisMQTemplate redisTemplate = SpringContextHolder.getBean(RedisMQTemplate.class); + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), terminal.toString()); + redisTemplate.delete(key); + log.info("断开连接,userId:{},终端类型:{},{}", userId, terminal, ctx.channel().id().asLongText()); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + IdleState state = ((IdleStateEvent) evt).state(); + if (state == IdleState.READER_IDLE) { + // 在规定时间内没有收到客户端的上行数据, 主动断开连接 + AttributeKey attr = AttributeKey.valueOf("USER_ID"); + Long userId = ctx.channel().attr(attr).get(); + AttributeKey terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE); + Integer terminal = ctx.channel().attr(terminalAttr).get(); + log.info("心跳超时,即将断开连接,用户id:{},终端类型:{} ", userId, terminal); + ctx.channel().close(); + } + } else { + super.userEventTriggered(ctx, evt); + } + + } +} \ No newline at end of file diff --git a/im-server/src/main/java/com/bx/imserver/netty/IMServer.java b/im-server/src/main/java/com/bx/imserver/netty/IMServer.java new file mode 100644 index 0000000..286e356 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/IMServer.java @@ -0,0 +1,10 @@ +package com.bx.imserver.netty; + +public interface IMServer { + + boolean isReady(); + + void start(); + + void stop(); +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/IMServerGroup.java b/im-server/src/main/java/com/bx/imserver/netty/IMServerGroup.java new file mode 100644 index 0000000..8c5a36a --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/IMServerGroup.java @@ -0,0 +1,55 @@ +package com.bx.imserver.netty; + +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.mq.RedisMQTemplate; +import jakarta.annotation.PreDestroy; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@AllArgsConstructor +public class IMServerGroup implements CommandLineRunner { + + public static volatile long serverId = 0; + + private final RedisMQTemplate redisMQTemplate; + + private final List imServers; + + /*** + * 判断服务器是否就绪 + * + **/ + public boolean isReady() { + for (IMServer imServer : imServers) { + if (!imServer.isReady()) { + return false; + } + } + return true; + } + + @Override + public void run(String... args) { + // 初始化SERVER_ID + String key = IMRedisKey.IM_MAX_SERVER_ID; + serverId = redisMQTemplate.opsForValue().increment(key, 1); + // 启动服务 + for (IMServer imServer : imServers) { + imServer.start(); + } + } + + @PreDestroy + public void destroy() { + // 停止服务 + for (IMServer imServer : imServers) { + imServer.stop(); + } + } +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/UserChannelCtxMap.java b/im-server/src/main/java/com/bx/imserver/netty/UserChannelCtxMap.java new file mode 100644 index 0000000..8841de1 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/UserChannelCtxMap.java @@ -0,0 +1,46 @@ +package com.bx.imserver.netty; + +import io.netty.channel.ChannelHandlerContext; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class UserChannelCtxMap { + + /** + * 维护userId和ctx的关联关系,格式:Map> + */ + private static Map> channelMap = new ConcurrentHashMap(); + + public static void addChannelCtx(Long userId, Integer channel, ChannelHandlerContext ctx) { + channelMap.computeIfAbsent(userId, key -> new ConcurrentHashMap()).put(channel, ctx); + } + + public static void removeChannelCtx(Long userId, Integer terminal) { + if (userId != null && terminal != null && channelMap.containsKey(userId)) { + Map userChannelMap = channelMap.get(userId); + userChannelMap.remove(terminal); + if (userChannelMap.isEmpty()) { + channelMap.remove(userId); + } + } + } + + public static ChannelHandlerContext getChannelCtx(Long userId, Integer terminal) { + if (userId != null && terminal != null && channelMap.containsKey(userId)) { + Map userChannelMap = channelMap.get(userId); + if (userChannelMap.containsKey(terminal)) { + return userChannelMap.get(terminal); + } + } + return null; + } + + public static Map getChannelCtx(Long userId) { + if (userId == null) { + return null; + } + return channelMap.get(userId); + } + +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/processor/AbstractMessageProcessor.java b/im-server/src/main/java/com/bx/imserver/netty/processor/AbstractMessageProcessor.java new file mode 100644 index 0000000..dde5665 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/processor/AbstractMessageProcessor.java @@ -0,0 +1,18 @@ +package com.bx.imserver.netty.processor; + +import io.netty.channel.ChannelHandlerContext; + +public abstract class AbstractMessageProcessor { + + public void process(ChannelHandlerContext ctx, T data) { + } + + public void process(T data) { + } + + public T transForm(Object o) { + return (T) o; + } + + +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/processor/GroupMessageProcessor.java b/im-server/src/main/java/com/bx/imserver/netty/processor/GroupMessageProcessor.java new file mode 100644 index 0000000..8aaaa37 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/processor/GroupMessageProcessor.java @@ -0,0 +1,71 @@ +package com.bx.imserver.netty.processor; + +import cn.hutool.core.util.StrUtil; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.enums.IMSendCode; +import com.bx.imcommon.model.IMRecvInfo; +import com.bx.imcommon.model.IMSendInfo; +import com.bx.imcommon.model.IMSendResult; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.mq.RedisMQTemplate; +import com.bx.imserver.netty.UserChannelCtxMap; +import io.netty.channel.ChannelHandlerContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupMessageProcessor extends AbstractMessageProcessor { + + private final RedisMQTemplate redisMQTemplate; + + @Override + public void process(IMRecvInfo recvInfo) { + IMUserInfo sender = recvInfo.getSender(); + List receivers = recvInfo.getReceivers(); + log.info("接收到群消息,发送者:{},接收用户数量:{},内容:{}", sender.getId(), receivers.size(), recvInfo.getData()); + for (IMUserInfo receiver : receivers) { + try { + ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(receiver.getId(), receiver.getTerminal()); + if (!Objects.isNull(channelCtx)) { + // 推送消息到用户 + IMSendInfo sendInfo = new IMSendInfo<>(); + sendInfo.setCmd(IMCmdType.GROUP_MESSAGE.code()); + sendInfo.setData(recvInfo.getData()); + channelCtx.channel().writeAndFlush(sendInfo); + // 消息发送成功确认 + sendResult(recvInfo, receiver, IMSendCode.SUCCESS); + + } else { + // 消息发送成功确认 + sendResult(recvInfo, receiver, IMSendCode.NOT_FIND_CHANNEL); + log.error("未找到channel,发送者:{},接收id:{},内容:{}", sender.getId(), receiver.getId(), recvInfo.getData()); + } + } catch (Exception e) { + // 消息发送失败确认 + sendResult(recvInfo, receiver, IMSendCode.UNKONW_ERROR); + log.error("发送消息异常,发送者:{},接收id:{},内容:{}", sender.getId(), receiver.getId(), recvInfo.getData()); + } + } + } + + + private void sendResult(IMRecvInfo recvInfo, IMUserInfo receiver, IMSendCode sendCode) { + if (recvInfo.getSendResult()) { + IMSendResult result = new IMSendResult<>(); + result.setSender(recvInfo.getSender()); + result.setReceiver(receiver); + result.setCode(sendCode.code()); + result.setData(recvInfo.getData()); + // 推送到结果队列 + String key = StrUtil.join(":",IMRedisKey.IM_RESULT_GROUP_QUEUE,recvInfo.getServiceName()); + redisMQTemplate.opsForList().rightPush(key, result); + } + } +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/processor/HeartbeatProcessor.java b/im-server/src/main/java/com/bx/imserver/netty/processor/HeartbeatProcessor.java new file mode 100644 index 0000000..01f032d --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/processor/HeartbeatProcessor.java @@ -0,0 +1,56 @@ +package com.bx.imserver.netty.processor; + +import cn.hutool.core.bean.BeanUtil; +import com.bx.imcommon.contant.IMConstant; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.model.IMHeartbeatInfo; +import com.bx.imcommon.model.IMSendInfo; +import com.bx.imcommon.mq.RedisMQTemplate; +import com.bx.imserver.constant.ChannelAttrKey; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.AttributeKey; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class HeartbeatProcessor extends AbstractMessageProcessor { + + private final RedisMQTemplate redisMQTemplate; + + @Override + public void process(ChannelHandlerContext ctx, IMHeartbeatInfo beatInfo) { + // 响应ws + IMSendInfo sendInfo = new IMSendInfo<>(); + sendInfo.setCmd(IMCmdType.HEART_BEAT.code()); + ctx.channel().writeAndFlush(sendInfo); + // 设置属性 + AttributeKey heartBeatAttr = AttributeKey.valueOf(ChannelAttrKey.HEARTBEAT_TIMES); + Long heartbeatTimes = ctx.channel().attr(heartBeatAttr).get(); + ctx.channel().attr(heartBeatAttr).set(++heartbeatTimes); + if (heartbeatTimes % 10 == 0) { + // 每心跳10次,用户在线状态续一次命 + AttributeKey userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID); + Long userId = ctx.channel().attr(userIdAttr).get(); + AttributeKey terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE); + Integer terminal = ctx.channel().attr(terminalAttr).get(); + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), terminal.toString()); + redisMQTemplate.expire(key, IMConstant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS); + } + AttributeKey userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID); + Long userId = ctx.channel().attr(userIdAttr).get(); + log.debug("心跳,userId:{},{}",userId,ctx.channel().id().asLongText()); + } + + @Override + public IMHeartbeatInfo transForm(Object o) { + HashMap map = (HashMap) o; + return BeanUtil.fillBeanWithMap(map, new IMHeartbeatInfo(), false); + } +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java b/im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java new file mode 100644 index 0000000..b5480f5 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java @@ -0,0 +1,83 @@ +package com.bx.imserver.netty.processor; + +import cn.hutool.core.bean.BeanUtil; +import com.alibaba.fastjson.JSON; +import com.bx.imcommon.contant.IMConstant; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.model.IMLoginInfo; +import com.bx.imcommon.model.IMSendInfo; +import com.bx.imcommon.model.IMSessionInfo; +import com.bx.imcommon.mq.RedisMQTemplate; +import com.bx.imcommon.util.JwtUtil; +import com.bx.imserver.constant.ChannelAttrKey; +import com.bx.imserver.netty.IMServerGroup; +import com.bx.imserver.netty.UserChannelCtxMap; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.AttributeKey; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LoginProcessor extends AbstractMessageProcessor { + + private final RedisMQTemplate redisMQTemplate; + + @Value("${jwt.accessToken.secret}") + private String accessTokenSecret; + + @Override + public void process(ChannelHandlerContext ctx, IMLoginInfo loginInfo) { + if (!JwtUtil.checkSign(loginInfo.getAccessToken(), accessTokenSecret)) { + ctx.channel().close(); + log.warn("用户token校验不通过,强制下线,token:{}", loginInfo.getAccessToken()); + return; + } + String strInfo = JwtUtil.getInfo(loginInfo.getAccessToken()); + IMSessionInfo sessionInfo = JSON.parseObject(strInfo, IMSessionInfo.class); + Long userId = sessionInfo.getUserId(); + Integer terminal = sessionInfo.getTerminal(); + log.info("用户登录,userId:{}", userId); + ChannelHandlerContext context = UserChannelCtxMap.getChannelCtx(userId, terminal); + if (context != null && !ctx.channel().id().equals(context.channel().id())) { + // 不允许多地登录,强制下线 + IMSendInfo sendInfo = new IMSendInfo<>(); + sendInfo.setCmd(IMCmdType.FORCE_LOGUT.code()); + sendInfo.setData("您已在其他地方登录,将被强制下线"); + context.channel().writeAndFlush(sendInfo); + log.info("异地登录,强制下线,userId:{}", userId); + } + // 绑定用户和channel + UserChannelCtxMap.addChannelCtx(userId, terminal, ctx); + // 设置用户id属性 + AttributeKey userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID); + ctx.channel().attr(userIdAttr).set(userId); + // 设置用户终端类型 + AttributeKey terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE); + ctx.channel().attr(terminalAttr).set(terminal); + // 初始化心跳次数 + AttributeKey heartBeatAttr = AttributeKey.valueOf(ChannelAttrKey.HEARTBEAT_TIMES); + ctx.channel().attr(heartBeatAttr).set(0L); + // 在redis上记录每个user的channelId,15秒没有心跳,则自动过期 + String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), terminal.toString()); + redisMQTemplate.opsForValue().set(key, IMServerGroup.serverId, IMConstant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS); + // 响应ws + IMSendInfo sendInfo = new IMSendInfo<>(); + sendInfo.setCmd(IMCmdType.LOGIN.code()); + ctx.channel().writeAndFlush(sendInfo); + } + + + @Override + public IMLoginInfo transForm(Object o) { + HashMap map = (HashMap) o; + return BeanUtil.fillBeanWithMap(map, new IMLoginInfo(), false); + } +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/processor/PrivateMessageProcessor.java b/im-server/src/main/java/com/bx/imserver/netty/processor/PrivateMessageProcessor.java new file mode 100644 index 0000000..8ab5101 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/processor/PrivateMessageProcessor.java @@ -0,0 +1,67 @@ +package com.bx.imserver.netty.processor; + +import cn.hutool.core.util.StrUtil; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.enums.IMSendCode; +import com.bx.imcommon.model.IMRecvInfo; +import com.bx.imcommon.model.IMSendInfo; +import com.bx.imcommon.model.IMSendResult; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.mq.RedisMQTemplate; +import com.bx.imserver.netty.UserChannelCtxMap; +import io.netty.channel.ChannelHandlerContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PrivateMessageProcessor extends AbstractMessageProcessor { + + private final RedisMQTemplate redisMQTemplate; + + @Override + public void process(IMRecvInfo recvInfo) { + IMUserInfo sender = recvInfo.getSender(); + IMUserInfo receiver = recvInfo.getReceivers().get(0); + log.info("接收到私聊消息,发送者:{},接收者:{},内容:{}", sender.getId(), receiver.getId(), recvInfo.getData()); + try { + ChannelHandlerContext channelCtx = UserChannelCtxMap.getChannelCtx(receiver.getId(), receiver.getTerminal()); + if (!Objects.isNull(channelCtx)) { + // 推送消息到用户 + IMSendInfo sendInfo = new IMSendInfo<>(); + sendInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code()); + sendInfo.setData(recvInfo.getData()); + channelCtx.channel().writeAndFlush(sendInfo); + // 消息发送成功确认 + sendResult(recvInfo, IMSendCode.SUCCESS); + } else { + // 消息推送失败确认 + sendResult(recvInfo, IMSendCode.NOT_FIND_CHANNEL); + log.error("未找到channel,发送者:{},接收者:{},内容:{}", sender.getId(), receiver.getId(), recvInfo.getData()); + } + } catch (Exception e) { + // 消息推送失败确认 + sendResult(recvInfo, IMSendCode.UNKONW_ERROR); + log.error("发送异常,发送者:{},接收者:{},内容:{}", sender.getId(), receiver.getId(), recvInfo.getData(), e); + } + + } + + private void sendResult(IMRecvInfo recvInfo, IMSendCode sendCode) { + if (recvInfo.getSendResult()) { + IMSendResult result = new IMSendResult<>(); + result.setSender(recvInfo.getSender()); + result.setReceiver(recvInfo.getReceivers().get(0)); + result.setCode(sendCode.code()); + result.setData(recvInfo.getData()); + // 推送到结果队列 + String key = StrUtil.join(":",IMRedisKey.IM_RESULT_PRIVATE_QUEUE,recvInfo.getServiceName()); + redisMQTemplate.opsForList().rightPush(key, result); + } + } +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/processor/ProcessorFactory.java b/im-server/src/main/java/com/bx/imserver/netty/processor/ProcessorFactory.java new file mode 100644 index 0000000..f68e0f9 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/processor/ProcessorFactory.java @@ -0,0 +1,19 @@ +package com.bx.imserver.netty.processor; + +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imserver.util.SpringContextHolder; + +public class ProcessorFactory { + + public static AbstractMessageProcessor createProcessor(IMCmdType cmd) { + return switch (cmd) { + case LOGIN->SpringContextHolder.getApplicationContext().getBean(LoginProcessor.class); + case HEART_BEAT -> SpringContextHolder.getApplicationContext().getBean(HeartbeatProcessor.class); + case PRIVATE_MESSAGE->SpringContextHolder.getApplicationContext().getBean(PrivateMessageProcessor.class); + case GROUP_MESSAGE->SpringContextHolder.getApplicationContext().getBean(GroupMessageProcessor.class); + case SYSTEM_MESSAGE->SpringContextHolder.getApplicationContext().getBean(SystemMessageProcessor.class); + default -> null; + }; + } + +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/processor/SystemMessageProcessor.java b/im-server/src/main/java/com/bx/imserver/netty/processor/SystemMessageProcessor.java new file mode 100644 index 0000000..906e4eb --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/processor/SystemMessageProcessor.java @@ -0,0 +1,67 @@ +package com.bx.imserver.netty.processor; + +import cn.hutool.core.util.StrUtil; +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.enums.IMSendCode; +import com.bx.imcommon.model.IMRecvInfo; +import com.bx.imcommon.model.IMSendInfo; +import com.bx.imcommon.model.IMSendResult; +import com.bx.imcommon.model.IMUserInfo; +import com.bx.imcommon.mq.RedisMQTemplate; +import com.bx.imserver.netty.UserChannelCtxMap; +import io.netty.channel.ChannelHandlerContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SystemMessageProcessor extends AbstractMessageProcessor { + + private final RedisMQTemplate redisMQTemplate; + + @Override + public void process(IMRecvInfo recvInfo) { + log.info("接收到系统消息,接收用户数量:{},内容:{}", recvInfo.getReceivers().size(), recvInfo.getData()); + for (IMUserInfo receiver : recvInfo.getReceivers()) { + try { + ChannelHandlerContext channelCtx = + UserChannelCtxMap.getChannelCtx(receiver.getId(), receiver.getTerminal()); + if (!Objects.isNull(channelCtx)) { + // 推送消息到用户 + IMSendInfo sendInfo = new IMSendInfo<>(); + sendInfo.setCmd(IMCmdType.SYSTEM_MESSAGE.code()); + sendInfo.setData(recvInfo.getData()); + channelCtx.channel().writeAndFlush(sendInfo); + // 消息发送成功确认 + sendResult(recvInfo, IMSendCode.SUCCESS); + } else { + // 消息推送失败确认 + sendResult(recvInfo, IMSendCode.NOT_FIND_CHANNEL); + log.error("未找到channel,接收者:{},内容:{}", receiver.getId(), recvInfo.getData()); + } + } catch (Exception e) { + // 消息推送失败确认 + sendResult(recvInfo, IMSendCode.UNKONW_ERROR); + log.error("发送异常,,接收者:{},内容:{}", receiver.getId(), recvInfo.getData(), e); + } + } + + } + + private void sendResult(IMRecvInfo recvInfo, IMSendCode sendCode) { + if (recvInfo.getSendResult()) { + IMSendResult result = new IMSendResult<>(); + result.setReceiver(recvInfo.getReceivers().get(0)); + result.setCode(sendCode.code()); + result.setData(recvInfo.getData()); + // 推送到结果队列 + String key = StrUtil.join(":",IMRedisKey.IM_RESULT_SYSTEM_QUEUE,recvInfo.getServiceName()); + redisMQTemplate.opsForList().rightPush(key, result); + } + } +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java b/im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java new file mode 100644 index 0000000..01f03ea --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java @@ -0,0 +1,98 @@ +package com.bx.imserver.netty.tcp; + +import com.bx.imserver.netty.IMChannelHandler; +import com.bx.imserver.netty.IMServer; +import com.bx.imserver.netty.tcp.endecode.MessageProtocolDecoder; +import com.bx.imserver.netty.tcp.endecode.MessageProtocolEncoder; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.timeout.IdleStateHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * TCP服务器,用于连接非网页的客户端,协议格式: 8字节内容的长度+IMSendInfo的JSON序列化 + * + * @author Blue + * @date 2022-11-20 + */ +@Slf4j +@Component +@ConditionalOnProperty(prefix = "tcpsocket", value = "enable", havingValue = "true", matchIfMissing = false) +public class TcpSocketServer implements IMServer { + + private volatile boolean ready = false; + + @Value("${tcpsocket.port}") + private int port; + + private EventLoopGroup bossGroup; + private EventLoopGroup workGroup; + + @Override + public boolean isReady() { + return ready; + } + + @Override + public void start() { + ServerBootstrap bootstrap = new ServerBootstrap(); + bossGroup = new NioEventLoopGroup(); + workGroup = new NioEventLoopGroup(); + // 设置为主从线程模型 + bootstrap.group(bossGroup, workGroup) + // 设置服务端NIO通信类型 + .channel(NioServerSocketChannel.class) + // 设置ChannelPipeline,也就是业务职责链,由处理的Handler串联而成,由从线程池处理 + .childHandler(new ChannelInitializer() { + // 添加处理的Handler,通常包括消息编解码、业务处理,也可以是日志、权限、过滤等 + @Override + protected void initChannel(Channel ch) throws Exception { + // 获取职责链 + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new IdleStateHandler(120, 0, 0, TimeUnit.SECONDS)); + pipeline.addLast("encode", new MessageProtocolEncoder()); + pipeline.addLast("decode", new MessageProtocolDecoder()); + pipeline.addLast("handler", new IMChannelHandler()); + } + }) + // bootstrap 还可以设置TCP参数,根据需要可以分别设置主线程池和从线程池参数,来优化服务端性能。 + // 其中主线程池使用option方法来设置,从线程池使用childOption方法设置。 + // backlog表示主线程池中在套接口排队的最大数量,队列由未连接队列(三次握手未完成的)和已连接队列 + .option(ChannelOption.SO_BACKLOG, 5) + // 表示连接保活,相当于心跳机制,默认为7200s + .childOption(ChannelOption.SO_KEEPALIVE, true); + + try { + // 绑定端口,启动select线程,轮询监听channel事件,监听到事件之后就会交给从线程池处理 + Channel channel = bootstrap.bind(port).sync().channel(); + // 就绪标志 + this.ready = true; + log.info("tcp server 初始化完成,端口:{}", port); + // 等待服务端口关闭 + //channel.closeFuture().sync(); + } catch (InterruptedException e) { + log.info("tcp server 初始化异常", e); + } + } + + @Override + public void stop() { + if (bossGroup != null && !bossGroup.isShuttingDown() && !bossGroup.isShutdown()) { + bossGroup.shutdownGracefully(); + } + if (workGroup != null && !workGroup.isShuttingDown() && !workGroup.isShutdown()) { + workGroup.shutdownGracefully(); + } + this.ready = false; + log.info("tcp server 停止"); + } + + +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/tcp/endecode/MessageProtocolDecoder.java b/im-server/src/main/java/com/bx/imserver/netty/tcp/endecode/MessageProtocolDecoder.java new file mode 100644 index 0000000..906d6e5 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/tcp/endecode/MessageProtocolDecoder.java @@ -0,0 +1,30 @@ +package com.bx.imserver.netty.tcp.endecode; + +import com.bx.imcommon.model.IMSendInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ReplayingDecoder; +import io.netty.util.CharsetUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class MessageProtocolDecoder extends ReplayingDecoder { + + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { + if (byteBuf.readableBytes() < 4) { + return; + } + // 获取到包的长度 + long length = byteBuf.readLong(); + // 转成IMSendInfo + ByteBuf contentBuf = byteBuf.readBytes((int) length); + String content = contentBuf.toString(CharsetUtil.UTF_8); + ObjectMapper objectMapper = new ObjectMapper(); + IMSendInfo sendInfo = objectMapper.readValue(content, IMSendInfo.class); + list.add(sendInfo); + } +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/tcp/endecode/MessageProtocolEncoder.java b/im-server/src/main/java/com/bx/imserver/netty/tcp/endecode/MessageProtocolEncoder.java new file mode 100644 index 0000000..84d1cdd --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/tcp/endecode/MessageProtocolEncoder.java @@ -0,0 +1,24 @@ +package com.bx.imserver.netty.tcp.endecode; + +import com.bx.imcommon.model.IMSendInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import java.nio.charset.StandardCharsets; + +public class MessageProtocolEncoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, IMSendInfo sendInfo, ByteBuf byteBuf) throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + String content = objectMapper.writeValueAsString(sendInfo); + byte[] bytes = content.getBytes(StandardCharsets.UTF_8); + // 写入长度 + byteBuf.writeLong(bytes.length); + // 写入命令体 + byteBuf.writeBytes(bytes); + } + +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java b/im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java new file mode 100644 index 0000000..14af9c4 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java @@ -0,0 +1,107 @@ +package com.bx.imserver.netty.ws; + +import com.bx.imserver.netty.IMChannelHandler; +import com.bx.imserver.netty.IMServer; +import com.bx.imserver.netty.ws.endecode.MessageProtocolDecoder; +import com.bx.imserver.netty.ws.endecode.MessageProtocolEncoder; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.handler.timeout.IdleStateHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * WS服务器,用于连接网页的客户端,协议格式: 直接IMSendInfo的JSON序列化 + * + * @author Blue + * @date 2022-11-20 + */ +@Slf4j +@Component +@ConditionalOnProperty(prefix = "websocket", value = "enable", havingValue = "true", matchIfMissing = false) +public class WebSocketServer implements IMServer { + + @Value("${websocket.port}") + private int port; + + private volatile boolean ready = false; + + private EventLoopGroup bossGroup; + private EventLoopGroup workGroup; + + + @Override + public boolean isReady() { + return ready; + } + + @Override + public void start() { + ServerBootstrap bootstrap = new ServerBootstrap(); + bossGroup = new NioEventLoopGroup(); + workGroup = new NioEventLoopGroup(); + // 设置为主从线程模型 + bootstrap.group(bossGroup, workGroup) + // 设置服务端NIO通信类型 + .channel(NioServerSocketChannel.class) + // 设置ChannelPipeline,也就是业务职责链,由处理的Handler串联而成,由从线程池处理 + .childHandler(new ChannelInitializer() { + // 添加处理的Handler,通常包括消息编解码、业务处理,也可以是日志、权限、过滤等 + @Override + protected void initChannel(Channel ch) { + // 获取职责链 + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS)); + pipeline.addLast("http-codec", new HttpServerCodec()); + pipeline.addLast("aggregator", new HttpObjectAggregator(65535)); + pipeline.addLast("http-chunked", new ChunkedWriteHandler()); + pipeline.addLast(new WebSocketServerProtocolHandler("/im")); + pipeline.addLast("encode", new MessageProtocolEncoder()); + pipeline.addLast("decode", new MessageProtocolDecoder()); + pipeline.addLast("handler", new IMChannelHandler()); + } + }) + // bootstrap 还可以设置TCP参数,根据需要可以分别设置主线程池和从线程池参数,来优化服务端性能。 + // 其中主线程池使用option方法来设置,从线程池使用childOption方法设置。 + // backlog表示主线程池中在套接口排队的最大数量,队列由未连接队列(三次握手未完成的)和已连接队列 + .option(ChannelOption.SO_BACKLOG, 5) + // 表示连接保活,相当于心跳机制,默认为7200s + .childOption(ChannelOption.SO_KEEPALIVE, true); + + try { + // 绑定端口,启动select线程,轮询监听channel事件,监听到事件之后就会交给从线程池处理 + bootstrap.bind(port).sync().channel(); + // 就绪标志 + this.ready = true; + log.info("websocket server 初始化完成,端口:{}", port); + // 等待服务端口关闭 + //channel.closeFuture().sync(); + } catch (InterruptedException e) { + log.info("websocket server 初始化异常", e); + } + } + + @Override + public void stop() { + if (bossGroup != null && !bossGroup.isShuttingDown() && !bossGroup.isShutdown()) { + bossGroup.shutdownGracefully(); + } + if (workGroup != null && !workGroup.isShuttingDown() && !workGroup.isShutdown()) { + workGroup.shutdownGracefully(); + } + this.ready = false; + log.info("websocket server 停止"); + } + + +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/ws/endecode/MessageProtocolDecoder.java b/im-server/src/main/java/com/bx/imserver/netty/ws/endecode/MessageProtocolDecoder.java new file mode 100644 index 0000000..7374048 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/ws/endecode/MessageProtocolDecoder.java @@ -0,0 +1,19 @@ +package com.bx.imserver.netty.ws.endecode; + +import com.bx.imcommon.model.IMSendInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; + +import java.util.List; + +public class MessageProtocolDecoder extends MessageToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame, List list) throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + IMSendInfo sendInfo = objectMapper.readValue(textWebSocketFrame.text(), IMSendInfo.class); + list.add(sendInfo); + } +} diff --git a/im-server/src/main/java/com/bx/imserver/netty/ws/endecode/MessageProtocolEncoder.java b/im-server/src/main/java/com/bx/imserver/netty/ws/endecode/MessageProtocolEncoder.java new file mode 100644 index 0000000..3c1a3ba --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/netty/ws/endecode/MessageProtocolEncoder.java @@ -0,0 +1,20 @@ +package com.bx.imserver.netty.ws.endecode; + +import com.bx.imcommon.model.IMSendInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; + +import java.util.List; + +public class MessageProtocolEncoder extends MessageToMessageEncoder { + + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, IMSendInfo sendInfo, List list) throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + String text = objectMapper.writeValueAsString(sendInfo); + TextWebSocketFrame frame = new TextWebSocketFrame(text); + list.add(frame); + } +} diff --git a/im-server/src/main/java/com/bx/imserver/task/AbstractPullMessageTask.java b/im-server/src/main/java/com/bx/imserver/task/AbstractPullMessageTask.java new file mode 100644 index 0000000..c6b2290 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/task/AbstractPullMessageTask.java @@ -0,0 +1,23 @@ +package com.bx.imserver.task; + +import com.bx.imcommon.mq.RedisMQConsumer; +import com.bx.imserver.netty.IMServerGroup; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +public abstract class AbstractPullMessageTask extends RedisMQConsumer { + + @Autowired + private IMServerGroup serverGroup; + + @Override + public String generateKey() { + return String.join(":", super.generateKey(), IMServerGroup.serverId + ""); + } + + @Override + public Boolean isReady() { + return serverGroup.isReady(); + } +} diff --git a/im-server/src/main/java/com/bx/imserver/task/PullGroupMessageTask.java b/im-server/src/main/java/com/bx/imserver/task/PullGroupMessageTask.java new file mode 100644 index 0000000..2d0db6b --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/task/PullGroupMessageTask.java @@ -0,0 +1,25 @@ +package com.bx.imserver.task; + +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.model.IMRecvInfo; +import com.bx.imcommon.mq.RedisMQListener; +import com.bx.imserver.netty.processor.AbstractMessageProcessor; +import com.bx.imserver.netty.processor.ProcessorFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +@RedisMQListener(queue = IMRedisKey.IM_MESSAGE_GROUP_QUEUE, batchSize = 10) +public class PullGroupMessageTask extends AbstractPullMessageTask { + + @Override + public void onMessage(IMRecvInfo recvInfo) { + AbstractMessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.GROUP_MESSAGE); + processor.process(recvInfo); + } + +} diff --git a/im-server/src/main/java/com/bx/imserver/task/PullPrivateMessageTask.java b/im-server/src/main/java/com/bx/imserver/task/PullPrivateMessageTask.java new file mode 100644 index 0000000..b9b3545 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/task/PullPrivateMessageTask.java @@ -0,0 +1,25 @@ +package com.bx.imserver.task; + +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.model.IMRecvInfo; +import com.bx.imcommon.mq.RedisMQListener; +import com.bx.imserver.netty.processor.AbstractMessageProcessor; +import com.bx.imserver.netty.processor.ProcessorFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +@RedisMQListener(queue = IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE, batchSize = 10) +public class PullPrivateMessageTask extends AbstractPullMessageTask { + + @Override + public void onMessage(IMRecvInfo recvInfo) { + AbstractMessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.PRIVATE_MESSAGE); + processor.process(recvInfo); + } + +} diff --git a/im-server/src/main/java/com/bx/imserver/task/PullSystemMessageTask.java b/im-server/src/main/java/com/bx/imserver/task/PullSystemMessageTask.java new file mode 100644 index 0000000..c1d6a2b --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/task/PullSystemMessageTask.java @@ -0,0 +1,28 @@ +package com.bx.imserver.task; + +import com.bx.imcommon.contant.IMRedisKey; +import com.bx.imcommon.enums.IMCmdType; +import com.bx.imcommon.model.IMRecvInfo; +import com.bx.imcommon.mq.RedisMQListener; +import com.bx.imserver.netty.processor.AbstractMessageProcessor; +import com.bx.imserver.netty.processor.ProcessorFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @author: Blue + * @date: 2024-07-16 + * @version: 1.0 + */ +@Slf4j +@Component +@RedisMQListener(queue = IMRedisKey.IM_MESSAGE_SYSTEM_QUEUE,batchSize = 10) +public class PullSystemMessageTask extends AbstractPullMessageTask { + + @Override + public void onMessage(IMRecvInfo recvInfo) { + AbstractMessageProcessor processor = ProcessorFactory.createProcessor(IMCmdType.SYSTEM_MESSAGE); + processor.process(recvInfo); + } + +} diff --git a/im-server/src/main/java/com/bx/imserver/util/SpringContextHolder.java b/im-server/src/main/java/com/bx/imserver/util/SpringContextHolder.java new file mode 100644 index 0000000..1a1b2e2 --- /dev/null +++ b/im-server/src/main/java/com/bx/imserver/util/SpringContextHolder.java @@ -0,0 +1,39 @@ +package com.bx.imserver.util; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +public class SpringContextHolder implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringContextHolder.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + assertApplicationContext(); + return applicationContext; + } + + public static T getBean(String beanName) { + assertApplicationContext(); + return (T) applicationContext.getBean(beanName); + } + + public static T getBean(Class requiredType) { + assertApplicationContext(); + return applicationContext.getBean(requiredType); + } + + private static void assertApplicationContext() { + if (SpringContextHolder.applicationContext == null) { + throw new RuntimeException("applicaitonContext属性为null,请检查是否注入了SpringContextHolder!"); + } + } + +} diff --git a/im-server/src/main/resources/application-dev.yml b/im-server/src/main/resources/application-dev.yml new file mode 100644 index 0000000..a06ec18 --- /dev/null +++ b/im-server/src/main/resources/application-dev.yml @@ -0,0 +1,5 @@ +spring: + data: + redis: + host: 127.0.0.1 + port: 6379 \ No newline at end of file diff --git a/im-server/src/main/resources/application-prod.yml b/im-server/src/main/resources/application-prod.yml new file mode 100644 index 0000000..a17e518 --- /dev/null +++ b/im-server/src/main/resources/application-prod.yml @@ -0,0 +1,6 @@ +spring: + data: + redis: + host: 127.0.0.1 + port: 6379 + password: 179965 \ No newline at end of file diff --git a/im-server/src/main/resources/application-test.yml b/im-server/src/main/resources/application-test.yml new file mode 100644 index 0000000..79724c4 --- /dev/null +++ b/im-server/src/main/resources/application-test.yml @@ -0,0 +1,6 @@ +spring: + data: + redis: + host: 127.0.0.1 + port: 6379 + password: PmEpfRjpBnTN6CgW \ No newline at end of file diff --git a/im-server/src/main/resources/application.yml b/im-server/src/main/resources/application.yml new file mode 100644 index 0000000..6c3f01b --- /dev/null +++ b/im-server/src/main/resources/application.yml @@ -0,0 +1,17 @@ +spring: + profiles: + active: prod + application: + name: im-server + +websocket: + enable: true + port: 8878 + +tcpsocket: + enable: false # 暂时不开启 + port: 8879 + +jwt: + accessToken: + secret: MIIBIjANBgkq # 跟im-platfrom的secret必须一致 diff --git a/im-server/src/main/resources/logback.xml b/im-server/src/main/resources/logback.xml new file mode 100644 index 0000000..ede4d60 --- /dev/null +++ b/im-server/src/main/resources/logback.xml @@ -0,0 +1,48 @@ + + + + + + + + info + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + ${LOG_PATH}/${APP_NAME}.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + ${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log + 100MB + 60 + 20GB + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/im-uniapp/.env.js b/im-uniapp/.env.js new file mode 100644 index 0000000..489c3d3 --- /dev/null +++ b/im-uniapp/.env.js @@ -0,0 +1,27 @@ +//设置环境(打包前修改此变量) +const ENV = "DEV"; +const UNI_APP = {} +// 表情包路径 +UNI_APP.EMO_URL = "/static/emoji/"; +// #ifdef MP-WEIXIN +// 微信小程序的本地表情包经常莫名失效,建议将表情放到服务器中 +// UNI_APP.EMO_URL = "https://www.boxim.online/emoji/"; +// #endif + +if(ENV=="DEV"){ + // UNI_APP.BASE_URL = "http://127.0.0.1:8888"; + // UNI_APP.WS_URL = "ws://127.0.0.1:8878/im"; + + UNI_APP.BASE_URL = "http://chatsever.zianxin.com" + UNI_APP.WS_URL = "ws://chatapp.zianxin.com/im" + + // H5 走本地代理解决跨域问题 + // #ifdef H5 + UNI_APP.BASE_URL = "/api"; + // #endif +} +if(ENV=="PROD"){ + UNI_APP.BASE_URL = "https://www.boxim.online/api"; + UNI_APP.WS_URL = "wss://www.boxim.online/im"; +} +export default UNI_APP \ No newline at end of file diff --git a/im-uniapp/App.vue b/im-uniapp/App.vue new file mode 100644 index 0000000..df3332c --- /dev/null +++ b/im-uniapp/App.vue @@ -0,0 +1,551 @@ + + + \ No newline at end of file diff --git a/im-uniapp/common/date.js b/im-uniapp/common/date.js new file mode 100644 index 0000000..cd4fc17 --- /dev/null +++ b/im-uniapp/common/date.js @@ -0,0 +1,66 @@ +let toTimeText = (timeStamp, simple) => { + var dateTime = new Date(timeStamp) + var currentTime = Date.parse(new Date()); //当前时间 + var timeDiff = currentTime - dateTime; //与当前时间误差 + var timeText = ''; + if (timeDiff <= 60000) { //一分钟内 + timeText = '刚刚'; + } else if (timeDiff > 60000 && timeDiff < 3600000) { + //1小时内 + timeText = Math.floor(timeDiff / 60000) + '分钟前'; + } else if (timeDiff >= 3600000 && timeDiff < 86400000 && !isYestday(dateTime)) { + //今日 + timeText = formatDateTime(dateTime).substr(11, 5); + } else if (isYestday(dateTime)) { + //昨天 + timeText = '昨天' + formatDateTime(dateTime).substr(11, 5); + } else if (isYear(dateTime)) { + //今年 + timeText = formatDateTime(dateTime).substr(5, simple ? 5 : 14); + } else { + //不属于今年 + timeText = formatDateTime(dateTime); + if (simple) { + timeText = timeText.substr(2, 8); + } + } + return timeText; +} + +let isYestday = (date) => { + var yesterday = new Date(new Date() - 1000 * 60 * 60 * 24); + return yesterday.getYear() === date.getYear() && + yesterday.getMonth() === date.getMonth() && + yesterday.getDate() === date.getDate(); +} + +let isYear = (date) => { + return date.getYear() === new Date().getYear(); +} + +let formatDateTime = (date) => { + if (date === '' || !date) { + return '' + } + var dateObject = new Date(date) + var y = dateObject.getFullYear() + var m = dateObject.getMonth() + 1 + m = m < 10 ? ('0' + m) : m + var d = dateObject.getDate() + d = d < 10 ? ('0' + d) : d + var h = dateObject.getHours() + h = h < 10 ? ('0' + h) : h + var minute = dateObject.getMinutes() + minute = minute < 10 ? ('0' + minute) : minute + var second = dateObject.getSeconds() + second = second < 10 ? ('0' + second) : second + return y + '/' + m + '/' + d + ' ' + h + ':' + minute + ':' + second +} + + +export { + toTimeText, + isYestday, + isYear, + formatDateTime +} \ No newline at end of file diff --git a/im-uniapp/common/emotion.js b/im-uniapp/common/emotion.js new file mode 100644 index 0000000..f7dfe37 --- /dev/null +++ b/im-uniapp/common/emotion.js @@ -0,0 +1,43 @@ +import UNI_APP from '@/.env.js' + + +const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心', '笑哭', '拍手', '惊喜', '打气', + '大哭', '流泪', '饥饿', '难受', '健身', '示爱', '色色', '眨眼', '暴怒', '惊恐', + '思考', '头晕', '大吐', '酷笑', '翻滚', '享受', '鼻涕', '快乐', '雀跃', '微笑', + '贪婪', '红心', '粉心', '星星', '大火', '眼睛', '音符', "叹号", "问号", "绿叶", + "燃烧", "喇叭", "警告", "信封", "房子", "礼物", "点赞", "举手", "拍手", "点头", + "摇头", "偷瞄", "庆祝", "疾跑", "打滚", "惊吓", "起跳" +]; + +const regex = /\#[\u4E00-\u9FA5]{1,3}\;/gi; + +let containEmoji = (content) => { + return regex.test(content) +} + +let transform = (content, extClass) => { + return content.replace(regex, (emoText)=>{ + // 将匹配结果替换表情图片 + let word = emoText.replace(/\#|\;/gi, ''); + let idx = emoTextList.indexOf(word); + if (idx == -1) { + return emoText; + } + let path = textToPath(emoText); + let img = ``; + return img; + }); +} + +let textToPath = (emoText) => { + let word = emoText.replace(/\#|\;/gi, ''); + let idx = emoTextList.indexOf(word); + return UNI_APP.EMO_URL + idx + ".gif"; +} + +export default { + containEmoji, + emoTextList, + transform, + textToPath +} \ No newline at end of file diff --git a/im-uniapp/common/enums.js b/im-uniapp/common/enums.js new file mode 100644 index 0000000..fa7ada0 --- /dev/null +++ b/im-uniapp/common/enums.js @@ -0,0 +1,70 @@ + +const MESSAGE_TYPE = { + TEXT: 0, + IMAGE: 1, + FILE: 2, + AUDIO: 3, + VIDEO: 4, + RECALL: 10, + READED: 11, + RECEIPT: 12, + TIP_TIME: 20, + TIP_TEXT: 21, + LOADING: 30, + ACT_RT_VOICE: 40, + ACT_RT_VIDEO: 41, + USER_BANNED: 50, + FRIEND_NEW: 80, + FRIEND_DEL: 81, + FRIEND_DND: 82, + GROUP_NEW: 90, + GROUP_DEL: 91, + GROUP_DND: 92, + RTC_CALL_VOICE: 100, + RTC_CALL_VIDEO: 101, + RTC_ACCEPT: 102, + RTC_REJECT: 103, + RTC_CANCEL: 104, + RTC_FAILED: 105, + RTC_HANDUP: 106, + RTC_CANDIDATE: 107, + RTC_GROUP_SETUP: 200, + RTC_GROUP_ACCEPT: 201, + RTC_GROUP_REJECT: 202, + RTC_GROUP_FAILED: 203, + RTC_GROUP_CANCEL: 204, + RTC_GROUP_QUIT: 205, + RTC_GROUP_INVITE: 206, + RTC_GROUP_JOIN: 207, + RTC_GROUP_OFFER: 208, + RTC_GROUP_ANSWER: 209, + RTC_GROUP_CANDIDATE: 210, + RTC_GROUP_DEVICE: 211 +} + +const USER_STATE = { + OFFLINE: 0, + FREE: 1, + BUSY: 2 +} + +const TERMINAL_TYPE = { + WEB: 0, + APP: 1 +} + +const MESSAGE_STATUS = { + FAILED: -2, // 发送失败 + SENDING: -1, // 发送中(消息没到服务器) + PENDING: 0, // 未送达(消息已到服务器,但对方没收到) + DELIVERED: 1, // 已送达(对方已收到,但是未读消息) + RECALL: 2, // 已撤回 + READED: 3, // 消息已读 +} + +export { + MESSAGE_TYPE, + USER_STATE, + TERMINAL_TYPE, + MESSAGE_STATUS +} diff --git a/im-uniapp/common/messageType.js b/im-uniapp/common/messageType.js new file mode 100644 index 0000000..f6b1caf --- /dev/null +++ b/im-uniapp/common/messageType.js @@ -0,0 +1,40 @@ + +// 是否普通消息 +let isNormal = function (type) { + return type >= 0 && type < 10; +} + +// 是否状态消息 +let isStatus = function (type) { + return type >= 10 && type < 20; +} + +// 是否提示消息 +let isTip = function (type) { + return type >= 20 && type < 30; +} + +// 操作交互类消息 +let isAction = function (type) { + return type >= 40 && type < 50; +} + +// 单人通话信令 +let isRtcPrivate = function (type) { + return type >= 100 && type < 200; +} + +// 多人通话信令 +let isRtcGroup = function (type) { + return type >= 200 && type < 300; +} + + +export { + isNormal, + isStatus, + isTip, + isAction, + isRtcPrivate, + isRtcGroup +} \ No newline at end of file diff --git a/im-uniapp/common/recorder-app.js b/im-uniapp/common/recorder-app.js new file mode 100644 index 0000000..f114bbb --- /dev/null +++ b/im-uniapp/common/recorder-app.js @@ -0,0 +1,67 @@ +import UNI_APP from '@/.env.js'; + +const rc = uni.getRecorderManager(); +// 录音开始时间 +let startTime = null; +let checkIsEnable = ()=>{ + return true; +} + +let start = () => { + return new Promise((resolve, reject) => { + rc.onStart(() => { + startTime = new Date(); + resolve() + }); + rc.onError((e) => { + console.log(e); + reject(e) + }) + rc.start({ + format: 'mp3' // 录音格式,可选值:aac/mp3 + }); + }) +} + +let close = () => { + rc.stop(); +} + +let upload = () => { + return new Promise((resolve, reject) => { + rc.onStop((wavFile, a, b) => { + uni.uploadFile({ + url: UNI_APP.BASE_URL + '/file/upload', + header: { + accessToken: uni.getStorageSync("loginInfo").accessToken + }, + filePath: wavFile.tempFilePath, + name: 'file', + success: (res) => { + let r = JSON.parse(res.data); + if (r.code != 200) { + reject(r.message); + } else { + const duration = (new Date().getTime() - startTime.getTime()) / + 1000 + const data = { + duration: Math.round(duration), + url: r.data + } + resolve(data); + } + }, + fail: (e) => { + reject(e); + } + }) + }); + }) +} + +export { + checkIsEnable, + start, + close, + upload +} \ No newline at end of file diff --git a/im-uniapp/common/recorder-h5.js b/im-uniapp/common/recorder-h5.js new file mode 100644 index 0000000..3855942 --- /dev/null +++ b/im-uniapp/common/recorder-h5.js @@ -0,0 +1,92 @@ +import UNI_APP from '@/.env.js'; + +let rc = null; +let duration = 0; +let chunks = []; +let stream = null; +let startTime = null; +let checkIsEnable = () => { + if (origin.indexOf('https') === -1 && origin.indexOf('localhost') === -1 && + origin.indexOf('127.0.0.1') === -1) { + uni.showToast({ + title: '请在https环境中使用录音功能', + icon: 'error' + }) + return false; + } + if (!navigator.mediaDevices || !window.MediaRecorder) { + uni.showToast({ + title: '当前浏览器不支持录音', + icon: 'error' + }) + return false; + } + return true; +} + +let start = () => { + return navigator.mediaDevices.getUserMedia({ audio: true }).then(audioStream => { + startTime = new Date().getTime(); + chunks = []; + stream = audioStream; + rc = new MediaRecorder(stream) + rc.start() + }) +} + +let close = () => { + stream.getTracks().forEach((track) => { + track.stop() + }) + rc.stop() +} + + +let upload = () => { + return new Promise((resolve, reject) => { + rc.ondataavailable = (e) => { + chunks.push(e.data) + } + rc.onstop = () => { + if(!chunks[0].size){ + chunks = []; + return; + } + duration = (new Date().getTime() - startTime) / 1000; + const newbolb = new Blob(chunks, { 'type': 'audio/mpeg' }); + const name = new Date().getDate() + '.mp3'; + const file = new File([newbolb], name) + uni.uploadFile({ + url: UNI_APP.BASE_URL + '/file/upload', + header: { + accessToken: uni.getStorageSync("loginInfo").accessToken + }, + file: file, + name: 'file', + success: (res) => { + let r = JSON.parse(res.data); + if (r.code != 200) { + console.log(res) + reject(r.message); + } else { + const data = { + duration: parseInt(duration), + url: r.data + } + resolve(data); + } + }, + fail: (e) => { + reject(e); + } + }) + } + }) +} + +export { + checkIsEnable, + start, + close, + upload +} \ No newline at end of file diff --git a/im-uniapp/common/request.js b/im-uniapp/common/request.js new file mode 100644 index 0000000..09cfd6b --- /dev/null +++ b/im-uniapp/common/request.js @@ -0,0 +1,93 @@ +import UNI_APP from '@/.env.js' + +// 请求队列 +let requestList = []; +// 是否正在刷新中 +let isRefreshToken = false; + +const request = (options) => { + const header = options.header || {}; + const loginInfo = uni.getStorageSync("loginInfo"); + if (loginInfo) { + header.accessToken = loginInfo.accessToken; + } + return new Promise(function (resolve, reject) { + uni.request({ + url: UNI_APP.BASE_URL + options.url, + method: options.method || 'GET', + header: header, + data: options.data || {}, + timeout: options.timeout || 3000, + async success(res) { + if (res.data.code == 200) { + return resolve(res.data.data) + } else if (res.data.code == 400) { + getApp().$vm.exit(); + } else if (res.data.code == 401) { + console.log("token失效,尝试重新获取") + if (isRefreshToken) { + // 正在刷新token,把其他请求存起来 + requestList.push(() => { + resolve(request(options)) + }) + return; + } + isRefreshToken = true; + // 发送请求, 进行刷新token操作, 获取新的token + const res = await reqRefreshToken(loginInfo); + if (!res || res.data.code != 200) { + requestList = []; + isRefreshToken = false; + console.log("刷新token失败") + getApp().$vm.exit(); + return; + } + let newInfo = res.data.data; + uni.setStorageSync("loginInfo", newInfo); + requestList.forEach(cb => cb()); + requestList = []; + isRefreshToken = false; + // 重新发送刚才的请求 + return resolve(request(options)) + + } else { + uni.showToast({ + icon: "none", + title: res.data.message, + duration: 1500 + }) + return reject(res.data) + } + }, + fail(error) { + uni.showToast({ + title: "网络似乎有点不给力哟", + icon: "none", + duration: 1500 + }) + return reject(error) + } + }); + }); +} + + +const reqRefreshToken = (loginInfo) => { + return new Promise(function (resolve, reject) { + uni.request({ + method: 'PUT', + url: UNI_APP.BASE_URL + '/refreshToken', + header: { + refreshToken: loginInfo.refreshToken + }, + success: (res) => { + resolve(res); + }, + fail: (res) => { + reject(res); + } + }); + }); +} + +export default request; \ No newline at end of file diff --git a/im-uniapp/common/str.js b/im-uniapp/common/str.js new file mode 100644 index 0000000..3f6de92 --- /dev/null +++ b/im-uniapp/common/str.js @@ -0,0 +1,14 @@ +let html2Escape = (strText) => { + return strText.replace(/[<>&"]/g, function(c) { + return { + '<': '<', + '>': '>', + '&': '&', + '"': '"' + } [c]; + }); +} + +export default { + html2Escape +} \ No newline at end of file diff --git a/im-uniapp/common/url.js b/im-uniapp/common/url.js new file mode 100644 index 0000000..c95d834 --- /dev/null +++ b/im-uniapp/common/url.js @@ -0,0 +1,23 @@ + +// 使用正则表达式匹配更广泛的URL格式(此正则由deepseek生成) +const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|]|\bwww\.[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; + +let containUrl = (content) => { + return regex.test(content) +} + + +let replaceURLWithHTMLLinks = (content, color) => { + return content.replace(regex, (url) => { + // 如果URL不以http(s)://开头,则添加http://前缀 + if (!url.startsWith("http")) { + url = "http://" + url; + } + return `${url}`; + }); +} + +export default { + containUrl, + replaceURLWithHTMLLinks +} \ No newline at end of file diff --git a/im-uniapp/common/wssocket.js b/im-uniapp/common/wssocket.js new file mode 100644 index 0000000..5747dc5 --- /dev/null +++ b/im-uniapp/common/wssocket.js @@ -0,0 +1,154 @@ +let accessToken = ""; +let messageCallBack = null; +let closeCallBack = null; +let connectCallBack = null; +let isConnect = false; //连接标识 避免重复连接 +let rec = null; +let lastConnectTime = new Date(); // 最后一次连接时间 +let socketTask = null; + +let connect = (wsurl, token) => { + accessToken = token; + if (isConnect) { + return; + } + lastConnectTime = new Date(); + socketTask = uni.connectSocket({ + url: wsurl, + success: (res) => {}, + fail: (e) => { + console.log(e); + console.log("WebSocket连接失败,10s后重连"); + setTimeout(() => { + connect(); + }, 10000) + } + }); + + socketTask.onOpen((res) => { + console.log("WebSocket连接成功"); + isConnect = true; + // 发送登录命令 + let loginInfo = { + cmd: 0, + data: { + accessToken: accessToken + } + }; + socketTask.send({ + data: JSON.stringify(loginInfo) + }); + }) + + socketTask.onMessage((res) => { + let sendInfo = JSON.parse(res.data) + if (sendInfo.cmd == 0) { + heartCheck.start() + connectCallBack && connectCallBack(); + console.log('WebSocket登录成功') + } else if (sendInfo.cmd == 1) { + // 重新开启心跳定时 + heartCheck.reset(); + } else { + // 其他消息转发出去 + console.log("接收到消息", sendInfo); + messageCallBack && messageCallBack(sendInfo.cmd, sendInfo.data) + } + }) + + socketTask.onClose((res) => { + console.log('WebSocket连接关闭') + isConnect = false; + closeCallBack && closeCallBack(res); + }) + + socketTask.onError(e => { + console.log("ws错误:", e) + close(1006); + isConnect = false; + closeCallBack && closeCallBack(e); + }) +} + +//定义重连函数 +let reconnect = (wsurl, accessToken) => { + console.log("尝试重新连接"); + if (isConnect) { + return; + } + // 延迟10秒重连 避免过多次过频繁请求重连 + let timeDiff = new Date().getTime() - lastConnectTime.getTime() + let delay = timeDiff < 10000 ? 10000 - timeDiff : 0; + rec && clearTimeout(rec); + rec = setTimeout(function() { + connect(wsurl, accessToken); + }, delay); +}; + +//设置关闭连接 +let close = (code) => { + if (!isConnect) { + return; + } + socketTask.close({ + code: code, + complete: (res) => { + isConnect = false; + console.log("关闭websocket连接"); + }, + fail: (e) => { + console.log("关闭websocket连接失败", e); + } + }); +}; + + +// 心跳设置 +let heartCheck = { + timeout: 20000, //每段时间发送一次心跳包 这里设置为20s + timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象) + start: function() { + if (isConnect) { + console.log('发送WebSocket心跳') + let heartBeat = { + cmd: 1, + data: {} + }; + sendMessage(JSON.stringify(heartBeat)) + } + }, + reset: function() { + clearTimeout(this.timeoutObj); + this.timeoutObj = setTimeout(() => heartCheck.start(), this.timeout); + } +}; + +let sendMessage = (message) => { + socketTask.send({ data: message }) +} + +let onConnect = (callback) => { + connectCallBack = callback; +} + + +let onMessage = (callback) => { + messageCallBack = callback; +} + + +let onClose = (callback) => { + closeCallBack = callback; +} + + +// 将方法暴露出去 +export { + connect, + reconnect, + close, + sendMessage, + onConnect, + onMessage, + onClose +} \ No newline at end of file diff --git a/im-uniapp/components/bar/arrow-bar.vue b/im-uniapp/components/bar/arrow-bar.vue new file mode 100644 index 0000000..6ecf189 --- /dev/null +++ b/im-uniapp/components/bar/arrow-bar.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/bar/bar-group.vue b/im-uniapp/components/bar/bar-group.vue new file mode 100644 index 0000000..570965f --- /dev/null +++ b/im-uniapp/components/bar/bar-group.vue @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/bar/btn-bar.vue b/im-uniapp/components/bar/btn-bar.vue new file mode 100644 index 0000000..cc9408c --- /dev/null +++ b/im-uniapp/components/bar/btn-bar.vue @@ -0,0 +1,68 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/bar/switch-bar.vue b/im-uniapp/components/bar/switch-bar.vue new file mode 100644 index 0000000..3616229 --- /dev/null +++ b/im-uniapp/components/bar/switch-bar.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/chat-at-box/chat-at-box.vue b/im-uniapp/components/chat-at-box/chat-at-box.vue new file mode 100644 index 0000000..8f7c257 --- /dev/null +++ b/im-uniapp/components/chat-at-box/chat-at-box.vue @@ -0,0 +1,173 @@ + + + + + + \ No newline at end of file diff --git a/im-uniapp/components/chat-group-readed/chat-group-readed.vue b/im-uniapp/components/chat-group-readed/chat-group-readed.vue new file mode 100644 index 0000000..9e2d7d6 --- /dev/null +++ b/im-uniapp/components/chat-group-readed/chat-group-readed.vue @@ -0,0 +1,133 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/chat-item/chat-item.vue b/im-uniapp/components/chat-item/chat-item.vue new file mode 100644 index 0000000..ddd79da --- /dev/null +++ b/im-uniapp/components/chat-item/chat-item.vue @@ -0,0 +1,200 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/chat-message-item/chat-message-item.vue b/im-uniapp/components/chat-message-item/chat-message-item.vue new file mode 100644 index 0000000..dbcecc3 --- /dev/null +++ b/im-uniapp/components/chat-message-item/chat-message-item.vue @@ -0,0 +1,546 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/chat-record/chat-record.vue b/im-uniapp/components/chat-record/chat-record.vue new file mode 100644 index 0000000..cb390d8 --- /dev/null +++ b/im-uniapp/components/chat-record/chat-record.vue @@ -0,0 +1,243 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/file-upload/file-upload.vue b/im-uniapp/components/file-upload/file-upload.vue new file mode 100644 index 0000000..c364ffd --- /dev/null +++ b/im-uniapp/components/file-upload/file-upload.vue @@ -0,0 +1,122 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/friend-item/friend-item.vue b/im-uniapp/components/friend-item/friend-item.vue new file mode 100644 index 0000000..543ef87 --- /dev/null +++ b/im-uniapp/components/friend-item/friend-item.vue @@ -0,0 +1,82 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/group-item/group-item.vue b/im-uniapp/components/group-item/group-item.vue new file mode 100644 index 0000000..48a7481 --- /dev/null +++ b/im-uniapp/components/group-item/group-item.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/im-uniapp/components/group-member-selector/group-member-selector.vue b/im-uniapp/components/group-member-selector/group-member-selector.vue new file mode 100644 index 0000000..58d9cd8 --- /dev/null +++ b/im-uniapp/components/group-member-selector/group-member-selector.vue @@ -0,0 +1,180 @@ + + + + + + + \ No newline at end of file diff --git a/im-uniapp/components/group-rtc-join/group-rtc-join.vue b/im-uniapp/components/group-rtc-join/group-rtc-join.vue new file mode 100644 index 0000000..6ee2459 --- /dev/null +++ b/im-uniapp/components/group-rtc-join/group-rtc-join.vue @@ -0,0 +1,88 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/head-image/head-image.vue b/im-uniapp/components/head-image/head-image.vue new file mode 100644 index 0000000..9fa8bc2 --- /dev/null +++ b/im-uniapp/components/head-image/head-image.vue @@ -0,0 +1,129 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/image-upload/image-upload.vue b/im-uniapp/components/image-upload/image-upload.vue new file mode 100644 index 0000000..74410da --- /dev/null +++ b/im-uniapp/components/image-upload/image-upload.vue @@ -0,0 +1,100 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/loading/loading.vue b/im-uniapp/components/loading/loading.vue new file mode 100644 index 0000000..b0f0c98 --- /dev/null +++ b/im-uniapp/components/loading/loading.vue @@ -0,0 +1,71 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/long-press-menu/long-press-menu.vue b/im-uniapp/components/long-press-menu/long-press-menu.vue new file mode 100644 index 0000000..22b7c8a --- /dev/null +++ b/im-uniapp/components/long-press-menu/long-press-menu.vue @@ -0,0 +1,129 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/nav-bar/nav-bar.vue b/im-uniapp/components/nav-bar/nav-bar.vue new file mode 100644 index 0000000..5ec1b65 --- /dev/null +++ b/im-uniapp/components/nav-bar/nav-bar.vue @@ -0,0 +1,121 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/components/virtual-scroller/virtual-scroller.vue b/im-uniapp/components/virtual-scroller/virtual-scroller.vue new file mode 100644 index 0000000..48a99e4 --- /dev/null +++ b/im-uniapp/components/virtual-scroller/virtual-scroller.vue @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/hybrid/html/rtc-group/index.html b/im-uniapp/hybrid/html/rtc-group/index.html new file mode 100644 index 0000000..dc38d0f --- /dev/null +++ b/im-uniapp/hybrid/html/rtc-group/index.html @@ -0,0 +1,23 @@ + + + + + + + + 语音通话 + + +
+ 音视频通话功能属于付费功能,如有需要请联系作者购买商业版源码... +
+
+ 点击下方文档了解详细信息: +
+ + + \ No newline at end of file diff --git a/im-uniapp/hybrid/html/rtc-private/index.html b/im-uniapp/hybrid/html/rtc-private/index.html new file mode 100644 index 0000000..6ebde9b --- /dev/null +++ b/im-uniapp/hybrid/html/rtc-private/index.html @@ -0,0 +1,23 @@ + + + + + + + + 视频通话 + + +
+ 音视频通话功能属于付费功能,如有需要请联系作者购买商业版源码... +
+
+ 点击下方文档了解详细信息: +
+ + + \ No newline at end of file diff --git a/im-uniapp/im-var.scss b/im-uniapp/im-var.scss new file mode 100644 index 0000000..abd9878 --- /dev/null +++ b/im-uniapp/im-var.scss @@ -0,0 +1,64 @@ +// 颜色 +$im-color-primary: #3e45d7; +$im-color-primary-light-1: mix(#fff, $im-color-primary, 10%); +$im-color-primary-light-2: mix(#fff, $im-color-primary, 20%); +$im-color-primary-light-3: mix(#fff, $im-color-primary, 30%); +$im-color-primary-light-4: mix(#fff, $im-color-primary, 40%); +$im-color-primary-light-5: mix(#fff, $im-color-primary, 50%); +$im-color-primary-light-6: mix(#fff, $im-color-primary, 60%); +$im-color-primary-light-7: mix(#fff, $im-color-primary, 70%); +$im-color-primary-light-8: mix(#fff, $im-color-primary, 80%); +$im-color-primary-light-9: mix(#fff, $im-color-primary, 90%); +$im-color-primary-dark-1: mix(#000, $im-color-primary, 10%); +$im-color-primary-dark-2: mix(#000, $im-color-primary, 20%); +$im-color-primary-dark-3: mix(#000, $im-color-primary, 30%); +$im-color-primary-dark-4: mix(#000, $im-color-primary, 40%); + +$im-color-success: #18bc37; +$im-color-warning: #f3a73f; +$im-color-danger: #e43d33; +$im-color-info: #8f939c; + +// 文字颜色 +$im-text-color: #000000; +$im-text-color-light: #6a6a6a; +$im-text-color-lighter: #909399; +$im-text-color-lighter-extra: #c7c7c7; + +// 边框颜色 +$im-border: #F0F0F0; +$im-border-light: #EDEDED; +$im-border-lighter: #DCDCDC; +$im-border-lighter-extra: #B9B9B9; + +// 文字大小 +$im-font-size: 30rpx; +$im-font-size-small: 28rpx; +$im-font-size-smaller: 26rpx; +$im-font-size-smaller-extra: 24rpx; +$im-font-size-large: 32rpx; +$im-font-size-larger: 34rpx; +$im-font-size-larger-extra: 36rpx; + +// 阴影 +$im-box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04); +$im-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1); +$im-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, .12); +$im-box-shadow-dark: 0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5); + +// 背景 +$im-bg: #f8f9ff; +$im-bg-linear: linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%); +$im-bg-active: #f7f8fc; +$im-bg-active-dark: $im-color-primary-light-9; +$im-bg-input: #fafafa; + + +// 标题 +$im-title-size: 26px; +$im-title-size-1: 22px; +$im-title-size-2: 18px; + +$font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif; + +$im-nav-bar-height: 50px; \ No newline at end of file diff --git a/im-uniapp/im.scss b/im-uniapp/im.scss new file mode 100644 index 0000000..f063472 --- /dev/null +++ b/im-uniapp/im.scss @@ -0,0 +1,258 @@ +/** 原生button样式 **/ +uni-button { + font-size: $im-font-size !important; + border-radius: 40rpx !important; +} +uni-button[type='primary'] { + color: #fff !important; + background: linear-gradient(135deg, $im-color-primary, lighten($im-color-primary, 15%)) !important; +} + +uni-button[type='primary'][plain] { + color: $im-color-primary !important; + border: 1px solid $im-color-primary; + background-color: transparent; +} + +uni-button[type='warn'] { + color: #fff !important; + background: linear-gradient(135deg, $im-color-danger, lighten($im-color-danger, 15%)) !important; +} + +uni-button[type='warn'][plain] { + color: $im-color-danger !important; + border: 1px solid $im-color-danger !important; + background: transparent !important; +} + +uni-button[size='mini'] { + font-size: $im-font-size-smaller !important; +} + +// #ifdef MP-WEIXIN +// wx小程序只有button,没有uni-botton +button { + font-size: $im-font-size !important; +} + +button[type='primary'] { + color: #fff !important; + background: linear-gradient(135deg, $im-color-primary, lighten($im-color-primary, 15%)) !important; +} + +button[type='primary'][plain] { + color: $im-color-primary !important; + border: 2px solid $im-color-primary; + background: transparent; +} + +button[type='warn'] { + color: #fff !important; + background: linear-gradient(135deg, $im-color-danger, lighten($im-color-danger, 15%)) !important; +} + +button[type='warn'][plain] { + color: $im-color-danger !important; + border: 1px solid $im-color-danger !important; + background: transparent !important; +} + +button[size='mini'] { + font-size: $im-font-size-smaller !important; +} +// #endif + +.button-hover[type='primary'] { + color: #fff !important; + background-color: $im-color-primary-dark-1 !important; +} + +/** uni-ui input激活后边框、图标颜色 **/ +.uni-easyinput__content.is-focused:not(.is-input-error-border) { + border-color: $im-color-primary-light-2 !important; + + .content-clear-icon { + color: $im-color-primary-light-2 !important; + } +} + +/** 底部导航 **/ +.uni-tabbar-bottom .uni-tabbar { + box-shadow: $im-box-shadow; +} + +.uni-tabbar-border { + display: none; +} + +.segmented-control { + border-color: $im-color-primary !important; + + .segmented-control__item--button { + border-color: $im-color-primary !important; + } + .segmented-control__item--button--active { + background-color: $im-color-primary !important; + + .segmented-control__text{ + color: #fff !important; + } + } + .segmented-control__text{ + color: $im-color-primary !important; + } +} + +.uni-radio-input svg{ + border-color: white !important; + background-color: $im-color-primary !important; +} + +.uni-radio-input svg { + background-color: $im-color-primary !important; + border-color: $im-color-primary !important; + background-clip: content-box !important; + box-sizing: border-box; + border-radius: 50%; + transform: translate(-50%, -50%) scale(0.7)!important; +} + +.uni-radio-input svg path{ + fill: $im-color-primary !important; +} + +.uni-radio-input { + background-color: white !important; + border-color: $im-color-primary !important; +} + +.uni-radio-input-disabled { + background-color: rgb(225, 225, 225) !important; + border-color: rgb(209, 209, 209) !important; + opacity: 0.5; +} + +.uni-section__content-title { + font-size: $im-font-size !important; + color: $im-text-color-light; +} + +.uni-forms-item__label { + color: $im-text-color; + font-size: $im-font-size !important; +} +.uni-forms-item { + //margin-bottom: 8px !important; +} +.uni-easyinput__content-input { + font-size: $im-font-size !important; +} +.uni-easyinput__placeholder-class { + color: $im-text-color-lighter; + font-size: $im-font-size !important;; +} +.uni-easyinput__content-textarea { + font-size: $im-font-size !important;; +} +.uni-input-input:disabled { + color: $im-text-color-light; +} + +.uni-forms-item.is-direction-top .uni-forms-item__label { + padding: 0 !important; +} +.uni-data-checklist .checklist-group .checklist-box .checklist-content .checklist-text { + font-size: $im-font-size !important; +} +.uni-card .uni-card__content { + color: unset !important; + padding: 10px 0 !important; +} + +.uni-tag{ + transform: scale(0.9); + margin-left: 5rpx; + border-radius: 12rpx !important; + font-weight: 600 !important; + white-space: nowrap; +} + +.uni-switch-input-checked { + background-color: $im-color-primary-light-1 !important; + border-color: $im-color-primary-light-1 !important; +} +.uni-modal__title { + font-size: $im-font-size-larger !important; +} +.uni-modal__bd { + font-size: $im-font-size; +} +.uni-modal__ft { + font-size: $im-font-size; + line-height: 90rpx !important; + + .uni-modal__btn_primary { + color: $im-color-primary !important; + } +} + + + + +.nav-bar { + height: 100rpx; + padding: 0 20rpx; + display: flex; + align-items: center; + border-bottom: 1rpx solid $im-border; + + .nav-search { + flex: 1; + } + + .nav-add { + cursor: pointer; + } +} + + +.bottom-btn { + margin: 40rpx 40rpx; + + uni-button + uni-button { + margin-top: 20rpx; + } +} + +.emoji-large { + width: 64rpx !important; + height: 64rpx !important; + vertical-align: bottom !important; +} + +.emoji-normal { + width: 54rpx !important; + height: 54rpx !important; + vertical-align: bottom !important; +} + +.emoji-small { + width: 36rpx !important; + height: 36rpx !important; + vertical-align: bottom !important; +} + +.none-pointer-events { + uni-image img { + // 阻止微信默认长按菜单 + pointer-events: none; + -webkit-pointer-events: none; + -ms-pointer-events: none; + -moz-pointer-events: none; + } +} + +p { + margin-block-start: 1em; + margin-block-end: 1em; +} diff --git a/im-uniapp/index.html b/im-uniapp/index.html new file mode 100644 index 0000000..c3ff205 --- /dev/null +++ b/im-uniapp/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/im-uniapp/main.js b/im-uniapp/main.js new file mode 100644 index 0000000..8fa350a --- /dev/null +++ b/im-uniapp/main.js @@ -0,0 +1,65 @@ +import App from './App' +import request from './common/request'; +import emotion from './common/emotion.js'; +import url from './common/url.js'; +import str from './common/str.js'; +import * as enums from './common/enums.js'; +import * as date from './common/date'; +import * as socketApi from './common/wssocket'; +import * as messageType from './common/messageType'; +import { createSSRApp } from 'vue' +import uviewPlus from '@/uni_modules/uview-plus' +import * as pinia from 'pinia'; +import useChatStore from '@/store/chatStore.js' +import useFriendStore from '@/store/friendStore.js' +import useGroupStore from '@/store/groupStore.js' +import useConfigStore from '@/store/configStore.js' +import useUserStore from '@/store/userStore.js' +import barGroup from '@/components/bar/bar-group' +import arrowBar from '@/components/bar/arrow-bar' +import btnBar from '@/components/bar/btn-bar' +import switchBar from '@/components/bar/switch-bar' +// #ifdef H5 +import * as recorder from './common/recorder-h5'; +import ImageResize from "quill-image-resize-mp"; +import Quill from "quill"; +// 以下组件用于兼容部分手机聊天边框无法输入的问题 +window.Quill = Quill; +window.ImageResize = { default: ImageResize }; +// 调试器 +// import VConsole from 'vconsole' +// new VConsole(); +// #endif +// #ifndef H5 +import * as recorder from './common/recorder-app'; +// #endif +export function createApp() { + const app = createSSRApp(App) + app.use(uviewPlus); + app.use(pinia.createPinia()); + app.component('bar-group', barGroup); + app.component('arrow-bar', arrowBar); + app.component('btn-bar', btnBar); + app.component('switch-bar', switchBar); + app.config.globalProperties.$http = request; + app.config.globalProperties.$wsApi = socketApi; + app.config.globalProperties.$msgType = messageType; + app.config.globalProperties.$emo = emotion; + app.config.globalProperties.$url = url; + app.config.globalProperties.$str = str; + app.config.globalProperties.$enums = enums; + app.config.globalProperties.$date = date; + app.config.globalProperties.$rc = recorder; + // 初始化时再挂载store对象 + app.config.globalProperties.$mountStore = () => { + app.config.globalProperties.chatStore = useChatStore(); + app.config.globalProperties.friendStore = useFriendStore(); + app.config.globalProperties.groupStore = useGroupStore(); + app.config.globalProperties.configStore = useConfigStore(); + app.config.globalProperties.userStore = useUserStore(); + } + return { + app, + pinia + } +} \ No newline at end of file diff --git a/im-uniapp/manifest.json b/im-uniapp/manifest.json new file mode 100644 index 0000000..ff617c0 --- /dev/null +++ b/im-uniapp/manifest.json @@ -0,0 +1,136 @@ +{ + "name" : "盒子IM", + "appid" : "__UNI__69DD57A", + "description" : "", + "versionName" : "3.4.0", + "versionCode" : 3400, + "transformPx" : false, + /* 5+App特有相关 */ + "app-plus" : { + "usingComponents" : true, + "nvueStyleCompiler" : "uni-app", + "compilerVersion" : 3, + "splashscreen" : { + "alwaysShowBeforeRender" : false, + "waiting" : false, + "autoclose" : false, + "delay" : 0 + }, + /* 模块配置 */ + "modules" : { + "Camera" : {}, + "Record" : {}, + "Bluetooth" : {} + }, + "softinput" : { + "mode" : "adjustResize" + }, + /* 应用发布信息 */ + "distribute" : { + /* android打包配置 */ + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + "abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ], + "minSdkVersion" : 21 + }, + /* ios打包配置 */ + "ios" : { + "dSYMs" : false, + "privacyDescription" : { + "NSMicrophoneUsageDescription" : "", + "NSCameraUsageDescription" : "" + }, + "idfa" : false + }, + /* SDK配置 */ + "sdkConfigs" : { + "ad" : {}, + "speech" : {} + }, + "icons" : { + "android" : { + "xhdpi" : "unpackage/res/icons/96x96.png", + "hdpi" : "unpackage/res/icons/72x72.png", + "xxhdpi" : "unpackage/res/icons/144x144.png", + "xxxhdpi" : "unpackage/res/icons/192x192.png" + }, + "ios" : { + "appstore" : "unpackage/res/icons/1024x1024.png", + "ipad" : { + "app" : "unpackage/res/icons/76x76.png", + "app@2x" : "unpackage/res/icons/152x152.png", + "notification" : "unpackage/res/icons/20x20.png", + "notification@2x" : "unpackage/res/icons/40x40.png", + "proapp@2x" : "unpackage/res/icons/167x167.png", + "settings" : "unpackage/res/icons/29x29.png", + "settings@2x" : "unpackage/res/icons/58x58.png", + "spotlight" : "unpackage/res/icons/40x40.png", + "spotlight@2x" : "unpackage/res/icons/80x80.png" + }, + "iphone" : { + "app@2x" : "unpackage/res/icons/120x120.png", + "app@3x" : "unpackage/res/icons/180x180.png", + "notification@2x" : "unpackage/res/icons/40x40.png", + "notification@3x" : "unpackage/res/icons/60x60.png", + "settings@2x" : "unpackage/res/icons/58x58.png", + "settings@3x" : "unpackage/res/icons/87x87.png", + "spotlight@2x" : "unpackage/res/icons/80x80.png", + "spotlight@3x" : "unpackage/res/icons/120x120.png" + } + } + } + } + }, + /* 快应用特有相关 */ + "quickapp" : {}, + /* 小程序特有相关 */ + "mp-weixin" : { + "appid" : "wxda94f40bfad0262c", + "libVersion" : "latest", + "setting" : { + "urlCheck" : false, + "minified" : true + }, + "usingComponents" : true, + "lazyCodeLoading" : "requiredComponents" + }, + "mp-alipay" : { + "usingComponents" : true + }, + "mp-baidu" : { + "usingComponents" : true + }, + "mp-toutiao" : { + "usingComponents" : true + }, + "uniStatistics" : { + "enable" : false + }, + "vueVersion" : "3", + "h5" : { + "title" : "盒子IM", + "router" : { + "base" : "/h5/" + } + } +} +/* ios打包配置 *//* SDK配置 */ + diff --git a/im-uniapp/package.json b/im-uniapp/package.json new file mode 100644 index 0000000..602b736 --- /dev/null +++ b/im-uniapp/package.json @@ -0,0 +1,12 @@ +{ + "name": "boxim", + "uni-app": { + "scripts": {} + }, + "dependencies": { + "pinyin-pro": "^3.23.1", + "quill": "^1.3.7", + "quill-image-resize-mp": "^3.0.1", + "vconsole": "^3.15.1" + } +} \ No newline at end of file diff --git a/im-uniapp/pages.json b/im-uniapp/pages.json new file mode 100644 index 0000000..e4df795 --- /dev/null +++ b/im-uniapp/pages.json @@ -0,0 +1,114 @@ +{ + "easycom": { + "autoscan": true, + "custom": { + "^u--(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue", + "^up-(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue", + "^u-([^-].*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue" + } + }, + "pages": [{ + "path": "pages/chat/chat" + }, { + "path": "pages/login/login" + }, + { + "path": "pages/register/register" + }, + { + "path": "pages/friend/friend" + }, + { + "path": "pages/group/group" + }, + { + "path": "pages/mine/mine" + }, + { + "path": "pages/common/user-info" + }, + { + "path": "pages/chat/chat-box", + "style": { + "mp-weixin": { + "disableScroll": true + }, + "app-plus": { + // adjustPan窗体高度不变,但窗体上推、adjustResize屏幕高度=webview窗体高度+软键盘高度 + "softinputMode": "adjustResize" + } + } + }, + { + "path": "pages/chat/chat-private-video" + }, + { + "path": "pages/chat/chat-group-video" + }, + { + "path": "pages/friend/friend-add" + }, + { + "path": "pages/group/group-info" + }, + { + "path": "pages/group/group-edit" + }, + { + "path": "pages/group/group-invite" + }, + { + "path": "pages/group/group-member" + }, + { + "path": "pages/mine/mine-edit" + }, + { + "path": "pages/mine/mine-password" + } + ], + "globalStyle": { + "navigationBarTitleText": "盒子IM", + "navigationBarTextStyle": "black", + /* #ifndef MP-WEIXIN */ + "navigationStyle": "custom", + /* #endif */ + /* #ifdef MP-WEIXIN */ + "navigationStyle": "default", + /* #endif */ + "navigationBarBackgroundColor": "#f7f7f7", + "backgroundColor": "#f7f7f7" + }, + "tabBar": { + "color": "#000000", + "selectedColor": "#587ff0", + "borderStyle": "black", + "backgroundColor": "#ffffff", + "list": [{ + "pagePath": "pages/chat/chat", + "iconPath": "static/tab/chat.png", + "selectedIconPath": "static/tab/chat_active.png", + "text": "消息" + }, + { + "pagePath": "pages/friend/friend", + "iconPath": "static/tab/friend.png", + "selectedIconPath": "static/tab/friend_active.png", + "text": "好友" + }, + { + "pagePath": "pages/group/group", + "iconPath": "static/tab/group.png", + "selectedIconPath": "static/tab/group_active.png", + "text": "群聊" + }, + { + "pagePath": "pages/mine/mine", + "iconPath": "static/tab/mine.png", + "selectedIconPath": "static/tab/mine_active.png", + "text": "我的" + } + ] + }, + "uniIdRouter": {} +} \ No newline at end of file diff --git a/im-uniapp/pages/chat/chat-box.vue b/im-uniapp/pages/chat/chat-box.vue new file mode 100644 index 0000000..ff3b7e7 --- /dev/null +++ b/im-uniapp/pages/chat/chat-box.vue @@ -0,0 +1,1358 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/chat/chat-group-video.vue b/im-uniapp/pages/chat/chat-group-video.vue new file mode 100644 index 0000000..434771f --- /dev/null +++ b/im-uniapp/pages/chat/chat-group-video.vue @@ -0,0 +1,138 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/chat/chat-private-video.vue b/im-uniapp/pages/chat/chat-private-video.vue new file mode 100644 index 0000000..0677af7 --- /dev/null +++ b/im-uniapp/pages/chat/chat-private-video.vue @@ -0,0 +1,100 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/chat/chat.vue b/im-uniapp/pages/chat/chat.vue new file mode 100644 index 0000000..a8df83e --- /dev/null +++ b/im-uniapp/pages/chat/chat.vue @@ -0,0 +1,207 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/common/external-link.vue b/im-uniapp/pages/common/external-link.vue new file mode 100644 index 0000000..678df4a --- /dev/null +++ b/im-uniapp/pages/common/external-link.vue @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/common/user-info.vue b/im-uniapp/pages/common/user-info.vue new file mode 100644 index 0000000..396cd88 --- /dev/null +++ b/im-uniapp/pages/common/user-info.vue @@ -0,0 +1,254 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/friend/friend-add.vue b/im-uniapp/pages/friend/friend-add.vue new file mode 100644 index 0000000..b8c312a --- /dev/null +++ b/im-uniapp/pages/friend/friend-add.vue @@ -0,0 +1,146 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/friend/friend.vue b/im-uniapp/pages/friend/friend.vue new file mode 100644 index 0000000..713df63 --- /dev/null +++ b/im-uniapp/pages/friend/friend.vue @@ -0,0 +1,192 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/group/group-edit.vue b/im-uniapp/pages/group/group-edit.vue new file mode 100644 index 0000000..fd7cdee --- /dev/null +++ b/im-uniapp/pages/group/group-edit.vue @@ -0,0 +1,199 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/group/group-info.vue b/im-uniapp/pages/group/group-info.vue new file mode 100644 index 0000000..df78bae --- /dev/null +++ b/im-uniapp/pages/group/group-info.vue @@ -0,0 +1,377 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/group/group-invite.vue b/im-uniapp/pages/group/group-invite.vue new file mode 100644 index 0000000..a0256df --- /dev/null +++ b/im-uniapp/pages/group/group-invite.vue @@ -0,0 +1,176 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/group/group-member.vue b/im-uniapp/pages/group/group-member.vue new file mode 100644 index 0000000..8d2cacf --- /dev/null +++ b/im-uniapp/pages/group/group-member.vue @@ -0,0 +1,127 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/group/group.vue b/im-uniapp/pages/group/group.vue new file mode 100644 index 0000000..ed71653 --- /dev/null +++ b/im-uniapp/pages/group/group.vue @@ -0,0 +1,119 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/login/login.vue b/im-uniapp/pages/login/login.vue new file mode 100644 index 0000000..03c9e5b --- /dev/null +++ b/im-uniapp/pages/login/login.vue @@ -0,0 +1,293 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/mine/mine-edit.vue b/im-uniapp/pages/mine/mine-edit.vue new file mode 100644 index 0000000..f3e6bc2 --- /dev/null +++ b/im-uniapp/pages/mine/mine-edit.vue @@ -0,0 +1,140 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/mine/mine-password.vue b/im-uniapp/pages/mine/mine-password.vue new file mode 100644 index 0000000..4b7f8f0 --- /dev/null +++ b/im-uniapp/pages/mine/mine-password.vue @@ -0,0 +1,101 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pages/mine/mine.vue b/im-uniapp/pages/mine/mine.vue new file mode 100644 index 0000000..ecc8654 --- /dev/null +++ b/im-uniapp/pages/mine/mine.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/im-uniapp/pages/register/register.vue b/im-uniapp/pages/register/register.vue new file mode 100644 index 0000000..d511311 --- /dev/null +++ b/im-uniapp/pages/register/register.vue @@ -0,0 +1,351 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/pnpm-lock.yaml b/im-uniapp/pnpm-lock.yaml new file mode 100644 index 0000000..2bf897b --- /dev/null +++ b/im-uniapp/pnpm-lock.yaml @@ -0,0 +1,373 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + pinyin-pro: + specifier: ^3.23.1 + version: 3.27.0 + quill: + specifier: ^1.3.7 + version: 1.3.7 + quill-image-resize-mp: + specifier: ^3.0.1 + version: 3.0.1 + vconsole: + specifier: ^3.15.1 + version: 3.15.1 + +packages: + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + copy-text-to-clipboard@3.2.2: + resolution: {integrity: sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==} + engines: {node: '>=12'} + + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + + deep-equal@1.1.2: + resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} + engines: {node: '>= 0.4'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + eventemitter3@2.0.3: + resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-diff@1.1.2: + resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mutation-observer@1.0.3: + resolution: {integrity: sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + parchment@1.1.4: + resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==} + + pinyin-pro@3.27.0: + resolution: {integrity: sha512-Osdgjwe7Rm17N2paDMM47yW+jUIUH3+0RGo8QP39ZTLpTaJVDK0T58hOLaMQJbcMmAebVuK2ePunTEVEx1clNQ==} + + quill-delta@3.6.3: + resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==} + engines: {node: '>=0.10'} + + quill-image-resize-mp@3.0.1: + resolution: {integrity: sha512-etBtUbNH3XY21PVwfclMNGkifjWKSVXei4Dc4GWVeUyxOoympcNaV+Bk7Ocjc6Y8VlDKs2tcf3zyJ9syIFL/KA==} + + quill@1.3.7: + resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + vconsole@3.15.1: + resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==} + +snapshots: + + '@babel/runtime@7.28.4': {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + clone@2.1.2: {} + + copy-text-to-clipboard@3.2.2: {} + + core-js@3.47.0: {} + + deep-equal@1.1.2: + dependencies: + is-arguments: 1.2.0 + is-date-object: 1.1.0 + is-regex: 1.2.1 + object-is: 1.1.6 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + eventemitter3@2.0.3: {} + + extend@3.0.2: {} + + fast-diff@1.1.2: {} + + function-bind@1.1.2: {} + + functions-have-names@1.2.3: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + lodash@4.17.21: {} + + math-intrinsics@1.1.0: {} + + mutation-observer@1.0.3: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + parchment@1.1.4: {} + + pinyin-pro@3.27.0: {} + + quill-delta@3.6.3: + dependencies: + deep-equal: 1.1.2 + extend: 3.0.2 + fast-diff: 1.1.2 + + quill-image-resize-mp@3.0.1: + dependencies: + lodash: 4.17.21 + + quill@1.3.7: + dependencies: + clone: 2.1.2 + deep-equal: 1.1.2 + eventemitter3: 2.0.3 + extend: 3.0.2 + parchment: 1.1.4 + quill-delta: 3.6.3 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + vconsole@3.15.1: + dependencies: + '@babel/runtime': 7.28.4 + copy-text-to-clipboard: 3.2.2 + core-js: 3.47.0 + mutation-observer: 1.0.3 diff --git a/im-uniapp/static/emoji/0.gif b/im-uniapp/static/emoji/0.gif new file mode 100644 index 0000000..e7848ec Binary files /dev/null and b/im-uniapp/static/emoji/0.gif differ diff --git a/im-uniapp/static/emoji/1.gif b/im-uniapp/static/emoji/1.gif new file mode 100644 index 0000000..cb5b26b Binary files /dev/null and b/im-uniapp/static/emoji/1.gif differ diff --git a/im-uniapp/static/emoji/10.gif b/im-uniapp/static/emoji/10.gif new file mode 100644 index 0000000..53a6c67 Binary files /dev/null and b/im-uniapp/static/emoji/10.gif differ diff --git a/im-uniapp/static/emoji/11.gif b/im-uniapp/static/emoji/11.gif new file mode 100644 index 0000000..d718e6d Binary files /dev/null and b/im-uniapp/static/emoji/11.gif differ diff --git a/im-uniapp/static/emoji/12.gif b/im-uniapp/static/emoji/12.gif new file mode 100644 index 0000000..2362b45 Binary files /dev/null and b/im-uniapp/static/emoji/12.gif differ diff --git a/im-uniapp/static/emoji/13.gif b/im-uniapp/static/emoji/13.gif new file mode 100644 index 0000000..4d8dc78 Binary files /dev/null and b/im-uniapp/static/emoji/13.gif differ diff --git a/im-uniapp/static/emoji/14.gif b/im-uniapp/static/emoji/14.gif new file mode 100644 index 0000000..131c3d9 Binary files /dev/null and b/im-uniapp/static/emoji/14.gif differ diff --git a/im-uniapp/static/emoji/15.gif b/im-uniapp/static/emoji/15.gif new file mode 100644 index 0000000..25c57ae Binary files /dev/null and b/im-uniapp/static/emoji/15.gif differ diff --git a/im-uniapp/static/emoji/16.gif b/im-uniapp/static/emoji/16.gif new file mode 100644 index 0000000..2711656 Binary files /dev/null and b/im-uniapp/static/emoji/16.gif differ diff --git a/im-uniapp/static/emoji/17.gif b/im-uniapp/static/emoji/17.gif new file mode 100644 index 0000000..4419ba2 Binary files /dev/null and b/im-uniapp/static/emoji/17.gif differ diff --git a/im-uniapp/static/emoji/18.gif b/im-uniapp/static/emoji/18.gif new file mode 100644 index 0000000..478a421 Binary files /dev/null and b/im-uniapp/static/emoji/18.gif differ diff --git a/im-uniapp/static/emoji/19.gif b/im-uniapp/static/emoji/19.gif new file mode 100644 index 0000000..59261ea Binary files /dev/null and b/im-uniapp/static/emoji/19.gif differ diff --git a/im-uniapp/static/emoji/2.gif b/im-uniapp/static/emoji/2.gif new file mode 100644 index 0000000..b01cbcf Binary files /dev/null and b/im-uniapp/static/emoji/2.gif differ diff --git a/im-uniapp/static/emoji/20.gif b/im-uniapp/static/emoji/20.gif new file mode 100644 index 0000000..45becb4 Binary files /dev/null and b/im-uniapp/static/emoji/20.gif differ diff --git a/im-uniapp/static/emoji/21.gif b/im-uniapp/static/emoji/21.gif new file mode 100644 index 0000000..d3f1afe Binary files /dev/null and b/im-uniapp/static/emoji/21.gif differ diff --git a/im-uniapp/static/emoji/22.gif b/im-uniapp/static/emoji/22.gif new file mode 100644 index 0000000..ee220d1 Binary files /dev/null and b/im-uniapp/static/emoji/22.gif differ diff --git a/im-uniapp/static/emoji/23.gif b/im-uniapp/static/emoji/23.gif new file mode 100644 index 0000000..5dbd9f3 Binary files /dev/null and b/im-uniapp/static/emoji/23.gif differ diff --git a/im-uniapp/static/emoji/24.gif b/im-uniapp/static/emoji/24.gif new file mode 100644 index 0000000..d3ae946 Binary files /dev/null and b/im-uniapp/static/emoji/24.gif differ diff --git a/im-uniapp/static/emoji/25.gif b/im-uniapp/static/emoji/25.gif new file mode 100644 index 0000000..5cf2a9e Binary files /dev/null and b/im-uniapp/static/emoji/25.gif differ diff --git a/im-uniapp/static/emoji/26.gif b/im-uniapp/static/emoji/26.gif new file mode 100644 index 0000000..793b2ac Binary files /dev/null and b/im-uniapp/static/emoji/26.gif differ diff --git a/im-uniapp/static/emoji/27.gif b/im-uniapp/static/emoji/27.gif new file mode 100644 index 0000000..b78a02b Binary files /dev/null and b/im-uniapp/static/emoji/27.gif differ diff --git a/im-uniapp/static/emoji/28.gif b/im-uniapp/static/emoji/28.gif new file mode 100644 index 0000000..4074630 Binary files /dev/null and b/im-uniapp/static/emoji/28.gif differ diff --git a/im-uniapp/static/emoji/29.gif b/im-uniapp/static/emoji/29.gif new file mode 100644 index 0000000..166f252 Binary files /dev/null and b/im-uniapp/static/emoji/29.gif differ diff --git a/im-uniapp/static/emoji/3.gif b/im-uniapp/static/emoji/3.gif new file mode 100644 index 0000000..3b15c53 Binary files /dev/null and b/im-uniapp/static/emoji/3.gif differ diff --git a/im-uniapp/static/emoji/30.gif b/im-uniapp/static/emoji/30.gif new file mode 100644 index 0000000..13499b9 Binary files /dev/null and b/im-uniapp/static/emoji/30.gif differ diff --git a/im-uniapp/static/emoji/31.gif b/im-uniapp/static/emoji/31.gif new file mode 100644 index 0000000..b4ec1d8 Binary files /dev/null and b/im-uniapp/static/emoji/31.gif differ diff --git a/im-uniapp/static/emoji/32.gif b/im-uniapp/static/emoji/32.gif new file mode 100644 index 0000000..ca86399 Binary files /dev/null and b/im-uniapp/static/emoji/32.gif differ diff --git a/im-uniapp/static/emoji/33.gif b/im-uniapp/static/emoji/33.gif new file mode 100644 index 0000000..8ec811a Binary files /dev/null and b/im-uniapp/static/emoji/33.gif differ diff --git a/im-uniapp/static/emoji/34.gif b/im-uniapp/static/emoji/34.gif new file mode 100644 index 0000000..2a3c006 Binary files /dev/null and b/im-uniapp/static/emoji/34.gif differ diff --git a/im-uniapp/static/emoji/35.gif b/im-uniapp/static/emoji/35.gif new file mode 100644 index 0000000..7fc7f94 Binary files /dev/null and b/im-uniapp/static/emoji/35.gif differ diff --git a/im-uniapp/static/emoji/36.gif b/im-uniapp/static/emoji/36.gif new file mode 100644 index 0000000..f0d46d0 Binary files /dev/null and b/im-uniapp/static/emoji/36.gif differ diff --git a/im-uniapp/static/emoji/37.gif b/im-uniapp/static/emoji/37.gif new file mode 100644 index 0000000..2c9c084 Binary files /dev/null and b/im-uniapp/static/emoji/37.gif differ diff --git a/im-uniapp/static/emoji/38.gif b/im-uniapp/static/emoji/38.gif new file mode 100644 index 0000000..b6b4e09 Binary files /dev/null and b/im-uniapp/static/emoji/38.gif differ diff --git a/im-uniapp/static/emoji/39.gif b/im-uniapp/static/emoji/39.gif new file mode 100644 index 0000000..cfb75c6 Binary files /dev/null and b/im-uniapp/static/emoji/39.gif differ diff --git a/im-uniapp/static/emoji/4.gif b/im-uniapp/static/emoji/4.gif new file mode 100644 index 0000000..fd60e4e Binary files /dev/null and b/im-uniapp/static/emoji/4.gif differ diff --git a/im-uniapp/static/emoji/40.gif b/im-uniapp/static/emoji/40.gif new file mode 100644 index 0000000..c5d7a8b Binary files /dev/null and b/im-uniapp/static/emoji/40.gif differ diff --git a/im-uniapp/static/emoji/41.gif b/im-uniapp/static/emoji/41.gif new file mode 100644 index 0000000..cf1c0a1 Binary files /dev/null and b/im-uniapp/static/emoji/41.gif differ diff --git a/im-uniapp/static/emoji/42.gif b/im-uniapp/static/emoji/42.gif new file mode 100644 index 0000000..cc87f9e Binary files /dev/null and b/im-uniapp/static/emoji/42.gif differ diff --git a/im-uniapp/static/emoji/43.gif b/im-uniapp/static/emoji/43.gif new file mode 100644 index 0000000..46153bc Binary files /dev/null and b/im-uniapp/static/emoji/43.gif differ diff --git a/im-uniapp/static/emoji/44.gif b/im-uniapp/static/emoji/44.gif new file mode 100644 index 0000000..dd45118 Binary files /dev/null and b/im-uniapp/static/emoji/44.gif differ diff --git a/im-uniapp/static/emoji/45.gif b/im-uniapp/static/emoji/45.gif new file mode 100644 index 0000000..a3e86d6 Binary files /dev/null and b/im-uniapp/static/emoji/45.gif differ diff --git a/im-uniapp/static/emoji/46.gif b/im-uniapp/static/emoji/46.gif new file mode 100644 index 0000000..b3fb681 Binary files /dev/null and b/im-uniapp/static/emoji/46.gif differ diff --git a/im-uniapp/static/emoji/47.gif b/im-uniapp/static/emoji/47.gif new file mode 100644 index 0000000..d24bdbb Binary files /dev/null and b/im-uniapp/static/emoji/47.gif differ diff --git a/im-uniapp/static/emoji/48.gif b/im-uniapp/static/emoji/48.gif new file mode 100644 index 0000000..ab98b2b Binary files /dev/null and b/im-uniapp/static/emoji/48.gif differ diff --git a/im-uniapp/static/emoji/49.gif b/im-uniapp/static/emoji/49.gif new file mode 100644 index 0000000..19a7e72 Binary files /dev/null and b/im-uniapp/static/emoji/49.gif differ diff --git a/im-uniapp/static/emoji/5.gif b/im-uniapp/static/emoji/5.gif new file mode 100644 index 0000000..85cd373 Binary files /dev/null and b/im-uniapp/static/emoji/5.gif differ diff --git a/im-uniapp/static/emoji/50.gif b/im-uniapp/static/emoji/50.gif new file mode 100644 index 0000000..51e4093 Binary files /dev/null and b/im-uniapp/static/emoji/50.gif differ diff --git a/im-uniapp/static/emoji/51.gif b/im-uniapp/static/emoji/51.gif new file mode 100644 index 0000000..9bf82a8 Binary files /dev/null and b/im-uniapp/static/emoji/51.gif differ diff --git a/im-uniapp/static/emoji/52.gif b/im-uniapp/static/emoji/52.gif new file mode 100644 index 0000000..18391ea Binary files /dev/null and b/im-uniapp/static/emoji/52.gif differ diff --git a/im-uniapp/static/emoji/53.gif b/im-uniapp/static/emoji/53.gif new file mode 100644 index 0000000..0f584b5 Binary files /dev/null and b/im-uniapp/static/emoji/53.gif differ diff --git a/im-uniapp/static/emoji/54.gif b/im-uniapp/static/emoji/54.gif new file mode 100644 index 0000000..5ab6d25 Binary files /dev/null and b/im-uniapp/static/emoji/54.gif differ diff --git a/im-uniapp/static/emoji/55.gif b/im-uniapp/static/emoji/55.gif new file mode 100644 index 0000000..3d75ad7 Binary files /dev/null and b/im-uniapp/static/emoji/55.gif differ diff --git a/im-uniapp/static/emoji/56.gif b/im-uniapp/static/emoji/56.gif new file mode 100644 index 0000000..2525970 Binary files /dev/null and b/im-uniapp/static/emoji/56.gif differ diff --git a/im-uniapp/static/emoji/6.gif b/im-uniapp/static/emoji/6.gif new file mode 100644 index 0000000..a49c862 Binary files /dev/null and b/im-uniapp/static/emoji/6.gif differ diff --git a/im-uniapp/static/emoji/7.gif b/im-uniapp/static/emoji/7.gif new file mode 100644 index 0000000..3a847b8 Binary files /dev/null and b/im-uniapp/static/emoji/7.gif differ diff --git a/im-uniapp/static/emoji/8.gif b/im-uniapp/static/emoji/8.gif new file mode 100644 index 0000000..eb6db6a Binary files /dev/null and b/im-uniapp/static/emoji/8.gif differ diff --git a/im-uniapp/static/emoji/9.gif b/im-uniapp/static/emoji/9.gif new file mode 100644 index 0000000..52562dc Binary files /dev/null and b/im-uniapp/static/emoji/9.gif differ diff --git a/im-uniapp/static/icon/iconfont.css b/im-uniapp/static/icon/iconfont.css new file mode 100644 index 0000000..295c2a6 --- /dev/null +++ b/im-uniapp/static/icon/iconfont.css @@ -0,0 +1,261 @@ +@font-face { + font-family: "iconfont"; /* Project id 4272106 */ + src: url('iconfont.ttf?t=1759053579007') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-chat:before { + content: "\e93e"; +} + +.icon-create-group:before { + content: "\e65b"; +} + +.icon-refresh:before { + content: "\e64c"; +} + +.icon-copy:before { + content: "\e604"; +} + +.icon-code:before { + content: "\e72c"; +} + +.icon-username:before { + content: "\e623"; +} + +.icon-pwd:before { + content: "\e60d"; +} + +.icon-pwd-hide:before { + content: "\e60c"; +} + +.icon-username2:before { + content: "\e647"; +} + +.icon-pwd-show:before { + content: "\e621"; +} + +.icon-teenager:before { + content: "\eba1"; +} + +.icon-warning-circle-empty:before { + content: "\e606"; +} + +.icon-loading2:before { + content: "\e6b6"; +} + +.icon-complaint:before { + content: "\e612"; +} + +.icon-about:before { + content: "\e637"; +} + +.icon-security:before { + content: "\e61d"; +} + +.icon-personal-info:before { + content: "\e62d"; +} + +.icon-success:before { + content: "\e649"; +} + +.icon-wait:before { + content: "\e701"; +} + +.icon-error:before { + content: "\e62b"; +} + +.icon-dnd:before { + content: "\e693"; +} + +.icon-create-group-2:before { + content: "\e616"; +} + +.icon-qrcode:before { + content: "\e642"; +} + +.icon-add-friend:before { + content: "\e64f"; +} + +.icon-scan:before { + content: "\e8b5"; +} + +.icon-remove:before { + content: "\e603"; +} + +.icon-doc:before { + content: "\e61c"; +} + +.icon-image:before { + content: "\e7f7"; +} + +.icon-top-message:before { + content: "\e6ff"; +} + +.icon-setting:before { + content: "\e851"; +} + +.icon-phone:before { + content: "\e692"; +} + +.icon-email:before { + content: "\e611"; +} + +.icon-chat-muted:before { + content: "\e634"; +} + +.icon-chat-unmuted:before { + content: "\ec44"; +} + +.icon-un-register:before { + content: "\e656"; +} + +.icon-modify-pwd:before { + content: "\e63c"; +} + +.icon-film:before { + content: "\e66b"; +} + +.icon-delete:before { + content: "\e605"; +} + +.icon-receipt:before { + content: "\e601"; +} + +.icon-pause:before { + content: "\e669"; +} + +.icon-play:before { + content: "\e620"; +} + +.icon-voice-play:before { + content: "\e675"; +} + +.icon-chat-video:before { + content: "\e73b"; +} + +.icon-chat-voice:before { + content: "\e633"; +} + +.icon-video:before { + content: "\e685"; +} + +.icon-ok:before { + content: "\e65a"; +} + +.icon-at:before { + content: "\e7de"; +} + +.icon-man:before { + content: "\e615"; +} + +.icon-girl:before { + content: "\e602"; +} + +.icon-file:before { + content: "\e671"; +} + +.icon-add:before { + content: "\e66c"; +} + +.icon-warning-circle-fill:before { + content: "\e848"; +} + +.icon-loading:before { + content: "\e93d"; +} + +.icon-camera:before { + content: "\e600"; +} + +.icon-folder:before { + content: "\e6a0"; +} + +.icon-microphone:before { + content: "\e63b"; +} + +.icon-icon_emoji:before { + content: "\e619"; +} + +.icon-call:before { + content: "\e610"; +} + +.icon-keyboard:before { + content: "\e679"; +} + +.icon-voice-circle:before { + content: "\e67f"; +} + +.icon-picture:before { + content: "\e653"; +} + +.icon-search:before { + content: "\e648"; +} + diff --git a/im-uniapp/static/icon/iconfont.ttf b/im-uniapp/static/icon/iconfont.ttf new file mode 100644 index 0000000..85b17d0 Binary files /dev/null and b/im-uniapp/static/icon/iconfont.ttf differ diff --git a/im-uniapp/static/image/online_app.png b/im-uniapp/static/image/online_app.png new file mode 100644 index 0000000..173d415 Binary files /dev/null and b/im-uniapp/static/image/online_app.png differ diff --git a/im-uniapp/static/image/online_web.png b/im-uniapp/static/image/online_web.png new file mode 100644 index 0000000..6e47078 Binary files /dev/null and b/im-uniapp/static/image/online_web.png differ diff --git a/im-uniapp/static/tab/chat.png b/im-uniapp/static/tab/chat.png new file mode 100644 index 0000000..61254e3 Binary files /dev/null and b/im-uniapp/static/tab/chat.png differ diff --git a/im-uniapp/static/tab/chat_active.png b/im-uniapp/static/tab/chat_active.png new file mode 100644 index 0000000..3ae1599 Binary files /dev/null and b/im-uniapp/static/tab/chat_active.png differ diff --git a/im-uniapp/static/tab/friend.png b/im-uniapp/static/tab/friend.png new file mode 100644 index 0000000..008e83f Binary files /dev/null and b/im-uniapp/static/tab/friend.png differ diff --git a/im-uniapp/static/tab/friend_active.png b/im-uniapp/static/tab/friend_active.png new file mode 100644 index 0000000..a033da3 Binary files /dev/null and b/im-uniapp/static/tab/friend_active.png differ diff --git a/im-uniapp/static/tab/group.png b/im-uniapp/static/tab/group.png new file mode 100644 index 0000000..72b9007 Binary files /dev/null and b/im-uniapp/static/tab/group.png differ diff --git a/im-uniapp/static/tab/group_active.png b/im-uniapp/static/tab/group_active.png new file mode 100644 index 0000000..733e2a8 Binary files /dev/null and b/im-uniapp/static/tab/group_active.png differ diff --git a/im-uniapp/static/tab/mine.png b/im-uniapp/static/tab/mine.png new file mode 100644 index 0000000..06dcc7f Binary files /dev/null and b/im-uniapp/static/tab/mine.png differ diff --git a/im-uniapp/static/tab/mine_active.png b/im-uniapp/static/tab/mine_active.png new file mode 100644 index 0000000..4c73eac Binary files /dev/null and b/im-uniapp/static/tab/mine_active.png differ diff --git a/im-uniapp/store/chatStore.js b/im-uniapp/store/chatStore.js new file mode 100644 index 0000000..56d8adc --- /dev/null +++ b/im-uniapp/store/chatStore.js @@ -0,0 +1,546 @@ +import { defineStore } from 'pinia'; +import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js'; +import useFriendStore from './friendStore.js'; +import useGroupStore from './groupStore.js'; +import useUserStore from './userStore'; + +let cacheChats = []; +export default defineStore('chatStore', { + state: () => { + return { + chats: [], + privateMsgMaxId: 0, + groupMsgMaxId: 0, + loading: false + } + }, + actions: { + initChats(chatsData) { + cacheChats = []; + this.chats = []; + for (let chat of chatsData.chats) { + chat.stored = false; + // 暂存至缓冲区 + cacheChats.push(JSON.parse(JSON.stringify(chat))); + // 加载期间显示只前15个会话做做样子,一切都为了加快初始化时间 + if (this.chats.length < 15) { + this.chats.push(chat); + } + } + this.privateMsgMaxId = chatsData.privateMsgMaxId || 0; + this.groupMsgMaxId = chatsData.groupMsgMaxId || 0; + }, + openChat(chatInfo) { + let chats = this.curChats; + let chat = null; + for (let idx in chats) { + if (chats[idx].type == chatInfo.type && + chats[idx].targetId === chatInfo.targetId) { + chat = chats[idx]; + // 放置头部 + this.moveTop(idx) + break; + } + } + // 创建会话 + if (chat == null) { + chat = { + targetId: chatInfo.targetId, + type: chatInfo.type, + showName: chatInfo.showName, + headImage: chatInfo.headImage, + isDnd: chatInfo.isDnd, + lastContent: "", + lastSendTime: new Date().getTime(), + unreadCount: 0, + hotMinIdx: 0, + readedMessageIdx: 0, + messages: [], + atMe: false, + atAll: false, + stored: false + }; + chats.unshift(chat); + this.saveToStorage(); + } + }, + activeChat(idx) { + let chats = this.curChats; + if (idx >= 0) { + chats[idx].unreadCount = 0; + } + }, + resetUnreadCount(chatInfo) { + let chats = this.curChats; + for (let idx in chats) { + if (chats[idx].type == chatInfo.type && + chats[idx].targetId == chatInfo.targetId) { + chats[idx].unreadCount = 0; + chats[idx].atMe = false; + chats[idx].atAll = false; + chats[idx].stored = false; + this.saveToStorage(); + } + } + + }, + readedMessage(pos) { + let chat = this.findChatByFriend(pos.friendId); + if (!chat) return; + for (let idx = chat.readedMessageIdx; idx < chat.messages.length; idx++) { + let m = chat.messages[idx]; + if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) { + // pos.maxId为空表示整个会话已读 + if (!pos.maxId || m.id <= pos.maxId) { + m.status = MESSAGE_STATUS.READED + chat.readedMessageIdx = idx; + chat.stored = false; + } + } + } + if (!chat.stored) { + this.saveToStorage(); + } + }, + cleanMessage(idx) { + let chat = this.curChats[idx]; + chat.lastContent = ''; + chat.hotMinIdx = 0; + chat.unreadCount = 0; + chat.atMe = false; + chat.atAll = false; + chat.stored = false + chat.messages = []; + this.saveToStorage(true); + }, + removeChat(idx) { + let chats = this.curChats; + chats[idx].delete = true; + chats[idx].stored = false; + this.saveToStorage(); + }, + removePrivateChat(userId) { + let chats = this.curChats; + for (let idx in chats) { + if (chats[idx].type == 'PRIVATE' && + chats[idx].targetId == userId) { + this.removeChat(idx); + } + } + }, + removeGroupChat(groupId) { + let chats = this.curChats; + for (let idx in chats) { + if (chats[idx].type == 'GROUP' && + chats[idx].targetId == groupId) { + this.removeChat(idx); + } + } + }, + moveTop(idx) { + if (this.loading) { + return; + } + let chats = this.curChats; + if (idx > 0) { + let chat = chats[idx]; + chats.splice(idx, 1); + chats.unshift(chat); + chat.lastSendTime = new Date().getTime(); + chat.stored = false; + this.saveToStorage(); + } + }, + insertMessage(msgInfo, chatInfo) { + // 获取对方id或群id + let type = chatInfo.type; + // 记录消息的最大id + if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) { + this.privateMsgMaxId = msgInfo.id; + } + if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) { + this.groupMsgMaxId = msgInfo.id; + } + // 如果是已存在消息,则覆盖旧的消息数据 + let chat = this.findChat(chatInfo); + let message = this.findMessage(chat, msgInfo); + if (message) { + console.log("message:", message) + Object.assign(message, msgInfo); + chat.stored = false; + this.saveToStorage(); + return; + } + // 会话列表内容 + if (msgInfo.type == MESSAGE_TYPE.IMAGE) { + chat.lastContent = "[图片]"; + } else if (msgInfo.type == MESSAGE_TYPE.FILE) { + chat.lastContent = "[文件]"; + } else if (msgInfo.type == MESSAGE_TYPE.AUDIO) { + chat.lastContent = "[语音]"; + } else if (msgInfo.type == MESSAGE_TYPE.ACT_RT_VOICE) { + chat.lastContent = "[语音通话]"; + } else if (msgInfo.type == MESSAGE_TYPE.ACT_RT_VIDEO) { + chat.lastContent = "[视频通话]"; + } else if (msgInfo.type == MESSAGE_TYPE.TEXT || + msgInfo.type == MESSAGE_TYPE.RECALL || + msgInfo.type == MESSAGE_TYPE.TIP_TEXT) { + chat.lastContent = msgInfo.content; + } + chat.lastSendTime = msgInfo.sendTime; + chat.sendNickName = msgInfo.sendNickName; + // 未读加1 + if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && + msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) { + chat.unreadCount++; + } + // 是否有人@我 + if (!msgInfo.selfSend && chat.type == "GROUP" && msgInfo.atUserIds && + msgInfo.status != MESSAGE_STATUS.READED) { + const userStore = useUserStore(); + let userId = userStore.userInfo.id; + if (msgInfo.atUserIds.indexOf(userId) >= 0) { + chat.atMe = true; + } + if (msgInfo.atUserIds.indexOf(-1) >= 0) { + chat.atAll = true; + } + } + // 间隔大于10分钟插入时间显示 + if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) { + chat.messages.push({ + sendTime: msgInfo.sendTime, + type: MESSAGE_TYPE.TIP_TIME, + }); + chat.lastTimeTip = msgInfo.sendTime; + } + // 插入消息 + chat.messages.push(msgInfo); + chat.stored = false; + this.saveToStorage(); + }, + updateMessage(msgInfo, chatInfo) { + // 获取对方id或群id + let chat = this.findChat(chatInfo); + let message = this.findMessage(chat, msgInfo); + if (message) { + // 属性拷贝 + Object.assign(message, msgInfo); + chat.stored = false; + this.saveToStorage(); + } + }, + deleteMessage(msgInfo, chatInfo) { + let isColdMessage = false; + let chat = this.findChat(chatInfo); + let delIdx = -1; + for (let idx in chat.messages) { + // 已经发送成功的,根据id删除 + if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) { + delIdx = idx; + break; + } + // 正在发送中的消息可能没有id,只有临时id + if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) { + delIdx = idx; + break; + } + } + if (delIdx >= 0) { + chat.messages.splice(delIdx, 1); + if (delIdx < chat.hotMinIdx) { + isColdMessage = true; + chat.hotMinIdx--; + } + if (delIdx < chat.readedMessageIdx) { + chat.readedMessageIdx--; + } + chat.stored = false; + this.saveToStorage(isColdMessage); + } + }, + recallMessage(msgInfo, chatInfo) { + let chat = this.findChat(chatInfo); + if (!chat) return; + let isColdMessage = false; + // 要撤回的消息id + let id = msgInfo.content; + let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName; + for (let idx in chat.messages) { + let m = chat.messages[idx]; + if (m.id && m.id == id) { + // 改造成一条提示消息 + m.status = MESSAGE_STATUS.RECALL; + m.content = name + "撤回了一条消息"; + m.type = MESSAGE_TYPE.TIP_TEXT + // 会话列表 + chat.lastContent = m.content; + chat.lastSendTime = msgInfo.sendTime; + chat.sendNickName = ''; + if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) { + chat.unreadCount++; + } + isColdMessage = idx < chat.hotMinIdx; + } + // 被引用的消息也要撤回 + if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) { + m.quoteMessage.content = "引用内容已撤回"; + m.quoteMessage.status = MESSAGE_STATUS.RECALL; + m.quoteMessage.type = MESSAGE_TYPE.TIP_TEXT + } + } + chat.stored = false; + this.saveToStorage(isColdMessage); + }, + updateChatFromFriend(friend) { + let chat = this.findChatByFriend(friend.id) + if (chat && (chat.headImage != friend.headImage || + chat.showName != friend.nickName)) { + // 更新会话中的群名和头像 + chat.headImage = friend.headImage; + chat.showName = friend.nickName; + chat.stored = false; + this.saveToStorage(); + } + }, + updateChatFromUser(user) { + let chat = this.findChatByFriend(user.id); + // 更新会话中的昵称和头像 + if (chat && (chat.headImage != user.headImageThumb || + chat.showName != user.nickName)) { + chat.headImage = user.headImageThumb; + chat.showName = user.nickName; + chat.stored = false; + this.saveToStorage(); + } + }, + updateChatFromGroup(group) { + let chat = this.findChatByGroup(group.id); + if (chat && (chat.headImage != group.headImageThumb || + chat.showName != group.showGroupName)) { + // 更新会话中的群名称和头像 + chat.headImage = group.headImageThumb; + chat.showName = group.showGroupName; + chat.stored = false; + this.saveToStorage(); + } + }, + setLoading(loading) { + this.loading = loading; + }, + setDnd(chatInfo, isDnd) { + let chat = this.findChat(chatInfo); + if (chat) { + chat.isDnd = isDnd; + } + }, + refreshChats() { + let chats = cacheChats || this.chats; + // 更新会话免打扰状态 + const friendStore = useFriendStore(); + const groupStore = useGroupStore(); + chats.forEach(chat => { + if (chat.type == 'PRIVATE') { + let friend = friendStore.findFriend(chat.targetId); + if (friend) { + chat.isDnd = friend.isDnd + } + } else if (chat.type == 'GROUP') { + let group = groupStore.findGroup(chat.targetId); + if (group) { + chat.isDnd = group.isDnd + } + } + }) + // 排序 + chats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime); + // #ifndef APP-PLUS + /** + * 由于h5和小程序的stroge只有5m,大约只能存储2w条消息,所以可能需要清理部分历史消息 + */ + this.fliterMessage(chats, 5000, 1000); + // #endif + // 记录热数据索引位置 + chats.forEach(chat => { + if(!chat.hotMinIdx || chat.hotMinIdx != chat.messages.length){ + chat.hotMinIdx = chat.messages.length; + chat.stored = false; + } + }); + // 将消息一次性装载回来 + this.chats = chats; + // 清空缓存,不再使用 + cacheChats = null; + // 消息持久化 + this.saveToStorage(true); + }, + fliterMessage(chats, maxTotalSize, maxPerChatSize) { + // 每个会话只保留maxPerChatSize条消息 + let remainTotalSize = 0; + chats.forEach(chat => { + if (chat.messages.length > maxPerChatSize) { + let idx = chat.messages.length - maxPerChatSize; + chat.messages = chat.messages.slice(idx); + } + remainTotalSize += chat.messages.length; + }) + // 保证消息总数不超过maxTotalSize条,否则继续清理 + if (remainTotalSize > maxTotalSize) { + this.fliterMessage(chats, maxTotalSize, maxPerChatSize / 2); + } + }, + saveToStorage(withColdMessage) { + // 加载中不保存,防止卡顿 + if (this.loading) { + return; + } + const userStore = useUserStore(); + let userId = userStore.userInfo.id; + let key = "chats-app-" + userId; + let chatKeys = []; + // 按会话为单位存储,只存储有改动的会话 + this.chats.forEach((chat) => { + let chatKey = `${key}-${chat.type}-${chat.targetId}` + if (!chat.stored) { + if (chat.delete) { + uni.removeStorageSync(chatKey); + } else { + // 存储冷数据 + if (withColdMessage) { + let coldChat = Object.assign({}, chat); + coldChat.messages = chat.messages.slice(0, chat.hotMinIdx); + uni.setStorageSync(chatKey, coldChat) + } + // 存储热消息 + let hotKey = chatKey + '-hot'; + let hotChat = Object.assign({}, chat); + hotChat.messages = chat.messages.slice(chat.hotMinIdx) + uni.setStorageSync(hotKey, hotChat); + } + chat.stored = true; + } + if (!chat.delete) { + chatKeys.push(chatKey); + } + }) + // 会话核心信息 + let chatsData = { + privateMsgMaxId: this.privateMsgMaxId, + groupMsgMaxId: this.groupMsgMaxId, + chatKeys: chatKeys + } + uni.setStorageSync(key, chatsData) + // 清理已删除的会话 + this.chats = this.chats.filter(chat => !chat.delete) + }, + clear(state) { + cacheChats = []; + this.chats = []; + this.privateMsgMaxId = 0; + this.groupMsgMaxId = 0; + this.loadingPrivateMsg = false; + this.loadingGroupMsg = false; + }, + loadChat() { + return new Promise((resolve, reject) => { + let userStore = useUserStore(); + let userId = userStore.userInfo.id; + let chatsData = uni.getStorageSync("chats-app-" + userId) + if (chatsData) { + if (chatsData.chatKeys) { + chatsData.chats = []; + chatsData.chatKeys.forEach(key => { + let coldChat = uni.getStorageSync(key); + let hotChat = uni.getStorageSync(key + '-hot'); + if (!coldChat && !hotChat) { + return; + } + // 防止消息一直处在发送中状态 + hotChat && hotChat.messages.forEach(msg => { + if (msg.status == MESSAGE_STATUS.SENDING) { + msg.status = MESSAGE_STATUS.FAILED + } + }) + // 冷热消息合并 + let chat = Object.assign({}, coldChat, hotChat); + if (hotChat && coldChat) { + chat.messages = coldChat.messages.concat(hotChat.messages) + } + // 历史版本没有readedMessageIdx字段,做兼容一下 + chat.readedMessageIdx = chat.readedMessageIdx || 0; + chatsData.chats.push(chat); + }) + } + this.initChats(chatsData); + } + resolve() + }) + } + }, + getters: { + curChats: (state) => { + if (cacheChats && state.loading) { + return cacheChats; + } + return state.chats; + }, + findChatIdx: (state) => (chat) => { + let chats = state.curChats; + for (let idx in chats) { + if (chats[idx].type == chat.type && + chats[idx].targetId === chat.targetId) { + chat = state.chats[idx]; + return idx; + } + } + }, + findChat: (state) => (chat) => { + let chats = state.curChats; + let idx = state.findChatIdx(chat); + return chats[idx]; + }, + findChatByFriend: (state) => (fid) => { + return state.curChats.find(chat => chat.type == 'PRIVATE' && + chat.targetId == fid) + }, + findChatByGroup: (state) => (gid) => { + return state.curChats.find(chat => chat.type == 'GROUP' && + chat.targetId == gid) + }, + findMessage: (state) => (chat, msgInfo) => { + if (!chat) { + return null; + } + // 通过id判断 + if (msgInfo.id) { + for (let idx = chat.messages.length - 1; idx >= 0; idx--) { + let m = chat.messages[idx]; + if (m.id && msgInfo.id == m.id) { + return m; + } + // 如果id比要查询的消息小,说明没有这条消息 + if (m.id && m.id < msgInfo.id) { + break; + } + } + } + // 正在发送中的临时消息可能没有id,只有tmpId + if (msgInfo.selfSend && msgInfo.tmpId) { + for (let idx = chat.messages.length - 1; idx >= 0; idx--) { + let m = chat.messages[idx]; + if (!m.selfSend || !m.tmpId) { + continue; + } + if (msgInfo.tmpId == m.tmpId) { + return m; + } + // 如果id比要查询的消息小,说明没有这条消息 + if (m.tmpId && m.tmpId < msgInfo.tmpId) { + break; + } + } + } + return null; + } + } +}); \ No newline at end of file diff --git a/im-uniapp/store/configStore.js b/im-uniapp/store/configStore.js new file mode 100644 index 0000000..0048818 --- /dev/null +++ b/im-uniapp/store/configStore.js @@ -0,0 +1,36 @@ +import { defineStore } from 'pinia'; +import http from '../common/request' + +export default defineStore('configStore', { + state: () => { + return { + appInit: false, + webrtc: {} + } + }, + actions: { + setConfig(config) { + this.webrtc = config.webrtc; + }, + setAppInit(appInit) { + this.appInit = appInit; + }, + clear() { + this.webrtc = {}; + }, + loadConfig() { + return new Promise((resolve, reject) => { + http({ + url: '/system/config', + method: 'GET' + }).then((config) => { + console.log("系统配置", config) + this.setConfig(config); + resolve(); + }).catch((res) => { + reject(res); + }); + }) + } + } +}) \ No newline at end of file diff --git a/im-uniapp/store/friendStore.js b/im-uniapp/store/friendStore.js new file mode 100644 index 0000000..c846e75 --- /dev/null +++ b/im-uniapp/store/friendStore.js @@ -0,0 +1,96 @@ +import { defineStore } from 'pinia'; +import http from '../common/request' +import { TERMINAL_TYPE } from '../common/enums.js' + +export default defineStore('friendStore', { + state: () => { + return { + friendMap: new Map(), + timer: null + } + }, + actions: { + setFriends(friends) { + this.friendMap.clear(); + friends.forEach(f => this.friendMap.set(f.id, f)) + }, + updateFriend(friend) { + let f = this.findFriend(friend.id); + friend.online = f.online; + friend.onlineWeb = f.onlineWeb; + friend.onlineApp = f.onlineApp; + this.friendMap.set(friend.id, friend) + }, + removeFriend(id) { + this.friendMap.get(id).deleted = true; + }, + addFriend(friend) { + this.friendMap.set(friend.id, friend) + }, + setOnlineStatus(onlineTerminals) { + this.friends.forEach((f) => { + let userTerminal = onlineTerminals.find((o) => f.id == o.userId); + if (userTerminal) { + f.online = true; + f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB) >= 0 + f.onlineApp = userTerminal.terminals.indexOf(TERMINAL_TYPE.APP) >= 0 + } else { + f.online = false; + f.onlineWeb = false; + f.onlineApp = false; + } + }); + }, + refreshOnlineStatus() { + let userIds = this.friends.filter((f) => !f.deleted).map((f) => f.id); + if (userIds.length == 0) { + return; + } + http({ + url: '/user/terminal/online?userIds=' + userIds.join(','), + method: 'GET' + }).then((onlineTerminals) => { + this.setOnlineStatus(onlineTerminals); + }) + // 30s后重新拉取 + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.refreshOnlineStatus(); + }, 30000) + }, + setDnd(id, isDnd) { + this.friendMap.get(id).isDnd = isDnd; + }, + clear() { + this.friendMap.clear(); + clearTimeout(this.timer); + this.timer = null; + }, + loadFriend() { + return new Promise((resolve, reject) => { + http({ + url: '/friend/list', + method: 'GET' + }).then((friends) => { + this.setFriends(friends); + this.refreshOnlineStatus(); + resolve() + }).catch((res) => { + reject(); + }) + }); + } + }, + getters: { + friends: (state) => { + return Array.from(state.friendMap.values()); + }, + findFriend: (state) => (userId) => { + return state.friendMap.get(userId); + }, + isFriend: (state) => (userId) => { + let f = state.findFriend(userId) + return f && !f.deleted + } + } +}) \ No newline at end of file diff --git a/im-uniapp/store/groupStore.js b/im-uniapp/store/groupStore.js new file mode 100644 index 0000000..3409b10 --- /dev/null +++ b/im-uniapp/store/groupStore.js @@ -0,0 +1,56 @@ +import { defineStore } from 'pinia'; +import http from '@/common/request'; + +export default defineStore('groupStore', { + state: () => { + return { + groupMap: new Map() + } + }, + actions: { + setGroups(groups) { + this.groupMap.clear(); + groups.forEach(g => this.groupMap.set(g.id, g)) + }, + addGroup(group) { + this.groupMap.set(group.id, group); + }, + removeGroup(id) { + this.groupMap.get(id).quit = true; + }, + updateGroup(group) { + this.groupMap.set(group.id, group); + }, + setDnd(id, isDnd) { + this.groupMap.get(id).isDnd = isDnd; + }, + clear() { + this.groupMap.clear(); + }, + loadGroup() { + return new Promise((resolve, reject) => { + http({ + url: '/group/list', + method: 'GET' + }).then((groups) => { + this.setGroups(groups); + resolve(); + }).catch((res) => { + reject(res); + }) + }); + } + }, + getters: { + groups: (state) => { + return Array.from(state.groupMap.values()); + }, + findGroup: (state) => (id) => { + return state.groupMap.get(id); + }, + isGroup: (state) => (id) => { + let group = state.findGroup(id); + return group && !group.quit + } + } +}) \ No newline at end of file diff --git a/im-uniapp/store/userStore.js b/im-uniapp/store/userStore.js new file mode 100644 index 0000000..7543bd0 --- /dev/null +++ b/im-uniapp/store/userStore.js @@ -0,0 +1,31 @@ +import { defineStore } from 'pinia'; +import http from '../common/request' + +export default defineStore('userStore', { + state: () => { + return { + userInfo: {} + } + }, + actions: { + setUserInfo(userInfo) { + this.userInfo = userInfo; + }, + clear() { + this.userInfo = {}; + }, + loadUser() { + return new Promise((resolve, reject) => { + http({ + url: '/user/self', + method: 'GET' + }).then((userInfo) => { + this.setUserInfo(userInfo); + resolve(); + }).catch((res) => { + reject(res); + }); + }) + } + } +}) \ No newline at end of file diff --git a/im-uniapp/uni.promisify.adaptor.js b/im-uniapp/uni.promisify.adaptor.js new file mode 100644 index 0000000..47fbce1 --- /dev/null +++ b/im-uniapp/uni.promisify.adaptor.js @@ -0,0 +1,10 @@ +uni.addInterceptor({ + returnValue (res) { + if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { + return res; + } + return new Promise((resolve, reject) => { + res.then((res) => res[0] ? reject(res[0]) : resolve(res[1])); + }); + }, +}); \ No newline at end of file diff --git a/im-uniapp/uni.scss b/im-uniapp/uni.scss new file mode 100644 index 0000000..f6d11bf --- /dev/null +++ b/im-uniapp/uni.scss @@ -0,0 +1,107 @@ +@import '@/uni_modules/uview-plus/theme.scss'; +@import '@/uni_modules/uni-scss/variables.scss'; +@import '@/im-var.scss'; + +// 修改uni-ui主题 +$uni-primary: $im-color-primary; +$uni-success: $im-color-success; +$uni-warning: $im-color-warning; +$uni-error: $im-color-danger; +$uni-info: $im-color-info; + +// $uni-main-color: #3a3a3a; // 主要文字 +// $uni-base-color: #6a6a6a; // 常规文字 +// $uni-secondary-color: #909399; // 次要文字 +// $uni-extra-color: #c7c7c7; // 辅助说明 + +// // 边框颜色 +// $uni-border-1: #F0F0F0; +// $uni-border-2: #EDEDED; +// $uni-border-3: #DCDCDC; +// $uni-border-4: #B9B9B9; + +// // 常规色 +// $uni-black: #000000; +// $uni-white: #ffffff; +// $uni-transparent: rgba($color: #000000, $alpha: 0); + +// // 背景色 +// $uni-bg-color: #f7f7f7; + +// /* 水平间距 */ +// $uni-spacing-sm: 8px; +// $uni-spacing-base: 15px; +// $uni-spacing-lg: 30px; + +// // 阴影 +// $uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5); +// $uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2); +// $uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5); + +// // 蒙版 +// $uni-mask: rgba($color: #000000, $alpha: 0.4); + + + + +/**************************************************/ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color:#333;//基本色 +$uni-text-color-inverse:#fff;//反色 +$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable: #c0c0c0; +/* 背景颜色 */ +$uni-bg-color:#ffffff; +$uni-bg-color-grey:#f8f8f8; +$uni-bg-color-hover:#f1f1f1;//点击状态颜色 +$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 + +/* 边框颜色 */ +$uni-border-color:#c8c7cc; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm:12px; +$uni-font-size-base:14px; +$uni-font-size-lg:16; + +/* 图片尺寸 */ +$uni-img-size-sm:20px; +$uni-img-size-base:26px; +$uni-img-size-lg:40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2C405A; // 文章标题颜色 +$uni-font-size-title:20px; +$uni-color-subtitle: #555555; // 二级标题颜色 +$uni-font-size-subtitle:26px; +$uni-color-paragraph: #3F536E; // 文章段落颜色 +$uni-font-size-paragraph:15px; diff --git a/im-uniapp/uni_modules/lsj-upload/changelog.md b/im-uniapp/uni_modules/lsj-upload/changelog.md new file mode 100644 index 0000000..090bca0 --- /dev/null +++ b/im-uniapp/uni_modules/lsj-upload/changelog.md @@ -0,0 +1,120 @@ +## 2.3.2(2024-06-13) +问题修复:2.3.1版本引起的部分设备不支持findLastIndex问题 +## 2.3.1(2024-05-20) +修复:文件不去重时返回文件列表name与组件内置列表key不一致问题。 +## 2.3.0(2024-05-20) +优化:1:增加属性distinct【选择文件是否去重】、2:对show/hide函数增加uni.$emit事件监听,若页面存在多个上传组件时,可通过uni.$emit控制所有上传组件webview透明层是否显示。 +## 2.2.9(2023-06-01) +优化:将是否多选与count字段解绑(原逻辑是count>1为允许多选),改为新增multiple属性控制是否多选。 +## 2.2.8(2023-06-01) +修复上版本提交时accept测试值未删除导致h5端只能选择图片的问题。 +## 2.2.7(2023-05-06) +应群友建议,当instantly为true时,触发change事件后延迟1000毫秒再自动上传,方便动态修改参数,其实个人还是建议想在change事件动态设置参数的伙伴将instantly设置为false,修改参数后手动调用upload() +## 2.2.6(2023-02-09) +修复多个文件同时选择时返回多次change回调的问题 +## 2.2.5(2022-12-27) +1.修复多选文件时未能正常校验数量的问题; +2.app端与H5端支持单选或多选文件,通过count数量控制,超过1开启多选。 +## 2.2.4(2022-12-27) +1.修复多选文件时未能正常校验数量的问题; +2.app端修复多选只取到第一个文件的问题。 +## 2.2.3(2022-12-06) +修复手动调用show()导致count失效的问题 +## 2.2.2(2022-12-01) +Vue3自行修改兼容 +## 2.2.1(2022-10-19) +修复childId警告提示 +## 2.2.0(2022-10-10) +更新app端webview窗口参数clidId,默认值添加时间戳保证唯一性 +## 2.1.9(2022-07-13) +[修复] app端选择文件后初始化设置的文件列表被清空问题 +## 2.1.8(2022-07-13) +[新增] ref方法初始化文件列表,用于已提交后再次编辑时需带入已上传文件:setFiles(files),可传入数组或Map对象,传入格式请与组件选择返回格式保持一致,且name为必须属性。 +## 2.1.7(2022-07-12) +修复ios端偶现创建webview初始化参数未生效的问题 +## 2.1.6(2022-07-11) +[修复]:修复上个版本更新导致nvue窗口组件不能选择文件的问题; +[新增]: +1.应群友建议(填写禁止格式太多)格式限制formats由原来填写禁止选择的格式改为填写允许被选择的格式; +2.应群友建议(增加上传结束回调事件),上传结束回调事件@uploadEnd +3.如能帮到你请留下你的免费好评,组件使用过程中有问题可以加QQ群交流,至于Map对象怎么使用这类前端基础问题请自行百度 +## 2.1.5(2022-07-01) +app端组件销毁时添加自动销毁webview功能,避免v-if销毁组件的情况控件还能被点击的问题 +## 2.1.4(2022-07-01) +修复小程序端回显问题 +## 2.1.3(2022-06-30) +回调事件返回参数新增path字段(文件临时地址),用于回显 +## 2.1.2(2022-06-16) +修复APP端Tabbar窗口无法选择文件的问题 +## 2.1.1(2022-06-16) +优化: +1.组件优化为允许在v-if中使用; +2.允许option直接在data赋值,不再强制在onRead中初始化; +## 2.1.0(2022-06-13) +h5 pc端更改为单次可多选 +## 2.0.9(2022-06-10) +更新演示内容,部分同学不知道怎么获取服务端返回的数据 +## 2.0.8(2022-06-09) +优化动态更新上传参数函数,具体查看下方说明:动态更新参数演示 +## 2.0.7(2022-06-07) +新增wxFileType属性,用于小程序端选择附件时可选文件类型 +## 2.0.6(2022-06-07) +修复小程序端真机选择文件提示失败的问题 +## 2.0.5(2022-06-02) +优化小程序端调用hide()后未阻止触发文件选择问题 +## 2.0.4(2022-06-01) +优化APP端选择器初始定位 +## 2.0.3(2022-05-31) +修复nvue窗口选择文件报错问题 +## 2.0.2(2022-05-20) +修复ios端opiton设置过早未传入webview导致不自动上传问题 +## 2.0.1(2022-05-19) +修复APP端子窗口点击选择文件不响应问题 +## 2.0.0(2022-05-18) +此次组件更新至2.0版本,与1.0版本使用上略有差异,已使用1.0的同学请自行斟酌是否需要升级! +部分差异: +一、 2.0新增异步触发上传功能; +二、2.0新增文件批量上传功能; +三、2.0优化option,剔除属性,只保留上传接口所需字段,且允许异步更改option的值; +四、组件增加size(文件大小限制)、count(文件个数限制)、formats(文件后缀限制)、accept(文件类型限制)、instantly(是否立即自动上传)、debug(日志打印)等属性; +五、回调事件取消input事件、callback事件,新增change事件和progress事件; +六、ref事件新增upload事件、clear事件; +七、优化组件代码,show和hide函数改为显示隐藏,不再重复开关webview; + +## 1.2.3(2022-03-22) +修复Demo里传入待完善功能[手动上传属性manual=true]导致不自动上传的问题,手动提交上传待下个版本更新 +## 1.2.2(2022-02-21) +修复上版本APP优化导致H5和小程序端不自动初始化的问题,此次更新仅修复此问题。异步提交功能下个版本更新~ +## 1.2.1(2022-01-25) +QQ1群已满,已开放2群:469580165 +## 1.2.0(2021-12-09) +优化APP端页面中DOM重排后每次需要重新定位的问题 +## 1.1.1(2021-12-09) +优化,与上版本使用方式有改变,请检查后确认是否需要更新,create更名为show, close更名为hide,取消初始化时手动create, 传参方式改为props=>option +## 1.1.0(2021-12-09) +新增refresh方法,用于DOM发生重排时重新定位控件(APP端) +## 1.0.9(2021-07-15) +修复上传进度未同步渲染,直接返回100%的BUG +## 1.0.8(2021-07-12) +修复H5端传入height和width未生效的bug +## 1.0.7(2021-07-07) +修复h5和小程序端上传完成callback未返回fileName字段问题 +## 1.0.6(2021-07-07) +修复h5端提示信息debug +## 1.0.5(2021-06-29) +感谢小伙伴找出bug,上传成功回调success未置为true,已修复 +## 1.0.4(2021-06-28) +新增兼容APP,H5,小程序手动关闭控件,关闭后不再弹出文件选择框,需要重新create再次开启 +## 1.0.3(2021-06-28) +close增加条件编译,除app端外不需要close +## 1.0.2(2021-06-28) +1.修复页面滚动位置后再create控件导致控件位置不正确的问题; +2.修复nvue无法create控件; +3.示例项目新增nvue使用案例; +## 1.0.1(2021-06-28) +因为有的朋友不清楚app端切换tab时应该怎么处理webview,现重新上传一版示例项目,需要做tab切换的朋友可以导入示例项目查看 +## 1.0.0(2021-06-25) +此插件为l-file插件中上传功能改版,更新内容为: +1. 按钮内嵌入页面,不再强制固定底部,可跟随页面滚动 +2.无需再单独弹框点击上传,减去中间层 +3.通过slot自定义按钮样式 diff --git a/im-uniapp/uni_modules/lsj-upload/components/lsj-upload/LsjFile.js b/im-uniapp/uni_modules/lsj-upload/components/lsj-upload/LsjFile.js new file mode 100644 index 0000000..a86233e --- /dev/null +++ b/im-uniapp/uni_modules/lsj-upload/components/lsj-upload/LsjFile.js @@ -0,0 +1,414 @@ +export class LsjFile { + constructor(data) { + this.dom = null; + // files.type = waiting(等待上传)|| loading(上传中)|| success(成功) || fail(失败) + this.files = new Map(); + this.debug = data.debug || false; + this.id = data.id; + this.width = data.width; + this.height = data.height; + this.option = data.option; + this.instantly = data.instantly; + this.prohibited = data.prohibited; + this.onchange = data.onchange; + this.onprogress = data.onprogress; + this.uploadHandle = this._uploadHandle; + // #ifdef MP-WEIXIN + this.uploadHandle = this._uploadHandleWX; + // #endif + } + + + /** + * 创建File节点 + * @param {string}path webview地址 + */ + create(path) { + if (!this.dom) { + // #ifdef H5 + let dom = document.createElement('input'); + dom.type = 'file' + dom.value = '' + dom.style.height = this.height + dom.style.width = this.width + dom.style.position = 'absolute' + dom.style.top = 0 + dom.style.left = 0 + dom.style.right = 0 + dom.style.bottom = 0 + dom.style.opacity = 0 + dom.style.zIndex = 999 + dom.accept = this.prohibited.accept; + if (this.prohibited.multiple) { + dom.multiple = 'multiple'; + } + dom.onchange = event => { + for (let file of event.target.files) { + if (this.files.size >= this.prohibited.count) { + this.toast(`只允许上传${this.prohibited.count}个文件`); + this.dom.value = ''; + break; + } + this.addFile(file); + } + + this._uploadAfter(); + + this.dom.value = ''; + }; + this.dom = dom; + // #endif + + // #ifdef APP-PLUS + let styles = { + top: '-200px', + left: 0, + width: '1px', + height: '200px', + background: 'transparent' + }; + let extras = { + debug: this.debug, + instantly: this.instantly, + prohibited: this.prohibited, + } + this.dom = plus.webview.create(path, this.id, styles,extras); + this.setData(this.option); + this._overrideUrlLoading(); + // #endif + return this.dom; + } + } + + + /** + * 设置上传参数 + * @param {object|string}name 上传参数,支持a.b 和 a[b] + */ + setData() { + let [name,value = ''] = arguments; + if (typeof name === 'object') { + Object.assign(this.option,name); + } + else { + this._setValue(this.option,name,value); + } + + this.debug&&console.log(JSON.stringify(this.option)); + + // #ifdef APP-PLUS + this.dom.evalJS(`vm.setData('${JSON.stringify(this.option)}')`); + // #endif + } + + /** + * 上传 + * @param {string}name 文件名称 + */ + async upload(name='') { + if (!this.option.url) { + throw Error('未设置上传地址'); + } + + // #ifndef APP-PLUS + if (name && this.files.has(name)) { + await this.uploadHandle(this.files.get(name)); + } + else { + for (let item of this.files.values()) { + if (item.type === 'waiting' || item.type === 'fail') { + await this.uploadHandle(item); + } + } + } + // #endif + + // #ifdef APP-PLUS + this.dom&&this.dom.evalJS(`vm.upload('${name}')`); + // #endif + } + + // 选择文件change + addFile(file,isCallChange) { + + let name = file.name; + this.debug&&console.log('文件名称',name,'大小',file.size); + + if (file) { + // 限制文件格式 + let path = ''; + let suffix = name.substring(name.lastIndexOf(".")+1).toLowerCase(); + let formats = this.prohibited.formats.toLowerCase(); + + // #ifndef MP-WEIXIN + path = URL.createObjectURL(file); + // #endif + // #ifdef MP-WEIXIN + path = file.path; + // #endif + if (formats&&!formats.includes(suffix)) { + this.toast(`不支持上传${suffix.toUpperCase()}格式文件`); + return false; + } + // 限制文件大小 + if (file.size > 1024 * 1024 * Math.abs(this.prohibited.size)) { + this.toast(`附件大小请勿超过${this.prohibited.size}M`) + return false; + } + + try{ + if (!this.prohibited.distinct) { + let homonymIndex = [...this.files.keys()].findIndex(item=>{ + return (item.substring(0,item.lastIndexOf("("))||item.substring(0,item.lastIndexOf("."))) == name.substring(0,name.lastIndexOf(".")) && + item.substring(item.lastIndexOf(".")+1).toLowerCase() === suffix; + }) + if (homonymIndex > -1) { + name = `${name.substring(0,name.lastIndexOf("."))}(${homonymIndex+1}).${suffix}`; + } + } + }catch(e){ + //TODO handle the exception + } + + this.files.set(name,{file,path,name: name,size: file.size,progress: 0,type: 'waiting'}); + return true; + } + } + + /** + * 移除文件 + * @param {string}name 不传name默认移除所有文件,传入name移除指定name的文件 + */ + clear(name='') { + // #ifdef APP-PLUS + this.dom&&this.dom.evalJS(`vm.clear('${name}')`); + // #endif + + if (!name) { + this.files.clear(); + } + else { + this.files.delete(name); + } + return this.onchange(this.files); + } + + /** + * 提示框 + * @param {string}msg 轻提示内容 + */ + toast(msg) { + uni.showToast({ + title: msg, + icon: 'none' + }); + } + + /** + * 微信小程序选择文件 + * @param {number}count 可选择文件数量 + */ + chooseMessageFile(type,count) { + wx.chooseMessageFile({ + count: count, + type: type, + success: ({ tempFiles }) => { + for (let file of tempFiles) { + this.addFile(file); + } + this._uploadAfter(); + }, + fail: () => { + this.toast(`打开失败`); + } + }) + } + + _copyObject(obj) { + if (typeof obj !== "undefined") { + return JSON.parse(JSON.stringify(obj)); + } else { + return obj; + } + } + + /** + * 自动根据字符串路径设置对象中的值 支持.和[] + * @param {Object} dataObj 数据源 + * @param {String} name 支持a.b 和 a[b] + * @param {String} value 值 + * setValue(dataObj, name, value); + */ + _setValue(dataObj, name, value) { + // 通过正则表达式 查找路径数据 + let dataValue; + if (typeof value === "object") { + dataValue = this._copyObject(value); + } else { + dataValue = value; + } + let regExp = new RegExp("([\\w$]+)|\\[(:\\d)\\]", "g"); + const patten = name.match(regExp); + // 遍历路径 逐级查找 最后一级用于直接赋值 + for (let i = 0; i < patten.length - 1; i++) { + let keyName = patten[i]; + if (typeof dataObj[keyName] !== "object") dataObj[keyName] = {}; + dataObj = dataObj[keyName]; + } + // 最后一级 + dataObj[patten[patten.length - 1]] = dataValue; + this.debug&&console.log('参数更新后',JSON.stringify(this.option)); + } + + _uploadAfter() { + this.onchange(this.files); + setTimeout(()=>{ + this.instantly&&this.upload(); + },1000) + } + + _overrideUrlLoading() { + this.dom.overrideUrlLoading({ mode: 'reject' }, e => { + let {retype,item,files,end} = this._getRequest( + e.url + ); + let _this = this; + switch (retype) { + case 'updateOption': + this.dom.evalJS(`vm.setData('${JSON.stringify(_this.option)}')`); + break + case 'change': + try { + _this.files = new Map([..._this.files,...JSON.parse(unescape(files))]); + } catch (e) { + return console.error('出错了,请检查代码') + } + _this.onchange(_this.files); + break + case 'progress': + try { + item = JSON.parse(unescape(item)); + } catch (e) { + return console.error('出错了,请检查代码') + } + _this._changeFilesItem(item,end); + break + default: + break + } + }) + } + + _getRequest(url) { + let theRequest = new Object() + let index = url.indexOf('?') + if (index != -1) { + let str = url.substring(index + 1) + let strs = str.split('&') + for (let i = 0; i < strs.length; i++) { + theRequest[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1]) + } + } + return theRequest + } + + _changeFilesItem(item,end=false) { + this.debug&&console.log('onprogress',JSON.stringify(item)); + this.onprogress(item,end); + this.files.set(item.name,item); + } + + _uploadHandle(item) { + item.type = 'loading'; + delete item.responseText; + return new Promise((resolve,reject)=>{ + this.debug&&console.log('option',JSON.stringify(this.option)); + let {url,name,method='POST',header,formData} = this.option; + let form = new FormData(); + for (let keys in formData) { + form.append(keys, formData[keys]) + } + form.append(name, item.file); + let xmlRequest = new XMLHttpRequest(); + xmlRequest.open(method, url, true); + for (let keys in header) { + xmlRequest.setRequestHeader(keys, header[keys]) + } + + xmlRequest.upload.addEventListener( + 'progress', + event => { + if (event.lengthComputable) { + let progress = Math.ceil((event.loaded * 100) / event.total) + if (progress <= 100) { + item.progress = progress; + this._changeFilesItem(item); + } + } + }, + false + ); + + xmlRequest.ontimeout = () => { + console.error('请求超时') + item.type = 'fail'; + this._changeFilesItem(item,true); + return resolve(false); + } + + xmlRequest.onreadystatechange = ev => { + if (xmlRequest.readyState == 4) { + if (xmlRequest.status == 200) { + this.debug&&console.log('上传完成:' + xmlRequest.responseText) + item['responseText'] = xmlRequest.responseText; + item.type = 'success'; + this._changeFilesItem(item,true); + return resolve(true); + } else if (xmlRequest.status == 0) { + console.error('status = 0 :请检查请求头Content-Type与服务端是否匹配,服务端已正确开启跨域,并且nginx未拦截阻止请求') + } + console.error('--ERROR--:status = ' + xmlRequest.status) + item.type = 'fail'; + this._changeFilesItem(item,true); + return resolve(false); + } + } + xmlRequest.send(form) + }); + } + + _uploadHandleWX(item) { + item.type = 'loading'; + delete item.responseText; + return new Promise((resolve,reject)=>{ + this.debug&&console.log('option',JSON.stringify(this.option)); + let form = {filePath: item.file.path,...this.option }; + form['fail'] = ({ errMsg = '' }) => { + console.error('--ERROR--:' + errMsg) + item.type = 'fail'; + this._changeFilesItem(item,true); + return resolve(false); + } + form['success'] = res => { + if (res.statusCode == 200) { + this.debug&&console.log('上传完成,微信端返回不一定是字符串,根据接口返回格式判断是否需要JSON.parse:' + res.data) + item['responseText'] = res.data; + item.type = 'success'; + this._changeFilesItem(item,true); + return resolve(true); + } + item.type = 'fail'; + this._changeFilesItem(item,true); + return resolve(false); + } + + let xmlRequest = uni.uploadFile(form); + xmlRequest.onProgressUpdate(({ progress = 0 }) => { + if (progress <= 100) { + item.progress = progress; + this._changeFilesItem(item); + } + }) + }); + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/lsj-upload/components/lsj-upload/lsj-upload.vue b/im-uniapp/uni_modules/lsj-upload/components/lsj-upload/lsj-upload.vue new file mode 100644 index 0000000..46ac296 --- /dev/null +++ b/im-uniapp/uni_modules/lsj-upload/components/lsj-upload/lsj-upload.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/im-uniapp/uni_modules/lsj-upload/hybrid/html/js/vue.min.js b/im-uniapp/uni_modules/lsj-upload/hybrid/html/js/vue.min.js new file mode 100644 index 0000000..97ac4cf --- /dev/null +++ b/im-uniapp/uni_modules/lsj-upload/hybrid/html/js/vue.min.js @@ -0,0 +1,8 @@ +/*! + * Vue.js v2.3.0 + * (c) 2014-2017 Evan You + * Released under the MIT License. + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";function e(e){return void 0===e||null===e}function t(e){return void 0!==e&&null!==e}function n(e){return!0===e}function r(e){return"string"==typeof e||"number"==typeof e}function i(e){return null!==e&&"object"==typeof e}function o(e){return"[object Object]"===Ai.call(e)}function a(e){return"[object RegExp]"===Ai.call(e)}function s(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function c(e){var t=parseFloat(e);return isNaN(t)?e:t}function u(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i-1)return e.splice(n,1)}}function f(e,t){return Si.call(e,t)}function p(e){var t=Object.create(null);return function(n){return t[n]||(t[n]=e(n))}}function d(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function v(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function h(e,t){for(var n in t)e[n]=t[n];return e}function m(e){for(var t={},n=0;n=0&&$o[n].id>e.id;)n--;$o.splice(Math.max(n,Ao)+1,0,e)}else $o.push(e);Co||(Co=!0,ro($e))}}function Ae(e){To.clear(),Oe(e,To)}function Oe(e,t){var n,r,o=Array.isArray(e);if((o||i(e))&&Object.isExtensible(e)){if(e.__ob__){var a=e.__ob__.dep.id;if(t.has(a))return;t.add(a)}if(o)for(n=e.length;n--;)Oe(e[n],t);else for(r=Object.keys(e),n=r.length;n--;)Oe(e[r[n]],t)}}function Se(e,t,n){Eo.get=function(){return this[t][n]},Eo.set=function(e){this[t][n]=e},Object.defineProperty(e,n,Eo)}function Te(e){e._watchers=[];var t=e.$options;t.props&&Ee(e,t.props),t.methods&&Me(e,t.methods),t.data?je(e):E(e._data={},!0),t.computed&&Le(e,t.computed),t.watch&&Pe(e,t.watch)}function Ee(e,t){var n=e.$options.propsData||{},r=e._props={},i=e.$options._propKeys=[],o=!e.$parent;lo.shouldConvert=o;for(var a in t)!function(o){i.push(o);var a=U(o,t,n,e);j(r,o,a),o in e||Se(e,"_props",o)}(a);lo.shouldConvert=!0}function je(e){var t=e.$options.data;t=e._data="function"==typeof t?Ne(t,e):t||{},o(t)||(t={});for(var n=Object.keys(t),r=e.$options.props,i=n.length;i--;)r&&f(r,n[i])||$(n[i])||Se(e,"_data",n[i]);E(t,!0)}function Ne(e,t){try{return e.call(t)}catch(e){return C(e,t,"data()"),{}}}function Le(e,t){var n=e._computedWatchers=Object.create(null);for(var r in t){var i=t[r],o="function"==typeof i?i:i.get;n[r]=new So(e,o,g,jo),r in e||Ie(e,r,i)}}function Ie(e,t,n){"function"==typeof n?(Eo.get=De(t),Eo.set=g):(Eo.get=n.get?!1!==n.cache?De(t):n.get:g,Eo.set=n.set?n.set:g),Object.defineProperty(e,t,Eo)}function De(e){return function(){var t=this._computedWatchers&&this._computedWatchers[e];if(t)return t.dirty&&t.evaluate(),oo.target&&t.depend(),t.value}}function Me(e,t){e.$options.props;for(var n in t)e[n]=null==t[n]?g:d(t[n],e)}function Pe(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i=0||n.indexOf(e[i])<0)&&r.push(e[i]);return r}return e}function pt(e){this._init(e)}function dt(e){e.use=function(e){if(!e.installed){var t=v(arguments,1);return t.unshift(this),"function"==typeof e.install?e.install.apply(e,t):"function"==typeof e&&e.apply(null,t),e.installed=!0,this}}}function vt(e){e.mixin=function(e){this.options=B(this.options,e)}}function ht(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,r=n.cid,i=e._Ctor||(e._Ctor={});if(i[r])return i[r];var o=e.name||n.options.name,a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=B(n.options,e),a.super=n,a.options.props&&mt(a),a.options.computed&>(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,Di.forEach(function(e){a[e]=n[e]}),o&&(a.options.components[o]=a),a.superOptions=n.options,a.extendOptions=e,a.sealedOptions=h({},a.options),i[r]=a,a}}function mt(e){var t=e.options.props;for(var n in t)Se(e.prototype,"_props",n)}function gt(e){var t=e.options.computed;for(var n in t)Ie(e.prototype,n,t[n])}function yt(e){Di.forEach(function(t){e[t]=function(e,n){return n?("component"===t&&o(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}})}function _t(e){return e&&(e.Ctor.options.name||e.tag)}function bt(e,t){return"string"==typeof e?e.split(",").indexOf(t)>-1:!!a(e)&&e.test(t)}function $t(e,t,n){for(var r in e){var i=e[r];if(i){var o=_t(i.componentOptions);o&&!n(o)&&(i!==t&&xt(i),e[r]=null)}}}function xt(e){e&&e.componentInstance.$destroy()}function wt(e){for(var n=e.data,r=e,i=e;t(i.componentInstance);)i=i.componentInstance._vnode,i.data&&(n=Ct(i.data,n));for(;t(r=r.parent);)r.data&&(n=Ct(n,r.data));return kt(n)}function Ct(e,n){return{staticClass:At(e.staticClass,n.staticClass),class:t(e.class)?[e.class,n.class]:n.class}}function kt(e){var n=e.class,r=e.staticClass;return t(r)||t(n)?At(r,Ot(n)):""}function At(e,t){return e?t?e+" "+t:e:t||""}function Ot(n){if(e(n))return"";if("string"==typeof n)return n;var r="";if(Array.isArray(n)){for(var o,a=0,s=n.length;a-1?ua[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:ua[e]=/HTMLUnknownElement/.test(t.toString())}function Et(e){if("string"==typeof e){var t=document.querySelector(e);return t||document.createElement("div")}return e}function jt(e,t){var n=document.createElement(e);return"select"!==e?n:(t.data&&t.data.attrs&&void 0!==t.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n)}function Nt(e,t){return document.createElementNS(ia[e],t)}function Lt(e){return document.createTextNode(e)}function It(e){return document.createComment(e)}function Dt(e,t,n){e.insertBefore(t,n)}function Mt(e,t){e.removeChild(t)}function Pt(e,t){e.appendChild(t)}function Rt(e){return e.parentNode}function Ft(e){return e.nextSibling}function Bt(e){return e.tagName}function Ht(e,t){e.textContent=t}function Ut(e,t,n){e.setAttribute(t,n)}function Vt(e,t){var n=e.data.ref;if(n){var r=e.context,i=e.componentInstance||e.elm,o=r.$refs;t?Array.isArray(o[n])?l(o[n],i):o[n]===i&&(o[n]=void 0):e.data.refInFor?Array.isArray(o[n])&&o[n].indexOf(i)<0?o[n].push(i):o[n]=[i]:o[n]=i}}function zt(e,n){return e.key===n.key&&e.tag===n.tag&&e.isComment===n.isComment&&t(e.data)===t(n.data)&&Jt(e,n)}function Jt(e,n){if("input"!==e.tag)return!0;var r;return(t(r=e.data)&&t(r=r.attrs)&&r.type)===(t(r=n.data)&&t(r=r.attrs)&&r.type)}function Kt(e,n,r){var i,o,a={};for(i=n;i<=r;++i)o=e[i].key,t(o)&&(a[o]=i);return a}function qt(e,t){(e.data.directives||t.data.directives)&&Wt(e,t)}function Wt(e,t){var n,r,i,o=e===pa,a=t===pa,s=Zt(e.data.directives,e.context),c=Zt(t.data.directives,t.context),u=[],l=[];for(n in c)r=s[n],i=c[n],r?(i.oldValue=r.value,Yt(i,"update",t,e),i.def&&i.def.componentUpdated&&l.push(i)):(Yt(i,"bind",t,e),i.def&&i.def.inserted&&u.push(i));if(u.length){var f=function(){for(var n=0;n=0&&" "===(m=e.charAt(h));h--);m&&_a.test(m)||(l=!0)}}else void 0===o?(v=i+1,o=e.slice(0,i).trim()):t();if(void 0===o?o=e.slice(0,i).trim():0!==v&&t(),a)for(i=0;i=Bo}function gn(e){return 34===e||39===e}function yn(e){var t=1;for(zo=Vo;!mn();)if(e=hn(),gn(e))_n(e);else if(91===e&&t++,93===e&&t--,0===t){Jo=Vo;break}}function _n(e){for(var t=e;!mn()&&(e=hn())!==t;);}function bn(e,t,n){Ko=n;var r=t.value,i=t.modifiers,o=e.tag,a=e.attrsMap.type;if("select"===o)wn(e,r,i);else if("input"===o&&"checkbox"===a)$n(e,r,i);else if("input"===o&&"radio"===a)xn(e,r,i);else if("input"===o||"textarea"===o)Cn(e,r,i);else if(!Pi.isReservedTag(o))return pn(e,r,i),!1;return!0}function $n(e,t,n){var r=n&&n.number,i=ln(e,"value")||"null",o=ln(e,"true-value")||"true",a=ln(e,"false-value")||"false";an(e,"checked","Array.isArray("+t+")?_i("+t+","+i+")>-1"+("true"===o?":("+t+")":":_q("+t+","+o+")")),un(e,$a,"var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+o+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$c){$$i<0&&("+t+"=$$a.concat($$v))}else{$$i>-1&&("+t+"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{"+dn(t,"$$c")+"}",null,!0)}function xn(e,t,n){var r=n&&n.number,i=ln(e,"value")||"null";i=r?"_n("+i+")":i,an(e,"checked","_q("+t+","+i+")"),un(e,$a,dn(t,i),null,!0)}function wn(e,t,n){var r=n&&n.number,i='Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return '+(r?"_n(val)":"val")+"})",o="var $$selectedVal = "+i+";";o=o+" "+dn(t,"$event.target.multiple ? $$selectedVal : $$selectedVal[0]"),un(e,"change",o,null,!0)}function Cn(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,u=o?"change":"range"===r?ba:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=dn(t,l);c&&(f="if($event.target.composing)return;"+f),an(e,"value","("+t+")"),un(e,u,f,null,!0),(s||a||"number"===r)&&un(e,"blur","$forceUpdate()")}function kn(e){var n;t(e[ba])&&(n=zi?"change":"input",e[n]=[].concat(e[ba],e[n]||[]),delete e[ba]),t(e[$a])&&(n=Zi?"click":"change",e[n]=[].concat(e[$a],e[n]||[]),delete e[$a])}function An(e,t,n,r,i){if(n){var o=t,a=qo;t=function(n){null!==(1===arguments.length?o(n):o.apply(null,arguments))&&On(e,t,r,a)}}qo.addEventListener(e,t,Gi?{capture:r,passive:i}:r)}function On(e,t,n,r){(r||qo).removeEventListener(e,t,n)}function Sn(t,n){if(!e(t.data.on)||!e(n.data.on)){var r=n.data.on||{},i=t.data.on||{};qo=n.elm,kn(r),G(r,i,An,On,n.context)}}function Tn(n,r){if(!e(n.data.domProps)||!e(r.data.domProps)){var i,o,a=r.elm,s=n.data.domProps||{},c=r.data.domProps||{};t(c.__ob__)&&(c=r.data.domProps=h({},c));for(i in s)e(c[i])&&(a[i]="");for(i in c)if(o=c[i],"textContent"!==i&&"innerHTML"!==i||(r.children&&(r.children.length=0),o!==s[i]))if("value"===i){a._value=o;var u=null==o?"":String(o);En(a,r,u)&&(a.value=u)}else a[i]=o}}function En(e,t,n){return!e.composing&&("option"===t.tag||jn(e,n)||Nn(e,n))}function jn(e,t){return document.activeElement!==e&&e.value!==t}function Nn(e,n){var r=e.value,i=e._vModifiers;return t(i)&&i.number||"number"===e.type?c(r)!==c(n):t(i)&&i.trim?r.trim()!==n.trim():r!==n}function Ln(e){var t=In(e.style);return e.staticStyle?h(e.staticStyle,t):t}function In(e){return Array.isArray(e)?m(e):"string"==typeof e?Ca(e):e}function Dn(e,t){var n,r={};if(t)for(var i=e;i.componentInstance;)i=i.componentInstance._vnode,i.data&&(n=Ln(i.data))&&h(r,n);(n=Ln(e.data))&&h(r,n);for(var o=e;o=o.parent;)o.data&&(n=Ln(o.data))&&h(r,n);return r}function Mn(n,r){var i=r.data,o=n.data;if(!(e(i.staticStyle)&&e(i.style)&&e(o.staticStyle)&&e(o.style))){var a,s,c=r.elm,u=o.staticStyle,l=o.normalizedStyle||o.style||{},f=u||l,p=In(r.data.style)||{};r.data.normalizedStyle=t(p.__ob__)?h({},p):p;var d=Dn(r,!0);for(s in f)e(d[s])&&Oa(c,s,"");for(s in d)(a=d[s])!==f[s]&&Oa(c,s,null==a?"":a)}}function Pn(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function Rn(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t);else{for(var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");e.setAttribute("class",n.trim())}}function Fn(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&h(t,ja(e.name||"v")),h(t,e),t}return"string"==typeof e?ja(e):void 0}}function Bn(e){Fa(function(){Fa(e)})}function Hn(e,t){(e._transitionClasses||(e._transitionClasses=[])).push(t),Pn(e,t)}function Un(e,t){e._transitionClasses&&l(e._transitionClasses,t),Rn(e,t)}function Vn(e,t,n){var r=zn(e,t),i=r.type,o=r.timeout,a=r.propCount;if(!i)return n();var s=i===La?Ma:Ra,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=a&&u()};setTimeout(function(){c0&&(n=La,l=a,f=o.length):t===Ia?u>0&&(n=Ia,l=u,f=c.length):(l=Math.max(a,u),n=l>0?a>u?La:Ia:null,f=n?n===La?o.length:c.length:0),{type:n,timeout:l,propCount:f,hasTransform:n===La&&Ba.test(r[Da+"Property"])}}function Jn(e,t){for(;e.length1}function Yn(e,t){!0!==t.data.show&&qn(t)}function Qn(e,t,n){var r=t.value,i=e.multiple;if(!i||Array.isArray(r)){for(var o,a,s=0,c=e.options.length;s-1,a.selected!==o&&(a.selected=o);else if(y(er(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function Xn(e,t){for(var n=0,r=t.length;n=0&&a[i].lowerCasedTag!==s;i--);else i=0;if(i>=0){for(var c=a.length-1;c>=i;c--)t.end&&t.end(a[c].tag,n,r);a.length=i,o=i&&a[i-1].tag}else"br"===s?t.start&&t.start(e,[],!0,n,r):"p"===s&&(t.start&&t.start(e,[],!1,n,r),t.end&&t.end(e,n,r))}for(var i,o,a=[],s=t.expectHTML,c=t.isUnaryTag||Ni,u=t.canBeLeftOpenTag||Ni,l=0;e;){if(i=e,o&&Ns(o)){var f=o.toLowerCase(),p=Ls[f]||(Ls[f]=new RegExp("([\\s\\S]*?)(]*>)","i")),d=0,v=e.replace(p,function(e,n,r){return d=r.length,Ns(f)||"noscript"===f||(n=n.replace(//g,"$1").replace(//g,"$1")),t.chars&&t.chars(n),""});l+=e.length-v.length,e=v,r(f,l-d,l)}else{var h=e.indexOf("<");if(0===h){if(fs.test(e)){var m=e.indexOf("--\x3e");if(m>=0){n(m+3);continue}}if(ps.test(e)){var g=e.indexOf("]>");if(g>=0){n(g+2);continue}}var y=e.match(ls);if(y){n(y[0].length);continue}var _=e.match(us);if(_){var b=l;n(_[0].length),r(_[1],b,l);continue}var $=function(){var t=e.match(ss);if(t){var r={tagName:t[1],attrs:[],start:l};n(t[0].length);for(var i,o;!(i=e.match(cs))&&(o=e.match(os));)n(o[0].length),r.attrs.push(o);if(i)return r.unarySlash=i[1],n(i[0].length),r.end=l,r}}();if($){!function(e){var n=e.tagName,i=e.unarySlash;s&&("p"===o&&rs(n)&&r(o),u(n)&&o===n&&r(n));for(var l=c(n)||"html"===n&&"head"===o||!!i,f=e.attrs.length,p=new Array(f),d=0;d=0){for(w=e.slice(h);!(us.test(w)||ss.test(w)||fs.test(w)||ps.test(w)||(C=w.indexOf("<",1))<0);)h+=C,w=e.slice(h);x=e.substring(0,h),n(h)}h<0&&(x=e,e=""),t.chars&&x&&t.chars(x)}if(e===i){t.chars&&t.chars(e);break}}r()}function mr(e,t){var n=t?Rs(t):Ps;if(n.test(e)){for(var r,i,o=[],a=n.lastIndex=0;r=n.exec(e);){i=r.index,i>a&&o.push(JSON.stringify(e.slice(a,i)));var s=tn(r[1].trim());o.push("_s("+s+")"),a=i+r[0].length}return a0,Ki=Vi&&Vi.indexOf("edge/")>0,qi=Vi&&Vi.indexOf("android")>0,Wi=Vi&&/iphone|ipad|ipod|ios/.test(Vi),Zi=Vi&&/chrome\/\d+/.test(Vi)&&!Ki,Gi=!1;if(Ui)try{var Yi={};Object.defineProperty(Yi,"passive",{get:function(){Gi=!0}}),window.addEventListener("test-passive",null,Yi)}catch(e){}var Qi,Xi,eo=function(){return void 0===Qi&&(Qi=!Ui&&"undefined"!=typeof global&&"server"===global.process.env.VUE_ENV),Qi},to=Ui&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,no="undefined"!=typeof Symbol&&k(Symbol)&&"undefined"!=typeof Reflect&&k(Reflect.ownKeys),ro=function(){function e(){r=!1;var e=n.slice(0);n.length=0;for(var t=0;t1?v(n):n;for(var r=v(arguments,1),i=0,o=n.length;i1&&(t[n[0].trim()]=n[1].trim())}}),t}),ka=/^--/,Aa=/\s*!important$/,Oa=function(e,t,n){if(ka.test(t))e.style.setProperty(t,n);else if(Aa.test(n))e.style.setProperty(t,n.replace(Aa,""),"important");else{var r=Ta(t);if(Array.isArray(n))for(var i=0,o=n.length;iv?(f=e(i[g+1])?null:i[g+1].elm,y(n,f,i,d,g,o)):d>g&&b(n,r,p,v)}function w(r,i,o,a){if(r!==i){if(n(i.isStatic)&&n(r.isStatic)&&i.key===r.key&&(n(i.isCloned)||n(i.isOnce)))return i.elm=r.elm,void(i.componentInstance=r.componentInstance);var s,c=i.data;t(c)&&t(s=c.hook)&&t(s=s.prepatch)&&s(r,i);var u=i.elm=r.elm,l=r.children,f=i.children;if(t(c)&&h(i)){for(s=0;s',n.innerHTML.indexOf(t)>0}("\n"," "),ts=u("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr"),ns=u("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source"),rs=u("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track"),is=[/"([^"]*)"+/.source,/'([^']*)'+/.source,/([^\s"'=<>`]+)/.source],os=new RegExp("^\\s*"+/([^\s"'<>\/=]+)/.source+"(?:\\s*("+/(?:=)/.source+")\\s*(?:"+is.join("|")+"))?"),as="[a-zA-Z_][\\w\\-\\.]*",ss=new RegExp("^<((?:"+as+"\\:)?"+as+")"),cs=/^\s*(\/?)>/,us=new RegExp("^<\\/((?:"+as+"\\:)?"+as+")[^>]*>"),ls=/^]+>/i,fs=/^ + + + + + + + + + + + + + + {{ title }} + {{ subTitle }} + + + + {{ extra }} + + + + + + + + + + + + + + + + diff --git a/im-uniapp/uni_modules/uni-card/package.json b/im-uniapp/uni_modules/uni-card/package.json new file mode 100644 index 0000000..f16224d --- /dev/null +++ b/im-uniapp/uni_modules/uni-card/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-card", + "displayName": "uni-card 卡片", + "version": "1.3.1", + "description": "Card 组件,提供常见的卡片样式。", + "keywords": [ + "uni-ui", + "uniui", + "card", + "", + "卡片" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-icons", + "uni-scss" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-card/readme.md b/im-uniapp/uni_modules/uni-card/readme.md new file mode 100644 index 0000000..7434e71 --- /dev/null +++ b/im-uniapp/uni_modules/uni-card/readme.md @@ -0,0 +1,12 @@ + + +## Card 卡片 +> **组件名:uni-card** +> 代码块: `uCard` + +卡片视图组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-card) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/im-uniapp/uni_modules/uni-collapse/changelog.md b/im-uniapp/uni_modules/uni-collapse/changelog.md new file mode 100644 index 0000000..292e4c7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-collapse/changelog.md @@ -0,0 +1,36 @@ +## 1.4.3(2022-01-25) +- 修复 初始化的时候 ,open 属性失效的bug +## 1.4.2(2022-01-21) +- 修复 微信小程序resize后组件收起的bug +## 1.4.1(2021-11-22) +- 修复 vue3中个别scss变量无法找到的问题 +## 1.4.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-collapse](https://uniapp.dcloud.io/component/uniui/uni-collapse) +## 1.3.3(2021-08-17) +- 优化 show-arrow 属性默认为true +## 1.3.2(2021-08-17) +- 新增 show-arrow 属性,控制是否显示右侧箭头 +## 1.3.1(2021-07-30) +- 优化 vue3下小程序事件警告的问题 +## 1.3.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.2.2(2021-07-21) +- 修复 由1.2.0版本引起的 change 事件返回 undefined 的Bug +## 1.2.1(2021-07-21) +- 优化 组件示例 +## 1.2.0(2021-07-21) +- 新增 组件折叠动画 +- 新增 value\v-model 属性 ,动态修改面板折叠状态 +- 新增 title 插槽 ,可定义面板标题 +- 新增 border 属性 ,显示隐藏面板内容分隔线 +- 新增 title-border 属性 ,显示隐藏面板标题分隔线 +- 修复 resize 方法失效的Bug +- 修复 change 事件返回参数不正确的Bug +- 优化 H5、App 平台自动更具内容更新高度,无需调用 reszie() 方法 +## 1.1.7(2021-05-12) +- 新增 组件示例地址 +## 1.1.6(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.1.5(2021-02-05) +- 调整为uni_modules目录规范 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-collapse/components/uni-collapse-item/uni-collapse-item.vue b/im-uniapp/uni_modules/uni-collapse/components/uni-collapse-item/uni-collapse-item.vue new file mode 100644 index 0000000..d62a6a7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-collapse/components/uni-collapse-item/uni-collapse-item.vue @@ -0,0 +1,402 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-collapse/components/uni-collapse/uni-collapse.vue b/im-uniapp/uni_modules/uni-collapse/components/uni-collapse/uni-collapse.vue new file mode 100644 index 0000000..384c39a --- /dev/null +++ b/im-uniapp/uni_modules/uni-collapse/components/uni-collapse/uni-collapse.vue @@ -0,0 +1,147 @@ + + + diff --git a/im-uniapp/uni_modules/uni-collapse/package.json b/im-uniapp/uni_modules/uni-collapse/package.json new file mode 100644 index 0000000..65349cf --- /dev/null +++ b/im-uniapp/uni_modules/uni-collapse/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-collapse", + "displayName": "uni-collapse 折叠面板", + "version": "1.4.3", + "description": "Collapse 组件,可以折叠 / 展开的内容区域。", + "keywords": [ + "uni-ui", + "折叠", + "折叠面板", + "手风琴" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-collapse/readme.md b/im-uniapp/uni_modules/uni-collapse/readme.md new file mode 100644 index 0000000..bc758eb --- /dev/null +++ b/im-uniapp/uni_modules/uni-collapse/readme.md @@ -0,0 +1,12 @@ + + +## Collapse 折叠面板 +> **组件名:uni-collapse** +> 代码块: `uCollapse` +> 关联组件:`uni-collapse-item`、`uni-icons`。 + + +折叠面板用来折叠/显示过长的内容或者是列表。通常是在多内容分类项使用,折叠不重要的内容,显示重要内容。点击可以展开折叠部分。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-collapse) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-combox/changelog.md b/im-uniapp/uni_modules/uni-combox/changelog.md new file mode 100644 index 0000000..23c2748 --- /dev/null +++ b/im-uniapp/uni_modules/uni-combox/changelog.md @@ -0,0 +1,15 @@ +## 1.0.1(2021-11-23) +- 优化 label、label-width 属性 +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-combox](https://uniapp.dcloud.io/component/uniui/uni-combox) +## 0.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.0.6(2021-05-12) +- 新增 组件示例地址 +## 0.0.5(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 0.0.4(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 0.0.3(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-combox/components/uni-combox/uni-combox.vue b/im-uniapp/uni_modules/uni-combox/components/uni-combox/uni-combox.vue new file mode 100644 index 0000000..d4cb79d --- /dev/null +++ b/im-uniapp/uni_modules/uni-combox/components/uni-combox/uni-combox.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-combox/package.json b/im-uniapp/uni_modules/uni-combox/package.json new file mode 100644 index 0000000..4a05c3f --- /dev/null +++ b/im-uniapp/uni_modules/uni-combox/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-combox", + "displayName": "uni-combox 组合框", + "version": "1.0.1", + "description": "可以选择也可以输入的表单项 ", + "keywords": [ + "uni-ui", + "uniui", + "combox", + "组合框", + "select" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-combox/readme.md b/im-uniapp/uni_modules/uni-combox/readme.md new file mode 100644 index 0000000..ffa2cc8 --- /dev/null +++ b/im-uniapp/uni_modules/uni-combox/readme.md @@ -0,0 +1,11 @@ + + +## Combox 组合框 +> **组件名:uni-combox** +> 代码块: `uCombox` + + +组合框组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-combox) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-countdown/changelog.md b/im-uniapp/uni_modules/uni-countdown/changelog.md new file mode 100644 index 0000000..f25beef --- /dev/null +++ b/im-uniapp/uni_modules/uni-countdown/changelog.md @@ -0,0 +1,24 @@ +## 1.2.2(2022-01-19) +- 修复 在微信小程序中样式不生效的bug +## 1.2.1(2022-01-18) +- 新增 update 方法 ,在动态更新时间后,刷新组件 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-countdown](https://uniapp.dcloud.io/component/uniui/uni-countdown) +## 1.1.3(2021-10-18) +- 重构 +- 新增 font-size 支持自定义字体大小 +## 1.1.2(2021-08-24) +- 新增 支持国际化 +## 1.1.1(2021-07-30) +- 优化 vue3下小程序事件警告的问题 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.5(2021-06-18) +- 修复 uni-countdown 重复赋值跳两秒的 bug +## 1.0.4(2021-05-12) +- 新增 组件示例地址 +## 1.0.3(2021-05-08) +- 修复 uni-countdown 不能控制倒计时的 bug +## 1.0.2(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/en.json b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/en.json new file mode 100644 index 0000000..06309cb --- /dev/null +++ b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/en.json @@ -0,0 +1,6 @@ +{ + "uni-countdown.day": "day", + "uni-countdown.h": "h", + "uni-countdown.m": "m", + "uni-countdown.s": "s" +} diff --git a/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/index.js b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hans.json new file mode 100644 index 0000000..358cdd1 --- /dev/null +++ b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hans.json @@ -0,0 +1,6 @@ +{ + "uni-countdown.day": "天", + "uni-countdown.h": "时", + "uni-countdown.m": "分", + "uni-countdown.s": "秒" +} diff --git a/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hant.json new file mode 100644 index 0000000..e5a63de --- /dev/null +++ b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hant.json @@ -0,0 +1,6 @@ +{ + "uni-countdown.day": "天", + "uni-countdown.h": "時", + "uni-countdown.m": "分", + "uni-countdown.s": "秒" +} diff --git a/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/uni-countdown.vue b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/uni-countdown.vue new file mode 100644 index 0000000..1f8ef4e --- /dev/null +++ b/im-uniapp/uni_modules/uni-countdown/components/uni-countdown/uni-countdown.vue @@ -0,0 +1,271 @@ + + + diff --git a/im-uniapp/uni_modules/uni-countdown/package.json b/im-uniapp/uni_modules/uni-countdown/package.json new file mode 100644 index 0000000..70e99ee --- /dev/null +++ b/im-uniapp/uni_modules/uni-countdown/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-countdown", + "displayName": "uni-countdown 倒计时", + "version": "1.2.2", + "description": "CountDown 倒计时组件", + "keywords": [ + "uni-ui", + "uniui", + "countdown", + "倒计时" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-countdown/readme.md b/im-uniapp/uni_modules/uni-countdown/readme.md new file mode 100644 index 0000000..4bcb1aa --- /dev/null +++ b/im-uniapp/uni_modules/uni-countdown/readme.md @@ -0,0 +1,10 @@ + + +## CountDown 倒计时 +> **组件名:uni-countdown** +> 代码块: `uCountDown` + +倒计时组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-countdown) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-data-checkbox/changelog.md b/im-uniapp/uni_modules/uni-data-checkbox/changelog.md new file mode 100644 index 0000000..c7a468a --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-checkbox/changelog.md @@ -0,0 +1,45 @@ +## 1.0.3(2022-09-16) +- 可以使用 uni-scss 控制主题色 +## 1.0.2(2022-06-30) +- 优化 在 uni-forms 中的依赖注入方式 +## 1.0.1(2022-02-07) +- 修复 multiple 为 true 时,v-model 的值为 null 报错的 bug +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-checkbox](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox) +## 0.2.5(2021-08-23) +- 修复 在uni-forms中 modelValue 中不存在当前字段,当前字段必填写也不参与校验的问题 +## 0.2.4(2021-08-17) +- 修复 单选 list 模式下 ,icon 为 left 时,选中图标不显示的问题 +## 0.2.3(2021-08-11) +- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 +## 0.2.2(2021-07-30) +- 优化 在uni-forms组件,与label不对齐的问题 +## 0.2.1(2021-07-27) +- 修复 单选默认值为0不能选中的Bug +## 0.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.1.11(2021-07-06) +- 优化 删除无用日志 +## 0.1.10(2021-07-05) +- 修复 由 0.1.9 引起的非 nvue 端图标不显示的问题 +## 0.1.9(2021-07-05) +- 修复 nvue 黑框样式问题 +## 0.1.8(2021-06-28) +- 修复 selectedTextColor 属性不生效的Bug +## 0.1.7(2021-06-02) +- 新增 map 属性,可以方便映射text/value属性 +## 0.1.6(2021-05-26) +- 修复 不关联服务空间的情况下组件报错的Bug +## 0.1.5(2021-05-12) +- 新增 组件示例地址 +## 0.1.4(2021-04-09) +- 修复 nvue 下无法选中的问题 +## 0.1.3(2021-03-22) +- 新增 disabled属性 +## 0.1.2(2021-02-24) +- 优化 默认颜色显示 +## 0.1.1(2021-02-24) +- 新增 支持nvue +## 0.1.0(2021-02-18) +- “暂无数据”显示居中 diff --git a/im-uniapp/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue b/im-uniapp/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue new file mode 100644 index 0000000..3c75d9f --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue @@ -0,0 +1,821 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-data-checkbox/package.json b/im-uniapp/uni_modules/uni-data-checkbox/package.json new file mode 100644 index 0000000..113c350 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-checkbox/package.json @@ -0,0 +1,84 @@ +{ + "id": "uni-data-checkbox", + "displayName": "uni-data-checkbox 数据选择器", + "version": "1.0.3", + "description": "通过数据驱动的单选框和复选框", + "keywords": [ + "uni-ui", + "checkbox", + "单选", + "多选", + "单选多选" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.1" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-load-more","uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-data-checkbox/readme.md b/im-uniapp/uni_modules/uni-data-checkbox/readme.md new file mode 100644 index 0000000..6eb253d --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-checkbox/readme.md @@ -0,0 +1,18 @@ + + +## DataCheckbox 数据驱动的单选复选框 +> **组件名:uni-data-checkbox** +> 代码块: `uDataCheckbox` + + +本组件是基于uni-app基础组件checkbox的封装。本组件要解决问题包括: + +1. 数据绑定型组件:给本组件绑定一个data,会自动渲染一组候选内容。再以往,开发者需要编写不少代码实现类似功能 +2. 自动的表单校验:组件绑定了data,且符合[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)组件的表单校验规范,搭配使用会自动实现表单校验 +3. 本组件合并了单选多选 +4. 本组件有若干风格选择,如普通的单选多选框、并列button风格、tag风格。开发者可以快速选择需要的风格。但作为一个封装组件,样式代码虽然不用自己写了,却会牺牲一定的样式自定义性 + +在uniCloud开发中,`DB Schema`中配置了enum枚举等类型后,在web控制台的[自动生成表单](https://uniapp.dcloud.io/uniCloud/schema?id=autocode)功能中,会自动生成``uni-data-checkbox``组件并绑定好data + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-data-picker/changelog.md b/im-uniapp/uni_modules/uni-data-picker/changelog.md new file mode 100644 index 0000000..1dd79f7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-picker/changelog.md @@ -0,0 +1,75 @@ +## 1.1.2(2023-04-11) +- 修复 更改 modelValue 报错的 bug +- 修复 v-for 未使用 key 值控制台 warning +## 1.1.1(2023-02-21) +- 修复代码合并时引发 value 属性为空时不渲染数据的问题 +## 1.1.0(2023-02-15) +- 修复 localdata 不支持动态更新的bug +## 1.0.9(2023-02-15) +- 修复 localdata 不支持动态更新的bug +## 1.0.8(2022-09-16) +- 可以使用 uni-scss 控制主题色 +## 1.0.7(2022-07-06) +- 优化 pc端图标位置不正确的问题 +## 1.0.6(2022-07-05) +- 优化 显示样式 +## 1.0.5(2022-07-04) +- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug +## 1.0.4(2022-04-19) +- 修复 字节小程序 本地数据无法选择下一级的Bug +## 1.0.3(2022-02-25) +- 修复 nvue 不支持的 v-show 的 bug +## 1.0.2(2022-02-25) +- 修复 条件编译 nvue 不支持的 css 样式 +## 1.0.1(2021-11-23) +- 修复 由上个版本引发的map、v-model等属性不生效的bug +## 1.0.0(2021-11-19) +- 优化 组件 UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-picker](https://uniapp.dcloud.io/component/uniui/uni-data-picker) +## 0.4.9(2021-10-28) +- 修复 VUE2 v-model 概率无效的 bug +## 0.4.8(2021-10-27) +- 修复 v-model 概率无效的 bug +## 0.4.7(2021-10-25) +- 新增 属性 spaceInfo 服务空间配置 HBuilderX 3.2.11+ +- 修复 树型 uniCloud 数据类型为 int 时报错的 bug +## 0.4.6(2021-10-19) +- 修复 非 VUE3 v-model 为 0 时无法选中的 bug +## 0.4.5(2021-09-26) +- 新增 清除已选项的功能(通过 clearIcon 属性配置是否显示按钮),同时提供 clear 方法以供调用,二者等效 +- 修复 readonly 为 true 时报错的 bug +## 0.4.4(2021-09-26) +- 修复 上一版本造成的 map 属性失效的 bug +- 新增 ellipsis 属性,支持配置 tab 选项长度过长时是否自动省略 +## 0.4.3(2021-09-24) +- 修复 某些情况下级联未触发的 bug +## 0.4.2(2021-09-23) +- 新增 提供 show 和 hide 方法,开发者可以通过 ref 调用 +- 新增 选项内容过长自动添加省略号 +## 0.4.1(2021-09-15) +- 新增 map 属性 字段映射,将 text/value 映射到数据中的其他字段 +## 0.4.0(2021-07-13) +- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.3.5(2021-06-04) +- 修复 无法加载云端数据的问题 +## 0.3.4(2021-05-28) +- 修复 v-model 无效问题 +- 修复 loaddata 为空数据组时加载时间过长问题 +- 修复 上个版本引出的本地数据无法选择带有 children 的 2 级节点 +## 0.3.3(2021-05-12) +- 新增 组件示例地址 +## 0.3.2(2021-04-22) +- 修复 非树形数据有 where 属性查询报错的问题 +## 0.3.1(2021-04-15) +- 修复 本地数据概率无法回显时问题 +## 0.3.0(2021-04-07) +- 新增 支持云端非树形表结构数据 +- 修复 根节点 parent_field 字段等于 null 时选择界面错乱问题 +## 0.2.0(2021-03-15) +- 修复 nodeclick、popupopened、popupclosed 事件无法触发的问题 +## 0.1.9(2021-03-09) +- 修复 微信小程序某些情况下无法选择的问题 +## 0.1.8(2021-02-05) +- 优化 部分样式在 nvue 上的兼容表现 +## 0.1.7(2021-02-05) +- 调整为 uni_modules 目录规范 diff --git a/im-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js b/im-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js new file mode 100644 index 0000000..6ef26a2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + this.$once('hook:beforeDestroy', () => { + document.removeEventListener('keyup', listener) + }) + }, + render: () => {} +} +// #endif diff --git a/im-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue b/im-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue new file mode 100644 index 0000000..179a4e0 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue @@ -0,0 +1,551 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js b/im-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js new file mode 100644 index 0000000..cfae22a --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js @@ -0,0 +1,622 @@ +export default { + props: { + localdata: { + type: [Array, Object], + default () { + return [] + } + }, + spaceInfo: { + type: Object, + default () { + return {} + } + }, + collection: { + type: String, + default: '' + }, + action: { + type: String, + default: '' + }, + field: { + type: String, + default: '' + }, + orderby: { + type: String, + default: '' + }, + where: { + type: [String, Object], + default: '' + }, + pageData: { + type: String, + default: 'add' + }, + pageCurrent: { + type: Number, + default: 1 + }, + pageSize: { + type: Number, + default: 500 + }, + getcount: { + type: [Boolean, String], + default: false + }, + getone: { + type: [Boolean, String], + default: false + }, + gettree: { + type: [Boolean, String], + default: false + }, + manual: { + type: Boolean, + default: false + }, + value: { + type: [Array, String, Number], + default () { + return [] + } + }, + modelValue: { + type: [Array, String, Number], + default () { + return [] + } + }, + preload: { + type: Boolean, + default: false + }, + stepSearh: { + type: Boolean, + default: true + }, + selfField: { + type: String, + default: '' + }, + parentField: { + type: String, + default: '' + }, + multiple: { + type: Boolean, + default: false + }, + map: { + type: Object, + default () { + return { + text: "text", + value: "value" + } + } + } + }, + data() { + return { + loading: false, + errorMessage: '', + loadMore: { + contentdown: '', + contentrefresh: '', + contentnomore: '' + }, + dataList: [], + selected: [], + selectedIndex: 0, + page: { + current: this.pageCurrent, + size: this.pageSize, + count: 0 + } + } + }, + computed: { + isLocalData() { + return !this.collection.length; + }, + isCloudData() { + return this.collection.length > 0; + }, + isCloudDataList() { + return (this.isCloudData && (!this.parentField && !this.selfField)); + }, + isCloudDataTree() { + return (this.isCloudData && this.parentField && this.selfField); + }, + dataValue() { + let isModelValue = Array.isArray(this.modelValue) ? (this.modelValue.length > 0) : (this.modelValue !== null || + this.modelValue !== undefined); + return isModelValue ? this.modelValue : this.value; + }, + hasValue() { + if (typeof this.dataValue === 'number') { + return true + } + return (this.dataValue != null) && (this.dataValue.length > 0) + } + }, + created() { + this.$watch(() => { + var al = []; + ['pageCurrent', + 'pageSize', + 'spaceInfo', + 'value', + 'modelValue', + 'localdata', + 'collection', + 'action', + 'field', + 'orderby', + 'where', + 'getont', + 'getcount', + 'gettree' + ].forEach(key => { + al.push(this[key]) + }); + return al + }, (newValue, oldValue) => { + let needReset = false + for (let i = 2; i < newValue.length; i++) { + if (newValue[i] != oldValue[i]) { + needReset = true + break + } + } + if (newValue[0] != oldValue[0]) { + this.page.current = this.pageCurrent + } + this.page.size = this.pageSize + + this.onPropsChange() + }) + this._treeData = [] + }, + methods: { + onPropsChange() { + this._treeData = []; + }, + + // 填充 pickview 数据 + async loadData() { + if (this.isLocalData) { + this.loadLocalData(); + } else if (this.isCloudDataList) { + this.loadCloudDataList(); + } else if (this.isCloudDataTree) { + this.loadCloudDataTree(); + } + }, + + // 加载本地数据 + async loadLocalData() { + this._treeData = []; + this._extractTree(this.localdata, this._treeData); + + let inputValue = this.dataValue; + if (inputValue === undefined) { + return; + } + + if (Array.isArray(inputValue)) { + inputValue = inputValue[inputValue.length - 1]; + if (typeof inputValue === 'object' && inputValue[this.map.value]) { + inputValue = inputValue[this.map.value]; + } + } + + this.selected = this._findNodePath(inputValue, this.localdata); + }, + + // 加载 Cloud 数据 (单列) + async loadCloudDataList() { + if (this.loading) { + return; + } + this.loading = true; + + try { + let response = await this.getCommand(); + let responseData = response.result.data; + + this._treeData = responseData; + + this._updateBindData(); + this._updateSelected(); + + this.onDataChange(); + } catch (e) { + this.errorMessage = e; + } finally { + this.loading = false; + } + }, + + // 加载 Cloud 数据 (树形) + async loadCloudDataTree() { + if (this.loading) { + return; + } + this.loading = true; + + try { + let commandOptions = { + field: this._cloudDataPostField(), + where: this._cloudDataTreeWhere() + }; + if (this.gettree) { + commandOptions.startwith = `${this.selfField}=='${this.dataValue}'`; + } + + let response = await this.getCommand(commandOptions); + let responseData = response.result.data; + + this._treeData = responseData; + this._updateBindData(); + this._updateSelected(); + + this.onDataChange(); + } catch (e) { + this.errorMessage = e; + } finally { + this.loading = false; + } + }, + + // 加载 Cloud 数据 (节点) + async loadCloudDataNode(callback) { + if (this.loading) { + return; + } + this.loading = true; + + try { + let commandOptions = { + field: this._cloudDataPostField(), + where: this._cloudDataNodeWhere() + }; + + let response = await this.getCommand(commandOptions); + let responseData = response.result.data; + + callback(responseData); + } catch (e) { + this.errorMessage = e; + } finally { + this.loading = false; + } + }, + + // 回显 Cloud 数据 + getCloudDataValue() { + if (this.isCloudDataList) { + return this.getCloudDataListValue(); + } + + if (this.isCloudDataTree) { + return this.getCloudDataTreeValue(); + } + }, + + // 回显 Cloud 数据 (单列) + getCloudDataListValue() { + // 根据 field's as value标识匹配 where 条件 + let where = []; + let whereField = this._getForeignKeyByField(); + if (whereField) { + where.push(`${whereField} == '${this.dataValue}'`) + } + + where = where.join(' || '); + + if (this.where) { + where = `(${this.where}) && (${where})` + } + + return this.getCommand({ + field: this._cloudDataPostField(), + where + }).then((res) => { + this.selected = res.result.data; + return res.result.data; + }); + }, + + // 回显 Cloud 数据 (树形) + getCloudDataTreeValue() { + return this.getCommand({ + field: this._cloudDataPostField(), + getTreePath: { + startWith: `${this.selfField}=='${this.dataValue}'` + } + }).then((res) => { + let treePath = []; + this._extractTreePath(res.result.data, treePath); + this.selected = treePath; + return treePath; + }); + }, + + getCommand(options = {}) { + /* eslint-disable no-undef */ + let db = uniCloud.database(this.spaceInfo) + + const action = options.action || this.action + if (action) { + db = db.action(action) + } + + const collection = options.collection || this.collection + db = db.collection(collection) + + const where = options.where || this.where + if (!(!where || !Object.keys(where).length)) { + db = db.where(where) + } + + const field = options.field || this.field + if (field) { + db = db.field(field) + } + + const orderby = options.orderby || this.orderby + if (orderby) { + db = db.orderBy(orderby) + } + + const current = options.pageCurrent !== undefined ? options.pageCurrent : this.page.current + const size = options.pageSize !== undefined ? options.pageSize : this.page.size + const getCount = options.getcount !== undefined ? options.getcount : this.getcount + const getTree = options.gettree !== undefined ? options.gettree : this.gettree + + const getOptions = { + getCount, + getTree + } + if (options.getTreePath) { + getOptions.getTreePath = options.getTreePath + } + + db = db.skip(size * (current - 1)).limit(size).get(getOptions) + + return db + }, + + _cloudDataPostField() { + let fields = [this.field]; + if (this.parentField) { + fields.push(`${this.parentField} as parent_value`); + } + return fields.join(','); + }, + + _cloudDataTreeWhere() { + let result = [] + let selected = this.selected + let parentField = this.parentField + if (parentField) { + result.push(`${parentField} == null || ${parentField} == ""`) + } + if (selected.length) { + for (var i = 0; i < selected.length - 1; i++) { + result.push(`${parentField} == '${selected[i].value}'`) + } + } + + let where = [] + if (this.where) { + where.push(`(${this.where})`) + } + + if (result.length) { + where.push(`(${result.join(' || ')})`) + } + + return where.join(' && ') + }, + + _cloudDataNodeWhere() { + let where = [] + let selected = this.selected; + if (selected.length) { + where.push(`${this.parentField} == '${selected[selected.length - 1].value}'`); + } + + where = where.join(' || '); + + if (this.where) { + return `(${this.where}) && (${where})` + } + + return where + }, + + _getWhereByForeignKey() { + let result = [] + let whereField = this._getForeignKeyByField(); + if (whereField) { + result.push(`${whereField} == '${this.dataValue}'`) + } + + if (this.where) { + return `(${this.where}) && (${result.join(' || ')})` + } + + return result.join(' || ') + }, + + _getForeignKeyByField() { + let fields = this.field.split(','); + let whereField = null; + for (let i = 0; i < fields.length; i++) { + const items = fields[i].split('as'); + if (items.length < 2) { + continue; + } + if (items[1].trim() === 'value') { + whereField = items[0].trim(); + break; + } + } + return whereField; + }, + + _updateBindData(node) { + const { + dataList, + hasNodes + } = this._filterData(this._treeData, this.selected) + + let isleaf = this._stepSearh === false && !hasNodes + + if (node) { + node.isleaf = isleaf + } + + this.dataList = dataList + this.selectedIndex = dataList.length - 1 + + if (!isleaf && this.selected.length < dataList.length) { + this.selected.push({ + value: null, + text: "请选择" + }) + } + + return { + isleaf, + hasNodes + } + }, + + _updateSelected() { + let dl = this.dataList + let sl = this.selected + let textField = this.map.text + let valueField = this.map.value + for (let i = 0; i < sl.length; i++) { + let value = sl[i].value + let dl2 = dl[i] + for (let j = 0; j < dl2.length; j++) { + let item2 = dl2[j] + if (item2[valueField] === value) { + sl[i].text = item2[textField] + break + } + } + } + }, + + _filterData(data, paths) { + let dataList = [] + let hasNodes = true + + dataList.push(data.filter((item) => { + return (item.parent_value === null || item.parent_value === undefined || item.parent_value === '') + })) + for (let i = 0; i < paths.length; i++) { + let value = paths[i].value + let nodes = data.filter((item) => { + return item.parent_value === value + }) + + if (nodes.length) { + dataList.push(nodes) + } else { + hasNodes = false + } + } + + return { + dataList, + hasNodes + } + }, + + _extractTree(nodes, result, parent_value) { + let list = result || [] + let valueField = this.map.value + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + + let child = {} + for (let key in node) { + if (key !== 'children') { + child[key] = node[key] + } + } + if (parent_value !== null && parent_value !== undefined && parent_value !== '') { + child.parent_value = parent_value + } + result.push(child) + + let children = node.children + if (children) { + this._extractTree(children, result, node[valueField]) + } + } + }, + + _extractTreePath(nodes, result) { + let list = result || [] + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + + let child = {} + for (let key in node) { + if (key !== 'children') { + child[key] = node[key] + } + } + result.push(child) + + let children = node.children + if (children) { + this._extractTreePath(children, result) + } + } + }, + + _findNodePath(key, nodes, path = []) { + let textField = this.map.text + let valueField = this.map.value + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + let children = node.children + let text = node[textField] + let value = node[valueField] + + path.push({ + value, + text + }) + + if (value === key) { + return path + } + + if (children) { + const p = this._findNodePath(key, children, path) + if (p.length) { + return p + } + } + + path.pop() + } + return [] + } + } +} diff --git a/im-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue b/im-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue new file mode 100644 index 0000000..6ebced9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue @@ -0,0 +1,323 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-data-picker/package.json b/im-uniapp/uni_modules/uni-data-picker/package.json new file mode 100644 index 0000000..038f9ce --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-picker/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-data-picker", + "displayName": "uni-data-picker 数据驱动的picker选择器", + "version": "1.1.2", + "description": "单列、多列级联选择器,常用于省市区城市选择、公司部门选择、多级分类等场景", + "keywords": [ + "uni-ui", + "uniui", + "picker", + "级联", + "省市区", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-load-more", + "uni-icons", + "uni-scss" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-data-picker/readme.md b/im-uniapp/uni_modules/uni-data-picker/readme.md new file mode 100644 index 0000000..6cda224 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-picker/readme.md @@ -0,0 +1,22 @@ +## DataPicker 级联选择 +> **组件名:uni-data-picker** +> 代码块: `uDataPicker` +> 关联组件:`uni-data-pickerview`、`uni-load-more`。 + + +`` 是一个选择类[datacom组件](https://uniapp.dcloud.net.cn/component/datacom)。 + +支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。 + +候选数据支持一次性加载完毕,也支持懒加载,比如示例图中,选择了“北京”后,动态加载北京的区县数据。 + +`` 组件尤其适用于地址选择、分类选择等选择类。 + +`` 支持本地数据、云端静态数据(json),uniCloud云数据库数据。 + +`` 可以通过JQL直连uniCloud云数据库,配套[DB Schema](https://uniapp.dcloud.net.cn/uniCloud/schema),可在schema2code中自动生成前端页面,还支持服务器端校验。 + +在uniCloud数据表中新建表“uni-id-address”和“opendb-city-china”,这2个表的schema自带foreignKey关联。在“uni-id-address”表的表结构页面使用schema2code生成前端页面,会自动生成地址管理的维护页面,自动从“opendb-city-china”表包含的中国所有省市区信息里选择地址。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-data-select/changelog.md b/im-uniapp/uni_modules/uni-data-select/changelog.md new file mode 100644 index 0000000..eb3d1ce --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-select/changelog.md @@ -0,0 +1,35 @@ +## 1.0.6(2023-04-12) +- 修复 微信小程序点击时会改变背景颜色的 bug +## 1.0.5(2023-02-03) +- 修复 禁用时会显示清空按钮 +## 1.0.4(2023-02-02) +- 优化 查询条件短期内多次变更只查询最后一次变更后的结果 +- 调整 内部缓存键名调整为 uni-data-select-lastSelectedValue +## 1.0.3(2023-01-16) +- 修复 不关联服务空间报错的问题 +## 1.0.2(2023-01-14) +- 新增 属性 `format` 可用于格式化显示选项内容 +## 1.0.1(2022-12-06) +- 修复 当where变化时,数据不会自动更新的问题 +## 0.1.9(2022-09-05) +- 修复 微信小程序下拉框出现后选择会点击到蒙板后面的输入框 +## 0.1.8(2022-08-29) +- 修复 点击的位置不准确 +## 0.1.7(2022-08-12) +- 新增 支持 disabled 属性 +## 0.1.6(2022-07-06) +- 修复 pc端宽度异常的bug +## 0.1.5 +- 修复 pc端宽度异常的bug +## 0.1.4(2022-07-05) +- 优化 显示样式 +## 0.1.3(2022-06-02) +- 修复 localdata 赋值不生效的 bug +- 新增 支持 uni.scss 修改颜色 +- 新增 支持选项禁用(数据选项设置 disabled: true 即禁用) +## 0.1.2(2022-05-08) +- 修复 当 value 为 0 时选择不生效的 bug +## 0.1.1(2022-05-07) +- 新增 记住上次的选项(仅 collection 存在时有效) +## 0.1.0(2022-04-22) +- 初始化 diff --git a/im-uniapp/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue b/im-uniapp/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue new file mode 100644 index 0000000..9b32348 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue @@ -0,0 +1,517 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-data-select/package.json b/im-uniapp/uni_modules/uni-data-select/package.json new file mode 100644 index 0000000..0187429 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-select/package.json @@ -0,0 +1,85 @@ +{ + "id": "uni-data-select", + "displayName": "uni-data-select 下拉框选择器", + "version": "1.0.6", + "description": "通过数据驱动的下拉框选择器", + "keywords": [ + "uni-ui", + "select", + "uni-data-select", + "下拉框", + "下拉选" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.1" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-load-more"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "u", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-data-select/readme.md b/im-uniapp/uni_modules/uni-data-select/readme.md new file mode 100644 index 0000000..eb58de3 --- /dev/null +++ b/im-uniapp/uni_modules/uni-data-select/readme.md @@ -0,0 +1,8 @@ +## DataSelect 下拉框选择器 +> **组件名:uni-data-select** +> 代码块: `uDataSelect` + +当选项过多时,使用下拉菜单展示并选择内容 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-select) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/im-uniapp/uni_modules/uni-dateformat/changelog.md b/im-uniapp/uni_modules/uni-dateformat/changelog.md new file mode 100644 index 0000000..d551d7b --- /dev/null +++ b/im-uniapp/uni_modules/uni-dateformat/changelog.md @@ -0,0 +1,10 @@ +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-dateformat](https://uniapp.dcloud.io/component/uniui/uni-dateformat) +## 0.0.5(2021-07-08) +- 调整 默认时间不再是当前时间,而是显示'-'字符 +## 0.0.4(2021-05-12) +- 新增 组件示例地址 +## 0.0.3(2021-02-04) +- 调整为uni_modules目录规范 +- 修复 iOS 平台日期格式化出错的问题 diff --git a/im-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js b/im-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js new file mode 100644 index 0000000..e00d559 --- /dev/null +++ b/im-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js @@ -0,0 +1,200 @@ +// yyyy-MM-dd hh:mm:ss.SSS 所有支持的类型 +function pad(str, length = 2) { + str += '' + while (str.length < length) { + str = '0' + str + } + return str.slice(-length) +} + +const parser = { + yyyy: (dateObj) => { + return pad(dateObj.year, 4) + }, + yy: (dateObj) => { + return pad(dateObj.year) + }, + MM: (dateObj) => { + return pad(dateObj.month) + }, + M: (dateObj) => { + return dateObj.month + }, + dd: (dateObj) => { + return pad(dateObj.day) + }, + d: (dateObj) => { + return dateObj.day + }, + hh: (dateObj) => { + return pad(dateObj.hour) + }, + h: (dateObj) => { + return dateObj.hour + }, + mm: (dateObj) => { + return pad(dateObj.minute) + }, + m: (dateObj) => { + return dateObj.minute + }, + ss: (dateObj) => { + return pad(dateObj.second) + }, + s: (dateObj) => { + return dateObj.second + }, + SSS: (dateObj) => { + return pad(dateObj.millisecond, 3) + }, + S: (dateObj) => { + return dateObj.millisecond + }, +} + +// 这都n年了iOS依然不认识2020-12-12,需要转换为2020/12/12 +function getDate(time) { + if (time instanceof Date) { + return time + } + switch (typeof time) { + case 'string': + { + // 2020-12-12T12:12:12.000Z、2020-12-12T12:12:12.000 + if (time.indexOf('T') > -1) { + return new Date(time) + } + return new Date(time.replace(/-/g, '/')) + } + default: + return new Date(time) + } +} + +export function formatDate(date, format = 'yyyy/MM/dd hh:mm:ss') { + if (!date && date !== 0) { + return '' + } + date = getDate(date) + const dateObj = { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + hour: date.getHours(), + minute: date.getMinutes(), + second: date.getSeconds(), + millisecond: date.getMilliseconds() + } + const tokenRegExp = /yyyy|yy|MM|M|dd|d|hh|h|mm|m|ss|s|SSS|SS|S/ + let flag = true + let result = format + while (flag) { + flag = false + result = result.replace(tokenRegExp, function(matched) { + flag = true + return parser[matched](dateObj) + }) + } + return result +} + +export function friendlyDate(time, { + locale = 'zh', + threshold = [60000, 3600000], + format = 'yyyy/MM/dd hh:mm:ss' +}) { + if (time === '-') { + return time + } + if (!time && time !== 0) { + return '' + } + const localeText = { + zh: { + year: '年', + month: '月', + day: '天', + hour: '小时', + minute: '分钟', + second: '秒', + ago: '前', + later: '后', + justNow: '刚刚', + soon: '马上', + template: '{num}{unit}{suffix}' + }, + en: { + year: 'year', + month: 'month', + day: 'day', + hour: 'hour', + minute: 'minute', + second: 'second', + ago: 'ago', + later: 'later', + justNow: 'just now', + soon: 'soon', + template: '{num} {unit} {suffix}' + } + } + const text = localeText[locale] || localeText.zh + let date = getDate(time) + let ms = date.getTime() - Date.now() + let absMs = Math.abs(ms) + if (absMs < threshold[0]) { + return ms < 0 ? text.justNow : text.soon + } + if (absMs >= threshold[1]) { + return formatDate(date, format) + } + let num + let unit + let suffix = text.later + if (ms < 0) { + suffix = text.ago + ms = -ms + } + const seconds = Math.floor((ms) / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + const months = Math.floor(days / 30) + const years = Math.floor(months / 12) + switch (true) { + case years > 0: + num = years + unit = text.year + break + case months > 0: + num = months + unit = text.month + break + case days > 0: + num = days + unit = text.day + break + case hours > 0: + num = hours + unit = text.hour + break + case minutes > 0: + num = minutes + unit = text.minute + break + default: + num = seconds + unit = text.second + break + } + + if (locale === 'en') { + if (num === 1) { + num = 'a' + } else { + unit += 's' + } + } + + return text.template.replace(/{\s*num\s*}/g, num + '').replace(/{\s*unit\s*}/g, unit).replace(/{\s*suffix\s*}/g, + suffix) +} diff --git a/im-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue b/im-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue new file mode 100644 index 0000000..c5ed030 --- /dev/null +++ b/im-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-dateformat/package.json b/im-uniapp/uni_modules/uni-dateformat/package.json new file mode 100644 index 0000000..786a670 --- /dev/null +++ b/im-uniapp/uni_modules/uni-dateformat/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-dateformat", + "displayName": "uni-dateformat 日期格式化", + "version": "1.0.0", + "description": "日期格式化组件,可以将日期格式化为1分钟前、刚刚等形式", + "keywords": [ + "uni-ui", + "uniui", + "日期格式化", + "时间格式化", + "格式化时间", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-dateformat/readme.md b/im-uniapp/uni_modules/uni-dateformat/readme.md new file mode 100644 index 0000000..37ddb6e --- /dev/null +++ b/im-uniapp/uni_modules/uni-dateformat/readme.md @@ -0,0 +1,11 @@ + + +### DateFormat 日期格式化 +> **组件名:uni-dateformat** +> 代码块: `uDateformat` + + +日期格式化组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-dateformat) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-datetime-picker/changelog.md b/im-uniapp/uni_modules/uni-datetime-picker/changelog.md new file mode 100644 index 0000000..1e82f46 --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/changelog.md @@ -0,0 +1,140 @@ +## 2.2.24(2023-06-02) +- 修复 部分情况修改时间,开始、结束时间显示异常的Bug [详情](https://ask.dcloud.net.cn/question/171146) +- 优化 当前月可以选择上月、下月的日期 +## 2.2.23(2023-05-02) +- 修复 部分情况修改时间,开始时间未更新 [详情](https://github.com/dcloudio/uni-ui/issues/737) +- 修复 部分平台及设备第一次点击无法显示弹框 +- 修复 ios 日期格式未补零显示及使用异常 [详情](https://ask.dcloud.net.cn/question/162979) +## 2.2.22(2023-03-30) +- 修复 日历 picker 修改年月后,自动选中当月1日 [详情](https://ask.dcloud.net.cn/question/165937) +- 修复 小程序端 低版本 ios NaN [详情](https://ask.dcloud.net.cn/question/162979) +## 2.2.21(2023-02-20) +- 修复 firefox 浏览器显示区域点击无法拉起日历弹框的Bug [详情](https://ask.dcloud.net.cn/question/163362) +## 2.2.20(2023-02-17) +- 优化 值为空依然选中当天问题 +- 优化 提供 default-value 属性支持配置选择器打开时默认显示的时间 +- 优化 非范围选择未选择日期时间,点击确认按钮选中当前日期时间 +- 优化 字节小程序日期时间范围选择,底部日期换行问题 +## 2.2.19(2023-02-09) +- 修复 2.2.18 引起范围选择配置 end 选择无效的Bug [详情](https://github.com/dcloudio/uni-ui/issues/686) +## 2.2.18(2023-02-08) +- 修复 移动端范围选择change事件触发异常的Bug [详情](https://github.com/dcloudio/uni-ui/issues/684) +- 优化 PC端输入日期格式错误时返回当前日期时间 +- 优化 PC端输入日期时间超出 start、end 限制的Bug +- 优化 移动端日期时间范围用法时间展示不完整问题 +## 2.2.17(2023-02-04) +- 修复 小程序端绑定 Date 类型报错的Bug [详情](https://github.com/dcloudio/uni-ui/issues/679) +- 修复 vue3 time-picker 无法显示绑定时分秒的Bug +## 2.2.16(2023-02-02) +- 修复 字节小程序报错的Bug +## 2.2.15(2023-02-02) +- 修复 某些情况切换月份错误的Bug +## 2.2.14(2023-01-30) +- 修复 某些情况切换月份错误的Bug [详情](https://ask.dcloud.net.cn/question/162033) +## 2.2.13(2023-01-10) +- 修复 多次加载组件造成内存占用的Bug +## 2.2.12(2022-12-01) +- 修复 vue3 下 i18n 国际化初始值不正确的Bug +## 2.2.11(2022-09-19) +- 修复 支付宝小程序样式错乱的Bug [详情](https://github.com/dcloudio/uni-app/issues/3861) +## 2.2.10(2022-09-19) +- 修复 反向选择日期范围,日期显示异常的Bug [详情](https://ask.dcloud.net.cn/question/153401?item_id=212892&rf=false) +## 2.2.9(2022-09-16) +- 可以使用 uni-scss 控制主题色 +## 2.2.8(2022-09-08) +- 修复 close事件无效的Bug +## 2.2.7(2022-09-05) +- 修复 移动端 maskClick 无效的Bug [详情](https://ask.dcloud.net.cn/question/140824) +## 2.2.6(2022-06-30) +- 优化 组件样式,调整了组件图标大小、高度、颜色等,与uni-ui风格保持一致 +## 2.2.5(2022-06-24) +- 修复 日历顶部年月及底部确认未国际化的Bug +## 2.2.4(2022-03-31) +- 修复 Vue3 下动态赋值,单选类型未响应的Bug +## 2.2.3(2022-03-28) +- 修复 Vue3 下动态赋值未响应的Bug +## 2.2.2(2021-12-10) +- 修复 clear-icon 属性在小程序平台不生效的Bug +## 2.2.1(2021-12-10) +- 修复 日期范围选在小程序平台,必须多点击一次才能取消选中状态的Bug +## 2.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源 [详情](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移 [https://uniapp.dcloud.io/component/uniui/uni-datetime-picker](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker) +## 2.1.5(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 2.1.4(2021-09-10) +- 修复 hide-second 在移动端的Bug +- 修复 单选赋默认值时,赋值日期未高亮的Bug +- 修复 赋默认值时,移动端未正确显示时间的Bug +## 2.1.3(2021-09-09) +- 新增 hide-second 属性,支持只使用时分,隐藏秒 +## 2.1.2(2021-09-03) +- 优化 取消选中时(范围选)直接开始下一次选择, 避免多点一次 +- 优化 移动端支持清除按钮,同时支持通过 ref 调用组件的 clear 方法 +- 优化 调整字号大小,美化日历界面 +- 修复 因国际化导致的 placeholder 失效的Bug +## 2.1.1(2021-08-24) +- 新增 支持国际化 +- 优化 范围选择器在 pc 端过宽的问题 +## 2.1.0(2021-08-09) +- 新增 适配 vue3 +## 2.0.19(2021-08-09) +- 新增 支持作为 uni-forms 子组件相关功能 +- 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的Bug +## 2.0.18(2021-08-05) +- 修复 type 属性动态赋值无效的Bug +- 修复 ‘确认’按钮被 tabbar 遮盖 bug +- 修复 组件未赋值时范围选左、右日历相同的Bug +## 2.0.17(2021-08-04) +- 修复 范围选未正确显示当前值的Bug +- 修复 h5 平台(移动端)报错 'cale' of undefined 的Bug +## 2.0.16(2021-07-21) +- 新增 return-type 属性支持返回 date 日期对象 +## 2.0.15(2021-07-14) +- 修复 单选日期类型,初始赋值后不在当前日历的Bug +- 新增 clearIcon 属性,显示框的清空按钮可配置显示隐藏(仅 pc 有效) +- 优化 移动端移除显示框的清空按钮,无实际用途 +## 2.0.14(2021-07-14) +- 修复 组件赋值为空,界面未更新的Bug +- 修复 start 和 end 不能动态赋值的Bug +- 修复 范围选类型,用户选择后再次选择右侧日历(结束日期)显示不正确的Bug +## 2.0.13(2021-07-08) +- 修复 范围选择不能动态赋值的Bug +## 2.0.12(2021-07-08) +- 修复 范围选择的初始时间在一个月内时,造成无法选择的bug +## 2.0.11(2021-07-08) +- 优化 弹出层在超出视窗边缘定位不准确的问题 +## 2.0.10(2021-07-08) +- 修复 范围起始点样式的背景色与今日样式的字体前景色融合,导致日期字体看不清的Bug +- 优化 弹出层在超出视窗边缘被遮盖的问题 +## 2.0.9(2021-07-07) +- 新增 maskClick 事件 +- 修复 特殊情况日历 rpx 布局错误的Bug,rpx -> px +- 修复 范围选择时清空返回值不合理的bug,['', ''] -> [] +## 2.0.8(2021-07-07) +- 新增 日期时间显示框支持插槽 +## 2.0.7(2021-07-01) +- 优化 添加 uni-icons 依赖 +## 2.0.6(2021-05-22) +- 修复 图标在小程序上不显示的Bug +- 优化 重命名引用组件,避免潜在组件命名冲突 +## 2.0.5(2021-05-20) +- 优化 代码目录扁平化 +## 2.0.4(2021-05-12) +- 新增 组件示例地址 +## 2.0.3(2021-05-10) +- 修复 ios 下不识别 '-' 日期格式的Bug +- 优化 pc 下弹出层添加边框和阴影 +## 2.0.2(2021-05-08) +- 修复 在 admin 中获取弹出层定位错误的bug +## 2.0.1(2021-05-08) +- 修复 type 属性向下兼容,默认值从 date 变更为 datetime +## 2.0.0(2021-04-30) +- 支持日历形式的日期+时间的范围选择 + > 注意:此版本不向后兼容,不再支持单独时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker) +## 1.0.6(2021-03-18) +- 新增 hide-second 属性,时间支持仅选择时、分 +- 修复 选择跟显示的日期不一样的Bug +- 修复 chang事件触发2次的Bug +- 修复 分、秒 end 范围错误的Bug +- 优化 更好的 nvue 适配 diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue new file mode 100644 index 0000000..dba9887 --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue new file mode 100644 index 0000000..29b1b03 --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue @@ -0,0 +1,929 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json new file mode 100644 index 0000000..024f22f --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "select date", + "uni-datetime-picker.selectTime": "select time", + "uni-datetime-picker.selectDateTime": "select date and time", + "uni-datetime-picker.startDate": "start date", + "uni-datetime-picker.endDate": "end date", + "uni-datetime-picker.startTime": "start time", + "uni-datetime-picker.endTime": "end time", + "uni-datetime-picker.ok": "ok", + "uni-datetime-picker.clear": "clear", + "uni-datetime-picker.cancel": "cancel", + "uni-datetime-picker.year": "-", + "uni-datetime-picker.month": "", + "uni-calender.MON": "MON", + "uni-calender.TUE": "TUE", + "uni-calender.WED": "WED", + "uni-calender.THU": "THU", + "uni-calender.FRI": "FRI", + "uni-calender.SAT": "SAT", + "uni-calender.SUN": "SUN", + "uni-calender.confirm": "confirm" +} diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json new file mode 100644 index 0000000..d2df5e7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "选择日期", + "uni-datetime-picker.selectTime": "选择时间", + "uni-datetime-picker.selectDateTime": "选择日期时间", + "uni-datetime-picker.startDate": "开始日期", + "uni-datetime-picker.endDate": "结束日期", + "uni-datetime-picker.startTime": "开始时间", + "uni-datetime-picker.endTime": "结束时间", + "uni-datetime-picker.ok": "确定", + "uni-datetime-picker.clear": "清除", + "uni-datetime-picker.cancel": "取消", + "uni-datetime-picker.year": "年", + "uni-datetime-picker.month": "月", + "uni-calender.SUN": "日", + "uni-calender.MON": "一", + "uni-calender.TUE": "二", + "uni-calender.WED": "三", + "uni-calender.THU": "四", + "uni-calender.FRI": "五", + "uni-calender.SAT": "六", + "uni-calender.confirm": "确认" +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json new file mode 100644 index 0000000..d23fa3c --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "選擇日期", + "uni-datetime-picker.selectTime": "選擇時間", + "uni-datetime-picker.selectDateTime": "選擇日期時間", + "uni-datetime-picker.startDate": "開始日期", + "uni-datetime-picker.endDate": "結束日期", + "uni-datetime-picker.startTime": "開始时间", + "uni-datetime-picker.endTime": "結束时间", + "uni-datetime-picker.ok": "確定", + "uni-datetime-picker.clear": "清除", + "uni-datetime-picker.cancel": "取消", + "uni-datetime-picker.year": "年", + "uni-datetime-picker.month": "月", + "uni-calender.SUN": "日", + "uni-calender.MON": "一", + "uni-calender.TUE": "二", + "uni-calender.WED": "三", + "uni-calender.THU": "四", + "uni-calender.FRI": "五", + "uni-calender.SAT": "六", + "uni-calender.confirm": "確認" +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue new file mode 100644 index 0000000..81a042a --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue @@ -0,0 +1,934 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue new file mode 100644 index 0000000..8769e83 --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue @@ -0,0 +1,1032 @@ + + + + diff --git a/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js new file mode 100644 index 0000000..fc98623 --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js @@ -0,0 +1,453 @@ +class Calendar { + constructor({ + selected, + startDate, + endDate, + range, + } = {}) { + // 当前日期 + this.date = this.getDateObj(new Date()) // 当前初入日期 + // 打点信息 + this.selected = selected || []; + // 起始时间 + this.startDate = startDate + // 终止时间 + this.endDate = endDate + // 是否范围选择 + this.range = range + // 多选状态 + this.cleanMultipleStatus() + // 每周日期 + this.weeks = {} + this.lastHover = false + } + /** + * 设置日期 + * @param {Object} date + */ + setDate(date) { + const selectDate = this.getDateObj(date) + this.getWeeks(selectDate.fullDate) + } + + /** + * 清理多选状态 + */ + cleanMultipleStatus() { + this.multipleStatus = { + before: '', + after: '', + data: [] + } + } + + setStartDate(startDate) { + this.startDate = startDate + } + + setEndDate(endDate) { + this.endDate = endDate + } + + getPreMonthObj(date){ + date = fixIosDateFormat(date) + date = new Date(date) + + const oldMonth = date.getMonth() + date.setMonth(oldMonth - 1) + const newMonth = date.getMonth() + if(oldMonth !== 0 && newMonth - oldMonth === 0){ + date.setMonth(newMonth - 1) + } + return this.getDateObj(date) + } + getNextMonthObj(date){ + date = fixIosDateFormat(date) + date = new Date(date) + + const oldMonth = date.getMonth() + date.setMonth(oldMonth + 1) + const newMonth = date.getMonth() + if(newMonth - oldMonth > 1){ + date.setMonth(newMonth - 1) + } + return this.getDateObj(date) + } + + /** + * 获取指定格式Date对象 + */ + getDateObj(date) { + date = fixIosDateFormat(date) + date = new Date(date) + + return { + fullDate: getDate(date), + year: date.getFullYear(), + month: addZero(date.getMonth() + 1), + date: addZero(date.getDate()), + day: date.getDay() + } + } + + /** + * 获取上一个月日期集合 + */ + getPreMonthDays(amount, dateObj) { + const result = [] + for (let i = amount - 1; i >= 0; i--) { + const month = dateObj.month > 1 ? dateObj.month -1 : 12 + const year = month === 12 ? dateObj.year - 1 : dateObj.year + const date = new Date(year,month,-i).getDate() + const fullDate = `${year}-${addZero(month)}-${addZero(date)}` + let multiples = this.multipleStatus.data + let multiplesStatus = -1 + if (this.range && multiples) { + multiplesStatus = multiples.findIndex((item) => { + return this.dateEqual(item, fullDate) + }) + } + const checked = multiplesStatus !== -1 + // 获取打点信息 + const extraInfo = this.selected && this.selected.find((item) => { + if (this.dateEqual(fullDate, item.date)) { + return item + } + }) + result.push({ + fullDate, + year, + month, + date, + multiple: this.range ? checked : false, + beforeMultiple: this.isLogicBefore(fullDate, this.multipleStatus.before, this.multipleStatus.after), + afterMultiple: this.isLogicAfter(fullDate, this.multipleStatus.before, this.multipleStatus.after), + disable: (this.startDate && !dateCompare(this.startDate, fullDate)) || (this.endDate && !dateCompare(fullDate,this.endDate)), + isToday: fullDate === this.date.fullDate, + userChecked: false, + extraInfo + }) + } + return result + } + /** + * 获取本月日期集合 + */ + getCurrentMonthDays(amount, dateObj) { + const result = [] + const fullDate = this.date.fullDate + for (let i = 1; i <= amount; i++) { + const currentDate = `${dateObj.year}-${dateObj.month}-${addZero(i)}` + const isToday = fullDate === currentDate + // 获取打点信息 + const extraInfo = this.selected && this.selected.find((item) => { + if (this.dateEqual(currentDate, item.date)) { + return item + } + }) + + // 日期禁用 + let disableBefore = true + let disableAfter = true + if (this.startDate) { + disableBefore = dateCompare(this.startDate, currentDate) + } + + if (this.endDate) { + disableAfter = dateCompare(currentDate, this.endDate) + } + + let multiples = this.multipleStatus.data + let multiplesStatus = -1 + if (this.range && multiples) { + multiplesStatus = multiples.findIndex((item) => { + return this.dateEqual(item, currentDate) + }) + } + const checked = multiplesStatus !== -1 + + result.push({ + fullDate: currentDate, + year: dateObj.year, + month: dateObj.month, + date: i, + multiple: this.range ? checked : false, + beforeMultiple: this.isLogicBefore(currentDate, this.multipleStatus.before, this.multipleStatus.after), + afterMultiple: this.isLogicAfter(currentDate, this.multipleStatus.before, this.multipleStatus.after), + disable: (this.startDate && !dateCompare(this.startDate, currentDate)) || (this.endDate && !dateCompare(currentDate,this.endDate)), + isToday, + userChecked: false, + extraInfo + }) + } + return result + } + /** + * 获取下一个月日期集合 + */ + _getNextMonthDays(amount, dateObj) { + const result = [] + const month = dateObj.month + 1 + for (let i = 1; i <= amount; i++) { + const month = dateObj.month === 12 ? 1 : dateObj.month*1 + 1 + const year = month === 1 ? dateObj.year + 1 : dateObj.year + const fullDate = `${year}-${addZero(month)}-${addZero(i)}` + let multiples = this.multipleStatus.data + let multiplesStatus = -1 + if (this.range && multiples) { + multiplesStatus = multiples.findIndex((item) => { + return this.dateEqual(item, fullDate) + }) + } + const checked = multiplesStatus !== -1 + // 获取打点信息 + const extraInfo = this.selected && this.selected.find((item) => { + if (this.dateEqual(fullDate, item.date)) { + return item + } + }) + result.push({ + fullDate, + year, + date: i, + month, + multiple: this.range ? checked : false, + beforeMultiple: this.isLogicBefore(fullDate, this.multipleStatus.before, this.multipleStatus.after), + afterMultiple: this.isLogicAfter(fullDate, this.multipleStatus.before, this.multipleStatus.after), + disable: (this.startDate && !dateCompare(this.startDate, fullDate)) || (this.endDate && !dateCompare(fullDate,this.endDate)), + isToday: fullDate === this.date.fullDate, + userChecked: false, + extraInfo + }) + } + return result + } + + /** + * 获取当前日期详情 + * @param {Object} date + */ + getInfo(date) { + if (!date) { + date = new Date() + } + + return this.calendar.find(item => item.fullDate === this.getDateObj(date).fullDate) + } + + /** + * 比较时间是否相等 + */ + dateEqual(before, after) { + before = new Date(fixIosDateFormat(before)) + after = new Date(fixIosDateFormat(after)) + return before.valueOf() === after.valueOf() + } + + /** + * 比较真实起始日期 + */ + + isLogicBefore(currentDate, before, after) { + let logicBefore = before + if (before && after) { + logicBefore = dateCompare(before, after) ? before : after + } + return this.dateEqual(logicBefore, currentDate) + } + + isLogicAfter(currentDate, before, after) { + let logicAfter = after + if (before && after) { + logicAfter = dateCompare(before, after) ? after : before + } + return this.dateEqual(logicAfter, currentDate) + } + + /** + * 获取日期范围内所有日期 + * @param {Object} begin + * @param {Object} end + */ + geDateAll(begin, end) { + var arr = [] + var ab = begin.split('-') + var ae = end.split('-') + var db = new Date() + db.setFullYear(ab[0], ab[1] - 1, ab[2]) + var de = new Date() + de.setFullYear(ae[0], ae[1] - 1, ae[2]) + var unixDb = db.getTime() - 24 * 60 * 60 * 1000 + var unixDe = de.getTime() - 24 * 60 * 60 * 1000 + for (var k = unixDb; k <= unixDe;) { + k = k + 24 * 60 * 60 * 1000 + arr.push(this.getDateObj(new Date(parseInt(k))).fullDate) + } + return arr + } + + /** + * 获取多选状态 + */ + setMultiple(fullDate) { + if (!this.range) return + + let { + before, + after + } = this.multipleStatus + if (before && after) { + if (!this.lastHover) { + this.lastHover = true + return + } + this.multipleStatus.before = fullDate + this.multipleStatus.after = '' + this.multipleStatus.data = [] + this.multipleStatus.fulldate = '' + this.lastHover = false + } else { + if (!before) { + this.multipleStatus.before = fullDate + this.lastHover = false + } else { + this.multipleStatus.after = fullDate + if (dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus + .after); + } else { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus + .before); + } + this.lastHover = true + } + } + this.getWeeks(fullDate) + } + + /** + * 鼠标 hover 更新多选状态 + */ + setHoverMultiple(fullDate) { + if (!this.range || this.lastHover) return + + const { before } = this.multipleStatus + + if (!before) { + this.multipleStatus.before = fullDate + } else { + this.multipleStatus.after = fullDate + if (dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after); + } else { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before); + } + } + this.getWeeks(fullDate) + } + + /** + * 更新默认值多选状态 + */ + setDefaultMultiple(before, after) { + this.multipleStatus.before = before + this.multipleStatus.after = after + if (before && after) { + if (dateCompare(before, after)) { + this.multipleStatus.data = this.geDateAll(before, after); + this.getWeeks(after) + } else { + this.multipleStatus.data = this.geDateAll(after, before); + this.getWeeks(before) + } + } + } + + /** + * 获取每周数据 + * @param {Object} dateData + */ + getWeeks(dateData) { + const { + year, + month, + } = this.getDateObj(dateData) + + const preMonthDayAmount = new Date(year, month - 1, 1).getDay() + const preMonthDays = this.getPreMonthDays(preMonthDayAmount, this.getDateObj(dateData)) + + const currentMonthDayAmount = new Date(year, month, 0).getDate() + const currentMonthDays = this.getCurrentMonthDays(currentMonthDayAmount, this.getDateObj(dateData)) + + const nextMonthDayAmount = 42 - preMonthDayAmount - currentMonthDayAmount + const nextMonthDays = this._getNextMonthDays(nextMonthDayAmount, this.getDateObj(dateData)) + + const calendarDays = [...preMonthDays, ...currentMonthDays, ...nextMonthDays] + + const weeks = new Array(6) + for (let i = 0; i < calendarDays.length; i++) { + const index = Math.floor(i / 7) + if(!weeks[index]){ + weeks[index] = new Array(7) + } + weeks[index][i % 7] = calendarDays[i] + } + + this.calendar = calendarDays + this.weeks = weeks + } +} + +function getDateTime(date, hideSecond){ + return `${getDate(date)} ${getTime(date, hideSecond)}` +} + +function getDate(date) { + date = fixIosDateFormat(date) + date = new Date(date) + const year = date.getFullYear() + const month = date.getMonth()+1 + const day = date.getDate() + return `${year}-${addZero(month)}-${addZero(day)}` +} + +function getTime(date, hideSecond){ + date = fixIosDateFormat(date) + date = new Date(date) + const hour = date.getHours() + const minute = date.getMinutes() + const second = date.getSeconds() + return hideSecond ? `${addZero(hour)}:${addZero(minute)}` : `${addZero(hour)}:${addZero(minute)}:${addZero(second)}` +} + +function addZero(num) { + if(num < 10){ + num = `0${num}` + } + return num +} + +function getDefaultSecond(hideSecond) { + return hideSecond ? '00:00' : '00:00:00' +} + +function dateCompare(startDate, endDate) { + startDate = new Date(fixIosDateFormat(startDate)) + endDate = new Date(fixIosDateFormat(endDate)) + return startDate <= endDate +} + +function checkDate(date){ + const dateReg = /((19|20)\d{2})(-|\/)\d{1,2}(-|\/)\d{1,2}/g + return date.match(dateReg) +} + +const dateTimeReg = /^\d{4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])( [0-5]?[0-9]:[0-5]?[0-9]:[0-5]?[0-9])?$/ +function fixIosDateFormat(value) { + if (typeof value === 'string' && dateTimeReg.test(value)) { + value = value.replace(/-/g, '/') + } + return value +} + +export {Calendar, getDateTime, getDate, getTime, addZero, getDefaultSecond, dateCompare, checkDate, fixIosDateFormat} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-datetime-picker/package.json b/im-uniapp/uni_modules/uni-datetime-picker/package.json new file mode 100644 index 0000000..cabb668 --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-datetime-picker", + "displayName": "uni-datetime-picker 日期选择器", + "version": "2.2.24", + "description": "uni-datetime-picker 日期时间选择器,支持日历,支持范围选择", + "keywords": [ + "uni-datetime-picker", + "uni-ui", + "uniui", + "日期时间选择器", + "日期时间" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-datetime-picker/readme.md b/im-uniapp/uni_modules/uni-datetime-picker/readme.md new file mode 100644 index 0000000..162fbef --- /dev/null +++ b/im-uniapp/uni_modules/uni-datetime-picker/readme.md @@ -0,0 +1,21 @@ + + +> `重要通知:组件升级更新 2.0.0 后,支持日期+时间范围选择,组件 ui 将使用日历选择日期,ui 变化较大,同时支持 PC 和 移动端。此版本不向后兼容,不再支持单独的时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker)。若仍需使用旧版本,可在插件市场下载*非uni_modules版本*,旧版本将不再维护` + +## DatetimePicker 时间选择器 + +> **组件名:uni-datetime-picker** +> 代码块: `uDatetimePicker` + + +该组件的优势是,支持**时间戳**输入和输出(起始时间、终止时间也支持时间戳),可**同时选择**日期和时间。 + +若只是需要单独选择日期和时间,不需要时间戳输入和输出,可使用原生的 picker 组件。 + +**_点击 picker 默认值规则:_** + +- 若设置初始值 value, 会显示在 picker 显示框中 +- 若无初始值 value,则初始值 value 为当前本地时间 Date.now(), 但不会显示在 picker 显示框中 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-drawer/changelog.md b/im-uniapp/uni_modules/uni-drawer/changelog.md new file mode 100644 index 0000000..6d2488c --- /dev/null +++ b/im-uniapp/uni_modules/uni-drawer/changelog.md @@ -0,0 +1,13 @@ +## 1.2.1(2021-11-22) +- 修复 vue3中个别scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-drawer](https://uniapp.dcloud.io/component/uniui/uni-drawer) +## 1.1.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-drawer/components/uni-drawer/keypress.js b/im-uniapp/uni_modules/uni-drawer/components/uni-drawer/keypress.js new file mode 100644 index 0000000..62dda46 --- /dev/null +++ b/im-uniapp/uni_modules/uni-drawer/components/uni-drawer/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + // this.$once('hook:beforeDestroy', () => { + // document.removeEventListener('keyup', listener) + // }) + }, + render: () => {} +} +// #endif diff --git a/im-uniapp/uni_modules/uni-drawer/components/uni-drawer/uni-drawer.vue b/im-uniapp/uni_modules/uni-drawer/components/uni-drawer/uni-drawer.vue new file mode 100644 index 0000000..2471521 --- /dev/null +++ b/im-uniapp/uni_modules/uni-drawer/components/uni-drawer/uni-drawer.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-drawer/package.json b/im-uniapp/uni_modules/uni-drawer/package.json new file mode 100644 index 0000000..dd056e4 --- /dev/null +++ b/im-uniapp/uni_modules/uni-drawer/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-drawer", + "displayName": "uni-drawer 抽屉", + "version": "1.2.1", + "description": "抽屉式导航,用于展示侧滑菜单,侧滑导航。", + "keywords": [ + "uni-ui", + "uniui", + "drawer", + "抽屉", + "侧滑导航" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-drawer/readme.md b/im-uniapp/uni_modules/uni-drawer/readme.md new file mode 100644 index 0000000..dcf6e6b --- /dev/null +++ b/im-uniapp/uni_modules/uni-drawer/readme.md @@ -0,0 +1,10 @@ + + +## Drawer 抽屉 +> **组件名:uni-drawer** +> 代码块: `uDrawer` + +抽屉侧滑菜单。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-drawer) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-easyinput/changelog.md b/im-uniapp/uni_modules/uni-easyinput/changelog.md new file mode 100644 index 0000000..765401a --- /dev/null +++ b/im-uniapp/uni_modules/uni-easyinput/changelog.md @@ -0,0 +1,97 @@ +## 1.1.9(2023-04-11) +- 修复 vue3 下 keyboardheightchange 事件报错的bug +## 1.1.8(2023-03-29) +- 优化 trim 属性默认值 +## 1.1.7(2023-03-29) +- 新增 cursor-spacing 属性 +## 1.1.6(2023-01-28) +- 新增 keyboardheightchange 事件,可监听键盘高度变化 +## 1.1.5(2022-11-29) +- 优化 主题样式 +## 1.1.4(2022-10-27) +- 修复 props 中背景颜色无默认值的bug +## 1.1.0(2022-06-30) + +- 新增 在 uni-forms 1.4.0 中使用可以在 blur 时校验内容 +- 新增 clear 事件,点击右侧叉号图标触发 +- 新增 change 事件 ,仅在输入框失去焦点或用户按下回车时触发 +- 优化 组件样式,组件获取焦点时高亮显示,图标颜色调整等 + +## 1.0.5(2022-06-07) + +- 优化 clearable 显示策略 + +## 1.0.4(2022-06-07) + +- 优化 clearable 显示策略 + +## 1.0.3(2022-05-20) + +- 修复 关闭图标某些情况下无法取消的 bug + +## 1.0.2(2022-04-12) + +- 修复 默认值不生效的 bug + +## 1.0.1(2022-04-02) + +- 修复 value 不能为 0 的 bug + +## 1.0.0(2021-11-19) + +- 优化 组件 UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-easyinput](https://uniapp.dcloud.io/component/uniui/uni-easyinput) + +## 0.1.4(2021-08-20) + +- 修复 在 uni-forms 的动态表单中默认值校验不通过的 bug + +## 0.1.3(2021-08-11) + +- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 + +## 0.1.2(2021-07-30) + +- 优化 vue3 下事件警告的问题 + +## 0.1.1 + +- 优化 errorMessage 属性支持 Boolean 类型 + +## 0.1.0(2021-07-13) + +- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) + +## 0.0.16(2021-06-29) + +- 修复 confirmType 属性(仅 type="text" 生效)导致多行文本框无法换行的 bug + +## 0.0.15(2021-06-21) + +- 修复 passwordIcon 属性拼写错误的 bug + +## 0.0.14(2021-06-18) + +- 新增 passwordIcon 属性,当 type=password 时是否显示小眼睛图标 +- 修复 confirmType 属性不生效的问题 + +## 0.0.13(2021-06-04) + +- 修复 disabled 状态可清出内容的 bug + +## 0.0.12(2021-05-12) + +- 新增 组件示例地址 + +## 0.0.11(2021-05-07) + +- 修复 input-border 属性不生效的问题 + +## 0.0.10(2021-04-30) + +- 修复 ios 遮挡文字、显示一半的问题 + +## 0.0.9(2021-02-05) + +- 调整为 uni_modules 目录规范 +- 优化 兼容 nvue 页面 diff --git a/im-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/common.js b/im-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/common.js new file mode 100644 index 0000000..df9abe1 --- /dev/null +++ b/im-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/common.js @@ -0,0 +1,56 @@ +/** + * @desc 函数防抖 + * @param func 目标函数 + * @param wait 延迟执行毫秒数 + * @param immediate true - 立即执行, false - 延迟执行 + */ +export const debounce = function(func, wait = 1000, immediate = true) { + let timer; + console.log(1); + return function() { + console.log(123); + let context = this, + args = arguments; + if (timer) clearTimeout(timer); + if (immediate) { + let callNow = !timer; + timer = setTimeout(() => { + timer = null; + }, wait); + if (callNow) func.apply(context, args); + } else { + timer = setTimeout(() => { + func.apply(context, args); + }, wait) + } + } +} +/** + * @desc 函数节流 + * @param func 函数 + * @param wait 延迟执行毫秒数 + * @param type 1 使用表时间戳,在时间段开始的时候触发 2 使用表定时器,在时间段结束的时候触发 + */ +export const throttle = (func, wait = 1000, type = 1) => { + let previous = 0; + let timeout; + return function() { + let context = this; + let args = arguments; + if (type === 1) { + let now = Date.now(); + + if (now - previous > wait) { + func.apply(context, args); + previous = now; + } + } else if (type === 2) { + if (!timeout) { + timeout = setTimeout(() => { + timeout = null; + func.apply(context, args) + }, wait) + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue b/im-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue new file mode 100644 index 0000000..2c7993a --- /dev/null +++ b/im-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue @@ -0,0 +1,657 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-easyinput/package.json b/im-uniapp/uni_modules/uni-easyinput/package.json new file mode 100644 index 0000000..bd128e1 --- /dev/null +++ b/im-uniapp/uni_modules/uni-easyinput/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-easyinput", + "displayName": "uni-easyinput 增强输入框", + "version": "1.1.9", + "description": "Easyinput 组件是对原生input组件的增强", + "keywords": [ + "uni-ui", + "uniui", + "input", + "uni-easyinput", + "输入框" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-easyinput/readme.md b/im-uniapp/uni_modules/uni-easyinput/readme.md new file mode 100644 index 0000000..f1faf8f --- /dev/null +++ b/im-uniapp/uni_modules/uni-easyinput/readme.md @@ -0,0 +1,11 @@ + + +### Easyinput 增强输入框 +> **组件名:uni-easyinput** +> 代码块: `uEasyinput` + + +easyinput 组件是对原生input组件的增强 ,是专门为配合表单组件[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)而设计的,easyinput 内置了边框,图标等,同时包含 input 所有功能 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-easyinput) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-fab/changelog.md b/im-uniapp/uni_modules/uni-fab/changelog.md new file mode 100644 index 0000000..9bd4729 --- /dev/null +++ b/im-uniapp/uni_modules/uni-fab/changelog.md @@ -0,0 +1,23 @@ +## 1.2.5(2023-03-29) +- 新增 pattern.icon 属性,可自定义图标 +## 1.2.4(2022-09-07) +小程序端由于 style 使用了对象导致报错,[详情](https://ask.dcloud.net.cn/question/152790?item_id=211778&rf=false) +## 1.2.3(2022-09-05) +- 修复 nvue 环境下,具有 tabBar 时,fab 组件下部位置无法正常获取 --window-bottom 的bug,详见:[https://ask.dcloud.net.cn/question/110638?notification_id=826310](https://ask.dcloud.net.cn/question/110638?notification_id=826310) +## 1.2.2(2021-12-29) +- 更新 组件依赖 +## 1.2.1(2021-11-19) +- 修复 阴影颜色不正确的bug +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-fab](https://uniapp.dcloud.io/component/uniui/uni-fab) +## 1.1.1(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-02-05) +- 调整为uni_modules目录规范 +- 优化 按钮背景色调整 +- 优化 兼容pc端 diff --git a/im-uniapp/uni_modules/uni-fab/components/uni-fab/uni-fab.vue b/im-uniapp/uni_modules/uni-fab/components/uni-fab/uni-fab.vue new file mode 100644 index 0000000..dfa65c1 --- /dev/null +++ b/im-uniapp/uni_modules/uni-fab/components/uni-fab/uni-fab.vue @@ -0,0 +1,491 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-fab/package.json b/im-uniapp/uni_modules/uni-fab/package.json new file mode 100644 index 0000000..18c0810 --- /dev/null +++ b/im-uniapp/uni_modules/uni-fab/package.json @@ -0,0 +1,84 @@ +{ + "id": "uni-fab", + "displayName": "uni-fab 悬浮按钮", + "version": "1.2.5", + "description": "悬浮按钮 fab button ,点击可展开一个图标按钮菜单。", + "keywords": [ + "uni-ui", + "uniui", + "按钮", + "悬浮按钮", + "fab" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-icons"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-fab/readme.md b/im-uniapp/uni_modules/uni-fab/readme.md new file mode 100644 index 0000000..9a444e8 --- /dev/null +++ b/im-uniapp/uni_modules/uni-fab/readme.md @@ -0,0 +1,9 @@ +## Fab 悬浮按钮 +> **组件名:uni-fab** +> 代码块: `uFab` + + +点击可展开一个图形按钮菜单 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-fab) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-fav/changelog.md b/im-uniapp/uni_modules/uni-fav/changelog.md new file mode 100644 index 0000000..d8a08d4 --- /dev/null +++ b/im-uniapp/uni_modules/uni-fav/changelog.md @@ -0,0 +1,19 @@ +## 1.2.1(2022-05-30) +- 新增 stat 属性 ,是否开启uni统计功能 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-fav](https://uniapp.dcloud.io/component/uniui/uni-fav) +## 1.1.1(2021-08-24) +- 新增 支持国际化 +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.6(2021-05-12) +- 新增 组件示例地址 +## 1.0.5(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.4(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.0.3(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.0.2(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/en.json b/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/en.json new file mode 100644 index 0000000..9a0759e --- /dev/null +++ b/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/en.json @@ -0,0 +1,4 @@ +{ + "uni-fav.collect": "collect", + "uni-fav.collected": "collected" +} diff --git a/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/index.js b/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hans.json new file mode 100644 index 0000000..67c89bf --- /dev/null +++ b/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hans.json @@ -0,0 +1,4 @@ +{ + "uni-fav.collect": "收藏", + "uni-fav.collected": "已收藏" +} diff --git a/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hant.json new file mode 100644 index 0000000..67c89bf --- /dev/null +++ b/im-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hant.json @@ -0,0 +1,4 @@ +{ + "uni-fav.collect": "收藏", + "uni-fav.collected": "已收藏" +} diff --git a/im-uniapp/uni_modules/uni-fav/components/uni-fav/uni-fav.vue b/im-uniapp/uni_modules/uni-fav/components/uni-fav/uni-fav.vue new file mode 100644 index 0000000..d2c58df --- /dev/null +++ b/im-uniapp/uni_modules/uni-fav/components/uni-fav/uni-fav.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-fav/package.json b/im-uniapp/uni_modules/uni-fav/package.json new file mode 100644 index 0000000..cc14697 --- /dev/null +++ b/im-uniapp/uni_modules/uni-fav/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-fav", + "displayName": "uni-fav 收藏按钮", + "version": "1.2.1", + "description": " Fav 收藏组件,可自定义颜色、大小。", + "keywords": [ + "fav", + "uni-ui", + "uniui", + "收藏" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-fav/readme.md b/im-uniapp/uni_modules/uni-fav/readme.md new file mode 100644 index 0000000..4de125d --- /dev/null +++ b/im-uniapp/uni_modules/uni-fav/readme.md @@ -0,0 +1,10 @@ + + +## Fav 收藏按钮 +> **组件名:uni-fav** +> 代码块: `uFav` + +用于收藏功能,可点击切换选中、不选中的状态。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-fav) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-file-picker/changelog.md b/im-uniapp/uni_modules/uni-file-picker/changelog.md new file mode 100644 index 0000000..b8230e9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-file-picker/changelog.md @@ -0,0 +1,67 @@ +## 1.0.4(2023-03-29) +- 修复 手动上传删除一个文件后不能再上传的bug +## 1.0.3(2022-12-19) +- 新增 sourceType 属性, 可以自定义图片和视频选择的来源 +## 1.0.2(2022-07-04) +- 修复 在uni-forms下样式不生效的bug +## 1.0.1(2021-11-23) +- 修复 参数为对象的情况下,url在某些情况显示错误的bug +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-file-picker](https://uniapp.dcloud.io/component/uniui/uni-file-picker) +## 0.2.16(2021-11-08) +- 修复 传入空对象 ,显示错误的Bug +## 0.2.15(2021-08-30) +- 修复 return-type="object" 时且存在v-model时,无法删除文件的Bug +## 0.2.14(2021-08-23) +- 新增 参数中返回 fileID 字段 +## 0.2.13(2021-08-23) +- 修复 腾讯云传入fileID 不能回显的bug +- 修复 选择图片后,不能放大的问题 +## 0.2.12(2021-08-17) +- 修复 由于 0.2.11 版本引起的不能回显图片的Bug +## 0.2.11(2021-08-16) +- 新增 clearFiles(index) 方法,可以手动删除指定文件 +- 修复 v-model 值设为 null 报错的Bug +## 0.2.10(2021-08-13) +- 修复 return-type="object" 时,无法删除文件的Bug +## 0.2.9(2021-08-03) +- 修复 auto-upload 属性失效的Bug +## 0.2.8(2021-07-31) +- 修复 fileExtname属性不指定值报错的Bug +## 0.2.7(2021-07-31) +- 修复 在某种场景下图片不回显的Bug +## 0.2.6(2021-07-30) +- 修复 return-type为object下,返回值不正确的Bug +## 0.2.5(2021-07-30) +- 修复(重要) H5 平台下如果和uni-forms组件一同使用导致页面卡死的问题 +## 0.2.3(2021-07-28) +- 优化 调整示例代码 +## 0.2.2(2021-07-27) +- 修复 vue3 下赋值错误的Bug +- 优化 h5平台下上传文件导致页面卡死的问题 +## 0.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.1.1(2021-07-02) +- 修复 sourceType 缺少默认值导致 ios 无法选择文件 +## 0.1.0(2021-06-30) +- 优化 解耦与uniCloud的强绑定关系 ,如不绑定服务空间,默认autoUpload为false且不可更改 +## 0.0.11(2021-06-30) +- 修复 由 0.0.10 版本引发的 returnType 属性失效的问题 +## 0.0.10(2021-06-29) +- 优化 文件上传后进度条消失时机 +## 0.0.9(2021-06-29) +- 修复 在uni-forms 中,删除文件 ,获取的值不对的Bug +## 0.0.8(2021-06-15) +- 修复 删除文件时无法触发 v-model 的Bug +## 0.0.7(2021-05-12) +- 新增 组件示例地址 +## 0.0.6(2021-04-09) +- 修复 选择的文件非 file-extname 字段指定的扩展名报错的Bug +## 0.0.5(2021-04-09) +- 优化 更新组件示例 +## 0.0.4(2021-04-09) +- 优化 file-extname 字段支持字符串写法,多个扩展名需要用逗号分隔 +## 0.0.3(2021-02-05) +- 调整为uni_modules目录规范 +- 修复 微信小程序不指定 fileExtname 属性选择失败的Bug diff --git a/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js new file mode 100644 index 0000000..aff0864 --- /dev/null +++ b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js @@ -0,0 +1,224 @@ +'use strict'; + +const ERR_MSG_OK = 'chooseAndUploadFile:ok'; +const ERR_MSG_FAIL = 'chooseAndUploadFile:fail'; + +function chooseImage(opts) { + const { + count, + sizeType = ['original', 'compressed'], + sourceType, + extension + } = opts + return new Promise((resolve, reject) => { + uni.chooseImage({ + count, + sizeType, + sourceType, + extension, + success(res) { + resolve(normalizeChooseAndUploadFileRes(res, 'image')); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function chooseVideo(opts) { + const { + camera, + compressed, + maxDuration, + sourceType, + extension + } = opts; + return new Promise((resolve, reject) => { + uni.chooseVideo({ + camera, + compressed, + maxDuration, + sourceType, + extension, + success(res) { + const { + tempFilePath, + duration, + size, + height, + width + } = res; + resolve(normalizeChooseAndUploadFileRes({ + errMsg: 'chooseVideo:ok', + tempFilePaths: [tempFilePath], + tempFiles: [ + { + name: (res.tempFile && res.tempFile.name) || '', + path: tempFilePath, + size, + type: (res.tempFile && res.tempFile.type) || '', + width, + height, + duration, + fileType: 'video', + cloudPath: '', + }, ], + }, 'video')); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function chooseAll(opts) { + const { + count, + extension + } = opts; + return new Promise((resolve, reject) => { + let chooseFile = uni.chooseFile; + if (typeof wx !== 'undefined' && + typeof wx.chooseMessageFile === 'function') { + chooseFile = wx.chooseMessageFile; + } + if (typeof chooseFile !== 'function') { + return reject({ + errMsg: ERR_MSG_FAIL + ' 请指定 type 类型,该平台仅支持选择 image 或 video。', + }); + } + chooseFile({ + type: 'all', + count, + extension, + success(res) { + resolve(normalizeChooseAndUploadFileRes(res)); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function normalizeChooseAndUploadFileRes(res, fileType) { + res.tempFiles.forEach((item, index) => { + if (!item.name) { + item.name = item.path.substring(item.path.lastIndexOf('/') + 1); + } + if (fileType) { + item.fileType = fileType; + } + item.cloudPath = + Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.')); + }); + if (!res.tempFilePaths) { + res.tempFilePaths = res.tempFiles.map((file) => file.path); + } + return res; +} + +function uploadCloudFiles(files, max = 5, onUploadProgress) { + files = JSON.parse(JSON.stringify(files)) + const len = files.length + let count = 0 + let self = this + return new Promise(resolve => { + while (count < max) { + next() + } + + function next() { + let cur = count++ + if (cur >= len) { + !files.find(item => !item.url && !item.errMsg) && resolve(files) + return + } + const fileItem = files[cur] + const index = self.files.findIndex(v => v.uuid === fileItem.uuid) + fileItem.url = '' + delete fileItem.errMsg + + uniCloud + .uploadFile({ + filePath: fileItem.path, + cloudPath: fileItem.cloudPath, + fileType: fileItem.fileType, + onUploadProgress: res => { + res.index = index + onUploadProgress && onUploadProgress(res) + } + }) + .then(res => { + fileItem.url = res.fileID + fileItem.index = index + if (cur < len) { + next() + } + }) + .catch(res => { + fileItem.errMsg = res.errMsg || res.message + fileItem.index = index + if (cur < len) { + next() + } + }) + } + }) +} + + + + + +function uploadFiles(choosePromise, { + onChooseFile, + onUploadProgress +}) { + return choosePromise + .then((res) => { + if (onChooseFile) { + const customChooseRes = onChooseFile(res); + if (typeof customChooseRes !== 'undefined') { + return Promise.resolve(customChooseRes).then((chooseRes) => typeof chooseRes === 'undefined' ? + res : chooseRes); + } + } + return res; + }) + .then((res) => { + if (res === false) { + return { + errMsg: ERR_MSG_OK, + tempFilePaths: [], + tempFiles: [], + }; + } + return res + }) +} + +function chooseAndUploadFile(opts = { + type: 'all' +}) { + if (opts.type === 'image') { + return uploadFiles(chooseImage(opts), opts); + } + else if (opts.type === 'video') { + return uploadFiles(chooseVideo(opts), opts); + } + return uploadFiles(chooseAll(opts), opts); +} + +export { + chooseAndUploadFile, + uploadCloudFiles +}; diff --git a/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue new file mode 100644 index 0000000..370441d --- /dev/null +++ b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue @@ -0,0 +1,667 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue new file mode 100644 index 0000000..625d92e --- /dev/null +++ b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue @@ -0,0 +1,325 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue new file mode 100644 index 0000000..2a29bc2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/utils.js b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/utils.js new file mode 100644 index 0000000..60aaa3e --- /dev/null +++ b/im-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/utils.js @@ -0,0 +1,109 @@ +/** + * 获取文件名和后缀 + * @param {String} name + */ +export const get_file_ext = (name) => { + const last_len = name.lastIndexOf('.') + const len = name.length + return { + name: name.substring(0, last_len), + ext: name.substring(last_len + 1, len) + } +} + +/** + * 获取扩展名 + * @param {Array} fileExtname + */ +export const get_extname = (fileExtname) => { + if (!Array.isArray(fileExtname)) { + let extname = fileExtname.replace(/(\[|\])/g, '') + return extname.split(',') + } else { + return fileExtname + } + return [] +} + +/** + * 获取文件和检测是否可选 + */ +export const get_files_and_is_max = (res, _extname) => { + let filePaths = [] + let files = [] + if(!_extname || _extname.length === 0){ + return { + filePaths, + files + } + } + res.tempFiles.forEach(v => { + let fileFullName = get_file_ext(v.name) + const extname = fileFullName.ext.toLowerCase() + if (_extname.indexOf(extname) !== -1) { + files.push(v) + filePaths.push(v.path) + } + }) + if (files.length !== res.tempFiles.length) { + uni.showToast({ + title: `当前选择了${res.tempFiles.length}个文件 ,${res.tempFiles.length - files.length} 个文件格式不正确`, + icon: 'none', + duration: 5000 + }) + } + + return { + filePaths, + files + } +} + + +/** + * 获取图片信息 + * @param {Object} filepath + */ +export const get_file_info = (filepath) => { + return new Promise((resolve, reject) => { + uni.getImageInfo({ + src: filepath, + success(res) { + resolve(res) + }, + fail(err) { + reject(err) + } + }) + }) +} +/** + * 获取封装数据 + */ +export const get_file_data = async (files, type = 'image') => { + // 最终需要上传数据库的数据 + let fileFullName = get_file_ext(files.name) + const extname = fileFullName.ext.toLowerCase() + let filedata = { + name: files.name, + uuid: files.uuid, + extname: extname || '', + cloudPath: files.cloudPath, + fileType: files.fileType, + url: files.path || files.path, + size: files.size, //单位是字节 + image: {}, + path: files.path, + video: {} + } + if (type === 'image') { + const imageinfo = await get_file_info(files.path) + delete filedata.video + filedata.image.width = imageinfo.width + filedata.image.height = imageinfo.height + filedata.image.location = imageinfo.path + } else { + delete filedata.image + } + return filedata +} diff --git a/im-uniapp/uni_modules/uni-file-picker/package.json b/im-uniapp/uni_modules/uni-file-picker/package.json new file mode 100644 index 0000000..35c926c --- /dev/null +++ b/im-uniapp/uni_modules/uni-file-picker/package.json @@ -0,0 +1,83 @@ +{ + "id": "uni-file-picker", + "displayName": "uni-file-picker 文件选择上传", + "version": "1.0.4", + "description": "文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间", + "keywords": [ + "uni-ui", + "uniui", + "图片上传", + "文件上传" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-file-picker/readme.md b/im-uniapp/uni_modules/uni-file-picker/readme.md new file mode 100644 index 0000000..c8399a5 --- /dev/null +++ b/im-uniapp/uni_modules/uni-file-picker/readme.md @@ -0,0 +1,11 @@ + +## FilePicker 文件选择上传 + +> **组件名:uni-file-picker** +> 代码块: `uFilePicker` + + +文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-file-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-forms/changelog.md b/im-uniapp/uni_modules/uni-forms/changelog.md new file mode 100644 index 0000000..8218df5 --- /dev/null +++ b/im-uniapp/uni_modules/uni-forms/changelog.md @@ -0,0 +1,92 @@ +## 1.4.9(2023-02-10) +- 修复 required 参数无法动态绑定 +## 1.4.8(2022-08-23) +- 优化 根据 rules 自动添加 required 的问题 +## 1.4.7(2022-08-22) +- 修复 item 未设置 require 属性,rules 设置 require 后,星号也显示的 bug,详见:[https://ask.dcloud.net.cn/question/151540](https://ask.dcloud.net.cn/question/151540) +## 1.4.6(2022-07-13) +- 修复 model 需要校验的值没有声明对应字段时,导致第一次不触发校验的bug +## 1.4.5(2022-07-05) +- 新增 更多表单示例 +- 优化 子表单组件过期提示的问题 +- 优化 子表单组件uni-datetime-picker、uni-data-select、uni-data-picker的显示样式 +## 1.4.4(2022-07-04) +- 更新 删除组件日志 +## 1.4.3(2022-07-04) +- 修复 由 1.4.0 引发的 label 插槽不生效的bug +## 1.4.2(2022-07-04) +- 修复 子组件找不到 setValue 报错的bug +## 1.4.1(2022-07-04) +- 修复 uni-data-picker 在 uni-forms-item 中报错的bug +- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug +## 1.4.0(2022-06-30) +- 【重要】组件逻辑重构,部分用法用旧版本不兼容,请注意兼容问题 +- 【重要】组件使用 Provide/Inject 方式注入依赖,提供了自定义表单组件调用 uni-forms 校验表单的能力 +- 新增 model 属性,等同于原 value/modelValue 属性,旧属性即将废弃 +- 新增 validateTrigger 属性的 blur 值,仅 uni-easyinput 生效 +- 新增 onFieldChange 方法,可以对子表单进行校验,可替代binddata方法 +- 新增 子表单的 setRules 方法,配合自定义校验函数使用 +- 新增 uni-forms-item 的 setRules 方法,配置动态表单使用可动态更新校验规则 +- 优化 动态表单校验方式,废弃拼接name的方式 +## 1.3.3(2022-06-22) +- 修复 表单校验顺序无序问题 +## 1.3.2(2021-12-09) +- +## 1.3.1(2021-11-19) +- 修复 label 插槽不生效的bug +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-forms](https://uniapp.dcloud.io/component/uniui/uni-forms) +## 1.2.7(2021-08-13) +- 修复 没有添加校验规则的字段依然报错的Bug +## 1.2.6(2021-08-11) +- 修复 重置表单错误信息无法清除的问题 +## 1.2.5(2021-08-11) +- 优化 组件文档 +## 1.2.4(2021-08-11) +- 修复 表单验证只生效一次的问题 +## 1.2.3(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.2.2(2021-07-26) +- 修复 vue2 下条件编译导致destroyed生命周期失效的Bug +- 修复 1.2.1 引起的示例在小程序平台报错的Bug +## 1.2.1(2021-07-22) +- 修复 动态校验表单,默认值为空的情况下校验失效的Bug +- 修复 不指定name属性时,运行报错的Bug +- 优化 label默认宽度从65调整至70,使required为true且四字时不换行 +- 优化 组件示例,新增动态校验示例代码 +- 优化 组件文档,使用方式更清晰 +## 1.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.2(2021-06-25) +- 修复 pattern 属性在微信小程序平台无效的问题 +## 1.1.1(2021-06-22) +- 修复 validate-trigger属性为submit且err-show-type属性为toast时不能弹出的Bug +## 1.1.0(2021-06-22) +- 修复 只写setRules方法而导致校验不生效的Bug +- 修复 由上个办法引发的错误提示文字错位的Bug +## 1.0.48(2021-06-21) +- 修复 不设置 label 属性 ,无法设置label插槽的问题 +## 1.0.47(2021-06-21) +- 修复 不设置label属性,label-width属性不生效的bug +- 修复 setRules 方法与rules属性冲突的问题 +## 1.0.46(2021-06-04) +- 修复 动态删减数据导致报错的问题 +## 1.0.45(2021-06-04) +- 新增 modelValue 属性 ,value 即将废弃 +## 1.0.44(2021-06-02) +- 新增 uni-forms-item 可以设置单独的 rules +- 新增 validate 事件增加 keepitem 参数,可以选择那些字段不过滤 +- 优化 submit 事件重命名为 validate +## 1.0.43(2021-05-12) +- 新增 组件示例地址 +## 1.0.42(2021-04-30) +- 修复 自定义检验器失效的问题 +## 1.0.41(2021-03-05) +- 更新 校验器 +- 修复 表单规则设置类型为 number 的情况下,值为0校验失败的Bug +## 1.0.40(2021-03-04) +- 修复 动态显示uni-forms-item的情况下,submit 方法获取值错误的Bug +## 1.0.39(2021-02-05) +- 调整为uni_modules目录规范 +- 修复 校验器传入 int 等类型 ,返回String类型的Bug diff --git a/im-uniapp/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue b/im-uniapp/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue new file mode 100644 index 0000000..91fe351 --- /dev/null +++ b/im-uniapp/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue @@ -0,0 +1,627 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-forms/components/uni-forms/uni-forms.vue b/im-uniapp/uni_modules/uni-forms/components/uni-forms/uni-forms.vue new file mode 100644 index 0000000..ed2f6d9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-forms/components/uni-forms/uni-forms.vue @@ -0,0 +1,397 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-forms/components/uni-forms/utils.js b/im-uniapp/uni_modules/uni-forms/components/uni-forms/utils.js new file mode 100644 index 0000000..6da2421 --- /dev/null +++ b/im-uniapp/uni_modules/uni-forms/components/uni-forms/utils.js @@ -0,0 +1,293 @@ +/** + * 简单处理对象拷贝 + * @param {Obejct} 被拷贝对象 + * @@return {Object} 拷贝对象 + */ +export const deepCopy = (val) => { + return JSON.parse(JSON.stringify(val)) +} +/** + * 过滤数字类型 + * @param {String} format 数字类型 + * @@return {Boolean} 返回是否为数字类型 + */ +export const typeFilter = (format) => { + return format === 'int' || format === 'double' || format === 'number' || format === 'timestamp'; +} + +/** + * 把 value 转换成指定的类型,用于处理初始值,原因是初始值需要入库不能为 undefined + * @param {String} key 字段名 + * @param {any} value 字段值 + * @param {Object} rules 表单校验规则 + */ +export const getValue = (key, value, rules) => { + const isRuleNumType = rules.find(val => val.format && typeFilter(val.format)); + const isRuleBoolType = rules.find(val => (val.format && val.format === 'boolean') || val.format === 'bool'); + // 输入类型为 number + if (!!isRuleNumType) { + if (!value && value !== 0) { + value = null + } else { + value = isNumber(Number(value)) ? Number(value) : value + } + } + + // 输入类型为 boolean + if (!!isRuleBoolType) { + value = isBoolean(value) ? value : false + } + + return value; +} + +/** + * 获取表单数据 + * @param {String|Array} name 真实名称,需要使用 realName 获取 + * @param {Object} data 原始数据 + * @param {any} value 需要设置的值 + */ +export const setDataValue = (field, formdata, value) => { + formdata[field] = value + return value || '' +} + +/** + * 获取表单数据 + * @param {String|Array} field 真实名称,需要使用 realName 获取 + * @param {Object} data 原始数据 + */ +export const getDataValue = (field, data) => { + return objGet(data, field) +} + +/** + * 获取表单类型 + * @param {String|Array} field 真实名称,需要使用 realName 获取 + */ +export const getDataValueType = (field, data) => { + const value = getDataValue(field, data) + return { + type: type(value), + value + } +} + +/** + * 获取表单可用的真实name + * @param {String|Array} name 表单name + * @@return {String} 表单可用的真实name + */ +export const realName = (name, data = {}) => { + const base_name = _basePath(name) + if (typeof base_name === 'object' && Array.isArray(base_name) && base_name.length > 1) { + const realname = base_name.reduce((a, b) => a += `#${b}`, '_formdata_') + return realname + } + return base_name[0] || name +} + +/** + * 判断是否表单可用的真实name + * @param {String|Array} name 表单name + * @@return {String} 表单可用的真实name + */ +export const isRealName = (name) => { + const reg = /^_formdata_#*/ + return reg.test(name) +} + +/** + * 获取表单数据的原始格式 + * @@return {Object|Array} object 需要解析的数据 + */ +export const rawData = (object = {}, name) => { + let newData = JSON.parse(JSON.stringify(object)) + let formData = {} + for(let i in newData){ + let path = name2arr(i) + objSet(formData,path,newData[i]) + } + return formData +} + +/** + * 真实name还原为 array + * @param {*} name + */ +export const name2arr = (name) => { + let field = name.replace('_formdata_#', '') + field = field.split('#').map(v => (isNumber(v) ? Number(v) : v)) + return field +} + +/** + * 对象中设置值 + * @param {Object|Array} object 源数据 + * @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c'] + * @param {String} value 需要设置的值 + */ +export const objSet = (object, path, value) => { + if (typeof object !== 'object') return object; + _basePath(path).reduce((o, k, i, _) => { + if (i === _.length - 1) { + // 若遍历结束直接赋值 + o[k] = value + return null + } else if (k in o) { + // 若存在对应路径,则返回找到的对象,进行下一次遍历 + return o[k] + } else { + // 若不存在对应路径,则创建对应对象,若下一路径是数字,新对象赋值为空数组,否则赋值为空对象 + o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {} + return o[k] + } + }, object) + // 返回object + return object; +} + +// 处理 path, path有三种形式:'a[0].b.c'、'a.0.b.c' 和 ['a','0','b','c'],需要统一处理成数组,便于后续使用 +function _basePath(path) { + // 若是数组,则直接返回 + if (Array.isArray(path)) return path + // 若有 '[',']',则替换成将 '[' 替换成 '.',去掉 ']' + return path.replace(/\[/g, '.').replace(/\]/g, '').split('.') +} + +/** + * 从对象中获取值 + * @param {Object|Array} object 源数据 + * @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c'] + * @param {String} defaultVal 如果无法从调用链中获取值的默认值 + */ +export const objGet = (object, path, defaultVal = 'undefined') => { + // 先将path处理成统一格式 + let newPath = _basePath(path) + // 递归处理,返回最后结果 + let val = newPath.reduce((o, k) => { + return (o || {})[k] + }, object); + return !val || val !== undefined ? val : defaultVal +} + + +/** + * 是否为 number 类型 + * @param {any} num 需要判断的值 + * @return {Boolean} 是否为 number + */ +export const isNumber = (num) => { + return !isNaN(Number(num)) +} + +/** + * 是否为 boolean 类型 + * @param {any} bool 需要判断的值 + * @return {Boolean} 是否为 boolean + */ +export const isBoolean = (bool) => { + return (typeof bool === 'boolean') +} +/** + * 是否有必填字段 + * @param {Object} rules 规则 + * @return {Boolean} 是否有必填字段 + */ +export const isRequiredField = (rules) => { + let isNoField = false; + for (let i = 0; i < rules.length; i++) { + const ruleData = rules[i]; + if (ruleData.required) { + isNoField = true; + break; + } + } + return isNoField; +} + + +/** + * 获取数据类型 + * @param {Any} obj 需要获取数据类型的值 + */ +export const type = (obj) => { + var class2type = {}; + + // 生成class2type映射 + "Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) { + class2type["[object " + item + "]"] = item.toLowerCase(); + }) + if (obj == null) { + return obj + ""; + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[Object.prototype.toString.call(obj)] || "object" : + typeof obj; +} + +/** + * 判断两个值是否相等 + * @param {any} a 值 + * @param {any} b 值 + * @return {Boolean} 是否相等 + */ +export const isEqual = (a, b) => { + //如果a和b本来就全等 + if (a === b) { + //判断是否为0和-0 + return a !== 0 || 1 / a === 1 / b; + } + //判断是否为null和undefined + if (a == null || b == null) { + return a === b; + } + //接下来判断a和b的数据类型 + var classNameA = toString.call(a), + classNameB = toString.call(b); + //如果数据类型不相等,则返回false + if (classNameA !== classNameB) { + return false; + } + //如果数据类型相等,再根据不同数据类型分别判断 + switch (classNameA) { + case '[object RegExp]': + case '[object String]': + //进行字符串转换比较 + return '' + a === '' + b; + case '[object Number]': + //进行数字转换比较,判断是否为NaN + if (+a !== +a) { + return +b !== +b; + } + //判断是否为0或-0 + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + return +a === +b; + } + //如果是对象类型 + if (classNameA == '[object Object]') { + //获取a和b的属性长度 + var propsA = Object.getOwnPropertyNames(a), + propsB = Object.getOwnPropertyNames(b); + if (propsA.length != propsB.length) { + return false; + } + for (var i = 0; i < propsA.length; i++) { + var propName = propsA[i]; + //如果对应属性对应值不相等,则返回false + if (a[propName] !== b[propName]) { + return false; + } + } + return true; + } + //如果是数组类型 + if (classNameA == '[object Array]') { + if (a.toString() == b.toString()) { + return true; + } + return false; + } +} diff --git a/im-uniapp/uni_modules/uni-forms/components/uni-forms/validate.js b/im-uniapp/uni_modules/uni-forms/components/uni-forms/validate.js new file mode 100644 index 0000000..1834c6c --- /dev/null +++ b/im-uniapp/uni_modules/uni-forms/components/uni-forms/validate.js @@ -0,0 +1,486 @@ +var pattern = { + email: /^\S+?@\S+?\.\S+?$/, + idcard: /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, + url: new RegExp( + "^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$", + 'i') +}; + +const FORMAT_MAPPING = { + "int": 'integer', + "bool": 'boolean', + "double": 'number', + "long": 'number', + "password": 'string' + // "fileurls": 'array' +} + +function formatMessage(args, resources = '') { + var defaultMessage = ['label'] + defaultMessage.forEach((item) => { + if (args[item] === undefined) { + args[item] = '' + } + }) + + let str = resources + for (let key in args) { + let reg = new RegExp('{' + key + '}') + str = str.replace(reg, args[key]) + } + return str +} + +function isEmptyValue(value, type) { + if (value === undefined || value === null) { + return true; + } + + if (typeof value === 'string' && !value) { + return true; + } + + if (Array.isArray(value) && !value.length) { + return true; + } + + if (type === 'object' && !Object.keys(value).length) { + return true; + } + + return false; +} + +const types = { + integer(value) { + return types.number(value) && parseInt(value, 10) === value; + }, + string(value) { + return typeof value === 'string'; + }, + number(value) { + if (isNaN(value)) { + return false; + } + return typeof value === 'number'; + }, + "boolean": function(value) { + return typeof value === 'boolean'; + }, + "float": function(value) { + return types.number(value) && !types.integer(value); + }, + array(value) { + return Array.isArray(value); + }, + object(value) { + return typeof value === 'object' && !types.array(value); + }, + date(value) { + return value instanceof Date; + }, + timestamp(value) { + if (!this.integer(value) || Math.abs(value).toString().length > 16) { + return false + } + return true; + }, + file(value) { + return typeof value.url === 'string'; + }, + email(value) { + return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255; + }, + url(value) { + return typeof value === 'string' && !!value.match(pattern.url); + }, + pattern(reg, value) { + try { + return new RegExp(reg).test(value); + } catch (e) { + return false; + } + }, + method(value) { + return typeof value === 'function'; + }, + idcard(value) { + return typeof value === 'string' && !!value.match(pattern.idcard); + }, + 'url-https'(value) { + return this.url(value) && value.startsWith('https://'); + }, + 'url-scheme'(value) { + return value.startsWith('://'); + }, + 'url-web'(value) { + return false; + } +} + +class RuleValidator { + + constructor(message) { + this._message = message + } + + async validateRule(fieldKey, fieldValue, value, data, allData) { + var result = null + + let rules = fieldValue.rules + + let hasRequired = rules.findIndex((item) => { + return item.required + }) + if (hasRequired < 0) { + if (value === null || value === undefined) { + return result + } + if (typeof value === 'string' && !value.length) { + return result + } + } + + var message = this._message + + if (rules === undefined) { + return message['default'] + } + + for (var i = 0; i < rules.length; i++) { + let rule = rules[i] + let vt = this._getValidateType(rule) + + Object.assign(rule, { + label: fieldValue.label || `["${fieldKey}"]` + }) + + if (RuleValidatorHelper[vt]) { + result = RuleValidatorHelper[vt](rule, value, message) + if (result != null) { + break + } + } + + if (rule.validateExpr) { + let now = Date.now() + let resultExpr = rule.validateExpr(value, allData, now) + if (resultExpr === false) { + result = this._getMessage(rule, rule.errorMessage || this._message['default']) + break + } + } + + if (rule.validateFunction) { + result = await this.validateFunction(rule, value, data, allData, vt) + if (result !== null) { + break + } + } + } + + if (result !== null) { + result = message.TAG + result + } + + return result + } + + async validateFunction(rule, value, data, allData, vt) { + let result = null + try { + let callbackMessage = null + const res = await rule.validateFunction(rule, value, allData || data, (message) => { + callbackMessage = message + }) + if (callbackMessage || (typeof res === 'string' && res) || res === false) { + result = this._getMessage(rule, callbackMessage || res, vt) + } + } catch (e) { + result = this._getMessage(rule, e.message, vt) + } + return result + } + + _getMessage(rule, message, vt) { + return formatMessage(rule, message || rule.errorMessage || this._message[vt] || message['default']) + } + + _getValidateType(rule) { + var result = '' + if (rule.required) { + result = 'required' + } else if (rule.format) { + result = 'format' + } else if (rule.arrayType) { + result = 'arrayTypeFormat' + } else if (rule.range) { + result = 'range' + } else if (rule.maximum !== undefined || rule.minimum !== undefined) { + result = 'rangeNumber' + } else if (rule.maxLength !== undefined || rule.minLength !== undefined) { + result = 'rangeLength' + } else if (rule.pattern) { + result = 'pattern' + } else if (rule.validateFunction) { + result = 'validateFunction' + } + return result + } +} + +const RuleValidatorHelper = { + required(rule, value, message) { + if (rule.required && isEmptyValue(value, rule.format || typeof value)) { + return formatMessage(rule, rule.errorMessage || message.required); + } + + return null + }, + + range(rule, value, message) { + const { + range, + errorMessage + } = rule; + + let list = new Array(range.length); + for (let i = 0; i < range.length; i++) { + const item = range[i]; + if (types.object(item) && item.value !== undefined) { + list[i] = item.value; + } else { + list[i] = item; + } + } + + let result = false + if (Array.isArray(value)) { + result = (new Set(value.concat(list)).size === list.length); + } else { + if (list.indexOf(value) > -1) { + result = true; + } + } + + if (!result) { + return formatMessage(rule, errorMessage || message['enum']); + } + + return null + }, + + rangeNumber(rule, value, message) { + if (!types.number(value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + let { + minimum, + maximum, + exclusiveMinimum, + exclusiveMaximum + } = rule; + let min = exclusiveMinimum ? value <= minimum : value < minimum; + let max = exclusiveMaximum ? value >= maximum : value > maximum; + + if (minimum !== undefined && min) { + return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMinimum ? + 'exclusiveMinimum' : 'minimum' + ]) + } else if (maximum !== undefined && max) { + return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMaximum ? + 'exclusiveMaximum' : 'maximum' + ]) + } else if (minimum !== undefined && maximum !== undefined && (min || max)) { + return formatMessage(rule, rule.errorMessage || message['number'].range) + } + + return null + }, + + rangeLength(rule, value, message) { + if (!types.string(value) && !types.array(value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + let min = rule.minLength; + let max = rule.maxLength; + let val = value.length; + + if (min !== undefined && val < min) { + return formatMessage(rule, rule.errorMessage || message['length'].minLength) + } else if (max !== undefined && val > max) { + return formatMessage(rule, rule.errorMessage || message['length'].maxLength) + } else if (min !== undefined && max !== undefined && (val < min || val > max)) { + return formatMessage(rule, rule.errorMessage || message['length'].range) + } + + return null + }, + + pattern(rule, value, message) { + if (!types['pattern'](rule.pattern, value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + return null + }, + + format(rule, value, message) { + var customTypes = Object.keys(types); + var format = FORMAT_MAPPING[rule.format] ? FORMAT_MAPPING[rule.format] : (rule.format || rule.arrayType); + + if (customTypes.indexOf(format) > -1) { + if (!types[format](value)) { + return formatMessage(rule, rule.errorMessage || message.typeError); + } + } + + return null + }, + + arrayTypeFormat(rule, value, message) { + if (!Array.isArray(value)) { + return formatMessage(rule, rule.errorMessage || message.typeError); + } + + for (let i = 0; i < value.length; i++) { + const element = value[i]; + let formatResult = this.format(rule, element, message) + if (formatResult !== null) { + return formatResult + } + } + + return null + } +} + +class SchemaValidator extends RuleValidator { + + constructor(schema, options) { + super(SchemaValidator.message); + + this._schema = schema + this._options = options || null + } + + updateSchema(schema) { + this._schema = schema + } + + async validate(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidate(data, false, allData) + } + return result.length ? result[0] : null + } + + async validateAll(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidate(data, true, allData) + } + return result + } + + async validateUpdate(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidateUpdate(data, false, allData) + } + return result.length ? result[0] : null + } + + async invokeValidate(data, all, allData) { + let result = [] + let schema = this._schema + for (let key in schema) { + let value = schema[key] + let errorMessage = await this.validateRule(key, value, data[key], data, allData) + if (errorMessage != null) { + result.push({ + key, + errorMessage + }) + if (!all) break + } + } + return result + } + + async invokeValidateUpdate(data, all, allData) { + let result = [] + for (let key in data) { + let errorMessage = await this.validateRule(key, this._schema[key], data[key], data, allData) + if (errorMessage != null) { + result.push({ + key, + errorMessage + }) + if (!all) break + } + } + return result + } + + _checkFieldInSchema(data) { + var keys = Object.keys(data) + var keys2 = Object.keys(this._schema) + if (new Set(keys.concat(keys2)).size === keys2.length) { + return '' + } + + var noExistFields = keys.filter((key) => { + return keys2.indexOf(key) < 0; + }) + var errorMessage = formatMessage({ + field: JSON.stringify(noExistFields) + }, SchemaValidator.message.TAG + SchemaValidator.message['defaultInvalid']) + return [{ + key: 'invalid', + errorMessage + }] + } +} + +function Message() { + return { + TAG: "", + default: '验证错误', + defaultInvalid: '提交的字段{field}在数据库中并不存在', + validateFunction: '验证无效', + required: '{label}必填', + 'enum': '{label}超出范围', + timestamp: '{label}格式无效', + whitespace: '{label}不能为空', + typeError: '{label}类型无效', + date: { + format: '{label}日期{value}格式无效', + parse: '{label}日期无法解析,{value}无效', + invalid: '{label}日期{value}无效' + }, + length: { + minLength: '{label}长度不能少于{minLength}', + maxLength: '{label}长度不能超过{maxLength}', + range: '{label}必须介于{minLength}和{maxLength}之间' + }, + number: { + minimum: '{label}不能小于{minimum}', + maximum: '{label}不能大于{maximum}', + exclusiveMinimum: '{label}不能小于等于{minimum}', + exclusiveMaximum: '{label}不能大于等于{maximum}', + range: '{label}必须介于{minimum}and{maximum}之间' + }, + pattern: { + mismatch: '{label}格式不匹配' + } + }; +} + + +SchemaValidator.message = new Message(); + +export default SchemaValidator diff --git a/im-uniapp/uni_modules/uni-forms/package.json b/im-uniapp/uni_modules/uni-forms/package.json new file mode 100644 index 0000000..1925611 --- /dev/null +++ b/im-uniapp/uni_modules/uni-forms/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-forms", + "displayName": "uni-forms 表单", + "version": "1.4.9", + "description": "由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据", + "keywords": [ + "uni-ui", + "表单", + "校验", + "表单校验", + "表单验证" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-forms/readme.md b/im-uniapp/uni_modules/uni-forms/readme.md new file mode 100644 index 0000000..63d5a04 --- /dev/null +++ b/im-uniapp/uni_modules/uni-forms/readme.md @@ -0,0 +1,23 @@ + + +## Forms 表单 + +> **组件名:uni-forms** +> 代码块: `uForms`、`uni-forms-item` +> 关联组件:`uni-forms-item`、`uni-easyinput`、`uni-data-checkbox`、`uni-group`。 + + +uni-app的内置组件已经有了 `
`组件,用于提交表单内容。 + +然而几乎每个表单都需要做表单验证,为了方便做表单验证,减少重复开发,`uni ui` 又基于 ``组件封装了 ``组件,内置了表单验证功能。 + +`` 提供了 `rules`属性来描述校验规则、``子组件来包裹具体的表单项,以及给原生或三方组件提供了 `binddata()` 来设置表单值。 + +每个要校验的表单项,不管input还是checkbox,都必须放在``组件中,且一个``组件只能放置一个表单项。 + +``组件内部预留了显示error message的区域,默认是在表单项的底部。 + +另外,``组件下面的各个表单项,可以通过``包裹为不同的分组。同一``下的不同表单项目将聚拢在一起,同其他group保持垂直间距。``仅影响视觉效果。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-forms) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-goods-nav/changelog.md b/im-uniapp/uni_modules/uni-goods-nav/changelog.md new file mode 100644 index 0000000..c6264c6 --- /dev/null +++ b/im-uniapp/uni_modules/uni-goods-nav/changelog.md @@ -0,0 +1,18 @@ +## 1.2.1(2022-05-30) +- 新增 stat属性,是否开启uni统计功能 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-goods-nav](https://uniapp.dcloud.io/component/uniui/uni-goods-nav) +## 1.1.1(2021-08-24) +- 新增 支持国际化 +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.5(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/en.json b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/en.json new file mode 100644 index 0000000..dcdba41 --- /dev/null +++ b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/en.json @@ -0,0 +1,6 @@ +{ + "uni-goods-nav.options.shop": "shop", + "uni-goods-nav.options.cart": "cart", + "uni-goods-nav.buttonGroup.addToCart": "add to cart", + "uni-goods-nav.buttonGroup.buyNow": "buy now" +} diff --git a/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/index.js b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hans.json new file mode 100644 index 0000000..48ee344 --- /dev/null +++ b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hans.json @@ -0,0 +1,6 @@ +{ + "uni-goods-nav.options.shop": "店铺", + "uni-goods-nav.options.cart": "购物车", + "uni-goods-nav.buttonGroup.addToCart": "加入购物车", + "uni-goods-nav.buttonGroup.buyNow": "立即购买" +} diff --git a/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hant.json new file mode 100644 index 0000000..d0a0255 --- /dev/null +++ b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hant.json @@ -0,0 +1,6 @@ +{ + "uni-goods-nav.options.shop": "店鋪", + "uni-goods-nav.options.cart": "購物車", + "uni-goods-nav.buttonGroup.addToCart": "加入購物車", + "uni-goods-nav.buttonGroup.buyNow": "立即購買" +} diff --git a/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/uni-goods-nav.vue b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/uni-goods-nav.vue new file mode 100644 index 0000000..8a16b17 --- /dev/null +++ b/im-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/uni-goods-nav.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-goods-nav/package.json b/im-uniapp/uni_modules/uni-goods-nav/package.json new file mode 100644 index 0000000..636e45e --- /dev/null +++ b/im-uniapp/uni_modules/uni-goods-nav/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-goods-nav", + "displayName": "uni-goods-nav 商品导航", + "version": "1.2.1", + "description": "商品导航组件主要用于电商类应用底部导航,可自定义加入购物车,购买等操作", + "keywords": [ + "uni-ui", + "uniui", + "商品导航" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-goods-nav/readme.md b/im-uniapp/uni_modules/uni-goods-nav/readme.md new file mode 100644 index 0000000..07df93f --- /dev/null +++ b/im-uniapp/uni_modules/uni-goods-nav/readme.md @@ -0,0 +1,10 @@ + + +## GoodsNav 商品导航 +> **组件名:uni-goods-nav** +> 代码块: `uGoodsNav` + +商品加入购物车,立即购买等。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-goods-nav) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-grid/changelog.md b/im-uniapp/uni_modules/uni-grid/changelog.md new file mode 100644 index 0000000..d301166 --- /dev/null +++ b/im-uniapp/uni_modules/uni-grid/changelog.md @@ -0,0 +1,13 @@ +## 1.4.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-grid](https://uniapp.dcloud.io/component/uniui/uni-grid) +## 1.3.2(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.3.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.3.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.2.4(2021-05-12) +- 新增 组件示例地址 +## 1.2.3(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-grid/components/uni-grid-item/uni-grid-item.vue b/im-uniapp/uni_modules/uni-grid/components/uni-grid-item/uni-grid-item.vue new file mode 100644 index 0000000..19c08d7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-grid/components/uni-grid-item/uni-grid-item.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-grid/components/uni-grid/uni-grid.vue b/im-uniapp/uni_modules/uni-grid/components/uni-grid/uni-grid.vue new file mode 100644 index 0000000..0edc7ff --- /dev/null +++ b/im-uniapp/uni_modules/uni-grid/components/uni-grid/uni-grid.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-grid/package.json b/im-uniapp/uni_modules/uni-grid/package.json new file mode 100644 index 0000000..ccb2c91 --- /dev/null +++ b/im-uniapp/uni_modules/uni-grid/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-grid", + "displayName": "uni-grid 宫格", + "version": "1.4.0", + "description": "Grid 宫格组件,提供移动端常见的宫格布局,如九宫格。", + "keywords": [ + "uni-ui", + "uniui", + "九宫格", + "表格" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-icons"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-grid/readme.md b/im-uniapp/uni_modules/uni-grid/readme.md new file mode 100644 index 0000000..0aa44cc --- /dev/null +++ b/im-uniapp/uni_modules/uni-grid/readme.md @@ -0,0 +1,11 @@ + + +## Grid 宫格 +> **组件名:uni-grid** +> 代码块: `uGrid` + + +宫格组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-grid) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-group/changelog.md b/im-uniapp/uni_modules/uni-group/changelog.md new file mode 100644 index 0000000..a7024fd --- /dev/null +++ b/im-uniapp/uni_modules/uni-group/changelog.md @@ -0,0 +1,16 @@ +## 1.2.2(2022-05-30) +- 新增 stat属性,是否开启uni统计功能 +## 1.2.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-group](https://uniapp.dcloud.io/component/uniui/uni-group) +## 1.1.7(2021-11-08) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +- 优化 组件文档 +## 1.0.3(2021-05-12) +- 新增 组件示例地址 +## 1.0.2(2021-02-05) +- 调整为uni_modules目录规范 +- 优化 兼容 nvue 页面 diff --git a/im-uniapp/uni_modules/uni-group/components/uni-group/uni-group.vue b/im-uniapp/uni_modules/uni-group/components/uni-group/uni-group.vue new file mode 100644 index 0000000..3425ecd --- /dev/null +++ b/im-uniapp/uni_modules/uni-group/components/uni-group/uni-group.vue @@ -0,0 +1,134 @@ + + + + diff --git a/im-uniapp/uni_modules/uni-group/package.json b/im-uniapp/uni_modules/uni-group/package.json new file mode 100644 index 0000000..ea00a08 --- /dev/null +++ b/im-uniapp/uni_modules/uni-group/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-group", + "displayName": "uni-group 分组", + "version": "1.2.2", + "description": "分组组件可用于将组件用于分组,添加间隔,以产生明显的区块", + "keywords": [ + "uni-ui", + "uniui", + "group", + "分组", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-group/readme.md b/im-uniapp/uni_modules/uni-group/readme.md new file mode 100644 index 0000000..bae67f4 --- /dev/null +++ b/im-uniapp/uni_modules/uni-group/readme.md @@ -0,0 +1,9 @@ + +## Group 分组 +> **组件名:uni-group** +> 代码块: `uGroup` + +分组组件可用于将组件分组,添加间隔,以产生明显的区块。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-group) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-icons/changelog.md b/im-uniapp/uni_modules/uni-icons/changelog.md new file mode 100644 index 0000000..6449885 --- /dev/null +++ b/im-uniapp/uni_modules/uni-icons/changelog.md @@ -0,0 +1,22 @@ +## 1.3.5(2022-01-24) +- 优化 size 属性可以传入不带单位的字符串数值 +## 1.3.4(2022-01-24) +- 优化 size 支持其他单位 +## 1.3.3(2022-01-17) +- 修复 nvue 有些图标不显示的bug,兼容老版本图标 +## 1.3.2(2021-12-01) +- 优化 示例可复制图标名称 +## 1.3.1(2021-11-23) +- 优化 兼容旧组件 type 值 +## 1.3.0(2021-11-19) +- 新增 更多图标 +- 优化 自定义图标使用方式 +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons) +## 1.1.7(2021-11-08) +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.5(2021-05-12) +- 新增 组件示例地址 +## 1.1.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-icons/components/uni-icons/icons.js b/im-uniapp/uni_modules/uni-icons/components/uni-icons/icons.js new file mode 100644 index 0000000..7889936 --- /dev/null +++ b/im-uniapp/uni_modules/uni-icons/components/uni-icons/icons.js @@ -0,0 +1,1169 @@ +export default { + "id": "2852637", + "name": "uniui图标库", + "font_family": "uniicons", + "css_prefix_text": "uniui-", + "description": "", + "glyphs": [ + { + "icon_id": "25027049", + "name": "yanse", + "font_class": "color", + "unicode": "e6cf", + "unicode_decimal": 59087 + }, + { + "icon_id": "25027048", + "name": "wallet", + "font_class": "wallet", + "unicode": "e6b1", + "unicode_decimal": 59057 + }, + { + "icon_id": "25015720", + "name": "settings-filled", + "font_class": "settings-filled", + "unicode": "e6ce", + "unicode_decimal": 59086 + }, + { + "icon_id": "25015434", + "name": "shimingrenzheng-filled", + "font_class": "auth-filled", + "unicode": "e6cc", + "unicode_decimal": 59084 + }, + { + "icon_id": "24934246", + "name": "shop-filled", + "font_class": "shop-filled", + "unicode": "e6cd", + "unicode_decimal": 59085 + }, + { + "icon_id": "24934159", + "name": "staff-filled-01", + "font_class": "staff-filled", + "unicode": "e6cb", + "unicode_decimal": 59083 + }, + { + "icon_id": "24932461", + "name": "VIP-filled", + "font_class": "vip-filled", + "unicode": "e6c6", + "unicode_decimal": 59078 + }, + { + "icon_id": "24932462", + "name": "plus_circle_fill", + "font_class": "plus-filled", + "unicode": "e6c7", + "unicode_decimal": 59079 + }, + { + "icon_id": "24932463", + "name": "folder_add-filled", + "font_class": "folder-add-filled", + "unicode": "e6c8", + "unicode_decimal": 59080 + }, + { + "icon_id": "24932464", + "name": "yanse-filled", + "font_class": "color-filled", + "unicode": "e6c9", + "unicode_decimal": 59081 + }, + { + "icon_id": "24932465", + "name": "tune-filled", + "font_class": "tune-filled", + "unicode": "e6ca", + "unicode_decimal": 59082 + }, + { + "icon_id": "24932455", + "name": "a-rilidaka-filled", + "font_class": "calendar-filled", + "unicode": "e6c0", + "unicode_decimal": 59072 + }, + { + "icon_id": "24932456", + "name": "notification-filled", + "font_class": "notification-filled", + "unicode": "e6c1", + "unicode_decimal": 59073 + }, + { + "icon_id": "24932457", + "name": "wallet-filled", + "font_class": "wallet-filled", + "unicode": "e6c2", + "unicode_decimal": 59074 + }, + { + "icon_id": "24932458", + "name": "paihangbang-filled", + "font_class": "medal-filled", + "unicode": "e6c3", + "unicode_decimal": 59075 + }, + { + "icon_id": "24932459", + "name": "gift-filled", + "font_class": "gift-filled", + "unicode": "e6c4", + "unicode_decimal": 59076 + }, + { + "icon_id": "24932460", + "name": "fire-filled", + "font_class": "fire-filled", + "unicode": "e6c5", + "unicode_decimal": 59077 + }, + { + "icon_id": "24928001", + "name": "refreshempty", + "font_class": "refreshempty", + "unicode": "e6bf", + "unicode_decimal": 59071 + }, + { + "icon_id": "24926853", + "name": "location-ellipse", + "font_class": "location-filled", + "unicode": "e6af", + "unicode_decimal": 59055 + }, + { + "icon_id": "24926735", + "name": "person-filled", + "font_class": "person-filled", + "unicode": "e69d", + "unicode_decimal": 59037 + }, + { + "icon_id": "24926703", + "name": "personadd-filled", + "font_class": "personadd-filled", + "unicode": "e698", + "unicode_decimal": 59032 + }, + { + "icon_id": "24923351", + "name": "back", + "font_class": "back", + "unicode": "e6b9", + "unicode_decimal": 59065 + }, + { + "icon_id": "24923352", + "name": "forward", + "font_class": "forward", + "unicode": "e6ba", + "unicode_decimal": 59066 + }, + { + "icon_id": "24923353", + "name": "arrowthinright", + "font_class": "arrow-right", + "unicode": "e6bb", + "unicode_decimal": 59067 + }, + { + "icon_id": "24923353", + "name": "arrowthinright", + "font_class": "arrowthinright", + "unicode": "e6bb", + "unicode_decimal": 59067 + }, + { + "icon_id": "24923354", + "name": "arrowthinleft", + "font_class": "arrow-left", + "unicode": "e6bc", + "unicode_decimal": 59068 + }, + { + "icon_id": "24923354", + "name": "arrowthinleft", + "font_class": "arrowthinleft", + "unicode": "e6bc", + "unicode_decimal": 59068 + }, + { + "icon_id": "24923355", + "name": "arrowthinup", + "font_class": "arrow-up", + "unicode": "e6bd", + "unicode_decimal": 59069 + }, + { + "icon_id": "24923355", + "name": "arrowthinup", + "font_class": "arrowthinup", + "unicode": "e6bd", + "unicode_decimal": 59069 + }, + { + "icon_id": "24923356", + "name": "arrowthindown", + "font_class": "arrow-down", + "unicode": "e6be", + "unicode_decimal": 59070 + },{ + "icon_id": "24923356", + "name": "arrowthindown", + "font_class": "arrowthindown", + "unicode": "e6be", + "unicode_decimal": 59070 + }, + { + "icon_id": "24923349", + "name": "arrowdown", + "font_class": "bottom", + "unicode": "e6b8", + "unicode_decimal": 59064 + },{ + "icon_id": "24923349", + "name": "arrowdown", + "font_class": "arrowdown", + "unicode": "e6b8", + "unicode_decimal": 59064 + }, + { + "icon_id": "24923346", + "name": "arrowright", + "font_class": "right", + "unicode": "e6b5", + "unicode_decimal": 59061 + }, + { + "icon_id": "24923346", + "name": "arrowright", + "font_class": "arrowright", + "unicode": "e6b5", + "unicode_decimal": 59061 + }, + { + "icon_id": "24923347", + "name": "arrowup", + "font_class": "top", + "unicode": "e6b6", + "unicode_decimal": 59062 + }, + { + "icon_id": "24923347", + "name": "arrowup", + "font_class": "arrowup", + "unicode": "e6b6", + "unicode_decimal": 59062 + }, + { + "icon_id": "24923348", + "name": "arrowleft", + "font_class": "left", + "unicode": "e6b7", + "unicode_decimal": 59063 + }, + { + "icon_id": "24923348", + "name": "arrowleft", + "font_class": "arrowleft", + "unicode": "e6b7", + "unicode_decimal": 59063 + }, + { + "icon_id": "24923334", + "name": "eye", + "font_class": "eye", + "unicode": "e651", + "unicode_decimal": 58961 + }, + { + "icon_id": "24923335", + "name": "eye-filled", + "font_class": "eye-filled", + "unicode": "e66a", + "unicode_decimal": 58986 + }, + { + "icon_id": "24923336", + "name": "eye-slash", + "font_class": "eye-slash", + "unicode": "e6b3", + "unicode_decimal": 59059 + }, + { + "icon_id": "24923337", + "name": "eye-slash-filled", + "font_class": "eye-slash-filled", + "unicode": "e6b4", + "unicode_decimal": 59060 + }, + { + "icon_id": "24923305", + "name": "info-filled", + "font_class": "info-filled", + "unicode": "e649", + "unicode_decimal": 58953 + }, + { + "icon_id": "24923299", + "name": "reload-01", + "font_class": "reload", + "unicode": "e6b2", + "unicode_decimal": 59058 + }, + { + "icon_id": "24923195", + "name": "mic_slash_fill", + "font_class": "micoff-filled", + "unicode": "e6b0", + "unicode_decimal": 59056 + }, + { + "icon_id": "24923165", + "name": "map-pin-ellipse", + "font_class": "map-pin-ellipse", + "unicode": "e6ac", + "unicode_decimal": 59052 + }, + { + "icon_id": "24923166", + "name": "map-pin", + "font_class": "map-pin", + "unicode": "e6ad", + "unicode_decimal": 59053 + }, + { + "icon_id": "24923167", + "name": "location", + "font_class": "location", + "unicode": "e6ae", + "unicode_decimal": 59054 + }, + { + "icon_id": "24923064", + "name": "starhalf", + "font_class": "starhalf", + "unicode": "e683", + "unicode_decimal": 59011 + }, + { + "icon_id": "24923065", + "name": "star", + "font_class": "star", + "unicode": "e688", + "unicode_decimal": 59016 + }, + { + "icon_id": "24923066", + "name": "star-filled", + "font_class": "star-filled", + "unicode": "e68f", + "unicode_decimal": 59023 + }, + { + "icon_id": "24899646", + "name": "a-rilidaka", + "font_class": "calendar", + "unicode": "e6a0", + "unicode_decimal": 59040 + }, + { + "icon_id": "24899647", + "name": "fire", + "font_class": "fire", + "unicode": "e6a1", + "unicode_decimal": 59041 + }, + { + "icon_id": "24899648", + "name": "paihangbang", + "font_class": "medal", + "unicode": "e6a2", + "unicode_decimal": 59042 + }, + { + "icon_id": "24899649", + "name": "font", + "font_class": "font", + "unicode": "e6a3", + "unicode_decimal": 59043 + }, + { + "icon_id": "24899650", + "name": "gift", + "font_class": "gift", + "unicode": "e6a4", + "unicode_decimal": 59044 + }, + { + "icon_id": "24899651", + "name": "link", + "font_class": "link", + "unicode": "e6a5", + "unicode_decimal": 59045 + }, + { + "icon_id": "24899652", + "name": "notification", + "font_class": "notification", + "unicode": "e6a6", + "unicode_decimal": 59046 + }, + { + "icon_id": "24899653", + "name": "staff", + "font_class": "staff", + "unicode": "e6a7", + "unicode_decimal": 59047 + }, + { + "icon_id": "24899654", + "name": "VIP", + "font_class": "vip", + "unicode": "e6a8", + "unicode_decimal": 59048 + }, + { + "icon_id": "24899655", + "name": "folder_add", + "font_class": "folder-add", + "unicode": "e6a9", + "unicode_decimal": 59049 + }, + { + "icon_id": "24899656", + "name": "tune", + "font_class": "tune", + "unicode": "e6aa", + "unicode_decimal": 59050 + }, + { + "icon_id": "24899657", + "name": "shimingrenzheng", + "font_class": "auth", + "unicode": "e6ab", + "unicode_decimal": 59051 + }, + { + "icon_id": "24899565", + "name": "person", + "font_class": "person", + "unicode": "e699", + "unicode_decimal": 59033 + }, + { + "icon_id": "24899566", + "name": "email-filled", + "font_class": "email-filled", + "unicode": "e69a", + "unicode_decimal": 59034 + }, + { + "icon_id": "24899567", + "name": "phone-filled", + "font_class": "phone-filled", + "unicode": "e69b", + "unicode_decimal": 59035 + }, + { + "icon_id": "24899568", + "name": "phone", + "font_class": "phone", + "unicode": "e69c", + "unicode_decimal": 59036 + }, + { + "icon_id": "24899570", + "name": "email", + "font_class": "email", + "unicode": "e69e", + "unicode_decimal": 59038 + }, + { + "icon_id": "24899571", + "name": "personadd", + "font_class": "personadd", + "unicode": "e69f", + "unicode_decimal": 59039 + }, + { + "icon_id": "24899558", + "name": "chatboxes-filled", + "font_class": "chatboxes-filled", + "unicode": "e692", + "unicode_decimal": 59026 + }, + { + "icon_id": "24899559", + "name": "contact", + "font_class": "contact", + "unicode": "e693", + "unicode_decimal": 59027 + }, + { + "icon_id": "24899560", + "name": "chatbubble-filled", + "font_class": "chatbubble-filled", + "unicode": "e694", + "unicode_decimal": 59028 + }, + { + "icon_id": "24899561", + "name": "contact-filled", + "font_class": "contact-filled", + "unicode": "e695", + "unicode_decimal": 59029 + }, + { + "icon_id": "24899562", + "name": "chatboxes", + "font_class": "chatboxes", + "unicode": "e696", + "unicode_decimal": 59030 + }, + { + "icon_id": "24899563", + "name": "chatbubble", + "font_class": "chatbubble", + "unicode": "e697", + "unicode_decimal": 59031 + }, + { + "icon_id": "24881290", + "name": "upload-filled", + "font_class": "upload-filled", + "unicode": "e68e", + "unicode_decimal": 59022 + }, + { + "icon_id": "24881292", + "name": "upload", + "font_class": "upload", + "unicode": "e690", + "unicode_decimal": 59024 + }, + { + "icon_id": "24881293", + "name": "weixin", + "font_class": "weixin", + "unicode": "e691", + "unicode_decimal": 59025 + }, + { + "icon_id": "24881274", + "name": "compose", + "font_class": "compose", + "unicode": "e67f", + "unicode_decimal": 59007 + }, + { + "icon_id": "24881275", + "name": "qq", + "font_class": "qq", + "unicode": "e680", + "unicode_decimal": 59008 + }, + { + "icon_id": "24881276", + "name": "download-filled", + "font_class": "download-filled", + "unicode": "e681", + "unicode_decimal": 59009 + }, + { + "icon_id": "24881277", + "name": "pengyouquan", + "font_class": "pyq", + "unicode": "e682", + "unicode_decimal": 59010 + }, + { + "icon_id": "24881279", + "name": "sound", + "font_class": "sound", + "unicode": "e684", + "unicode_decimal": 59012 + }, + { + "icon_id": "24881280", + "name": "trash-filled", + "font_class": "trash-filled", + "unicode": "e685", + "unicode_decimal": 59013 + }, + { + "icon_id": "24881281", + "name": "sound-filled", + "font_class": "sound-filled", + "unicode": "e686", + "unicode_decimal": 59014 + }, + { + "icon_id": "24881282", + "name": "trash", + "font_class": "trash", + "unicode": "e687", + "unicode_decimal": 59015 + }, + { + "icon_id": "24881284", + "name": "videocam-filled", + "font_class": "videocam-filled", + "unicode": "e689", + "unicode_decimal": 59017 + }, + { + "icon_id": "24881285", + "name": "spinner-cycle", + "font_class": "spinner-cycle", + "unicode": "e68a", + "unicode_decimal": 59018 + }, + { + "icon_id": "24881286", + "name": "weibo", + "font_class": "weibo", + "unicode": "e68b", + "unicode_decimal": 59019 + }, + { + "icon_id": "24881288", + "name": "videocam", + "font_class": "videocam", + "unicode": "e68c", + "unicode_decimal": 59020 + }, + { + "icon_id": "24881289", + "name": "download", + "font_class": "download", + "unicode": "e68d", + "unicode_decimal": 59021 + }, + { + "icon_id": "24879601", + "name": "help", + "font_class": "help", + "unicode": "e679", + "unicode_decimal": 59001 + }, + { + "icon_id": "24879602", + "name": "navigate-filled", + "font_class": "navigate-filled", + "unicode": "e67a", + "unicode_decimal": 59002 + }, + { + "icon_id": "24879603", + "name": "plusempty", + "font_class": "plusempty", + "unicode": "e67b", + "unicode_decimal": 59003 + }, + { + "icon_id": "24879604", + "name": "smallcircle", + "font_class": "smallcircle", + "unicode": "e67c", + "unicode_decimal": 59004 + }, + { + "icon_id": "24879605", + "name": "minus-filled", + "font_class": "minus-filled", + "unicode": "e67d", + "unicode_decimal": 59005 + }, + { + "icon_id": "24879606", + "name": "micoff", + "font_class": "micoff", + "unicode": "e67e", + "unicode_decimal": 59006 + }, + { + "icon_id": "24879588", + "name": "closeempty", + "font_class": "closeempty", + "unicode": "e66c", + "unicode_decimal": 58988 + }, + { + "icon_id": "24879589", + "name": "clear", + "font_class": "clear", + "unicode": "e66d", + "unicode_decimal": 58989 + }, + { + "icon_id": "24879590", + "name": "navigate", + "font_class": "navigate", + "unicode": "e66e", + "unicode_decimal": 58990 + }, + { + "icon_id": "24879591", + "name": "minus", + "font_class": "minus", + "unicode": "e66f", + "unicode_decimal": 58991 + }, + { + "icon_id": "24879592", + "name": "image", + "font_class": "image", + "unicode": "e670", + "unicode_decimal": 58992 + }, + { + "icon_id": "24879593", + "name": "mic", + "font_class": "mic", + "unicode": "e671", + "unicode_decimal": 58993 + }, + { + "icon_id": "24879594", + "name": "paperplane", + "font_class": "paperplane", + "unicode": "e672", + "unicode_decimal": 58994 + }, + { + "icon_id": "24879595", + "name": "close", + "font_class": "close", + "unicode": "e673", + "unicode_decimal": 58995 + }, + { + "icon_id": "24879596", + "name": "help-filled", + "font_class": "help-filled", + "unicode": "e674", + "unicode_decimal": 58996 + }, + { + "icon_id": "24879597", + "name": "plus-filled", + "font_class": "paperplane-filled", + "unicode": "e675", + "unicode_decimal": 58997 + }, + { + "icon_id": "24879598", + "name": "plus", + "font_class": "plus", + "unicode": "e676", + "unicode_decimal": 58998 + }, + { + "icon_id": "24879599", + "name": "mic-filled", + "font_class": "mic-filled", + "unicode": "e677", + "unicode_decimal": 58999 + }, + { + "icon_id": "24879600", + "name": "image-filled", + "font_class": "image-filled", + "unicode": "e678", + "unicode_decimal": 59000 + }, + { + "icon_id": "24855900", + "name": "locked-filled", + "font_class": "locked-filled", + "unicode": "e668", + "unicode_decimal": 58984 + }, + { + "icon_id": "24855901", + "name": "info", + "font_class": "info", + "unicode": "e669", + "unicode_decimal": 58985 + }, + { + "icon_id": "24855903", + "name": "locked", + "font_class": "locked", + "unicode": "e66b", + "unicode_decimal": 58987 + }, + { + "icon_id": "24855884", + "name": "camera-filled", + "font_class": "camera-filled", + "unicode": "e658", + "unicode_decimal": 58968 + }, + { + "icon_id": "24855885", + "name": "chat-filled", + "font_class": "chat-filled", + "unicode": "e659", + "unicode_decimal": 58969 + }, + { + "icon_id": "24855886", + "name": "camera", + "font_class": "camera", + "unicode": "e65a", + "unicode_decimal": 58970 + }, + { + "icon_id": "24855887", + "name": "circle", + "font_class": "circle", + "unicode": "e65b", + "unicode_decimal": 58971 + }, + { + "icon_id": "24855888", + "name": "checkmarkempty", + "font_class": "checkmarkempty", + "unicode": "e65c", + "unicode_decimal": 58972 + }, + { + "icon_id": "24855889", + "name": "chat", + "font_class": "chat", + "unicode": "e65d", + "unicode_decimal": 58973 + }, + { + "icon_id": "24855890", + "name": "circle-filled", + "font_class": "circle-filled", + "unicode": "e65e", + "unicode_decimal": 58974 + }, + { + "icon_id": "24855891", + "name": "flag", + "font_class": "flag", + "unicode": "e65f", + "unicode_decimal": 58975 + }, + { + "icon_id": "24855892", + "name": "flag-filled", + "font_class": "flag-filled", + "unicode": "e660", + "unicode_decimal": 58976 + }, + { + "icon_id": "24855893", + "name": "gear-filled", + "font_class": "gear-filled", + "unicode": "e661", + "unicode_decimal": 58977 + }, + { + "icon_id": "24855894", + "name": "home", + "font_class": "home", + "unicode": "e662", + "unicode_decimal": 58978 + }, + { + "icon_id": "24855895", + "name": "home-filled", + "font_class": "home-filled", + "unicode": "e663", + "unicode_decimal": 58979 + }, + { + "icon_id": "24855896", + "name": "gear", + "font_class": "gear", + "unicode": "e664", + "unicode_decimal": 58980 + }, + { + "icon_id": "24855897", + "name": "smallcircle-filled", + "font_class": "smallcircle-filled", + "unicode": "e665", + "unicode_decimal": 58981 + }, + { + "icon_id": "24855898", + "name": "map-filled", + "font_class": "map-filled", + "unicode": "e666", + "unicode_decimal": 58982 + }, + { + "icon_id": "24855899", + "name": "map", + "font_class": "map", + "unicode": "e667", + "unicode_decimal": 58983 + }, + { + "icon_id": "24855825", + "name": "refresh-filled", + "font_class": "refresh-filled", + "unicode": "e656", + "unicode_decimal": 58966 + }, + { + "icon_id": "24855826", + "name": "refresh", + "font_class": "refresh", + "unicode": "e657", + "unicode_decimal": 58967 + }, + { + "icon_id": "24855808", + "name": "cloud-upload", + "font_class": "cloud-upload", + "unicode": "e645", + "unicode_decimal": 58949 + }, + { + "icon_id": "24855809", + "name": "cloud-download-filled", + "font_class": "cloud-download-filled", + "unicode": "e646", + "unicode_decimal": 58950 + }, + { + "icon_id": "24855810", + "name": "cloud-download", + "font_class": "cloud-download", + "unicode": "e647", + "unicode_decimal": 58951 + }, + { + "icon_id": "24855811", + "name": "cloud-upload-filled", + "font_class": "cloud-upload-filled", + "unicode": "e648", + "unicode_decimal": 58952 + }, + { + "icon_id": "24855813", + "name": "redo", + "font_class": "redo", + "unicode": "e64a", + "unicode_decimal": 58954 + }, + { + "icon_id": "24855814", + "name": "images-filled", + "font_class": "images-filled", + "unicode": "e64b", + "unicode_decimal": 58955 + }, + { + "icon_id": "24855815", + "name": "undo-filled", + "font_class": "undo-filled", + "unicode": "e64c", + "unicode_decimal": 58956 + }, + { + "icon_id": "24855816", + "name": "more", + "font_class": "more", + "unicode": "e64d", + "unicode_decimal": 58957 + }, + { + "icon_id": "24855817", + "name": "more-filled", + "font_class": "more-filled", + "unicode": "e64e", + "unicode_decimal": 58958 + }, + { + "icon_id": "24855818", + "name": "undo", + "font_class": "undo", + "unicode": "e64f", + "unicode_decimal": 58959 + }, + { + "icon_id": "24855819", + "name": "images", + "font_class": "images", + "unicode": "e650", + "unicode_decimal": 58960 + }, + { + "icon_id": "24855821", + "name": "paperclip", + "font_class": "paperclip", + "unicode": "e652", + "unicode_decimal": 58962 + }, + { + "icon_id": "24855822", + "name": "settings", + "font_class": "settings", + "unicode": "e653", + "unicode_decimal": 58963 + }, + { + "icon_id": "24855823", + "name": "search", + "font_class": "search", + "unicode": "e654", + "unicode_decimal": 58964 + }, + { + "icon_id": "24855824", + "name": "redo-filled", + "font_class": "redo-filled", + "unicode": "e655", + "unicode_decimal": 58965 + }, + { + "icon_id": "24841702", + "name": "list", + "font_class": "list", + "unicode": "e644", + "unicode_decimal": 58948 + }, + { + "icon_id": "24841489", + "name": "mail-open-filled", + "font_class": "mail-open-filled", + "unicode": "e63a", + "unicode_decimal": 58938 + }, + { + "icon_id": "24841491", + "name": "hand-thumbsdown-filled", + "font_class": "hand-down-filled", + "unicode": "e63c", + "unicode_decimal": 58940 + }, + { + "icon_id": "24841492", + "name": "hand-thumbsdown", + "font_class": "hand-down", + "unicode": "e63d", + "unicode_decimal": 58941 + }, + { + "icon_id": "24841493", + "name": "hand-thumbsup-filled", + "font_class": "hand-up-filled", + "unicode": "e63e", + "unicode_decimal": 58942 + }, + { + "icon_id": "24841494", + "name": "hand-thumbsup", + "font_class": "hand-up", + "unicode": "e63f", + "unicode_decimal": 58943 + }, + { + "icon_id": "24841496", + "name": "heart-filled", + "font_class": "heart-filled", + "unicode": "e641", + "unicode_decimal": 58945 + }, + { + "icon_id": "24841498", + "name": "mail-open", + "font_class": "mail-open", + "unicode": "e643", + "unicode_decimal": 58947 + }, + { + "icon_id": "24841488", + "name": "heart", + "font_class": "heart", + "unicode": "e639", + "unicode_decimal": 58937 + }, + { + "icon_id": "24839963", + "name": "loop", + "font_class": "loop", + "unicode": "e633", + "unicode_decimal": 58931 + }, + { + "icon_id": "24839866", + "name": "pulldown", + "font_class": "pulldown", + "unicode": "e632", + "unicode_decimal": 58930 + }, + { + "icon_id": "24813798", + "name": "scan", + "font_class": "scan", + "unicode": "e62a", + "unicode_decimal": 58922 + }, + { + "icon_id": "24813786", + "name": "bars", + "font_class": "bars", + "unicode": "e627", + "unicode_decimal": 58919 + }, + { + "icon_id": "24813788", + "name": "cart-filled", + "font_class": "cart-filled", + "unicode": "e629", + "unicode_decimal": 58921 + }, + { + "icon_id": "24813790", + "name": "checkbox", + "font_class": "checkbox", + "unicode": "e62b", + "unicode_decimal": 58923 + }, + { + "icon_id": "24813791", + "name": "checkbox-filled", + "font_class": "checkbox-filled", + "unicode": "e62c", + "unicode_decimal": 58924 + }, + { + "icon_id": "24813794", + "name": "shop", + "font_class": "shop", + "unicode": "e62f", + "unicode_decimal": 58927 + }, + { + "icon_id": "24813795", + "name": "headphones", + "font_class": "headphones", + "unicode": "e630", + "unicode_decimal": 58928 + }, + { + "icon_id": "24813796", + "name": "cart", + "font_class": "cart", + "unicode": "e631", + "unicode_decimal": 58929 + } + ] +} diff --git a/im-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue b/im-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue new file mode 100644 index 0000000..86e7444 --- /dev/null +++ b/im-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css b/im-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css new file mode 100644 index 0000000..2f56eab --- /dev/null +++ b/im-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css @@ -0,0 +1,663 @@ +.uniui-color:before { + content: "\e6cf"; +} + +.uniui-wallet:before { + content: "\e6b1"; +} + +.uniui-settings-filled:before { + content: "\e6ce"; +} + +.uniui-auth-filled:before { + content: "\e6cc"; +} + +.uniui-shop-filled:before { + content: "\e6cd"; +} + +.uniui-staff-filled:before { + content: "\e6cb"; +} + +.uniui-vip-filled:before { + content: "\e6c6"; +} + +.uniui-plus-filled:before { + content: "\e6c7"; +} + +.uniui-folder-add-filled:before { + content: "\e6c8"; +} + +.uniui-color-filled:before { + content: "\e6c9"; +} + +.uniui-tune-filled:before { + content: "\e6ca"; +} + +.uniui-calendar-filled:before { + content: "\e6c0"; +} + +.uniui-notification-filled:before { + content: "\e6c1"; +} + +.uniui-wallet-filled:before { + content: "\e6c2"; +} + +.uniui-medal-filled:before { + content: "\e6c3"; +} + +.uniui-gift-filled:before { + content: "\e6c4"; +} + +.uniui-fire-filled:before { + content: "\e6c5"; +} + +.uniui-refreshempty:before { + content: "\e6bf"; +} + +.uniui-location-filled:before { + content: "\e6af"; +} + +.uniui-person-filled:before { + content: "\e69d"; +} + +.uniui-personadd-filled:before { + content: "\e698"; +} + +.uniui-back:before { + content: "\e6b9"; +} + +.uniui-forward:before { + content: "\e6ba"; +} + +.uniui-arrow-right:before { + content: "\e6bb"; +} + +.uniui-arrowthinright:before { + content: "\e6bb"; +} + +.uniui-arrow-left:before { + content: "\e6bc"; +} + +.uniui-arrowthinleft:before { + content: "\e6bc"; +} + +.uniui-arrow-up:before { + content: "\e6bd"; +} + +.uniui-arrowthinup:before { + content: "\e6bd"; +} + +.uniui-arrow-down:before { + content: "\e6be"; +} + +.uniui-arrowthindown:before { + content: "\e6be"; +} + +.uniui-bottom:before { + content: "\e6b8"; +} + +.uniui-arrowdown:before { + content: "\e6b8"; +} + +.uniui-right:before { + content: "\e6b5"; +} + +.uniui-arrowright:before { + content: "\e6b5"; +} + +.uniui-top:before { + content: "\e6b6"; +} + +.uniui-arrowup:before { + content: "\e6b6"; +} + +.uniui-left:before { + content: "\e6b7"; +} + +.uniui-arrowleft:before { + content: "\e6b7"; +} + +.uniui-eye:before { + content: "\e651"; +} + +.uniui-eye-filled:before { + content: "\e66a"; +} + +.uniui-eye-slash:before { + content: "\e6b3"; +} + +.uniui-eye-slash-filled:before { + content: "\e6b4"; +} + +.uniui-info-filled:before { + content: "\e649"; +} + +.uniui-reload:before { + content: "\e6b2"; +} + +.uniui-micoff-filled:before { + content: "\e6b0"; +} + +.uniui-map-pin-ellipse:before { + content: "\e6ac"; +} + +.uniui-map-pin:before { + content: "\e6ad"; +} + +.uniui-location:before { + content: "\e6ae"; +} + +.uniui-starhalf:before { + content: "\e683"; +} + +.uniui-star:before { + content: "\e688"; +} + +.uniui-star-filled:before { + content: "\e68f"; +} + +.uniui-calendar:before { + content: "\e6a0"; +} + +.uniui-fire:before { + content: "\e6a1"; +} + +.uniui-medal:before { + content: "\e6a2"; +} + +.uniui-font:before { + content: "\e6a3"; +} + +.uniui-gift:before { + content: "\e6a4"; +} + +.uniui-link:before { + content: "\e6a5"; +} + +.uniui-notification:before { + content: "\e6a6"; +} + +.uniui-staff:before { + content: "\e6a7"; +} + +.uniui-vip:before { + content: "\e6a8"; +} + +.uniui-folder-add:before { + content: "\e6a9"; +} + +.uniui-tune:before { + content: "\e6aa"; +} + +.uniui-auth:before { + content: "\e6ab"; +} + +.uniui-person:before { + content: "\e699"; +} + +.uniui-email-filled:before { + content: "\e69a"; +} + +.uniui-phone-filled:before { + content: "\e69b"; +} + +.uniui-phone:before { + content: "\e69c"; +} + +.uniui-email:before { + content: "\e69e"; +} + +.uniui-personadd:before { + content: "\e69f"; +} + +.uniui-chatboxes-filled:before { + content: "\e692"; +} + +.uniui-contact:before { + content: "\e693"; +} + +.uniui-chatbubble-filled:before { + content: "\e694"; +} + +.uniui-contact-filled:before { + content: "\e695"; +} + +.uniui-chatboxes:before { + content: "\e696"; +} + +.uniui-chatbubble:before { + content: "\e697"; +} + +.uniui-upload-filled:before { + content: "\e68e"; +} + +.uniui-upload:before { + content: "\e690"; +} + +.uniui-weixin:before { + content: "\e691"; +} + +.uniui-compose:before { + content: "\e67f"; +} + +.uniui-qq:before { + content: "\e680"; +} + +.uniui-download-filled:before { + content: "\e681"; +} + +.uniui-pyq:before { + content: "\e682"; +} + +.uniui-sound:before { + content: "\e684"; +} + +.uniui-trash-filled:before { + content: "\e685"; +} + +.uniui-sound-filled:before { + content: "\e686"; +} + +.uniui-trash:before { + content: "\e687"; +} + +.uniui-videocam-filled:before { + content: "\e689"; +} + +.uniui-spinner-cycle:before { + content: "\e68a"; +} + +.uniui-weibo:before { + content: "\e68b"; +} + +.uniui-videocam:before { + content: "\e68c"; +} + +.uniui-download:before { + content: "\e68d"; +} + +.uniui-help:before { + content: "\e679"; +} + +.uniui-navigate-filled:before { + content: "\e67a"; +} + +.uniui-plusempty:before { + content: "\e67b"; +} + +.uniui-smallcircle:before { + content: "\e67c"; +} + +.uniui-minus-filled:before { + content: "\e67d"; +} + +.uniui-micoff:before { + content: "\e67e"; +} + +.uniui-closeempty:before { + content: "\e66c"; +} + +.uniui-clear:before { + content: "\e66d"; +} + +.uniui-navigate:before { + content: "\e66e"; +} + +.uniui-minus:before { + content: "\e66f"; +} + +.uniui-image:before { + content: "\e670"; +} + +.uniui-mic:before { + content: "\e671"; +} + +.uniui-paperplane:before { + content: "\e672"; +} + +.uniui-close:before { + content: "\e673"; +} + +.uniui-help-filled:before { + content: "\e674"; +} + +.uniui-paperplane-filled:before { + content: "\e675"; +} + +.uniui-plus:before { + content: "\e676"; +} + +.uniui-mic-filled:before { + content: "\e677"; +} + +.uniui-image-filled:before { + content: "\e678"; +} + +.uniui-locked-filled:before { + content: "\e668"; +} + +.uniui-info:before { + content: "\e669"; +} + +.uniui-locked:before { + content: "\e66b"; +} + +.uniui-camera-filled:before { + content: "\e658"; +} + +.uniui-chat-filled:before { + content: "\e659"; +} + +.uniui-camera:before { + content: "\e65a"; +} + +.uniui-circle:before { + content: "\e65b"; +} + +.uniui-checkmarkempty:before { + content: "\e65c"; +} + +.uniui-chat:before { + content: "\e65d"; +} + +.uniui-circle-filled:before { + content: "\e65e"; +} + +.uniui-flag:before { + content: "\e65f"; +} + +.uniui-flag-filled:before { + content: "\e660"; +} + +.uniui-gear-filled:before { + content: "\e661"; +} + +.uniui-home:before { + content: "\e662"; +} + +.uniui-home-filled:before { + content: "\e663"; +} + +.uniui-gear:before { + content: "\e664"; +} + +.uniui-smallcircle-filled:before { + content: "\e665"; +} + +.uniui-map-filled:before { + content: "\e666"; +} + +.uniui-map:before { + content: "\e667"; +} + +.uniui-refresh-filled:before { + content: "\e656"; +} + +.uniui-refresh:before { + content: "\e657"; +} + +.uniui-cloud-upload:before { + content: "\e645"; +} + +.uniui-cloud-download-filled:before { + content: "\e646"; +} + +.uniui-cloud-download:before { + content: "\e647"; +} + +.uniui-cloud-upload-filled:before { + content: "\e648"; +} + +.uniui-redo:before { + content: "\e64a"; +} + +.uniui-images-filled:before { + content: "\e64b"; +} + +.uniui-undo-filled:before { + content: "\e64c"; +} + +.uniui-more:before { + content: "\e64d"; +} + +.uniui-more-filled:before { + content: "\e64e"; +} + +.uniui-undo:before { + content: "\e64f"; +} + +.uniui-images:before { + content: "\e650"; +} + +.uniui-paperclip:before { + content: "\e652"; +} + +.uniui-settings:before { + content: "\e653"; +} + +.uniui-search:before { + content: "\e654"; +} + +.uniui-redo-filled:before { + content: "\e655"; +} + +.uniui-list:before { + content: "\e644"; +} + +.uniui-mail-open-filled:before { + content: "\e63a"; +} + +.uniui-hand-down-filled:before { + content: "\e63c"; +} + +.uniui-hand-down:before { + content: "\e63d"; +} + +.uniui-hand-up-filled:before { + content: "\e63e"; +} + +.uniui-hand-up:before { + content: "\e63f"; +} + +.uniui-heart-filled:before { + content: "\e641"; +} + +.uniui-mail-open:before { + content: "\e643"; +} + +.uniui-heart:before { + content: "\e639"; +} + +.uniui-loop:before { + content: "\e633"; +} + +.uniui-pulldown:before { + content: "\e632"; +} + +.uniui-scan:before { + content: "\e62a"; +} + +.uniui-bars:before { + content: "\e627"; +} + +.uniui-cart-filled:before { + content: "\e629"; +} + +.uniui-checkbox:before { + content: "\e62b"; +} + +.uniui-checkbox-filled:before { + content: "\e62c"; +} + +.uniui-shop:before { + content: "\e62f"; +} + +.uniui-headphones:before { + content: "\e630"; +} + +.uniui-cart:before { + content: "\e631"; +} diff --git a/im-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf b/im-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf new file mode 100644 index 0000000..835f33b Binary files /dev/null and b/im-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf differ diff --git a/im-uniapp/uni_modules/uni-icons/package.json b/im-uniapp/uni_modules/uni-icons/package.json new file mode 100644 index 0000000..d1c4e77 --- /dev/null +++ b/im-uniapp/uni_modules/uni-icons/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-icons", + "displayName": "uni-icons 图标", + "version": "1.3.5", + "description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。", + "keywords": [ + "uni-ui", + "uniui", + "icon", + "图标" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.2.14" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-icons/readme.md b/im-uniapp/uni_modules/uni-icons/readme.md new file mode 100644 index 0000000..86234ba --- /dev/null +++ b/im-uniapp/uni_modules/uni-icons/readme.md @@ -0,0 +1,8 @@ +## Icons 图标 +> **组件名:uni-icons** +> 代码块: `uIcons` + +用于展示 icons 图标 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/im-uniapp/uni_modules/uni-indexed-list/changelog.md b/im-uniapp/uni_modules/uni-indexed-list/changelog.md new file mode 100644 index 0000000..08fa71c --- /dev/null +++ b/im-uniapp/uni_modules/uni-indexed-list/changelog.md @@ -0,0 +1,17 @@ +## 1.2.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-indexed-list](https://uniapp.dcloud.io/component/uniui/uni-indexed-list) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.11(2021-05-12) +- 新增 组件示例地址 +## 1.0.10(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.9(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.8(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 支持 PC 端 diff --git a/im-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list-item.vue b/im-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list-item.vue new file mode 100644 index 0000000..19284bd --- /dev/null +++ b/im-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list-item.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list.vue b/im-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list.vue new file mode 100644 index 0000000..ee3a7ec --- /dev/null +++ b/im-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list.vue @@ -0,0 +1,367 @@ + + + diff --git a/im-uniapp/uni_modules/uni-indexed-list/package.json b/im-uniapp/uni_modules/uni-indexed-list/package.json new file mode 100644 index 0000000..125c0e7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-indexed-list/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-indexed-list", + "displayName": "uni-indexed-list 索引列表", + "version": "1.2.1", + "description": "索引列表组件,右侧带索引的列表,方便快速定位到具体内容,通常用于城市/机场选择等场景", + "keywords": [ + "uni-ui", + "索引列表", + "索引", + "列表" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-indexed-list/readme.md b/im-uniapp/uni_modules/uni-indexed-list/readme.md new file mode 100644 index 0000000..44ad84b --- /dev/null +++ b/im-uniapp/uni_modules/uni-indexed-list/readme.md @@ -0,0 +1,11 @@ + + +## IndexedList 索引列表 +> **组件名:uni-indexed-list** +> 代码块: `uIndexedList` + + +用于展示索引列表。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-indexed-list) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/im-uniapp/uni_modules/uni-link/changelog.md b/im-uniapp/uni_modules/uni-link/changelog.md new file mode 100644 index 0000000..2cfbf59 --- /dev/null +++ b/im-uniapp/uni_modules/uni-link/changelog.md @@ -0,0 +1,17 @@ +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-link](https://uniapp.dcloud.io/component/uniui/uni-link) +## 1.1.7(2021-11-08) +## 0.0.7(2021-09-03) +- 修复 在 nvue 下不显示的 bug +## 0.0.6(2021-07-30) +- 新增 支持自定义插槽 +## 0.0.5(2021-06-21) +- 新增 download 属性,H5平台下载文件名 +## 0.0.4(2021-05-12) +- 新增 组件示例地址 +## 0.0.3(2021-03-09) +- 新增 href 属性支持 tel:|mailto: + +## 0.0.2(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-link/components/uni-link/uni-link.vue b/im-uniapp/uni_modules/uni-link/components/uni-link/uni-link.vue new file mode 100644 index 0000000..27c5468 --- /dev/null +++ b/im-uniapp/uni_modules/uni-link/components/uni-link/uni-link.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-link/package.json b/im-uniapp/uni_modules/uni-link/package.json new file mode 100644 index 0000000..77b1986 --- /dev/null +++ b/im-uniapp/uni_modules/uni-link/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-link", + "displayName": "uni-link 超链接", + "version": "1.0.0", + "description": "uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打", + "keywords": [ + "uni-ui", + "uniui", + "link", + "超链接", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-link/readme.md b/im-uniapp/uni_modules/uni-link/readme.md new file mode 100644 index 0000000..7f09e94 --- /dev/null +++ b/im-uniapp/uni_modules/uni-link/readme.md @@ -0,0 +1,11 @@ + + +## Link 链接 +> **组件名:uni-link** +> 代码块: `uLink` + + +uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打开新网页。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-link) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-list/changelog.md b/im-uniapp/uni_modules/uni-list/changelog.md new file mode 100644 index 0000000..8254a18 --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/changelog.md @@ -0,0 +1,46 @@ +## 1.2.14(2023-04-14) +- 优化 uni-list-chat 具名插槽`header` 非app端套一层元素,方便使用时通过外层元素定位实现样式修改 +## 1.2.13(2023-03-03) +- uni-list-chat 新增 支持具名插槽`header` +## 1.2.12(2023-02-01) +- 新增 列表图标新增 customPrefix 属性 ,用法 [详见](https://uniapp.dcloud.net.cn/component/uniui/uni-icons.html#icons-props) +## 1.2.11(2023-01-31) +- 修复 无反馈效果呈现的bug +## 1.2.9(2022-11-22) +- 修复 uni-list-chat 在vue3下跳转报错的bug +## 1.2.8(2022-11-21) +- 修复 uni-list-chat avatar属性 值为本地路径时错误的问题 +## 1.2.7(2022-11-21) +- 修复 uni-list-chat avatar属性 在腾讯云版uniCloud下错误的问题 +## 1.2.6(2022-11-18) +- 修复 uni-list-chat note属性 支持:“草稿”字样功能 文本少1位的问题 +## 1.2.5(2022-11-15) +- 修复 uni-list-item 的 customStyle 属性 padding值在 H5端 无效的bug +## 1.2.4(2022-11-15) +- 修复 uni-list-item 的 customStyle 属性 padding值在nvue(vue2)下无效的bug +## 1.2.3(2022-11-14) +- uni-list-chat 新增 avatar 支持 fileId +## 1.2.2(2022-11-11) +- uni-list 新增属性 render-reverse 详情参考:[https://uniapp.dcloud.net.cn/component/list.html](https://uniapp.dcloud.net.cn/component/list.html) +- uni-list-chat note属性 支持:“草稿”字样 加红显示 详情参考uni-im:[https://ext.dcloud.net.cn/plugin?name=uni-im](https://ext.dcloud.net.cn/plugin?name=uni-im) +- uni-list-item 新增属性 customStyle 支持设置padding、backgroundColor +## 1.2.1(2022-03-30) +- 删除无用文件 +## 1.2.0(2021-11-23) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-list](https://uniapp.dcloud.io/component/uniui/uni-list) +## 1.1.3(2021-08-30) +- 修复 在vue3中to属性在发行应用的时候报错的bug +## 1.1.2(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.1.1(2021-07-21) +- 修复 与其他组件嵌套使用时,点击失效的Bug +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.17(2021-05-12) +- 新增 组件示例地址 +## 1.0.16(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.0.15(2021-02-05) +- 调整为uni_modules目录规范 +- 修复 uni-list-chat 角标显示不正常的问题 diff --git a/im-uniapp/uni_modules/uni-list/components/uni-list-ad/uni-list-ad.vue b/im-uniapp/uni_modules/uni-list/components/uni-list-ad/uni-list-ad.vue new file mode 100644 index 0000000..b9349c2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/components/uni-list-ad/uni-list-ad.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.scss b/im-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.scss new file mode 100644 index 0000000..311f8d9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.scss @@ -0,0 +1,58 @@ +/** + * 这里是 uni-list 组件内置的常用样式变量 + * 如果需要覆盖样式,这里提供了基本的组件样式变量,您可以尝试修改这里的变量,去完成样式替换,而不用去修改源码 + * + */ + +// 背景色 +$background-color : #fff; +// 分割线颜色 +$divide-line-color : #e5e5e5; + +// 默认头像大小,如需要修改此值,注意同步修改 js 中的值 const avatarWidth = xx ,目前只支持方形头像 +// nvue 页面不支持修改头像大小 +$avatar-width : 45px ; + +// 头像边框 +$avatar-border-radius: 5px; +$avatar-border-color: #eee; +$avatar-border-width: 1px; + +// 标题文字样式 +$title-size : 16px; +$title-color : #3b4144; +$title-weight : normal; + +// 描述文字样式 +$note-size : 12px; +$note-color : #999; +$note-weight : normal; + +// 右侧额外内容默认样式 +$right-text-size : 12px; +$right-text-color : #999; +$right-text-weight : normal; + +// 角标样式 +// nvue 页面不支持修改圆点位置以及大小 +// 角标在左侧时,角标的位置,默认为 0 ,负数左/下移动,正数右/上移动 +$badge-left: 0px; +$badge-top: 0px; + +// 显示圆点时,圆点大小 +$dot-width: 10px; +$dot-height: 10px; + +// 显示角标时,角标大小和字体大小 +$badge-size : 18px; +$badge-font : 12px; +// 显示角标时,角标前景色 +$badge-color : #fff; +// 显示角标时,角标背景色 +$badge-background-color : #ff5a5f; +// 显示角标时,角标左右间距 +$badge-space : 6px; + +// 状态样式 +// 选中颜色 +$hover : #f5f5f5; diff --git a/im-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.vue b/im-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.vue new file mode 100644 index 0000000..d49fd7c --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.vue @@ -0,0 +1,593 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-list/components/uni-list-item/uni-list-item.vue b/im-uniapp/uni_modules/uni-list/components/uni-list-item/uni-list-item.vue new file mode 100644 index 0000000..a274ac8 --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/components/uni-list-item/uni-list-item.vue @@ -0,0 +1,534 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-list/components/uni-list/uni-list.vue b/im-uniapp/uni_modules/uni-list/components/uni-list/uni-list.vue new file mode 100644 index 0000000..6ef5972 --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/components/uni-list/uni-list.vue @@ -0,0 +1,123 @@ + + + + diff --git a/im-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.vue b/im-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.vue new file mode 100644 index 0000000..3b4c5a2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.wxs b/im-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.wxs new file mode 100644 index 0000000..818a6b7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.wxs @@ -0,0 +1,87 @@ +var pullDown = { + threshold: 95, + maxHeight: 200, + callRefresh: 'onrefresh', + callPullingDown: 'onpullingdown', + refreshSelector: '.uni-refresh' +}; + +function ready(newValue, oldValue, ownerInstance, instance) { + var state = instance.getState() + state.canPullDown = newValue; + // console.log(newValue); +} + +function touchStart(e, instance) { + var state = instance.getState(); + state.refreshInstance = instance.selectComponent(pullDown.refreshSelector); + state.canPullDown = (state.refreshInstance != null && state.refreshInstance != undefined); + if (!state.canPullDown) { + return + } + + // console.log("touchStart"); + + state.height = 0; + state.touchStartY = e.touches[0].pageY || e.changedTouches[0].pageY; + state.refreshInstance.setStyle({ + 'height': 0 + }); + state.refreshInstance.callMethod("onchange", true); +} + +function touchMove(e, ownerInstance) { + var instance = e.instance; + var state = instance.getState(); + if (!state.canPullDown) { + return + } + + var oldHeight = state.height; + var endY = e.touches[0].pageY || e.changedTouches[0].pageY; + var height = endY - state.touchStartY; + if (height > pullDown.maxHeight) { + return; + } + + var refreshInstance = state.refreshInstance; + refreshInstance.setStyle({ + 'height': height + 'px' + }); + + height = height < pullDown.maxHeight ? height : pullDown.maxHeight; + state.height = height; + refreshInstance.callMethod(pullDown.callPullingDown, { + height: height + }); +} + +function touchEnd(e, ownerInstance) { + var state = e.instance.getState(); + if (!state.canPullDown) { + return + } + + state.refreshInstance.callMethod("onchange", false); + + var refreshInstance = state.refreshInstance; + if (state.height > pullDown.threshold) { + refreshInstance.callMethod(pullDown.callRefresh); + return; + } + + refreshInstance.setStyle({ + 'height': 0 + }); +} + +function propObserver(newValue, oldValue, instance) { + pullDown = newValue; +} + +module.exports = { + touchmove: touchMove, + touchstart: touchStart, + touchend: touchEnd, + propObserver: propObserver +} diff --git a/im-uniapp/uni_modules/uni-list/package.json b/im-uniapp/uni_modules/uni-list/package.json new file mode 100644 index 0000000..8350efc --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-list", + "displayName": "uni-list 列表", + "version": "1.2.14", + "description": "List 组件 ,帮助使用者快速构建列表。", + "keywords": [ + "", + "uni-ui", + "uniui", + "列表", + "", + "list" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-badge", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-list/readme.md b/im-uniapp/uni_modules/uni-list/readme.md new file mode 100644 index 0000000..32c2865 --- /dev/null +++ b/im-uniapp/uni_modules/uni-list/readme.md @@ -0,0 +1,346 @@ +## List 列表 +> **组件名:uni-list** +> 代码块: `uList`、`uListItem` +> 关联组件:`uni-list-item`、`uni-badge`、`uni-icons`、`uni-list-chat`、`uni-list-ad` + + +List 列表组件,包含基本列表样式、可扩展插槽机制、长列表性能优化、多端兼容。 + +在vue页面里,它默认使用页面级滚动。在app-nvue页面里,它默认使用原生list组件滚动。这样的长列表,在滚动出屏幕外后,系统会回收不可见区域的渲染内存资源,不会造成滚动越长手机越卡的问题。 + +uni-list组件是父容器,里面的核心是uni-list-item子组件,它代表列表中的一个可重复行,子组件可以无限循环。 + +uni-list-item有很多风格,uni-list-item组件通过内置的属性,满足一些常用的场景。当内置属性不满足需求时,可以通过扩展插槽来自定义列表内容。 + +内置属性可以覆盖的场景包括:导航列表、设置列表、小图标列表、通信录列表、聊天记录列表。 + +涉及很多大图或丰富内容的列表,比如类今日头条的新闻列表、类淘宝的电商列表,需要通过扩展插槽实现。 + +下文均有样例给出。 + +uni-list不包含下拉刷新和上拉翻页。上拉翻页另见组件:[uni-load-more](https://ext.dcloud.net.cn/plugin?id=29) + + +### 安装方式 + +本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。 + +如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55) + +> **注意事项** +> 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。 +> - 组件需要依赖 `sass` 插件 ,请自行手动安装 +> - 组件内部依赖 `'uni-icons'` 、`uni-badge` 组件 +> - `uni-list` 和 `uni-list-item` 需要配套使用,暂不支持单独使用 `uni-list-item` +> - 只有开启点击反馈后,会有点击选中效果 +> - 使用插槽时,可以完全自定义内容 +> - note 、rightText 属性暂时没做限制,不支持文字溢出隐藏,使用时应该控制长度显示或通过默认插槽自行扩展 +> - 支付宝小程序平台需要在支付宝小程序开发者工具里开启 component2 编译模式,开启方式: 详情 --> 项目配置 --> 启用 component2 编译 +> - 如果需要修改 `switch`、`badge` 样式,请使用插槽自定义 +> - 在 `HBuilderX` 低版本中,可能会出现组件显示 `undefined` 的问题,请升级最新的 `HBuilderX` 或者 `cli` +> - 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + +### 基本用法 + +- 设置 `title` 属性,可以显示列表标题 +- 设置 `disabled` 属性,可以禁用当前项 + +```html + + + + + +``` + +### 多行内容显示 + +- 设置 `note` 属性 ,可以在第二行显示描述文本信息 + +```html + + + + + +``` + +### 右侧显示角标、switch + +- 设置 `show-badge` 属性 ,可以显示角标内容 +- 设置 `show-switch` 属性,可以显示 switch 开关 + +```html + + + + + +``` + +### 左侧显示略缩图、图标 + +- 设置 `thumb` 属性 ,可以在列表左侧显示略缩图 +- 设置 `show-extra-icon` 属性,并指定 `extra-icon` 可以在左侧显示图标 + +```html + + + + +``` + +### 开启点击反馈和右侧箭头 +- 设置 `clickable` 为 `true` ,则表示这是一个可点击的列表,会默认给一个点击效果,并可以监听 `click` 事件 +- 设置 `link` 属性,会自动开启点击反馈,并给列表右侧添加一个箭头 +- 设置 `to` 属性,可以跳转页面,`link` 的值表示跳转方式,如果不指定,默认为 `navigateTo` + +```html + + + + + + + +``` + + +### 聊天列表示例 +- 设置 `clickable` 为 `true` ,则表示这是一个可点击的列表,会默认给一个点击效果,并可以监听 `click` 事件 +- 设置 `link` 属性,会自动开启点击反馈,`link` 的值表示跳转方式,如果不指定,默认为 `navigateTo` +- 设置 `to` 属性,可以跳转页面 +- `time` 属性,通常会设置成时间显示,但是这个属性不仅仅可以设置时间,你可以传入任何文本,注意文本长度可能会影响显示 +- `avatar` 和 `avatarList` 属性同时只会有一个生效,同时设置的话,`avatarList` 属性的长度大于1 ,`avatar` 属性将失效 +- 可以通过默认插槽自定义列表右侧内容 + +```html + + + + + + + + + + + + + + + + + 刚刚 + + + + + + + +``` + +```javascript + +export default { + components: {}, + data() { + return { + avatarList: [{ + url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' + }, { + url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' + }, { + url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' + }] + } + } +} + +``` + + +```css + +.chat-custom-right { + flex: 1; + /* #ifndef APP-NVUE */ + display: flex; + /* #endif */ + flex-direction: column; + justify-content: space-between; + align-items: flex-end; +} + +.chat-custom-text { + font-size: 12px; + color: #999; +} + +``` + +## API + +### List Props + +属性名 |类型 |默认值 | 说明 +:-: |:-: |:-: | :-: +border |Boolean |true | 是否显示边框 + + +### ListItem Props + +属性名 |类型 |默认值 | 说明 +:-: |:-: |:-: | :-: +title |String |- | 标题 +note |String |- | 描述 +ellipsis |Number |0 | title 是否溢出隐藏,可选值,0:默认; 1:显示一行; 2:显示两行;【nvue 暂不支持】 +thumb |String |- | 左侧缩略图,若thumb有值,则不会显示扩展图标 +thumbSize |String |medium | 略缩图尺寸,可选值,lg:大图; medium:一般; sm:小图; +showBadge |Boolean |false | 是否显示数字角标 +badgeText |String |- | 数字角标内容 +badgeType |String |- | 数字角标类型,参考[uni-icons](https://ext.dcloud.net.cn/plugin?id=21) +badgeStyle |Object |- | 数字角标样式,使用uni-badge的custom-style参数 +rightText |String |- | 右侧文字内容 +disabled |Boolean |false | 是否禁用 +showArrow |Boolean |true | 是否显示箭头图标 +link |String |navigateTo | 新页面跳转方式,可选值见下表 +to |String |- | 新页面跳转地址,如填写此属性,click 会返回页面是否跳转成功 +clickable |Boolean |false | 是否开启点击反馈 +showSwitch |Boolean |false | 是否显示Switch +switchChecked |Boolean |false | Switch是否被选中 +showExtraIcon |Boolean |false | 左侧是否显示扩展图标 +extraIcon |Object |- | 扩展图标参数,格式为 ``{color: '#4cd964',size: '22',type: 'spinner'}``,参考 [uni-icons](https://ext.dcloud.net.cn/plugin?id=28) +direction | String |row | 排版方向,可选值,row:水平排列; column:垂直排列; 3个插槽是水平排还是垂直排,也受此属性控制 + + +#### Link Options + +属性名 | 说明 +:-: | :-: +navigateTo | 同 uni.navigateTo() +redirectTo | 同 uni.reLaunch() +reLaunch | 同 uni.reLaunch() +switchTab | 同 uni.switchTab() + +### ListItem Events + +事件称名 |说明 |返回参数 +:-: |:-: |:-: +click |点击 uniListItem 触发事件,需开启点击反馈 |- +switchChange |点击切换 Switch 时触发,需显示 switch |e={value:checked} + + + +### ListItem Slots + +名称 | 说明 +:-: | :-: +header | 左/上内容插槽,可完全自定义默认显示 +body | 中间内容插槽,可完全自定义中间内容 +footer | 右/下内容插槽,可完全自定义右侧内容 + + +> **通过插槽扩展** +> 需要注意的是当使用插槽时,内置样式将会失效,只保留排版样式,此时的样式需要开发者自己实现 +> 如果 `uni-list-item` 组件内置属性样式无法满足需求,可以使用插槽来自定义uni-list-item里的内容。 +> uni-list-item提供了3个可扩展的插槽:`header`、`body`、`footer` +> - 当 `direction` 属性为 `row` 时表示水平排列,此时 `header` 表示列表的左边部分,`body` 表示列表的中间部分,`footer` 表示列表的右边部分 +> - 当 `direction` 属性为 `column` 时表示垂直排列,此时 `header` 表示列表的上边部分,`body` 表示列表的中间部分,`footer` 表示列表的下边部分 +> 开发者可以只用1个插槽,也可以3个一起使用。在插槽中可自主编写view标签,实现自己所需的效果。 + + +**示例** + +```html + + + + + + + + + 自定义插槽 + + + + +``` + + + + + +### ListItemChat Props + +属性名 |类型 |默认值 | 说明 +:-: |:-: |:-: | :-: +title |String |- | 标题 +note |String |- | 描述 +clickable |Boolean |false | 是否开启点击反馈 +badgeText |String |- | 数字角标内容,设置为 `dot` 将显示圆点 +badgePositon |String |right | 角标位置 +link |String |navigateTo | 是否展示右侧箭头并开启点击反馈,可选值见下表 +clickable |Boolean |false | 是否开启点击反馈 +to |String |- | 跳转页面地址,如填写此属性,click 会返回页面是否跳转成功 +time |String |- | 右侧时间显示 +avatarCircle |Boolean |false | 是否显示圆形头像 +avatar |String |- | 头像地址,avatarCircle 不填时生效 +avatarList |Array |- | 头像组,格式为 [{url:''}] + +#### Link Options + +属性名 | 说明 +:-: | :-: +navigateTo | 同 uni.navigateTo() +redirectTo | 同 uni.reLaunch() +reLaunch | 同 uni.reLaunch() +switchTab | 同 uni.switchTab() + +### ListItemChat Slots + +名称 | 说明 +:- | :- +default | 自定义列表右侧内容(包括时间和角标显示) + +### ListItemChat Events +事件称名 | 说明 | 返回参数 +:-: | :-: | :-: +@click | 点击 uniListChat 触发事件 | {data:{}} ,如有 to 属性,会返回页面跳转信息 + + + + + + +## 基于uni-list扩展的页面模板 + +通过扩展插槽,可实现多种常见样式的列表 + +**新闻列表类** + +1. 云端一体混合布局:[https://ext.dcloud.net.cn/plugin?id=2546](https://ext.dcloud.net.cn/plugin?id=2546) +2. 云端一体垂直布局,大图模式:[https://ext.dcloud.net.cn/plugin?id=2583](https://ext.dcloud.net.cn/plugin?id=2583) +3. 云端一体垂直布局,多行图文混排:[https://ext.dcloud.net.cn/plugin?id=2584](https://ext.dcloud.net.cn/plugin?id=2584) +4. 云端一体垂直布局,多图模式:[https://ext.dcloud.net.cn/plugin?id=2585](https://ext.dcloud.net.cn/plugin?id=2585) +5. 云端一体水平布局,左图右文:[https://ext.dcloud.net.cn/plugin?id=2586](https://ext.dcloud.net.cn/plugin?id=2586) +6. 云端一体水平布局,左文右图:[https://ext.dcloud.net.cn/plugin?id=2587](https://ext.dcloud.net.cn/plugin?id=2587) +7. 云端一体垂直布局,无图模式,主标题+副标题:[https://ext.dcloud.net.cn/plugin?id=2588](https://ext.dcloud.net.cn/plugin?id=2588) + +**商品列表类** + +1. 云端一体列表/宫格视图互切:[https://ext.dcloud.net.cn/plugin?id=2651](https://ext.dcloud.net.cn/plugin?id=2651) +2. 云端一体列表(宫格模式):[https://ext.dcloud.net.cn/plugin?id=2671](https://ext.dcloud.net.cn/plugin?id=2671) +3. 云端一体列表(列表模式):[https://ext.dcloud.net.cn/plugin?id=2672](https://ext.dcloud.net.cn/plugin?id=2672) + +## 组件示例 + +点击查看:[https://hellouniapp.dcloud.net.cn/pages/extUI/list/list](https://hellouniapp.dcloud.net.cn/pages/extUI/list/list) \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-load-more/changelog.md b/im-uniapp/uni_modules/uni-load-more/changelog.md new file mode 100644 index 0000000..8f03f1d --- /dev/null +++ b/im-uniapp/uni_modules/uni-load-more/changelog.md @@ -0,0 +1,19 @@ +## 1.3.3(2022-01-20) +- 新增 showText属性 ,是否显示文本 +## 1.3.2(2022-01-19) +- 修复 nvue 平台下不显示文本的bug +## 1.3.1(2022-01-19) +- 修复 微信小程序平台样式选择器报警告的问题 +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-load-more](https://uniapp.dcloud.io/component/uniui/uni-load-more) +## 1.2.1(2021-08-24) +- 新增 支持国际化 +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.8(2021-05-12) +- 新增 组件示例地址 +## 1.1.7(2021-03-30) +- 修复 uni-load-more 在首页使用时,h5 平台报 'uni is not defined' 的 bug +## 1.1.6(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/en.json b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/en.json new file mode 100644 index 0000000..a4f14a5 --- /dev/null +++ b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/en.json @@ -0,0 +1,5 @@ +{ + "uni-load-more.contentdown": "Pull up to show more", + "uni-load-more.contentrefresh": "loading...", + "uni-load-more.contentnomore": "No more data" +} diff --git a/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/index.js b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hans.json new file mode 100644 index 0000000..f15d510 --- /dev/null +++ b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hans.json @@ -0,0 +1,5 @@ +{ + "uni-load-more.contentdown": "上拉显示更多", + "uni-load-more.contentrefresh": "正在加载...", + "uni-load-more.contentnomore": "没有更多数据了" +} diff --git a/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hant.json new file mode 100644 index 0000000..a255c6d --- /dev/null +++ b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "uni-load-more.contentdown": "上拉顯示更多", + "uni-load-more.contentrefresh": "正在加載...", + "uni-load-more.contentnomore": "沒有更多數據了" +} diff --git a/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/uni-load-more.vue b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/uni-load-more.vue new file mode 100644 index 0000000..e5eff4d --- /dev/null +++ b/im-uniapp/uni_modules/uni-load-more/components/uni-load-more/uni-load-more.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-load-more/package.json b/im-uniapp/uni_modules/uni-load-more/package.json new file mode 100644 index 0000000..2fa6f04 --- /dev/null +++ b/im-uniapp/uni_modules/uni-load-more/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-load-more", + "displayName": "uni-load-more 加载更多", + "version": "1.3.3", + "description": "LoadMore 组件,常用在列表里面,做滚动加载使用。", + "keywords": [ + "uni-ui", + "uniui", + "加载更多", + "load-more" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-load-more/readme.md b/im-uniapp/uni_modules/uni-load-more/readme.md new file mode 100644 index 0000000..54dc1fa --- /dev/null +++ b/im-uniapp/uni_modules/uni-load-more/readme.md @@ -0,0 +1,14 @@ + + +### LoadMore 加载更多 +> **组件名:uni-load-more** +> 代码块: `uLoadMore` + + +用于列表中,做滚动加载使用,展示 loading 的各种状态。 + + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-load-more) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/im-uniapp/uni_modules/uni-nav-bar/changelog.md b/im-uniapp/uni_modules/uni-nav-bar/changelog.md new file mode 100644 index 0000000..0f9a2f1 --- /dev/null +++ b/im-uniapp/uni_modules/uni-nav-bar/changelog.md @@ -0,0 +1,51 @@ +## 1.3.11(2023-03-29) +- 修复 自定义状态栏高度闪动BUG +## 1.3.10(2023-03-29) +- 修复 暗黑模式下边线颜色错误的bug +## 1.3.9(2022-10-13) +- 修复 条件编译错误的bug +## 1.3.8(2022-10-12) +- 修复 nvue 环境 fixed 为 true 的情况下,无法置顶的 bug +## 1.3.7(2022-08-11) +- 修复 nvue 环境下 fixed 为 true 的情况下,无法置顶的 bug +## 1.3.6(2022-06-30) +- 修复 组件示例中插槽用法无法显示内容的bug +## 1.3.5(2022-05-24) +- 新增 stat 属性 ,可开启统计title 上报 ,仅使用了title 属性且项目开启了uni统计生效 +## 1.3.4(2022-01-24) +- 更新 组件示例 +## 1.3.3(2022-01-24) +- 新增 left-width/right-width属性 ,可修改左右两侧的宽度 +## 1.3.2(2022-01-18) +- 修复 在vue下,标题不垂直居中的bug +## 1.3.1(2022-01-18) +- 修复 height 属性类型错误 +## 1.3.0(2022-01-18) +- 新增 height 属性,可修改组件高度 +- 新增 dark 属性可可开启暗黑模式 +- 优化 标题字数过多显示省略号 +- 优化 插槽,插入内容可完全覆盖 +## 1.2.1(2022-01-10) +- 修复 color 属性不生效的bug +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-nav-bar](https://uniapp.dcloud.io/component/uniui/uni-nav-bar) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.11(2021-05-12) +- 新增 组件示例地址 +## 1.0.10(2021-04-30) +- 修复 在nvue下fixed为true,宽度不能撑满的Bug +## 1.0.9(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.8(2021-04-14) +- uni-ui 修复 uni-nav-bar 当 fixed 属性为 true 时铺不满屏幕的 bug + +## 1.0.7(2021-02-25) +- 修复 easycom 下,找不到 uni-status-bar 的bug + +## 1.0.6(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.5(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue b/im-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue new file mode 100644 index 0000000..c890860 --- /dev/null +++ b/im-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue @@ -0,0 +1,357 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar.vue b/im-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar.vue new file mode 100644 index 0000000..4ac73ae --- /dev/null +++ b/im-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-nav-bar/package.json b/im-uniapp/uni_modules/uni-nav-bar/package.json new file mode 100644 index 0000000..240ae95 --- /dev/null +++ b/im-uniapp/uni_modules/uni-nav-bar/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-nav-bar", + "displayName": "uni-nav-bar 自定义导航栏", + "version": "1.3.11", + "description": "自定义导航栏组件,主要用于头部导航。", + "keywords": [ + "uni-ui", + "导航", + "导航栏", + "自定义导航栏" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-nav-bar/readme.md b/im-uniapp/uni_modules/uni-nav-bar/readme.md new file mode 100644 index 0000000..3934b32 --- /dev/null +++ b/im-uniapp/uni_modules/uni-nav-bar/readme.md @@ -0,0 +1,15 @@ + + +## NavBar 导航栏 +> **组件名:uni-nav-bar** +> 代码块: `uNavBar` + +导航栏组件,主要用于头部导航。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-nav-bar) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + + + + diff --git a/im-uniapp/uni_modules/uni-notice-bar/changelog.md b/im-uniapp/uni_modules/uni-notice-bar/changelog.md new file mode 100644 index 0000000..d526811 --- /dev/null +++ b/im-uniapp/uni_modules/uni-notice-bar/changelog.md @@ -0,0 +1,18 @@ +## 1.2.1(2022-09-05) +- 新增 属性 fontSize,可修改文字大小。 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-notice-bar](https://uniapp.dcloud.io/component/uniui/uni-notice-bar) +## 1.1.1(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.9(2021-05-12) +- 新增 组件示例地址 +## 1.0.8(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.7(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.6(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-notice-bar/components/uni-notice-bar/uni-notice-bar.vue b/im-uniapp/uni_modules/uni-notice-bar/components/uni-notice-bar/uni-notice-bar.vue new file mode 100644 index 0000000..98d4720 --- /dev/null +++ b/im-uniapp/uni_modules/uni-notice-bar/components/uni-notice-bar/uni-notice-bar.vue @@ -0,0 +1,426 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-notice-bar/package.json b/im-uniapp/uni_modules/uni-notice-bar/package.json new file mode 100644 index 0000000..8d9b13c --- /dev/null +++ b/im-uniapp/uni_modules/uni-notice-bar/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-notice-bar", + "displayName": "uni-notice-bar 通告栏", + "version": "1.2.1", + "description": "NoticeBar 通告栏组件,常用于展示公告信息,可设为滚动公告", + "keywords": [ + "uni-ui", + "uniui", + "通告栏", + "公告", + "跑马灯" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-notice-bar/readme.md b/im-uniapp/uni_modules/uni-notice-bar/readme.md new file mode 100644 index 0000000..fb2ede2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-notice-bar/readme.md @@ -0,0 +1,13 @@ + + +## NoticeBar 通告栏 +> **组件名:uni-notice-bar** +> 代码块: `uNoticeBar` + + +通告栏组件 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-notice-bar) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/im-uniapp/uni_modules/uni-number-box/changelog.md b/im-uniapp/uni_modules/uni-number-box/changelog.md new file mode 100644 index 0000000..aab060d --- /dev/null +++ b/im-uniapp/uni_modules/uni-number-box/changelog.md @@ -0,0 +1,29 @@ +## 1.2.3(2023-05-23) +更新示例工程 +## 1.2.2(2023-05-08) +- 修复 change 事件执行顺序错误的问题 +## 1.2.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-number-box](https://uniapp.dcloud.io/component/uniui/uni-number-box) +## 1.1.2(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.1.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-04-20) +- 修复 uni-number-box 浮点数运算不精确的 bug +- 修复 uni-number-box change 事件触发不正确的 bug +- 新增 uni-number-box v-model 双向绑定 +## 1.0.5(2021-02-05) +- 调整为uni_modules目录规范 + +## 1.0.7(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 支持 v-model +- 新增 支持 focus、blur 事件 +- 新增 支持 PC 端 diff --git a/im-uniapp/uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue b/im-uniapp/uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue new file mode 100644 index 0000000..40786f5 --- /dev/null +++ b/im-uniapp/uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue @@ -0,0 +1,221 @@ + + + diff --git a/im-uniapp/uni_modules/uni-number-box/package.json b/im-uniapp/uni_modules/uni-number-box/package.json new file mode 100644 index 0000000..743d2e4 --- /dev/null +++ b/im-uniapp/uni_modules/uni-number-box/package.json @@ -0,0 +1,82 @@ +{ + "id": "uni-number-box", + "displayName": "uni-number-box 数字输入框", + "version": "1.2.3", + "description": "NumberBox 带加减按钮的数字输入框组件,用户可以控制每次点击增加的数值,支持小数。", + "keywords": [ + "uni-ui", + "uniui", + "数字输入框" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-number-box/readme.md b/im-uniapp/uni_modules/uni-number-box/readme.md new file mode 100644 index 0000000..affc56f --- /dev/null +++ b/im-uniapp/uni_modules/uni-number-box/readme.md @@ -0,0 +1,13 @@ + + +## NumberBox 数字输入框 +> **组件名:uni-number-box** +> 代码块: `uNumberBox` + + +带加减按钮的数字输入框。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-number-box) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/im-uniapp/uni_modules/uni-pagination/changelog.md b/im-uniapp/uni_modules/uni-pagination/changelog.md new file mode 100644 index 0000000..2e94adc --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/changelog.md @@ -0,0 +1,27 @@ +## 1.2.4(2022-09-19) +- 修复,未对主题色设置默认色,导致未引入 uni-scss 变量文件报错。 +- 修复,未对移动端当前页文字做主题色适配。 +## 1.2.3(2022-09-15) +- 修复未使用 uni-scss 主题色的 bug。 +## 1.2.2(2022-07-06) +- 修复 es 语言 i18n 错误 +## 1.2.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-pagination](https://uniapp.dcloud.io/component/uniui/uni-pagination) +## 1.1.2(2021-10-08) +- 修复 current 、value 属性未监听,导致高亮样式失效的 bug +## 1.1.1(2021-08-20) +- 新增 支持国际化 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-04-12) +- 新增 PC 和 移动端适配不同的 ui +## 1.0.5(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/en.json b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/en.json new file mode 100644 index 0000000..d6e2897 --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/en.json @@ -0,0 +1,5 @@ +{ + "uni-pagination.prevText": "prev", + "uni-pagination.nextText": "next", + "uni-pagination.piecePerPage": "piece/page" +} diff --git a/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json new file mode 100644 index 0000000..604a113 --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json @@ -0,0 +1,5 @@ +{ + "uni-pagination.prevText": "anterior", + "uni-pagination.nextText": "prxima", + "uni-pagination.piecePerPage": "Art¨ªculo/P¨¢gina" +} diff --git a/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/fr.json b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/fr.json new file mode 100644 index 0000000..a7a0c77 --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/fr.json @@ -0,0 +1,5 @@ +{ + "uni-pagination.prevText": "précédente", + "uni-pagination.nextText": "suivante", + "uni-pagination.piecePerPage": "Articles/Pages" +} diff --git a/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/index.js b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/index.js new file mode 100644 index 0000000..2469dd0 --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/index.js @@ -0,0 +1,12 @@ +import en from './en.json' +import es from './es.json' +import fr from './fr.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + es, + fr, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hans.json new file mode 100644 index 0000000..782bbe4 --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hans.json @@ -0,0 +1,5 @@ +{ + "uni-pagination.prevText": "上一页", + "uni-pagination.nextText": "下一页", + "uni-pagination.piecePerPage": "条/页" +} diff --git a/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hant.json new file mode 100644 index 0000000..180fddb --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "uni-pagination.prevText": "上一頁", + "uni-pagination.nextText": "下一頁", + "uni-pagination.piecePerPage": "條/頁" +} diff --git a/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/uni-pagination.vue b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/uni-pagination.vue new file mode 100644 index 0000000..5305b5f --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/components/uni-pagination/uni-pagination.vue @@ -0,0 +1,465 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-pagination/package.json b/im-uniapp/uni_modules/uni-pagination/package.json new file mode 100644 index 0000000..862d5ab --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/package.json @@ -0,0 +1,83 @@ +{ + "id": "uni-pagination", + "displayName": "uni-pagination 分页器", + "version": "1.2.4", + "description": "Pagination 分页器组件,用于展示页码、请求数据等。", + "keywords": [ + "uni-ui", + "uniui", + "分页器", + "页码" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-icons"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-pagination/readme.md b/im-uniapp/uni_modules/uni-pagination/readme.md new file mode 100644 index 0000000..97ea1d6 --- /dev/null +++ b/im-uniapp/uni_modules/uni-pagination/readme.md @@ -0,0 +1,11 @@ + + +## Pagination 分页器 +> **组件名:uni-pagination** +> 代码块: `uPagination` + + +分页器组件,用于展示页码、请求数据等。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-pagination) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/im-uniapp/uni_modules/uni-popup/changelog.md b/im-uniapp/uni_modules/uni-popup/changelog.md new file mode 100644 index 0000000..bc59f07 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/changelog.md @@ -0,0 +1,68 @@ +## 1.8.3(2023-04-17) +- 修复 uni-popup 重复打开时的 bug +## 1.8.2(2023-02-02) +- uni-popup-dialog 组件新增 inputType 属性 +## 1.8.1(2022-12-01) +- 修复 nvue 下 v-show 报错 +## 1.8.0(2022-11-29) +- 优化 主题样式 +## 1.7.9(2022-04-02) +- 修复 弹出层内部无法滚动的bug +## 1.7.8(2022-03-28) +- 修复 小程序中高度错误的bug +## 1.7.7(2022-03-17) +- 修复 快速调用open出现问题的Bug +## 1.7.6(2022-02-14) +- 修复 safeArea 属性不能设置为false的bug +## 1.7.5(2022-01-19) +- 修复 isMaskClick 失效的bug +## 1.7.4(2022-01-19) +- 新增 cancelText \ confirmText 属性 ,可自定义文本 +- 新增 maskBackgroundColor 属性 ,可以修改蒙版颜色 +- 优化 maskClick属性 更新为 isMaskClick ,解决微信小程序警告的问题 +## 1.7.3(2022-01-13) +- 修复 设置 safeArea 属性不生效的bug +## 1.7.2(2021-11-26) +- 优化 组件示例 +## 1.7.1(2021-11-26) +- 修复 vuedoc 文字错误 +## 1.7.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-popup](https://uniapp.dcloud.io/component/uniui/uni-popup) +## 1.6.2(2021-08-24) +- 新增 支持国际化 +## 1.6.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.6.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.5.0(2021-06-23) +- 新增 mask-click 遮罩层点击事件 +## 1.4.5(2021-06-22) +- 修复 nvue 平台中间弹出后,点击内容,再点击遮罩无法关闭的Bug +## 1.4.4(2021-06-18) +- 修复 H5平台中间弹出后,点击内容,再点击遮罩无法关闭的Bug +## 1.4.3(2021-06-08) +- 修复 错误的 watch 字段 +- 修复 safeArea 属性不生效的问题 +- 修复 点击内容,再点击遮罩无法关闭的Bug +## 1.4.2(2021-05-12) +- 新增 组件示例地址 +## 1.4.1(2021-04-29) +- 修复 组件内放置 input 、textarea 组件,无法聚焦的问题 +## 1.4.0 (2021-04-29) +- 新增 type 属性的 left\right 值,支持左右弹出 +- 新增 open(String:type) 方法参数 ,可以省略 type 属性 ,直接传入类型打开指定弹窗 +- 新增 backgroundColor 属性,可定义主窗口背景色,默认不显示背景色 +- 新增 safeArea 属性,是否适配底部安全区 +- 修复 App\h5\微信小程序底部安全区占位不对的Bug +- 修复 App 端弹出等待的Bug +- 优化 提升低配设备性能,优化动画卡顿问题 +- 优化 更简单的组件自定义方式 +## 1.2.9(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.2.8(2021-02-05) +- 调整为uni_modules目录规范 +## 1.2.7(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 支持 PC 端 +- 新增 uni-popup-message 、uni-popup-dialog扩展组件支持 PC 端 diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/keypress.js b/im-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/keypress.js new file mode 100644 index 0000000..6ef26a2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + this.$once('hook:beforeDestroy', () => { + document.removeEventListener('keyup', listener) + }) + }, + render: () => {} +} +// #endif diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/uni-popup-dialog.vue b/im-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/uni-popup-dialog.vue new file mode 100644 index 0000000..b5eee79 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/uni-popup-dialog.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup-message/uni-popup-message.vue b/im-uniapp/uni_modules/uni-popup/components/uni-popup-message/uni-popup-message.vue new file mode 100644 index 0000000..91370a8 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup-message/uni-popup-message.vue @@ -0,0 +1,143 @@ + + + + diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup-share/uni-popup-share.vue b/im-uniapp/uni_modules/uni-popup/components/uni-popup-share/uni-popup-share.vue new file mode 100644 index 0000000..5be7624 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup-share/uni-popup-share.vue @@ -0,0 +1,187 @@ + + + + diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/en.json b/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/en.json new file mode 100644 index 0000000..7f1bd06 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/en.json @@ -0,0 +1,7 @@ +{ + "uni-popup.cancel": "cancel", + "uni-popup.ok": "ok", + "uni-popup.placeholder": "pleace enter", + "uni-popup.title": "Hint", + "uni-popup.shareTitle": "Share to" +} diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/index.js b/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hans.json new file mode 100644 index 0000000..5e3003c --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "uni-popup.cancel": "取消", + "uni-popup.ok": "确定", + "uni-popup.placeholder": "请输入", + "uni-popup.title": "提示", + "uni-popup.shareTitle": "分享到" +} diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hant.json new file mode 100644 index 0000000..13e39eb --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hant.json @@ -0,0 +1,7 @@ +{ + "uni-popup.cancel": "取消", + "uni-popup.ok": "確定", + "uni-popup.placeholder": "請輸入", + "uni-popup.title": "提示", + "uni-popup.shareTitle": "分享到" +} diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup/keypress.js b/im-uniapp/uni_modules/uni-popup/components/uni-popup/keypress.js new file mode 100644 index 0000000..62dda46 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + // this.$once('hook:beforeDestroy', () => { + // document.removeEventListener('keyup', listener) + // }) + }, + render: () => {} +} +// #endif diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup/popup.js b/im-uniapp/uni_modules/uni-popup/components/uni-popup/popup.js new file mode 100644 index 0000000..c4e5781 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup/popup.js @@ -0,0 +1,26 @@ + +export default { + data() { + return { + + } + }, + created(){ + this.popup = this.getParent() + }, + methods:{ + /** + * 获取父元素实例 + */ + getParent(name = 'uniPopup') { + let parent = this.$parent; + let parentName = parent.$options.name; + while (parentName !== name) { + parent = parent.$parent; + if (!parent) return false + parentName = parent.$options.name; + } + return parent; + }, + } +} diff --git a/im-uniapp/uni_modules/uni-popup/components/uni-popup/uni-popup.vue b/im-uniapp/uni_modules/uni-popup/components/uni-popup/uni-popup.vue new file mode 100644 index 0000000..54afee2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/components/uni-popup/uni-popup.vue @@ -0,0 +1,473 @@ + + + + diff --git a/im-uniapp/uni_modules/uni-popup/package.json b/im-uniapp/uni_modules/uni-popup/package.json new file mode 100644 index 0000000..f40556b --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-popup", + "displayName": "uni-popup 弹出层", + "version": "1.8.3", + "description": " Popup 组件,提供常用的弹层", + "keywords": [ + "uni-ui", + "弹出层", + "弹窗", + "popup", + "弹框" + ], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-transition" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-popup/readme.md b/im-uniapp/uni_modules/uni-popup/readme.md new file mode 100644 index 0000000..fdad4b3 --- /dev/null +++ b/im-uniapp/uni_modules/uni-popup/readme.md @@ -0,0 +1,17 @@ + + +## Popup 弹出层 +> **组件名:uni-popup** +> 代码块: `uPopup` +> 关联组件:`uni-transition` + + +弹出层组件,在应用中弹出一个消息提示窗口、提示框等 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-popup) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + + + + diff --git a/im-uniapp/uni_modules/uni-rate/changelog.md b/im-uniapp/uni_modules/uni-rate/changelog.md new file mode 100644 index 0000000..8a98a61 --- /dev/null +++ b/im-uniapp/uni_modules/uni-rate/changelog.md @@ -0,0 +1,25 @@ +## 1.3.1(2022-02-25) +- 修复 条件判断 `NaN` 错误的 bug +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-rate](https://uniapp.dcloud.io/component/uniui/uni-rate) +## 1.2.2(2021-09-10) +- 优化 默认值修改为 0 颗星 +## 1.2.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.2(2021-05-12) +- 新增 组件示例地址 +## 1.1.1(2021-04-21) +- 修复 布局变化后 uni-rate 星星计算不准确的 bug +- 优化 添加依赖 uni-icons, 导入 uni-rate 自动下载依赖 +## 1.1.0(2021-04-16) +- 修复 uni-rate 属性 margin 值为 string 组件失效的 bug + +## 1.0.9(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.8(2021-02-05) +- 调整为uni_modules目录规范 +- 支持 pc 端 diff --git a/im-uniapp/uni_modules/uni-rate/components/uni-rate/uni-rate.vue b/im-uniapp/uni_modules/uni-rate/components/uni-rate/uni-rate.vue new file mode 100644 index 0000000..857f5f9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-rate/components/uni-rate/uni-rate.vue @@ -0,0 +1,361 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-rate/package.json b/im-uniapp/uni_modules/uni-rate/package.json new file mode 100644 index 0000000..64e8e33 --- /dev/null +++ b/im-uniapp/uni_modules/uni-rate/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-rate", + "displayName": "uni-rate 评分", + "version": "1.3.1", + "description": "Rate 评分组件,可自定义评分星星图标的大小、间隔、评分数。", + "keywords": [ + "uni-ui", + "uniui", + "评分" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-rate/readme.md b/im-uniapp/uni_modules/uni-rate/readme.md new file mode 100644 index 0000000..eae7b5c --- /dev/null +++ b/im-uniapp/uni_modules/uni-rate/readme.md @@ -0,0 +1,12 @@ + + +## Rate 评分 +> **组件名:uni-rate** +> 代码块: `uRate` +> 关联组件:`uni-icons` + + +评分组件,多用于购买商品后,对商品进行评价等场景 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-rate) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-row/changelog.md b/im-uniapp/uni_modules/uni-row/changelog.md new file mode 100644 index 0000000..5b465bc --- /dev/null +++ b/im-uniapp/uni_modules/uni-row/changelog.md @@ -0,0 +1,10 @@ +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-row](https://uniapp.dcloud.io/component/uniui/uni-row) +## 0.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.0.4(2021-05-12) +- 新增 组件示例地址 +## 0.0.3(2021-02-05) +- 调整为uni_modules目录规范 +- 新增uni-row组件 diff --git a/im-uniapp/uni_modules/uni-row/components/uni-col/uni-col.vue b/im-uniapp/uni_modules/uni-row/components/uni-col/uni-col.vue new file mode 100644 index 0000000..84e2deb --- /dev/null +++ b/im-uniapp/uni_modules/uni-row/components/uni-col/uni-col.vue @@ -0,0 +1,317 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-row/components/uni-row/uni-row.vue b/im-uniapp/uni_modules/uni-row/components/uni-row/uni-row.vue new file mode 100644 index 0000000..f8e8542 --- /dev/null +++ b/im-uniapp/uni_modules/uni-row/components/uni-row/uni-row.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-row/package.json b/im-uniapp/uni_modules/uni-row/package.json new file mode 100644 index 0000000..3f52fa6 --- /dev/null +++ b/im-uniapp/uni_modules/uni-row/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-row", + "displayName": "uni-row 布局-行", + "version": "1.0.0", + "description": "流式栅格系统,随着屏幕或视口分为 24 份,可以迅速简便地创建布局。", + "keywords": [ + "uni-ui", + "uniui", + "栅格", + "布局", + "layout" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "u" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-row/readme.md b/im-uniapp/uni_modules/uni-row/readme.md new file mode 100644 index 0000000..3c9c8b9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-row/readme.md @@ -0,0 +1,10 @@ +## Layout 布局 + +> **组件名 uni-row、uni-col** +> 代码块: `uRow`、`uCol` + + +流式栅格系统,随着屏幕或视口分为 24 份,可以迅速简便地创建布局。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-row) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-scss/changelog.md b/im-uniapp/uni_modules/uni-scss/changelog.md new file mode 100644 index 0000000..b863bb0 --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/changelog.md @@ -0,0 +1,8 @@ +## 1.0.3(2022-01-21) +- 优化 组件示例 +## 1.0.2(2021-11-22) +- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题 +## 1.0.1(2021-11-22) +- 修复 vue3中scss语法兼容问题 +## 1.0.0(2021-11-18) +- init diff --git a/im-uniapp/uni_modules/uni-scss/index.scss b/im-uniapp/uni_modules/uni-scss/index.scss new file mode 100644 index 0000000..1744a5f --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/index.scss @@ -0,0 +1 @@ +@import './styles/index.scss'; diff --git a/im-uniapp/uni_modules/uni-scss/package.json b/im-uniapp/uni_modules/uni-scss/package.json new file mode 100644 index 0000000..7cc0ccb --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/package.json @@ -0,0 +1,82 @@ +{ + "id": "uni-scss", + "displayName": "uni-scss 辅助样式", + "version": "1.0.3", + "description": "uni-sass是uni-ui提供的一套全局样式 ,通过一些简单的类名和sass变量,实现简单的页面布局操作,比如颜色、边距、圆角等。", + "keywords": [ + "uni-scss", + "uni-ui", + "辅助样式" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.0" + }, + "dcloudext": { + "category": [ + "JS SDK", + "通用 SDK" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "n", + "联盟": "n" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-scss/readme.md b/im-uniapp/uni_modules/uni-scss/readme.md new file mode 100644 index 0000000..b7d1c25 --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/readme.md @@ -0,0 +1,4 @@ +`uni-sass` 是 `uni-ui`提供的一套全局样式 ,通过一些简单的类名和`sass`变量,实现简单的页面布局操作,比如颜色、边距、圆角等。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-sass) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-scss/styles/index.scss b/im-uniapp/uni_modules/uni-scss/styles/index.scss new file mode 100644 index 0000000..ffac4fe --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/index.scss @@ -0,0 +1,7 @@ +@import './setting/_variables.scss'; +@import './setting/_border.scss'; +@import './setting/_color.scss'; +@import './setting/_space.scss'; +@import './setting/_radius.scss'; +@import './setting/_text.scss'; +@import './setting/_styles.scss'; diff --git a/im-uniapp/uni_modules/uni-scss/styles/setting/_border.scss b/im-uniapp/uni_modules/uni-scss/styles/setting/_border.scss new file mode 100644 index 0000000..12a11c3 --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/setting/_border.scss @@ -0,0 +1,3 @@ +.uni-border { + border: 1px $uni-border-1 solid; +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-scss/styles/setting/_color.scss b/im-uniapp/uni_modules/uni-scss/styles/setting/_color.scss new file mode 100644 index 0000000..1ededd9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/setting/_color.scss @@ -0,0 +1,66 @@ + +// TODO 暂时不需要 class ,需要用户使用变量实现 ,如果使用类名其实并不推荐 +// @mixin get-styles($k,$c) { +// @if $k == size or $k == weight{ +// font-#{$k}:#{$c} +// }@else{ +// #{$k}:#{$c} +// } +// } +$uni-ui-color:( + // 主色 + primary: $uni-primary, + primary-disable: $uni-primary-disable, + primary-light: $uni-primary-light, + // 辅助色 + success: $uni-success, + success-disable: $uni-success-disable, + success-light: $uni-success-light, + warning: $uni-warning, + warning-disable: $uni-warning-disable, + warning-light: $uni-warning-light, + error: $uni-error, + error-disable: $uni-error-disable, + error-light: $uni-error-light, + info: $uni-info, + info-disable: $uni-info-disable, + info-light: $uni-info-light, + // 中性色 + main-color: $uni-main-color, + base-color: $uni-base-color, + secondary-color: $uni-secondary-color, + extra-color: $uni-extra-color, + // 背景色 + bg-color: $uni-bg-color, + // 边框颜色 + border-1: $uni-border-1, + border-2: $uni-border-2, + border-3: $uni-border-3, + border-4: $uni-border-4, + // 黑色 + black:$uni-black, + // 白色 + white:$uni-white, + // 透明 + transparent:$uni-transparent +) !default; +@each $key, $child in $uni-ui-color { + .uni-#{"" + $key} { + color: $child; + } + .uni-#{"" + $key}-bg { + background-color: $child; + } +} +.uni-shadow-sm { + box-shadow: $uni-shadow-sm; +} +.uni-shadow-base { + box-shadow: $uni-shadow-base; +} +.uni-shadow-lg { + box-shadow: $uni-shadow-lg; +} +.uni-mask { + background-color:$uni-mask; +} diff --git a/im-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss b/im-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss new file mode 100644 index 0000000..9a0428b --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss @@ -0,0 +1,55 @@ +@mixin radius($r,$d:null ,$important: false){ + $radius-value:map-get($uni-radius, $r) if($important, !important, null); + // Key exists within the $uni-radius variable + @if (map-has-key($uni-radius, $r) and $d){ + @if $d == t { + border-top-left-radius:$radius-value; + border-top-right-radius:$radius-value; + }@else if $d == r { + border-top-right-radius:$radius-value; + border-bottom-right-radius:$radius-value; + }@else if $d == b { + border-bottom-left-radius:$radius-value; + border-bottom-right-radius:$radius-value; + }@else if $d == l { + border-top-left-radius:$radius-value; + border-bottom-left-radius:$radius-value; + }@else if $d == tl { + border-top-left-radius:$radius-value; + }@else if $d == tr { + border-top-right-radius:$radius-value; + }@else if $d == br { + border-bottom-right-radius:$radius-value; + }@else if $d == bl { + border-bottom-left-radius:$radius-value; + } + }@else{ + border-radius:$radius-value; + } +} + +@each $key, $child in $uni-radius { + @if($key){ + .uni-radius-#{"" + $key} { + @include radius($key) + } + }@else{ + .uni-radius { + @include radius($key) + } + } +} + +@each $direction in t, r, b, l,tl, tr, br, bl { + @each $key, $child in $uni-radius { + @if($key){ + .uni-radius-#{"" + $direction}-#{"" + $key} { + @include radius($key,$direction,false) + } + }@else{ + .uni-radius-#{$direction} { + @include radius($key,$direction,false) + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-scss/styles/setting/_space.scss b/im-uniapp/uni_modules/uni-scss/styles/setting/_space.scss new file mode 100644 index 0000000..3c89528 --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/setting/_space.scss @@ -0,0 +1,56 @@ + +@mixin fn($space,$direction,$size,$n) { + @if $n { + #{$space}-#{$direction}: #{$size*$uni-space-root}px + } @else { + #{$space}-#{$direction}: #{-$size*$uni-space-root}px + } +} +@mixin get-styles($direction,$i,$space,$n){ + @if $direction == t { + @include fn($space, top,$i,$n); + } + @if $direction == r { + @include fn($space, right,$i,$n); + } + @if $direction == b { + @include fn($space, bottom,$i,$n); + } + @if $direction == l { + @include fn($space, left,$i,$n); + } + @if $direction == x { + @include fn($space, left,$i,$n); + @include fn($space, right,$i,$n); + } + @if $direction == y { + @include fn($space, top,$i,$n); + @include fn($space, bottom,$i,$n); + } + @if $direction == a { + @if $n { + #{$space}:#{$i*$uni-space-root}px; + } @else { + #{$space}:#{-$i*$uni-space-root}px; + } + } +} + +@each $orientation in m,p { + $space: margin; + @if $orientation == m { + $space: margin; + } @else { + $space: padding; + } + @for $i from 0 through 16 { + @each $direction in t, r, b, l, x, y, a { + .uni-#{$orientation}#{$direction}-#{$i} { + @include get-styles($direction,$i,$space,true); + } + .uni-#{$orientation}#{$direction}-n#{$i} { + @include get-styles($direction,$i,$space,false); + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss b/im-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss new file mode 100644 index 0000000..689afec --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss @@ -0,0 +1,167 @@ +/* #ifndef APP-NVUE */ + +$-color-white:#fff; +$-color-black:#000; +@mixin base-style($color) { + color: #fff; + background-color: $color; + border-color: mix($-color-black, $color, 8%); + &:not([hover-class]):active { + background: mix($-color-black, $color, 10%); + border-color: mix($-color-black, $color, 20%); + color: $-color-white; + outline: none; + } +} +@mixin is-color($color) { + @include base-style($color); + &[loading] { + @include base-style($color); + &::before { + margin-right:5px; + } + } + &[disabled] { + &, + &[loading], + &:not([hover-class]):active { + color: $-color-white; + border-color: mix(darken($color,10%), $-color-white); + background-color: mix($color, $-color-white); + } + } + +} +@mixin base-plain-style($color) { + color:$color; + background-color: mix($-color-white, $color, 90%); + border-color: mix($-color-white, $color, 70%); + &:not([hover-class]):active { + background: mix($-color-white, $color, 80%); + color: $color; + outline: none; + border-color: mix($-color-white, $color, 50%); + } +} +@mixin is-plain($color){ + &[plain] { + @include base-plain-style($color); + &[loading] { + @include base-plain-style($color); + &::before { + margin-right:5px; + } + } + &[disabled] { + &, + &:active { + color: mix($-color-white, $color, 40%); + background-color: mix($-color-white, $color, 90%); + border-color: mix($-color-white, $color, 80%); + } + } + } +} + + +.uni-btn { + margin: 5px; + color: #393939; + border:1px solid #ccc; + font-size: 16px; + font-weight: 200; + background-color: #F9F9F9; + // TODO 暂时处理边框隐藏一边的问题 + overflow: visible; + &::after{ + border: none; + } + + &:not([type]),&[type=default] { + color: #999; + &[loading] { + background: none; + &::before { + margin-right:5px; + } + } + + + + &[disabled]{ + color: mix($-color-white, #999, 60%); + &, + &[loading], + &:active { + color: mix($-color-white, #999, 60%); + background-color: mix($-color-white,$-color-black , 98%); + border-color: mix($-color-white, #999, 85%); + } + } + + &[plain] { + color: #999; + background: none; + border-color: $uni-border-1; + &:not([hover-class]):active { + background: none; + color: mix($-color-white, $-color-black, 80%); + border-color: mix($-color-white, $-color-black, 90%); + outline: none; + } + &[disabled]{ + &, + &[loading], + &:active { + background: none; + color: mix($-color-white, #999, 60%); + border-color: mix($-color-white, #999, 85%); + } + } + } + } + + &:not([hover-class]):active { + color: mix($-color-white, $-color-black, 50%); + } + + &[size=mini] { + font-size: 16px; + font-weight: 200; + border-radius: 8px; + } + + + + &.uni-btn-small { + font-size: 14px; + } + &.uni-btn-mini { + font-size: 12px; + } + + &.uni-btn-radius { + border-radius: 999px; + } + &[type=primary] { + @include is-color($uni-primary); + @include is-plain($uni-primary) + } + &[type=success] { + @include is-color($uni-success); + @include is-plain($uni-success) + } + &[type=error] { + @include is-color($uni-error); + @include is-plain($uni-error) + } + &[type=warning] { + @include is-color($uni-warning); + @include is-plain($uni-warning) + } + &[type=info] { + @include is-color($uni-info); + @include is-plain($uni-info) + } +} +/* #endif */ diff --git a/im-uniapp/uni_modules/uni-scss/styles/setting/_text.scss b/im-uniapp/uni_modules/uni-scss/styles/setting/_text.scss new file mode 100644 index 0000000..a34d08f --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/setting/_text.scss @@ -0,0 +1,24 @@ +@mixin get-styles($k,$c) { + @if $k == size or $k == weight{ + font-#{$k}:#{$c} + }@else{ + #{$k}:#{$c} + } +} + +@each $key, $child in $uni-headings { + /* #ifndef APP-NVUE */ + .uni-#{$key} { + @each $k, $c in $child { + @include get-styles($k,$c) + } + } + /* #endif */ + /* #ifdef APP-NVUE */ + .container .uni-#{$key} { + @each $k, $c in $child { + @include get-styles($k,$c) + } + } + /* #endif */ +} diff --git a/im-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss b/im-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss new file mode 100644 index 0000000..557d3d7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss @@ -0,0 +1,146 @@ +// @use "sass:math"; +@import '../tools/functions.scss'; +// 间距基础倍数 +$uni-space-root: 2 !default; +// 边框半径默认值 +$uni-radius-root:5px !default; +$uni-radius: () !default; +// 边框半径断点 +$uni-radius: map-deep-merge( + ( + 0: 0, + // TODO 当前版本暂时不支持 sm 属性 + // 'sm': math.div($uni-radius-root, 2), + null: $uni-radius-root, + 'lg': $uni-radius-root * 2, + 'xl': $uni-radius-root * 6, + 'pill': 9999px, + 'circle': 50% + ), + $uni-radius +); +// 字体家族 +$body-font-family: 'Roboto', sans-serif !default; +// 文本 +$heading-font-family: $body-font-family !default; +$uni-headings: () !default; +$letterSpacing: -0.01562em; +$uni-headings: map-deep-merge( + ( + 'h1': ( + size: 32px, + weight: 300, + line-height: 50px, + // letter-spacing:-0.01562em + ), + 'h2': ( + size: 28px, + weight: 300, + line-height: 40px, + // letter-spacing: -0.00833em + ), + 'h3': ( + size: 24px, + weight: 400, + line-height: 32px, + // letter-spacing: normal + ), + 'h4': ( + size: 20px, + weight: 400, + line-height: 30px, + // letter-spacing: 0.00735em + ), + 'h5': ( + size: 16px, + weight: 400, + line-height: 24px, + // letter-spacing: normal + ), + 'h6': ( + size: 14px, + weight: 500, + line-height: 18px, + // letter-spacing: 0.0125em + ), + 'subtitle': ( + size: 12px, + weight: 400, + line-height: 20px, + // letter-spacing: 0.00937em + ), + 'body': ( + font-size: 14px, + font-weight: 400, + line-height: 22px, + // letter-spacing: 0.03125em + ), + 'caption': ( + 'size': 12px, + 'weight': 400, + 'line-height': 20px, + // 'letter-spacing': 0.03333em, + // 'text-transform': false + ) + ), + $uni-headings +); + + + +// 主色 +$uni-primary: #2979ff !default; +$uni-primary-disable:lighten($uni-primary,20%) !default; +$uni-primary-light: lighten($uni-primary,25%) !default; + +// 辅助色 +// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。 +$uni-success: #18bc37 !default; +$uni-success-disable:lighten($uni-success,20%) !default; +$uni-success-light: lighten($uni-success,25%) !default; + +$uni-warning: #f3a73f !default; +$uni-warning-disable:lighten($uni-warning,20%) !default; +$uni-warning-light: lighten($uni-warning,25%) !default; + +$uni-error: #e43d33 !default; +$uni-error-disable:lighten($uni-error,20%) !default; +$uni-error-light: lighten($uni-error,25%) !default; + +$uni-info: #8f939c !default; +$uni-info-disable:lighten($uni-info,20%) !default; +$uni-info-light: lighten($uni-info,25%) !default; + +// 中性色 +// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。 +$uni-main-color: #3a3a3a !default; // 主要文字 +$uni-base-color: #6a6a6a !default; // 常规文字 +$uni-secondary-color: #909399 !default; // 次要文字 +$uni-extra-color: #c7c7c7 !default; // 辅助说明 + +// 边框颜色 +$uni-border-1: #F0F0F0 !default; +$uni-border-2: #EDEDED !default; +$uni-border-3: #DCDCDC !default; +$uni-border-4: #B9B9B9 !default; + +// 常规色 +$uni-black: #000000 !default; +$uni-white: #ffffff !default; +$uni-transparent: rgba($color: #000000, $alpha: 0) !default; + +// 背景色 +$uni-bg-color: #f7f7f7 !default; + +/* 水平间距 */ +$uni-spacing-sm: 8px !default; +$uni-spacing-base: 15px !default; +$uni-spacing-lg: 30px !default; + +// 阴影 +$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5) !default; +$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2) !default; +$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5) !default; + +// 蒙版 +$uni-mask: rgba($color: #000000, $alpha: 0.4) !default; diff --git a/im-uniapp/uni_modules/uni-scss/styles/tools/functions.scss b/im-uniapp/uni_modules/uni-scss/styles/tools/functions.scss new file mode 100644 index 0000000..ac6f63e --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/styles/tools/functions.scss @@ -0,0 +1,19 @@ +// 合并 map +@function map-deep-merge($parent-map, $child-map){ + $result: $parent-map; + @each $key, $child in $child-map { + $parent-has-key: map-has-key($result, $key); + $parent-value: map-get($result, $key); + $parent-type: type-of($parent-value); + $child-type: type-of($child); + $parent-is-map: $parent-type == map; + $child-is-map: $child-type == map; + + @if (not $parent-has-key) or ($parent-type != $child-type) or (not ($parent-is-map and $child-is-map)){ + $result: map-merge($result, ( $key: $child )); + }@else { + $result: map-merge($result, ( $key: map-deep-merge($parent-value, $child) )); + } + } + @return $result; +}; diff --git a/im-uniapp/uni_modules/uni-scss/theme.scss b/im-uniapp/uni_modules/uni-scss/theme.scss new file mode 100644 index 0000000..80ee62f --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/theme.scss @@ -0,0 +1,31 @@ +// 间距基础倍数 +$uni-space-root: 2; +// 边框半径默认值 +$uni-radius-root:5px; +// 主色 +$uni-primary: #2979ff; +// 辅助色 +$uni-success: #4cd964; +// 警告色 +$uni-warning: #f0ad4e; +// 错误色 +$uni-error: #dd524d; +// 描述色 +$uni-info: #909399; +// 中性色 +$uni-main-color: #303133; +$uni-base-color: #606266; +$uni-secondary-color: #909399; +$uni-extra-color: #C0C4CC; +// 背景色 +$uni-bg-color: #f5f5f5; +// 边框颜色 +$uni-border-1: #DCDFE6; +$uni-border-2: #E4E7ED; +$uni-border-3: #EBEEF5; +$uni-border-4: #F2F6FC; + +// 常规色 +$uni-black: #000000; +$uni-white: #ffffff; +$uni-transparent: rgba($color: #000000, $alpha: 0); diff --git a/im-uniapp/uni_modules/uni-scss/variables.scss b/im-uniapp/uni_modules/uni-scss/variables.scss new file mode 100644 index 0000000..1c062d4 --- /dev/null +++ b/im-uniapp/uni_modules/uni-scss/variables.scss @@ -0,0 +1,62 @@ +@import './styles/setting/_variables.scss'; +// 间距基础倍数 +$uni-space-root: 2; +// 边框半径默认值 +$uni-radius-root:5px; + +// 主色 +$uni-primary: #2979ff; +$uni-primary-disable:mix(#fff,$uni-primary,50%); +$uni-primary-light: mix(#fff,$uni-primary,80%); + +// 辅助色 +// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。 +$uni-success: #18bc37; +$uni-success-disable:mix(#fff,$uni-success,50%); +$uni-success-light: mix(#fff,$uni-success,80%); + +$uni-warning: #f3a73f; +$uni-warning-disable:mix(#fff,$uni-warning,50%); +$uni-warning-light: mix(#fff,$uni-warning,80%); + +$uni-error: #e43d33; +$uni-error-disable:mix(#fff,$uni-error,50%); +$uni-error-light: mix(#fff,$uni-error,80%); + +$uni-info: #8f939c; +$uni-info-disable:mix(#fff,$uni-info,50%); +$uni-info-light: mix(#fff,$uni-info,80%); + +// 中性色 +// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。 +$uni-main-color: #3a3a3a; // 主要文字 +$uni-base-color: #6a6a6a; // 常规文字 +$uni-secondary-color: #909399; // 次要文字 +$uni-extra-color: #c7c7c7; // 辅助说明 + +// 边框颜色 +$uni-border-1: #F0F0F0; +$uni-border-2: #EDEDED; +$uni-border-3: #DCDCDC; +$uni-border-4: #B9B9B9; + +// 常规色 +$uni-black: #000000; +$uni-white: #ffffff; +$uni-transparent: rgba($color: #000000, $alpha: 0); + +// 背景色 +$uni-bg-color: #f7f7f7; + +/* 水平间距 */ +$uni-spacing-sm: 8px; +$uni-spacing-base: 15px; +$uni-spacing-lg: 30px; + +// 阴影 +$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5); +$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2); +$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5); + +// 蒙版 +$uni-mask: rgba($color: #000000, $alpha: 0.4); diff --git a/im-uniapp/uni_modules/uni-search-bar/changelog.md b/im-uniapp/uni_modules/uni-search-bar/changelog.md new file mode 100644 index 0000000..a01fcf2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-search-bar/changelog.md @@ -0,0 +1,35 @@ +## 1.2.4(2023-05-09) +- 修复 i18n 国际化不正确的 Bug +## 1.2.3(2022-05-24) +- 新增 readonly 属性,组件只读 +## 1.2.2(2022-05-06) +- 修复 vue3 input 事件不生效的bug +## 1.2.1(2022-05-06) +- 修复 多余代码导致的bug +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-search-bar](https://uniapp.dcloud.io/component/uniui/uni-search-bar) +## 1.1.2(2021-08-30) +- 修复 value 属性与 modelValue 属性不兼容的Bug +## 1.1.1(2021-08-24) +- 新增 支持国际化 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.9(2021-05-12) +- 新增 项目示例地址 +## 1.0.8(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.7(2021-04-15) +- uni-ui 新增 uni-search-bar 的 focus 事件 + +## 1.0.6(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.5(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 支持双向绑定 +- 更改 input 事件的返回值,e={value:Number} --> e=value +- 新增 支持图标插槽 +- 新增 支持 clear、blur 事件 +- 新增 支持 focus 属性 +- 去掉组件背景色 diff --git a/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/en.json b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/en.json new file mode 100644 index 0000000..dd083a5 --- /dev/null +++ b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/en.json @@ -0,0 +1,4 @@ +{ + "uni-search-bar.cancel": "cancel", + "uni-search-bar.placeholder": "Search enter content" +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/index.js b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hans.json new file mode 100644 index 0000000..d2a1ced --- /dev/null +++ b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hans.json @@ -0,0 +1,4 @@ +{ + "uni-search-bar.cancel": "取消", + "uni-search-bar.placeholder": "请输入搜索内容" +} diff --git a/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hant.json new file mode 100644 index 0000000..f1c96bc --- /dev/null +++ b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hant.json @@ -0,0 +1,4 @@ +{ + "uni-search-bar.cancel": "取消", + "uni-search-bar.placeholder": "請輸入搜索內容" +} diff --git a/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/uni-search-bar.vue b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/uni-search-bar.vue new file mode 100644 index 0000000..e0a7372 --- /dev/null +++ b/im-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/uni-search-bar.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-search-bar/package.json b/im-uniapp/uni_modules/uni-search-bar/package.json new file mode 100644 index 0000000..ede02ef --- /dev/null +++ b/im-uniapp/uni_modules/uni-search-bar/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-search-bar", + "displayName": "uni-search-bar 搜索栏", + "version": "1.2.4", + "description": "搜索栏组件,通常用于搜索商品、文章等", + "keywords": [ + "uni-ui", + "uniui", + "搜索框", + "搜索栏" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-search-bar/readme.md b/im-uniapp/uni_modules/uni-search-bar/readme.md new file mode 100644 index 0000000..253092f --- /dev/null +++ b/im-uniapp/uni_modules/uni-search-bar/readme.md @@ -0,0 +1,14 @@ + + +## SearchBar 搜索栏 + +> **组件名:uni-search-bar** +> 代码块: `uSearchBar` + + +搜索栏组件 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-search-bar) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/im-uniapp/uni_modules/uni-section/changelog.md b/im-uniapp/uni_modules/uni-section/changelog.md new file mode 100644 index 0000000..738f2b3 --- /dev/null +++ b/im-uniapp/uni_modules/uni-section/changelog.md @@ -0,0 +1,2 @@ +## 0.0.1(2022-07-22) +- 初始化 diff --git a/im-uniapp/uni_modules/uni-section/components/uni-section/uni-section.vue b/im-uniapp/uni_modules/uni-section/components/uni-section/uni-section.vue new file mode 100644 index 0000000..9a52e0b --- /dev/null +++ b/im-uniapp/uni_modules/uni-section/components/uni-section/uni-section.vue @@ -0,0 +1,167 @@ + + + + diff --git a/im-uniapp/uni_modules/uni-section/package.json b/im-uniapp/uni_modules/uni-section/package.json new file mode 100644 index 0000000..0a31fb5 --- /dev/null +++ b/im-uniapp/uni_modules/uni-section/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-section", + "displayName": "uni-section 标题栏", + "version": "0.0.1", + "description": "标题栏组件", + "keywords": [ + "uni-ui", + "uniui", + "标题栏" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-section/readme.md b/im-uniapp/uni_modules/uni-section/readme.md new file mode 100644 index 0000000..d47faab --- /dev/null +++ b/im-uniapp/uni_modules/uni-section/readme.md @@ -0,0 +1,8 @@ +## Section 标题栏 +> **组件名:uni-section** +> 代码块: `uSection` + +uni-section 组件主要用于文章、列表详情等标题展示 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-section) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/im-uniapp/uni_modules/uni-segmented-control/changelog.md b/im-uniapp/uni_modules/uni-segmented-control/changelog.md new file mode 100644 index 0000000..a44385d --- /dev/null +++ b/im-uniapp/uni_modules/uni-segmented-control/changelog.md @@ -0,0 +1,9 @@ +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-segmented-control](https://uniapp.dcloud.io/component/uniui/uni-segmented-control) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.5(2021-05-12) +- 新增 项目示例地址 +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-segmented-control/components/uni-segmented-control/uni-segmented-control.vue b/im-uniapp/uni_modules/uni-segmented-control/components/uni-segmented-control/uni-segmented-control.vue new file mode 100644 index 0000000..044a495 --- /dev/null +++ b/im-uniapp/uni_modules/uni-segmented-control/components/uni-segmented-control/uni-segmented-control.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-segmented-control/package.json b/im-uniapp/uni_modules/uni-segmented-control/package.json new file mode 100644 index 0000000..6cae41d --- /dev/null +++ b/im-uniapp/uni_modules/uni-segmented-control/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-segmented-control", + "displayName": "uni-segmented-control 分段器", + "version": "1.2.0", + "description": "分段器由至少 2 个分段控件组成,用作不同视图的显示", + "keywords": [ + "uni-ui", + "uniui", + "分段器", + "segement", + "顶部选择" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-segmented-control/readme.md b/im-uniapp/uni_modules/uni-segmented-control/readme.md new file mode 100644 index 0000000..3527b03 --- /dev/null +++ b/im-uniapp/uni_modules/uni-segmented-control/readme.md @@ -0,0 +1,13 @@ + + +## SegmentedControl 分段器 +> **组件名:uni-segmented-control** +> 代码块: `uSegmentedControl` + + +用作不同视图的显示 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-segmented-control) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/im-uniapp/uni_modules/uni-steps/changelog.md b/im-uniapp/uni_modules/uni-steps/changelog.md new file mode 100644 index 0000000..cb9d367 --- /dev/null +++ b/im-uniapp/uni_modules/uni-steps/changelog.md @@ -0,0 +1,16 @@ +## 1.1.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.1.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-steps](https://uniapp.dcloud.io/component/uniui/uni-steps) +## 1.0.8(2021-05-12) +- 新增 项目示例地址 +## 1.0.7(2021-05-06) +- 修复 uni-steps 横向布局时,多行文字高度不合理的 bug +## 1.0.6(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.5(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-steps/components/uni-steps/uni-steps.vue b/im-uniapp/uni_modules/uni-steps/components/uni-steps/uni-steps.vue new file mode 100644 index 0000000..a6c8f28 --- /dev/null +++ b/im-uniapp/uni_modules/uni-steps/components/uni-steps/uni-steps.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-steps/package.json b/im-uniapp/uni_modules/uni-steps/package.json new file mode 100644 index 0000000..c687b40 --- /dev/null +++ b/im-uniapp/uni_modules/uni-steps/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-steps", + "displayName": "uni-steps 步骤条", + "version": "1.1.1", + "description": "步骤条组件,提供横向和纵向两种布局格式。", + "keywords": [ + "uni-ui", + "uniui", + "步骤条", + "时间轴" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-steps/readme.md b/im-uniapp/uni_modules/uni-steps/readme.md new file mode 100644 index 0000000..da7a4bf --- /dev/null +++ b/im-uniapp/uni_modules/uni-steps/readme.md @@ -0,0 +1,13 @@ + + +## Steps 步骤条 +> **组件名:uni-steps** +> 代码块: `uSteps` + + +步骤条,常用于显示进度 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-steps) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/im-uniapp/uni_modules/uni-swipe-action/changelog.md b/im-uniapp/uni_modules/uni-swipe-action/changelog.md new file mode 100644 index 0000000..c916427 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/changelog.md @@ -0,0 +1,43 @@ +## 1.3.8(2023-04-13) +- 修复`uni-swipe-action`和`uni-swipe-action-item`不同时使用导致 closeOther 方法报错的 bug +## 1.3.7(2022-06-06) +- 修复 vue3 下使用组件不能正常运行的Bug +## 1.3.6(2022-05-31) +- 修复 h5端点击click触发两次的Bug +## 1.3.5(2022-05-23) +- 修复 isPC 找不到的Bug +## 1.3.4(2022-05-19) +- 修复 在 nvue 下 disabled 失效的bug +## 1.3.3(2022-03-31) +- 修复 按钮字体大小不能设置的bug +## 1.3.2(2022-03-16) +- 修复 h5和app端下报el错误的bug +## 1.3.1(2022-03-07) +- 修复 HBuilderX 1.4.X 版本中,h5和app端下报错的bug +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-swipe-action](https://uniapp.dcloud.io/component/uniui/uni-swipe-action) +## 1.2.4(2021-08-20) +- 优化 close-all 方法 +## 1.2.3(2021-08-20) +- 新增 close-all 方法,关闭所有已打开的组件 +## 1.2.2(2021-08-17) +- 新增 resize() 方法,在非微信小程序、h5、app-vue端出现不能滑动的问题的时候,重置组件 +- 修复 app 端偶尔出现类似 Page[x][-x,xx;-x,xx,x,x-x] 的问题 +- 优化 微信小程序、h5、app-vue 滑动逻辑,避免出现动态新增组件后不能滑动的问题 +## 1.2.1(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +- 修复 跨页面修改组件数据 ,导致不能滑动的问题 +## 1.1.10(2021-06-17) +- 修复 按钮点击执行两次的bug +## 1.1.9(2021-05-12) +- 新增 项目示例地址 +## 1.1.8(2021-03-26) +- 修复 微信小程序 nv_navigator is not defined 报错的bug +## 1.1.7(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 左侧滑动 +- 新增 插槽使用方式 +- 新增 threshold 属性,可以控制滑动缺省值 +- 优化 长列表滚动性能 +- 修复 滚动页面时触发组件滑动的Bug diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/bindingx.js b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/bindingx.js new file mode 100644 index 0000000..707e432 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/bindingx.js @@ -0,0 +1,302 @@ +let bindIngXMixins = {} + +// #ifdef APP-NVUE +const BindingX = uni.requireNativePlugin('bindingx'); +const dom = uni.requireNativePlugin('dom'); +const animation = uni.requireNativePlugin('animation'); + +bindIngXMixins = { + data() { + return {} + }, + + watch: { + show(newVal) { + if (this.autoClose) return + if (this.stop) return + this.stop = true + if (newVal) { + this.open(newVal) + } else { + this.close() + } + }, + leftOptions() { + this.getSelectorQuery() + this.init() + }, + rightOptions(newVal) { + this.init() + } + }, + created() { + this.swipeaction = this.getSwipeAction() + if (this.swipeaction && Array.isArray(this.swipeaction.children)) { + this.swipeaction.children.push(this) + } + }, + mounted() { + this.box = this.getEl(this.$refs['selector-box--hock']) + this.selector = this.getEl(this.$refs['selector-content--hock']); + this.leftButton = this.getEl(this.$refs['selector-left-button--hock']); + this.rightButton = this.getEl(this.$refs['selector-right-button--hock']); + this.init() + }, + // beforeDestroy() { + // this.swipeaction.children.forEach((item, index) => { + // if (item === this) { + // this.swipeaction.children.splice(index, 1) + // } + // }) + // }, + methods: { + init() { + this.$nextTick(() => { + this.x = 0 + this.button = { + show: false + } + setTimeout(() => { + this.getSelectorQuery() + }, 200) + }) + }, + onClick(index, item, position) { + this.$emit('click', { + content: item, + index, + position + }) + }, + touchstart(e) { + // fix by mehaotian 禁止滑动 + if (this.disabled) return + // 每次只触发一次,避免多次监听造成闪烁 + if (this.stop) return + this.stop = true + if (this.autoClose && this.swipeaction) { + this.swipeaction.closeOther(this) + } + + const leftWidth = this.button.left.width + const rightWidth = this.button.right.width + let expression = this.range(this.x, -rightWidth, leftWidth) + let leftExpression = this.range(this.x - leftWidth, -leftWidth, 0) + let rightExpression = this.range(this.x + rightWidth, 0, rightWidth) + + this.eventpan = BindingX.bind({ + anchor: this.box, + eventType: 'pan', + props: [{ + element: this.selector, + property: 'transform.translateX', + expression + }, { + element: this.leftButton, + property: 'transform.translateX', + expression: leftExpression + }, { + element: this.rightButton, + property: 'transform.translateX', + expression: rightExpression + }, ] + }, (e) => { + // nope + if (e.state === 'end') { + this.x = e.deltaX + this.x; + this.isclick = true + this.bindTiming(e.deltaX) + } + }); + }, + touchend(e) { + if (this.isopen !== 'none' && !this.isclick) { + this.open('none') + } + }, + bindTiming(x) { + const left = this.x + const leftWidth = this.button.left.width + const rightWidth = this.button.right.width + const threshold = this.threshold + if (!this.isopen || this.isopen === 'none') { + if (left > threshold) { + this.open('left') + } else if (left < -threshold) { + this.open('right') + } else { + this.open('none') + } + } else { + if ((x > -leftWidth && x < 0) || x > rightWidth) { + if ((x > -threshold && x < 0) || (x - rightWidth > threshold)) { + this.open('left') + } else { + this.open('none') + } + } else { + if ((x < threshold && x > 0) || (x + leftWidth < -threshold)) { + this.open('right') + } else { + this.open('none') + } + } + } + }, + + /** + * 移动范围 + * @param {Object} num + * @param {Object} mix + * @param {Object} max + */ + range(num, mix, max) { + return `min(max(x+${num}, ${mix}), ${max})` + }, + + /** + * 开启swipe + */ + open(type) { + this.animation(type) + }, + + /** + * 关闭swipe + */ + close() { + this.animation('none') + }, + + /** + * 开启关闭动画 + * @param {Object} type + */ + animation(type) { + const time = 300 + const leftWidth = this.button.left.width + const rightWidth = this.button.right.width + if (this.eventpan && this.eventpan.token) { + BindingX.unbind({ + token: this.eventpan.token, + eventType: 'pan' + }) + } + + switch (type) { + case 'left': + Promise.all([ + this.move(this.selector, leftWidth), + this.move(this.leftButton, 0), + this.move(this.rightButton, rightWidth * 2) + ]).then(() => { + this.setEmit(leftWidth, type) + }) + break + case 'right': + Promise.all([ + this.move(this.selector, -rightWidth), + this.move(this.leftButton, -leftWidth * 2), + this.move(this.rightButton, 0) + ]).then(() => { + this.setEmit(-rightWidth, type) + }) + break + default: + Promise.all([ + this.move(this.selector, 0), + this.move(this.leftButton, -leftWidth), + this.move(this.rightButton, rightWidth) + ]).then(() => { + this.setEmit(0, type) + }) + + } + }, + setEmit(x, type) { + const leftWidth = this.button.left.width + const rightWidth = this.button.right.width + this.isopen = this.isopen || 'none' + this.stop = false + this.isclick = false + // 只有状态不一致才会返回结果 + if (this.isopen !== type && this.x !== x) { + if (type === 'left' && leftWidth > 0) { + this.$emit('change', 'left') + } + if (type === 'right' && rightWidth > 0) { + this.$emit('change', 'right') + } + if (type === 'none') { + this.$emit('change', 'none') + } + } + this.x = x + this.isopen = type + }, + move(ref, value) { + return new Promise((resolve, reject) => { + animation.transition(ref, { + styles: { + transform: `translateX(${value})`, + }, + duration: 150, //ms + timingFunction: 'linear', + needLayout: false, + delay: 0 //ms + }, function(res) { + resolve(res) + }) + }) + + }, + + /** + * 获取ref + * @param {Object} el + */ + getEl(el) { + return el.ref + }, + /** + * 获取节点信息 + */ + getSelectorQuery() { + Promise.all([ + this.getDom('left'), + this.getDom('right'), + ]).then((data) => { + let show = 'none' + if (this.autoClose) { + show = 'none' + } else { + show = this.show + } + + if (show === 'none') { + // this.close() + } else { + this.open(show) + } + + }) + + }, + getDom(str) { + return new Promise((resolve, reject) => { + dom.getComponentRect(this.$refs[`selector-${str}-button--hock`], (data) => { + if (data) { + this.button[str] = data.size + resolve(data) + } else { + reject() + } + }) + }) + } + } +} + +// #endif + +export default bindIngXMixins diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/isPC.js b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/isPC.js new file mode 100644 index 0000000..917cb48 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/isPC.js @@ -0,0 +1,12 @@ +export function isPC() { + var userAgentInfo = navigator.userAgent; + var Agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]; + var flag = true; + for (let v = 0; v < Agents.length - 1; v++) { + if (userAgentInfo.indexOf(Agents[v]) > 0) { + flag = false; + break; + } + } + return flag; +} diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpalipay.js b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpalipay.js new file mode 100644 index 0000000..35c796b --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpalipay.js @@ -0,0 +1,195 @@ +export default { + data() { + return { + x: 0, + transition: false, + width: 0, + viewWidth: 0, + swipeShow: 0 + } + }, + watch: { + show(newVal) { + if (this.autoClose) return + if (newVal && newVal !== 'none') { + this.transition = true + this.open(newVal) + } else { + this.close() + } + } + }, + created() { + this.swipeaction = this.getSwipeAction() + if (this.swipeaction && Array.isArray(this.swipeaction.children)) { + this.swipeaction.children.push(this) + } + }, + mounted() { + this.isopen = false + setTimeout(() => { + this.getQuerySelect() + }, 50) + }, + methods: { + appTouchStart(e) { + const { + clientX + } = e.changedTouches[0] + this.clientX = clientX + this.timestamp = new Date().getTime() + }, + appTouchEnd(e, index, item, position) { + const { + clientX + } = e.changedTouches[0] + // fixed by xxxx 模拟点击事件,解决 ios 13 点击区域错位的问题 + let diff = Math.abs(this.clientX - clientX) + let time = (new Date().getTime()) - this.timestamp + if (diff < 40 && time < 300) { + this.$emit('click', { + content: item, + index, + position + }) + } + }, + /** + * 移动触发 + * @param {Object} e + */ + onChange(e) { + this.moveX = e.detail.x + this.isclose = false + }, + touchstart(e) { + this.transition = false + this.isclose = true + if (this.autoClose && this.swipeaction) { + this.swipeaction.closeOther(this) + } + }, + touchmove(e) {}, + touchend(e) { + // 0的位置什么都不执行 + if (this.isclose && this.isopen === 'none') return + if (this.isclose && this.isopen !== 'none') { + this.transition = true + this.close() + } else { + this.move(this.moveX + this.leftWidth) + } + }, + + /** + * 移动 + * @param {Object} moveX + */ + move(moveX) { + // 打开关闭的处理逻辑不太一样 + this.transition = true + // 未打开状态 + if (!this.isopen || this.isopen === 'none') { + if (moveX > this.threshold) { + this.open('left') + } else if (moveX < -this.threshold) { + this.open('right') + } else { + this.close() + } + } else { + if (moveX < 0 && moveX < this.rightWidth) { + const rightX = this.rightWidth + moveX + if (rightX < this.threshold) { + this.open('right') + } else { + this.close() + } + } else if (moveX > 0 && moveX < this.leftWidth) { + const leftX = this.leftWidth - moveX + if (leftX < this.threshold) { + this.open('left') + } else { + this.close() + } + } + + } + + }, + + /** + * 打开 + */ + open(type) { + this.x = this.moveX + this.animation(type) + }, + + /** + * 关闭 + */ + close() { + this.x = this.moveX + // TODO 解决 x 值不更新的问题,所以会多触发一次 nextTick ,待优化 + this.$nextTick(() => { + this.x = -this.leftWidth + if (this.isopen !== 'none') { + this.$emit('change', 'none') + } + this.isopen = 'none' + }) + }, + + /** + * 执行结束动画 + * @param {Object} type + */ + animation(type) { + this.$nextTick(() => { + if (type === 'left') { + this.x = 0 + } else { + this.x = -this.rightWidth - this.leftWidth + } + + if (this.isopen !== type) { + this.$emit('change', type) + } + this.isopen = type + }) + + }, + getSlide(x) {}, + getQuerySelect() { + const query = uni.createSelectorQuery().in(this); + query.selectAll('.movable-view--hock').boundingClientRect(data => { + this.leftWidth = data[1].width + this.rightWidth = data[2].width + this.width = data[0].width + this.viewWidth = this.width + this.rightWidth + this.leftWidth + if (this.leftWidth === 0) { + // TODO 疑似bug ,初始化的时候如果x 是0,会导致移动位置错误,所以让元素超出一点 + this.x = -0.1 + } else { + this.x = -this.leftWidth + } + this.moveX = this.x + this.$nextTick(() => { + this.swipeShow = 1 + }) + + if (!this.buttonWidth) { + this.disabledView = true + } + + if (this.autoClose) return + if (this.show !== 'none') { + this.transition = true + this.open(this.shows) + } + }).exec(); + + } + } +} diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpother.js b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpother.js new file mode 100644 index 0000000..d389bce --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpother.js @@ -0,0 +1,260 @@ +let otherMixins = {} + +// #ifndef APP-PLUS|| MP-WEIXIN || H5 +const MIN_DISTANCE = 10; +otherMixins = { + data() { + // TODO 随机生生元素ID,解决百度小程序获取同一个元素位置信息的bug + const elClass = `Uni_${Math.ceil(Math.random() * 10e5).toString(36)}` + return { + uniShow: false, + left: 0, + buttonShow: 'none', + ani: false, + moveLeft: '', + elClass + } + }, + watch: { + show(newVal) { + if (this.autoClose) return + this.openState(newVal) + }, + left() { + this.moveLeft = `translateX(${this.left}px)` + }, + buttonShow(newVal) { + if (this.autoClose) return + this.openState(newVal) + }, + leftOptions() { + this.init() + }, + rightOptions() { + this.init() + } + }, + mounted() { + this.swipeaction = this.getSwipeAction() + if (this.swipeaction && Array.isArray(this.swipeaction.children)) { + this.swipeaction.children.push(this) + } + this.init() + }, + methods: { + init() { + clearTimeout(this.timer) + this.timer = setTimeout(() => { + this.getSelectorQuery() + }, 100) + // 移动距离 + this.left = 0 + this.x = 0 + }, + + closeSwipe(e) { + if (this.autoClose && this.swipeaction) { + this.swipeaction.closeOther(this) + } + }, + appTouchStart(e) { + const { + clientX + } = e.changedTouches[0] + this.clientX = clientX + this.timestamp = new Date().getTime() + }, + appTouchEnd(e, index, item, position) { + const { + clientX + } = e.changedTouches[0] + // fixed by xxxx 模拟点击事件,解决 ios 13 点击区域错位的问题 + let diff = Math.abs(this.clientX - clientX) + let time = (new Date().getTime()) - this.timestamp + if (diff < 40 && time < 300) { + this.$emit('click', { + content: item, + index, + position + }) + } + }, + touchstart(e) { + if (this.disabled) return + this.ani = false + this.x = this.left || 0 + this.stopTouchStart(e) + this.autoClose && this.closeSwipe() + }, + touchmove(e) { + if (this.disabled) return + // 是否可以滑动页面 + this.stopTouchMove(e); + if (this.direction !== 'horizontal') { + return; + } + this.move(this.x + this.deltaX) + return false + }, + touchend() { + if (this.disabled) return + this.moveDirection(this.left) + }, + /** + * 设置移动距离 + * @param {Object} value + */ + move(value) { + value = value || 0 + const leftWidth = this.leftWidth + const rightWidth = this.rightWidth + // 获取可滑动范围 + this.left = this.range(value, -rightWidth, leftWidth); + }, + + /** + * 获取范围 + * @param {Object} num + * @param {Object} min + * @param {Object} max + */ + range(num, min, max) { + return Math.min(Math.max(num, min), max); + }, + /** + * 移动方向判断 + * @param {Object} left + * @param {Object} value + */ + moveDirection(left) { + const threshold = this.threshold + const isopen = this.isopen || 'none' + const leftWidth = this.leftWidth + const rightWidth = this.rightWidth + if (this.deltaX === 0) { + this.openState('none') + return + } + if ((isopen === 'none' && rightWidth > 0 && -left > threshold) || (isopen !== 'none' && rightWidth > + 0 && rightWidth + + left < threshold)) { + // right + this.openState('right') + } else if ((isopen === 'none' && leftWidth > 0 && left > threshold) || (isopen !== 'none' && leftWidth > + 0 && + leftWidth - left < threshold)) { + // left + this.openState('left') + } else { + // default + this.openState('none') + } + }, + + /** + * 开启状态 + * @param {Boolean} type + */ + openState(type) { + const leftWidth = this.leftWidth + const rightWidth = this.rightWidth + let left = '' + this.isopen = this.isopen ? this.isopen : 'none' + switch (type) { + case "left": + left = leftWidth + break + case "right": + left = -rightWidth + break + default: + left = 0 + } + + + if (this.isopen !== type) { + this.throttle = true + this.$emit('change', type) + } + + this.isopen = type + // 添加动画类 + this.ani = true + this.$nextTick(() => { + this.move(left) + }) + // 设置最终移动位置,理论上只要进入到这个函数,肯定是要打开的 + }, + close() { + this.openState('none') + }, + getDirection(x, y) { + if (x > y && x > MIN_DISTANCE) { + return 'horizontal'; + } + if (y > x && y > MIN_DISTANCE) { + return 'vertical'; + } + return ''; + }, + + /** + * 重置滑动状态 + * @param {Object} event + */ + resetTouchStatus() { + this.direction = ''; + this.deltaX = 0; + this.deltaY = 0; + this.offsetX = 0; + this.offsetY = 0; + }, + + /** + * 设置滑动开始位置 + * @param {Object} event + */ + stopTouchStart(event) { + this.resetTouchStatus(); + const touch = event.touches[0]; + this.startX = touch.clientX; + this.startY = touch.clientY; + }, + + /** + * 滑动中,是否禁止打开 + * @param {Object} event + */ + stopTouchMove(event) { + const touch = event.touches[0]; + this.deltaX = touch.clientX - this.startX; + this.deltaY = touch.clientY - this.startY; + this.offsetX = Math.abs(this.deltaX); + this.offsetY = Math.abs(this.deltaY); + this.direction = this.direction || this.getDirection(this.offsetX, this.offsetY); + }, + + getSelectorQuery() { + const views = uni.createSelectorQuery().in(this) + views + .selectAll('.' + this.elClass) + .boundingClientRect(data => { + if (data.length === 0) return + let show = 'none' + if (this.autoClose) { + show = 'none' + } else { + show = this.show + } + this.leftWidth = data[0].width || 0 + this.rightWidth = data[1].width || 0 + this.buttonShow = show + }) + .exec() + } + } +} + +// #endif + +export default otherMixins diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpwxs.js b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpwxs.js new file mode 100644 index 0000000..08de1c9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpwxs.js @@ -0,0 +1,84 @@ +let mpMixins = {} +let is_pc = null +// #ifdef H5 +import { + isPC +} from "./isPC" +is_pc = isPC() +// #endif +// #ifdef APP-VUE|| MP-WEIXIN || H5 + +mpMixins = { + data() { + return { + is_show: 'none' + } + }, + watch: { + show(newVal) { + this.is_show = this.show + } + }, + created() { + this.swipeaction = this.getSwipeAction() + if (this.swipeaction && Array.isArray(this.swipeaction.children)) { + this.swipeaction.children.push(this) + } + }, + mounted() { + this.is_show = this.show + }, + methods: { + // wxs 中调用 + closeSwipe(e) { + if (this.autoClose && this.swipeaction) { + this.swipeaction.closeOther(this) + } + }, + + change(e) { + this.$emit('change', e.open) + if (this.is_show !== e.open) { + this.is_show = e.open + } + }, + + appTouchStart(e) { + if (is_pc) return + const { + clientX + } = e.changedTouches[0] + this.clientX = clientX + this.timestamp = new Date().getTime() + }, + appTouchEnd(e, index, item, position) { + if (is_pc) return + const { + clientX + } = e.changedTouches[0] + // fixed by xxxx 模拟点击事件,解决 ios 13 点击区域错位的问题 + let diff = Math.abs(this.clientX - clientX) + let time = (new Date().getTime()) - this.timestamp + if (diff < 40 && time < 300) { + this.$emit('click', { + content: item, + index, + position + }) + } + }, + onClickForPC(index, item, position) { + if (!is_pc) return + // #ifdef H5 + this.$emit('click', { + content: item, + index, + position + }) + // #endif + } + } +} + +// #endif +export default mpMixins diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/render.js b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/render.js new file mode 100644 index 0000000..78f0ec6 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/render.js @@ -0,0 +1,270 @@ +const MIN_DISTANCE = 10; +export default { + showWatch(newVal, oldVal, ownerInstance, instance, self) { + var state = self.state + var $el = ownerInstance.$el || ownerInstance.$vm && ownerInstance.$vm.$el + if (!$el) return + this.getDom(instance, ownerInstance, self) + if (newVal && newVal !== 'none') { + this.openState(newVal, instance, ownerInstance, self) + return + } + + if (state.left) { + this.openState('none', instance, ownerInstance, self) + } + this.resetTouchStatus(instance, self) + }, + + /** + * 开始触摸操作 + * @param {Object} e + * @param {Object} ins + */ + touchstart(e, ownerInstance, self) { + let instance = e.instance; + let disabled = instance.getDataset().disabled + let state = self.state; + this.getDom(instance, ownerInstance, self) + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = this.getDisabledType(disabled) + if (disabled) return + // 开始触摸时移除动画类 + instance.requestAnimationFrame(function() { + instance.removeClass('ani'); + ownerInstance.callMethod('closeSwipe'); + }) + + // 记录上次的位置 + state.x = state.left || 0 + // 计算滑动开始位置 + this.stopTouchStart(e, ownerInstance, self) + }, + + /** + * 开始滑动操作 + * @param {Object} e + * @param {Object} ownerInstance + */ + touchmove(e, ownerInstance, self) { + let instance = e.instance; + // 删除之后已经那不到实例了 + if (!instance) return; + let disabled = instance.getDataset().disabled + let state = self.state + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = this.getDisabledType(disabled) + if (disabled) return + // 是否可以滑动页面 + this.stopTouchMove(e, self); + if (state.direction !== 'horizontal') { + return; + } + if (e.preventDefault) { + // 阻止页面滚动 + e.preventDefault() + } + let x = state.x + state.deltaX + this.move(x, instance, ownerInstance, self) + }, + + /** + * 结束触摸操作 + * @param {Object} e + * @param {Object} ownerInstance + */ + touchend(e, ownerInstance, self) { + let instance = e.instance; + let disabled = instance.getDataset().disabled + let state = self.state + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = this.getDisabledType(disabled) + + if (disabled) return + // 滑动过程中触摸结束,通过阙值判断是开启还是关闭 + // fixed by mehaotian 定时器解决点击按钮,touchend 触发比 click 事件时机早的问题 ,主要是 ios13 + this.moveDirection(state.left, instance, ownerInstance, self) + + }, + + /** + * 设置移动距离 + * @param {Object} value + * @param {Object} instance + * @param {Object} ownerInstance + */ + move(value, instance, ownerInstance, self) { + value = value || 0 + let state = self.state + let leftWidth = state.leftWidth + let rightWidth = state.rightWidth + // 获取可滑动范围 + state.left = this.range(value, -rightWidth, leftWidth); + instance.requestAnimationFrame(function() { + instance.setStyle({ + transform: 'translateX(' + state.left + 'px)', + '-webkit-transform': 'translateX(' + state.left + 'px)' + }) + }) + + }, + + /** + * 获取元素信息 + * @param {Object} instance + * @param {Object} ownerInstance + */ + getDom(instance, ownerInstance, self) { + var state = self.state + var $el = ownerInstance.$el || ownerInstance.$vm && ownerInstance.$vm.$el + var leftDom = $el.querySelector('.button-group--left') + var rightDom = $el.querySelector('.button-group--right') + + state.leftWidth = leftDom.offsetWidth || 0 + state.rightWidth = rightDom.offsetWidth || 0 + state.threshold = instance.getDataset().threshold + }, + + getDisabledType(value) { + return (typeof(value) === 'string' ? JSON.parse(value) : value) || false; + }, + + /** + * 获取范围 + * @param {Object} num + * @param {Object} min + * @param {Object} max + */ + range(num, min, max) { + return Math.min(Math.max(num, min), max); + }, + + + /** + * 移动方向判断 + * @param {Object} left + * @param {Object} value + * @param {Object} ownerInstance + * @param {Object} ins + */ + moveDirection(left, ins, ownerInstance, self) { + var state = self.state + var threshold = state.threshold + var position = state.position + var isopen = state.isopen || 'none' + var leftWidth = state.leftWidth + var rightWidth = state.rightWidth + if (state.deltaX === 0) { + this.openState('none', ins, ownerInstance, self) + return + } + if ((isopen === 'none' && rightWidth > 0 && -left > threshold) || (isopen !== 'none' && rightWidth > 0 && + rightWidth + + left < threshold)) { + // right + this.openState('right', ins, ownerInstance, self) + } else if ((isopen === 'none' && leftWidth > 0 && left > threshold) || (isopen !== 'none' && leftWidth > 0 && + leftWidth - left < threshold)) { + // left + this.openState('left', ins, ownerInstance, self) + } else { + // default + this.openState('none', ins, ownerInstance, self) + } + }, + + + /** + * 开启状态 + * @param {Boolean} type + * @param {Object} ins + * @param {Object} ownerInstance + */ + openState(type, ins, ownerInstance, self) { + let state = self.state + let leftWidth = state.leftWidth + let rightWidth = state.rightWidth + let left = '' + state.isopen = state.isopen ? state.isopen : 'none' + switch (type) { + case "left": + left = leftWidth + break + case "right": + left = -rightWidth + break + default: + left = 0 + } + + // && !state.throttle + + if (state.isopen !== type) { + state.throttle = true + ownerInstance.callMethod('change', { + open: type + }) + + } + + state.isopen = type + // 添加动画类 + ins.requestAnimationFrame(() => { + ins.addClass('ani'); + this.move(left, ins, ownerInstance, self) + }) + }, + + + getDirection(x, y) { + if (x > y && x > MIN_DISTANCE) { + return 'horizontal'; + } + if (y > x && y > MIN_DISTANCE) { + return 'vertical'; + } + return ''; + }, + + /** + * 重置滑动状态 + * @param {Object} event + */ + resetTouchStatus(instance, self) { + let state = self.state; + state.direction = ''; + state.deltaX = 0; + state.deltaY = 0; + state.offsetX = 0; + state.offsetY = 0; + }, + + /** + * 设置滑动开始位置 + * @param {Object} event + */ + stopTouchStart(event, ownerInstance, self) { + let instance = event.instance; + let state = self.state + this.resetTouchStatus(instance, self); + var touch = event.touches[0]; + state.startX = touch.clientX; + state.startY = touch.clientY; + }, + + /** + * 滑动中,是否禁止打开 + * @param {Object} event + */ + stopTouchMove(event, self) { + let instance = event.instance; + let state = self.state; + let touch = event.touches[0]; + + state.deltaX = touch.clientX - state.startX; + state.deltaY = touch.clientY - state.startY; + state.offsetY = Math.abs(state.deltaY); + state.offsetX = Math.abs(state.deltaX); + state.direction = state.direction || this.getDirection(state.offsetX, state.offsetY); + } +} diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/uni-swipe-action-item.vue b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/uni-swipe-action-item.vue new file mode 100644 index 0000000..d79c297 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/uni-swipe-action-item.vue @@ -0,0 +1,347 @@ + + + + + + diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/wx.wxs b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/wx.wxs new file mode 100644 index 0000000..b394244 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/wx.wxs @@ -0,0 +1,341 @@ +var MIN_DISTANCE = 10; + +/** + * 判断当前是否为H5、app-vue + */ +var IS_HTML5 = false +if (typeof window === 'object') IS_HTML5 = true + +/** + * 监听页面内值的变化,主要用于动态开关swipe-action + * @param {Object} newValue + * @param {Object} oldValue + * @param {Object} ownerInstance + * @param {Object} instance + */ +function showWatch(newVal, oldVal, ownerInstance, instance) { + var state = instance.getState() + getDom(instance, ownerInstance) + if (newVal && newVal !== 'none') { + openState(newVal, instance, ownerInstance) + return + } + + if (state.left) { + openState('none', instance, ownerInstance) + } + resetTouchStatus(instance) +} + +/** + * 开始触摸操作 + * @param {Object} e + * @param {Object} ins + */ +function touchstart(e, ownerInstance) { + var instance = e.instance; + var disabled = instance.getDataset().disabled + var state = instance.getState(); + getDom(instance, ownerInstance) + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false; + if (disabled) return + // 开始触摸时移除动画类 + instance.requestAnimationFrame(function() { + instance.removeClass('ani'); + ownerInstance.callMethod('closeSwipe'); + }) + + // 记录上次的位置 + state.x = state.left || 0 + // 计算滑动开始位置 + stopTouchStart(e, ownerInstance) +} + +/** + * 开始滑动操作 + * @param {Object} e + * @param {Object} ownerInstance + */ +function touchmove(e, ownerInstance) { + var instance = e.instance; + var disabled = instance.getDataset().disabled + var state = instance.getState() + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false; + if (disabled) return + // 是否可以滑动页面 + stopTouchMove(e); + if (state.direction !== 'horizontal') { + return; + } + + if (e.preventDefault) { + // 阻止页面滚动 + e.preventDefault() + } + + move(state.x + state.deltaX, instance, ownerInstance) +} + +/** + * 结束触摸操作 + * @param {Object} e + * @param {Object} ownerInstance + */ +function touchend(e, ownerInstance) { + var instance = e.instance; + var disabled = instance.getDataset().disabled + var state = instance.getState() + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false; + + if (disabled) return + // 滑动过程中触摸结束,通过阙值判断是开启还是关闭 + // fixed by mehaotian 定时器解决点击按钮,touchend 触发比 click 事件时机早的问题 ,主要是 ios13 + moveDirection(state.left, instance, ownerInstance) + +} + +/** + * 设置移动距离 + * @param {Object} value + * @param {Object} instance + * @param {Object} ownerInstance + */ +function move(value, instance, ownerInstance) { + value = value || 0 + var state = instance.getState() + var leftWidth = state.leftWidth + var rightWidth = state.rightWidth + // 获取可滑动范围 + state.left = range(value, -rightWidth, leftWidth); + instance.requestAnimationFrame(function() { + instance.setStyle({ + transform: 'translateX(' + state.left + 'px)', + '-webkit-transform': 'translateX(' + state.left + 'px)' + }) + }) + +} + +/** + * 获取元素信息 + * @param {Object} instance + * @param {Object} ownerInstance + */ +function getDom(instance, ownerInstance) { + var state = instance.getState() + var leftDom = ownerInstance.selectComponent('.button-group--left') + var rightDom = ownerInstance.selectComponent('.button-group--right') + var leftStyles = { + width: 0 + } + var rightStyles = { + width: 0 + } + leftStyles = leftDom.getBoundingClientRect() + rightStyles = rightDom.getBoundingClientRect() + + state.leftWidth = leftStyles.width || 0 + state.rightWidth = rightStyles.width || 0 + state.threshold = instance.getDataset().threshold +} + +/** + * 获取范围 + * @param {Object} num + * @param {Object} min + * @param {Object} max + */ +function range(num, min, max) { + return Math.min(Math.max(num, min), max); +} + + +/** + * 移动方向判断 + * @param {Object} left + * @param {Object} value + * @param {Object} ownerInstance + * @param {Object} ins + */ +function moveDirection(left, ins, ownerInstance) { + var state = ins.getState() + var threshold = state.threshold + var position = state.position + var isopen = state.isopen || 'none' + var leftWidth = state.leftWidth + var rightWidth = state.rightWidth + if (state.deltaX === 0) { + openState('none', ins, ownerInstance) + return + } + if ((isopen === 'none' && rightWidth > 0 && -left > threshold) || (isopen !== 'none' && rightWidth > 0 && + rightWidth + + left < threshold)) { + // right + openState('right', ins, ownerInstance) + } else if ((isopen === 'none' && leftWidth > 0 && left > threshold) || (isopen !== 'none' && leftWidth > 0 && + leftWidth - left < threshold)) { + // left + openState('left', ins, ownerInstance) + } else { + // default + openState('none', ins, ownerInstance) + } +} + + +/** + * 开启状态 + * @param {Boolean} type + * @param {Object} ins + * @param {Object} ownerInstance + */ +function openState(type, ins, ownerInstance) { + var state = ins.getState() + var leftWidth = state.leftWidth + var rightWidth = state.rightWidth + var left = '' + state.isopen = state.isopen ? state.isopen : 'none' + switch (type) { + case "left": + left = leftWidth + break + case "right": + left = -rightWidth + break + default: + left = 0 + } + + // && !state.throttle + + if (state.isopen !== type) { + state.throttle = true + ownerInstance.callMethod('change', { + open: type + }) + + } + + state.isopen = type + // 添加动画类 + ins.requestAnimationFrame(function() { + ins.addClass('ani'); + move(left, ins, ownerInstance) + }) + // 设置最终移动位置,理论上只要进入到这个函数,肯定是要打开的 +} + + +function getDirection(x, y) { + if (x > y && x > MIN_DISTANCE) { + return 'horizontal'; + } + if (y > x && y > MIN_DISTANCE) { + return 'vertical'; + } + return ''; +} + +/** + * 重置滑动状态 + * @param {Object} event + */ +function resetTouchStatus(instance) { + var state = instance.getState(); + state.direction = ''; + state.deltaX = 0; + state.deltaY = 0; + state.offsetX = 0; + state.offsetY = 0; +} + +/** + * 设置滑动开始位置 + * @param {Object} event + */ +function stopTouchStart(event) { + var instance = event.instance; + var state = instance.getState(); + resetTouchStatus(instance); + var touch = event.touches[0]; + if (IS_HTML5 && isPC()) { + touch = event; + } + state.startX = touch.clientX; + state.startY = touch.clientY; +} + +/** + * 滑动中,是否禁止打开 + * @param {Object} event + */ +function stopTouchMove(event) { + var instance = event.instance; + var state = instance.getState(); + var touch = event.touches[0]; + if (IS_HTML5 && isPC()) { + touch = event; + } + state.deltaX = touch.clientX - state.startX; + state.deltaY = touch.clientY - state.startY; + state.offsetY = Math.abs(state.deltaY); + state.offsetX = Math.abs(state.deltaX); + state.direction = state.direction || getDirection(state.offsetX, state.offsetY); +} + +function isPC() { + var userAgentInfo = navigator.userAgent; + var Agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]; + var flag = true; + for (var v = 0; v < Agents.length - 1; v++) { + if (userAgentInfo.indexOf(Agents[v]) > 0) { + flag = false; + break; + } + } + return flag; +} + +var movable = false + +function mousedown(e, ins) { + if (!IS_HTML5) return + if (!isPC()) return + touchstart(e, ins) + movable = true +} + +function mousemove(e, ins) { + if (!IS_HTML5) return + if (!isPC()) return + if (!movable) return + touchmove(e, ins) +} + +function mouseup(e, ins) { + if (!IS_HTML5) return + if (!isPC()) return + touchend(e, ins) + movable = false +} + +function mouseleave(e, ins) { + if (!IS_HTML5) return + if (!isPC()) return + movable = false +} + +module.exports = { + showWatch: showWatch, + touchstart: touchstart, + touchmove: touchmove, + touchend: touchend, + mousedown: mousedown, + mousemove: mousemove, + mouseup: mouseup, + mouseleave: mouseleave +} diff --git a/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action/uni-swipe-action.vue b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action/uni-swipe-action.vue new file mode 100644 index 0000000..4971782 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action/uni-swipe-action.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-swipe-action/package.json b/im-uniapp/uni_modules/uni-swipe-action/package.json new file mode 100644 index 0000000..69135de --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/package.json @@ -0,0 +1,84 @@ +{ + "id": "uni-swipe-action", + "displayName": "uni-swipe-action 滑动操作", + "version": "1.3.8", + "description": "SwipeAction 滑动操作操作组件", + "keywords": [ + "", + "uni-ui", + "uniui", + "滑动删除", + "侧滑删除" + ], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/im-uniapp/uni_modules/uni-swipe-action/readme.md b/im-uniapp/uni_modules/uni-swipe-action/readme.md new file mode 100644 index 0000000..93a5cac --- /dev/null +++ b/im-uniapp/uni_modules/uni-swipe-action/readme.md @@ -0,0 +1,11 @@ + + +## SwipeAction 滑动操作 +> **组件名:uni-swipe-action** +> 代码块: `uSwipeAction`、`uSwipeActionItem` + + +通过滑动触发选项的容器 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-swipe-action) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-swiper-dot/changelog.md b/im-uniapp/uni_modules/uni-swiper-dot/changelog.md new file mode 100644 index 0000000..85cf54d --- /dev/null +++ b/im-uniapp/uni_modules/uni-swiper-dot/changelog.md @@ -0,0 +1,12 @@ +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-swiper-dot](https://uniapp.dcloud.io/component/uniui/uni-swiper-dot) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.6(2021-05-12) +- 新增 示例地址 +- 修复 示例项目缺少组件的Bug +## 1.0.5(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 clickItem 事件,支持指示点控制轮播 +- 新增 支持 pc 可用 diff --git a/im-uniapp/uni_modules/uni-swiper-dot/components/uni-swiper-dot/uni-swiper-dot.vue b/im-uniapp/uni_modules/uni-swiper-dot/components/uni-swiper-dot/uni-swiper-dot.vue new file mode 100644 index 0000000..e66b6c7 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swiper-dot/components/uni-swiper-dot/uni-swiper-dot.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-swiper-dot/package.json b/im-uniapp/uni_modules/uni-swiper-dot/package.json new file mode 100644 index 0000000..f2dd8d2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swiper-dot/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-swiper-dot", + "displayName": "uni-swiper-dot 轮播图指示点", + "version": "1.2.0", + "description": "自定义轮播图指示点组件", + "keywords": [ + "uni-ui", + "uniui", + "轮播图指示点", + "dot", + "swiper" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-swiper-dot/readme.md b/im-uniapp/uni_modules/uni-swiper-dot/readme.md new file mode 100644 index 0000000..7d397e2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-swiper-dot/readme.md @@ -0,0 +1,11 @@ + + +## SwiperDot 轮播图指示点 +> **组件名:uni-swiper-dot** +> 代码块: `uSwiperDot` + + +自定义轮播图指示点 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-swiper-dot) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-table/changelog.md b/im-uniapp/uni_modules/uni-table/changelog.md new file mode 100644 index 0000000..9f87c67 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/changelog.md @@ -0,0 +1,27 @@ +## 1.2.3(2023-03-28) +- 修复 在vue3模式下可能会出现错误的问题 +## 1.2.2(2022-11-29) +- 优化 主题样式 +## 1.2.1(2022-06-06) +- 修复 微信小程序存在无使用组件的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-table](https://uniapp.dcloud.io/component/uniui/uni-table) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-07-08) +- 新增 uni-th 支持 date 日期筛选范围 +## 1.0.6(2021-07-05) +- 新增 uni-th 支持 range 筛选范围 +## 1.0.5(2021-06-28) +- 新增 uni-th 筛选功能 +## 1.0.4(2021-05-12) +- 新增 示例地址 +- 修复 示例项目缺少组件的Bug +## 1.0.3(2021-04-16) +- 新增 sortable 属性,是否开启单列排序 +- 优化 表格多选逻辑 +## 1.0.2(2021-03-22) +- uni-tr 添加 disabled 属性,用于 type=selection 时,设置某行是否可由全选按钮控制 +## 1.0.1(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-table/components/uni-table/uni-table.vue b/im-uniapp/uni_modules/uni-table/components/uni-table/uni-table.vue new file mode 100644 index 0000000..21d9527 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/components/uni-table/uni-table.vue @@ -0,0 +1,455 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-table/components/uni-tbody/uni-tbody.vue b/im-uniapp/uni_modules/uni-table/components/uni-tbody/uni-tbody.vue new file mode 100644 index 0000000..fbe1bdc --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/components/uni-tbody/uni-tbody.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-table/components/uni-td/uni-td.vue b/im-uniapp/uni_modules/uni-table/components/uni-td/uni-td.vue new file mode 100644 index 0000000..9ce93e9 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/components/uni-td/uni-td.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-table/components/uni-th/filter-dropdown.vue b/im-uniapp/uni_modules/uni-table/components/uni-th/filter-dropdown.vue new file mode 100644 index 0000000..df22a71 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/components/uni-th/filter-dropdown.vue @@ -0,0 +1,511 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-table/components/uni-th/uni-th.vue b/im-uniapp/uni_modules/uni-table/components/uni-th/uni-th.vue new file mode 100644 index 0000000..14889dd --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/components/uni-th/uni-th.vue @@ -0,0 +1,285 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-table/components/uni-thead/uni-thead.vue b/im-uniapp/uni_modules/uni-table/components/uni-thead/uni-thead.vue new file mode 100644 index 0000000..0dd18cd --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/components/uni-thead/uni-thead.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-table/components/uni-tr/table-checkbox.vue b/im-uniapp/uni_modules/uni-table/components/uni-tr/table-checkbox.vue new file mode 100644 index 0000000..1089187 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/components/uni-tr/table-checkbox.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-table/components/uni-tr/uni-tr.vue b/im-uniapp/uni_modules/uni-table/components/uni-tr/uni-tr.vue new file mode 100644 index 0000000..f9b9671 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/components/uni-tr/uni-tr.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-table/i18n/en.json b/im-uniapp/uni_modules/uni-table/i18n/en.json new file mode 100644 index 0000000..e32023c --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/i18n/en.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "Reset", + "filter-dropdown.search": "Search", + "filter-dropdown.submit": "Submit", + "filter-dropdown.filter": "Filter", + "filter-dropdown.gt": "Greater or equal to", + "filter-dropdown.lt": "Less than or equal to", + "filter-dropdown.date": "Date" +} diff --git a/im-uniapp/uni_modules/uni-table/i18n/es.json b/im-uniapp/uni_modules/uni-table/i18n/es.json new file mode 100644 index 0000000..9afd04b --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/i18n/es.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "Reiniciar", + "filter-dropdown.search": "Búsqueda", + "filter-dropdown.submit": "Entregar", + "filter-dropdown.filter": "Filtrar", + "filter-dropdown.gt": "Mayor o igual a", + "filter-dropdown.lt": "Menos que o igual a", + "filter-dropdown.date": "Fecha" +} diff --git a/im-uniapp/uni_modules/uni-table/i18n/fr.json b/im-uniapp/uni_modules/uni-table/i18n/fr.json new file mode 100644 index 0000000..b006237 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/i18n/fr.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "Réinitialiser", + "filter-dropdown.search": "Chercher", + "filter-dropdown.submit": "Soumettre", + "filter-dropdown.filter": "Filtre", + "filter-dropdown.gt": "Supérieur ou égal à", + "filter-dropdown.lt": "Inférieur ou égal à", + "filter-dropdown.date": "Date" +} diff --git a/im-uniapp/uni_modules/uni-table/i18n/index.js b/im-uniapp/uni_modules/uni-table/i18n/index.js new file mode 100644 index 0000000..2469dd0 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/i18n/index.js @@ -0,0 +1,12 @@ +import en from './en.json' +import es from './es.json' +import fr from './fr.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + es, + fr, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/im-uniapp/uni_modules/uni-table/i18n/zh-Hans.json b/im-uniapp/uni_modules/uni-table/i18n/zh-Hans.json new file mode 100644 index 0000000..862af17 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/i18n/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "重置", + "filter-dropdown.search": "搜索", + "filter-dropdown.submit": "确定", + "filter-dropdown.filter": "筛选", + "filter-dropdown.gt": "大于等于", + "filter-dropdown.lt": "小于等于", + "filter-dropdown.date": "日期范围" +} diff --git a/im-uniapp/uni_modules/uni-table/i18n/zh-Hant.json b/im-uniapp/uni_modules/uni-table/i18n/zh-Hant.json new file mode 100644 index 0000000..64f8061 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/i18n/zh-Hant.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "重置", + "filter-dropdown.search": "搜索", + "filter-dropdown.submit": "確定", + "filter-dropdown.filter": "篩選", + "filter-dropdown.gt": "大於等於", + "filter-dropdown.lt": "小於等於", + "filter-dropdown.date": "日期範圍" +} diff --git a/im-uniapp/uni_modules/uni-table/package.json b/im-uniapp/uni_modules/uni-table/package.json new file mode 100644 index 0000000..7c2f91c --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/package.json @@ -0,0 +1,83 @@ +{ + "id": "uni-table", + "displayName": "uni-table 表格", + "version": "1.2.3", + "description": "表格组件,多用于展示多条结构类似的数据,如", + "keywords": [ + "uni-ui", + "uniui", + "table", + "表格" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-datetime-picker"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "n", + "QQ": "y" + }, + "快应用": { + "华为": "n", + "联盟": "n" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-table/readme.md b/im-uniapp/uni_modules/uni-table/readme.md new file mode 100644 index 0000000..bb08c79 --- /dev/null +++ b/im-uniapp/uni_modules/uni-table/readme.md @@ -0,0 +1,13 @@ + + +## Table 表单 +> 组件名:``uni-table``,代码块: `uTable`。 + +用于展示多条结构类似的数据 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-table) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + + + diff --git a/im-uniapp/uni_modules/uni-tag/changelog.md b/im-uniapp/uni_modules/uni-tag/changelog.md new file mode 100644 index 0000000..c0c5839 --- /dev/null +++ b/im-uniapp/uni_modules/uni-tag/changelog.md @@ -0,0 +1,21 @@ +## 2.1.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-tag](https://uniapp.dcloud.io/component/uniui/uni-tag) +## 2.0.0(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +- 移除 插槽 +- 移除 type 属性的 royal 选项 +## 1.1.1(2021-08-11) +- type 不是 default 时,size 为 small 字体大小显示不正确 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-06-18) +- 修复 uni-tag 在字节跳动小程序上 css 类名编译错误的 bug +## 1.0.6(2021-06-04) +- 修复 未定义 sass 变量 "$uni-color-royal" 的bug +## 1.0.5(2021-05-10) +- 修复 royal 类型无效的bug +- 修复 uni-tag 宽度不自适应的bug +- 新增 uni-tag 支持属性 custom-style 自定义样式 +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-tag/components/uni-tag/uni-tag.vue b/im-uniapp/uni_modules/uni-tag/components/uni-tag/uni-tag.vue new file mode 100644 index 0000000..418c955 --- /dev/null +++ b/im-uniapp/uni_modules/uni-tag/components/uni-tag/uni-tag.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-tag/package.json b/im-uniapp/uni_modules/uni-tag/package.json new file mode 100644 index 0000000..1878088 --- /dev/null +++ b/im-uniapp/uni_modules/uni-tag/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-tag", + "displayName": "uni-tag 标签", + "version": "2.1.0", + "description": "Tag 组件,用于展示1个或多个文字标签,可点击切换选中、不选中的状态。", + "keywords": [ + "uni-ui", + "uniui", + "", + "tag", + "标签" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-tag/readme.md b/im-uniapp/uni_modules/uni-tag/readme.md new file mode 100644 index 0000000..6e78ff5 --- /dev/null +++ b/im-uniapp/uni_modules/uni-tag/readme.md @@ -0,0 +1,13 @@ + + +## Tag 标签 +> **组件名:uni-tag** +> 代码块: `uTag` + + +用于展示1个或多个文字标签,可点击切换选中、不选中的状态 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-tag) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/im-uniapp/uni_modules/uni-title/changelog.md b/im-uniapp/uni_modules/uni-title/changelog.md new file mode 100644 index 0000000..7626216 --- /dev/null +++ b/im-uniapp/uni_modules/uni-title/changelog.md @@ -0,0 +1,10 @@ +## 1.1.1(2022-05-19) +- 修改组件描述 +## 1.1.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-title](https://uniapp.dcloud.io/component/uniui/uni-title) +## 1.0.2(2021-05-12) +- 新增 示例地址 +- 修复 示例项目缺少组件的Bug +## 1.0.1(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/im-uniapp/uni_modules/uni-title/components/uni-title/uni-title.vue b/im-uniapp/uni_modules/uni-title/components/uni-title/uni-title.vue new file mode 100644 index 0000000..bf4f926 --- /dev/null +++ b/im-uniapp/uni_modules/uni-title/components/uni-title/uni-title.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-title/package.json b/im-uniapp/uni_modules/uni-title/package.json new file mode 100644 index 0000000..2249f5a --- /dev/null +++ b/im-uniapp/uni_modules/uni-title/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-title", + "displayName": "uni-title 章节标题", + "version": "1.1.1", + "description": "章节标题,通常用于记录页面标题,使用当前组件,uni-app 如果开启统计,将会自动统计页面标题", + "keywords": [ + "uni-ui", + "uniui", + "标题", + "章节", + "章节标题", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-title/readme.md b/im-uniapp/uni_modules/uni-title/readme.md new file mode 100644 index 0000000..0e60b1b --- /dev/null +++ b/im-uniapp/uni_modules/uni-title/readme.md @@ -0,0 +1,14 @@ + + +## Title 标题 +> **组件名:uni-title** +> 代码块: `uTitle` + + +章节标题,通常用于记录页面标题,使用当前组件,uni-app 如果开启统计,将会自动统计页面标题 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-title) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + + diff --git a/im-uniapp/uni_modules/uni-tooltip/changelog.md b/im-uniapp/uni_modules/uni-tooltip/changelog.md new file mode 100644 index 0000000..00f1572 --- /dev/null +++ b/im-uniapp/uni_modules/uni-tooltip/changelog.md @@ -0,0 +1,10 @@ +## 0.2.1(2022-05-09) +- 修复 content 为空时仍然弹出的bug +## 0.2.0(2022-05-07) +**注意:破坏性更新** +- 更新 text 属性变更为 content +- 更新 移除 width 属性 +## 0.1.1(2022-04-27) +- 修复 组件根 text 嵌套组件 warning +## 0.1.0(2022-04-21) +- 初始化 diff --git a/im-uniapp/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue b/im-uniapp/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue new file mode 100644 index 0000000..ffbb6fa --- /dev/null +++ b/im-uniapp/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/im-uniapp/uni_modules/uni-tooltip/package.json b/im-uniapp/uni_modules/uni-tooltip/package.json new file mode 100644 index 0000000..b626efb --- /dev/null +++ b/im-uniapp/uni_modules/uni-tooltip/package.json @@ -0,0 +1,83 @@ +{ + "id": "uni-tooltip", + "displayName": "uni-tooltip", + "version": "0.2.1", + "description": "Tooltip 提示文字", + "keywords": [ + "uni-tooltip", + "uni-ui", + "tooltip", + "tip", + "文字提示" +], + "repository": "", +"engines": { + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无 ", + "data": "无", + "permissions": "无" + }, + "npmurl": "" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "y", + "vue3": "y" + }, + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-tooltip/readme.md b/im-uniapp/uni_modules/uni-tooltip/readme.md new file mode 100644 index 0000000..faafa2e --- /dev/null +++ b/im-uniapp/uni_modules/uni-tooltip/readme.md @@ -0,0 +1,8 @@ +## Badge 数字角标 +> **组件名:uni-tooltip** +> 代码块: `uTooltip` + +数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景, + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-tooltip) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/im-uniapp/uni_modules/uni-transition/changelog.md b/im-uniapp/uni_modules/uni-transition/changelog.md new file mode 100644 index 0000000..70c1cd4 --- /dev/null +++ b/im-uniapp/uni_modules/uni-transition/changelog.md @@ -0,0 +1,22 @@ +## 1.3.2(2023-05-04) +- 修复 NVUE 平台报错的问题 +## 1.3.1(2021-11-23) +- 修复 init 方法初始化问题 +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-transition](https://uniapp.dcloud.io/component/uniui/uni-transition) +## 1.2.1(2021-09-27) +- 修复 init 方法不生效的 Bug +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.1(2021-05-12) +- 新增 示例地址 +- 修复 示例项目缺少组件的 Bug +## 1.1.0(2021-04-22) +- 新增 通过方法自定义动画 +- 新增 custom-class 非 NVUE 平台支持自定义 class 定制样式 +- 优化 动画触发逻辑,使动画更流畅 +- 优化 支持单独的动画类型 +- 优化 文档示例 +## 1.0.2(2021-02-05) +- 调整为 uni_modules 目录规范 diff --git a/im-uniapp/uni_modules/uni-transition/components/uni-transition/createAnimation.js b/im-uniapp/uni_modules/uni-transition/components/uni-transition/createAnimation.js new file mode 100644 index 0000000..8f89b18 --- /dev/null +++ b/im-uniapp/uni_modules/uni-transition/components/uni-transition/createAnimation.js @@ -0,0 +1,131 @@ +// const defaultOption = { +// duration: 300, +// timingFunction: 'linear', +// delay: 0, +// transformOrigin: '50% 50% 0' +// } +// #ifdef APP-NVUE +const nvueAnimation = uni.requireNativePlugin('animation') +// #endif +class MPAnimation { + constructor(options, _this) { + this.options = options + // 在iOS10+QQ小程序平台下,传给原生的对象一定是个普通对象而不是Proxy对象,否则会报parameter should be Object instead of ProxyObject的错误 + this.animation = uni.createAnimation({ + ...options + }) + this.currentStepAnimates = {} + this.next = 0 + this.$ = _this + + } + + _nvuePushAnimates(type, args) { + let aniObj = this.currentStepAnimates[this.next] + let styles = {} + if (!aniObj) { + styles = { + styles: {}, + config: {} + } + } else { + styles = aniObj + } + if (animateTypes1.includes(type)) { + if (!styles.styles.transform) { + styles.styles.transform = '' + } + let unit = '' + if(type === 'rotate'){ + unit = 'deg' + } + styles.styles.transform += `${type}(${args+unit}) ` + } else { + styles.styles[type] = `${args}` + } + this.currentStepAnimates[this.next] = styles + } + _animateRun(styles = {}, config = {}) { + let ref = this.$.$refs['ani'].ref + if (!ref) return + return new Promise((resolve, reject) => { + nvueAnimation.transition(ref, { + styles, + ...config + }, res => { + resolve() + }) + }) + } + + _nvueNextAnimate(animates, step = 0, fn) { + let obj = animates[step] + if (obj) { + let { + styles, + config + } = obj + this._animateRun(styles, config).then(() => { + step += 1 + this._nvueNextAnimate(animates, step, fn) + }) + } else { + this.currentStepAnimates = {} + typeof fn === 'function' && fn() + this.isEnd = true + } + } + + step(config = {}) { + // #ifndef APP-NVUE + this.animation.step(config) + // #endif + // #ifdef APP-NVUE + this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config) + this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin + this.next++ + // #endif + return this + } + + run(fn) { + // #ifndef APP-NVUE + this.$.animationData = this.animation.export() + this.$.timer = setTimeout(() => { + typeof fn === 'function' && fn() + }, this.$.durationTime) + // #endif + // #ifdef APP-NVUE + this.isEnd = false + let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref + if(!ref) return + this._nvueNextAnimate(this.currentStepAnimates, 0, fn) + this.next = 0 + // #endif + } +} + + +const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d', + 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY', + 'translateZ' +] +const animateTypes2 = ['opacity', 'backgroundColor'] +const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom'] +animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => { + MPAnimation.prototype[type] = function(...args) { + // #ifndef APP-NVUE + this.animation[type](...args) + // #endif + // #ifdef APP-NVUE + this._nvuePushAnimates(type, args) + // #endif + return this + } +}) + +export function createAnimation(option, _this) { + if(!_this) return + clearTimeout(_this.timer) + return new MPAnimation(option, _this) +} diff --git a/im-uniapp/uni_modules/uni-transition/components/uni-transition/uni-transition.vue b/im-uniapp/uni_modules/uni-transition/components/uni-transition/uni-transition.vue new file mode 100644 index 0000000..bfbba93 --- /dev/null +++ b/im-uniapp/uni_modules/uni-transition/components/uni-transition/uni-transition.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/im-uniapp/uni_modules/uni-transition/package.json b/im-uniapp/uni_modules/uni-transition/package.json new file mode 100644 index 0000000..ea995a2 --- /dev/null +++ b/im-uniapp/uni_modules/uni-transition/package.json @@ -0,0 +1,84 @@ +{ + "id": "uni-transition", + "displayName": "uni-transition 过渡动画", + "version": "1.3.2", + "description": "元素的简单过渡动画", + "keywords": [ + "uni-ui", + "uniui", + "动画", + "过渡", + "过渡动画" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-transition/readme.md b/im-uniapp/uni_modules/uni-transition/readme.md new file mode 100644 index 0000000..2f8a77e --- /dev/null +++ b/im-uniapp/uni_modules/uni-transition/readme.md @@ -0,0 +1,11 @@ + + +## Transition 过渡动画 +> **组件名:uni-transition** +> 代码块: `uTransition` + + +元素过渡动画 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-transition) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-ui/changelog.md b/im-uniapp/uni_modules/uni-ui/changelog.md new file mode 100644 index 0000000..61b26d0 --- /dev/null +++ b/im-uniapp/uni_modules/uni-ui/changelog.md @@ -0,0 +1,541 @@ +## 1.4.27(2023-04-23) +- uni-calendar 修复 某些情况 monthSwitch 未触发的Bug +- uni-calendar 修复 某些情况切换月份错误的Bug +- uni-data-picker 修复 更改 modelValue 报错的 bug +- uni-data-picker 修复 v-for 未使用 key 值控制台 warning +- uni-data-picker 修复代码合并时引发 value 属性为空时不渲染数据的问题 +- uni-data-picker 修复 localdata 不支持动态更新的bug +- uni-data-select 修复 微信小程序点击时会改变背景颜色的 bug +- uni-data-select 修复 禁用时会显示清空按钮 +- uni-data-select 优化 查询条件短期内多次变更只查询最后一次变更后的结果 +- uni-data-select 调整 内部缓存键名调整为 uni-data-select-lastSelectedValue +- uni-datetime-picker 修复 日历 picker 修改年月后,自动选中当月1日 [详情](https://ask.dcloud.net.cn/question/165937) +- uni-datetime-picker 修复 小程序端 低版本 ios NaN [详情](https://ask.dcloud.net.cn/question/162979) +- uni-datetime-picker 修复 firefox 浏览器显示区域点击无法拉起日历弹框的Bug [详情](https://ask.dcloud.net.cn/question/163362) +- uni-datetime-picker 优化 值为空依然选中当天问题 +- uni-datetime-picker 优化 提供 default-value 属性支持配置选择器打开时默认显示的时间 +- uni-datetime-picker 优化 非范围选择未选择日期时间,点击确认按钮选中当前日期时间 +- uni-datetime-picker 优化 字节小程序日期时间范围选择,底部日期换行问题 +- uni-datetime-picker 修复 2.2.18 引起范围选择配置 end 选择无效的Bug [详情](https://github.com/dcloudio/uni-ui/issues/686) +- uni-datetime-picker 修复 移动端范围选择change事件触发异常的Bug [详情](https://github.com/dcloudio/uni-ui/issues/684) +- uni-datetime-picker 优化 PC端输入日期格式错误时返回当前日期时间 +- uni-datetime-picker 优化 PC端输入日期时间超出 start、end 限制的Bug +- uni-datetime-picker 优化 移动端日期时间范围用法时间展示不完整问题 +- uni-datetime-picker 修复 小程序端绑定 Date 类型报错的Bug [详情](https://github.com/dcloudio/uni-ui/issues/679) +- uni-datetime-picker 修复 vue3 time-picker 无法显示绑定时分秒的Bug +- uni-datetime-picker 修复 字节小程序报错的Bug +- uni-datetime-picker 修复 某些情况切换月份错误的Bug +- uni-easyinput 修复 vue3 下 keyboardheightchange 事件报错的bug +- uni-easyinput 优化 trim 属性默认值 +- uni-easyinput 新增 cursor-spacing 属性 +- uni-fab 新增 pattern.icon 属性,可自定义图标 +- uni-file-picker 修复 手动上传删除一个文件后不能再上传的bug +- uni-forms 修复 required 参数无法动态绑定 +- uni-list 优化 uni-list-chat 具名插槽`header` 非app端套一层元素,方便使用时通过外层元素定位实现样式修改 +- uni-list uni-list-chat 新增 支持具名插槽`header` +- uni-list 新增 列表图标新增 customPrefix 属性 ,用法 [详见](https://uniapp.dcloud.net.cn/component/uniui/uni-icons.html#icons-props) +- uni-nav-bar 修复 自定义状态栏高度闪动BUG +- uni-nav-bar 修复 暗黑模式下边线颜色错误的bug +- uni-popup 修复 uni-popup 重复打开时的 bug +- uni-popup uni-popup-dialog 组件新增 inputType 属性 +- uni-swipe-action 修复`uni-swipe-action`和`uni-swipe-action-item`不同时使用导致 closeOther 方法报错的 bug +- uni-table 修复 在vue3模式下可能会出现错误的问题 +## 1.4.26(2023-01-31) +- uni-badge 修复 运行/打包 控制台警告问题 +- uni-calendar 修复 某些情况切换月份错误问题 +- uni-data-select 修复 不关联服务空间报错的问题 +- uni-data-select 新增 属性 `format` 可用于格式化显示选项内容 +- uni-datetime-picker 修复 某些情况切换月份错误问题 +- uni-easyinput 新增 keyboardheightchange 事件,可监听键盘高度变化 +- uni-list 修复 无反馈效果呈现的bug +## 1.4.25(2023-01-11) +- uni-file-picker 新增 sourceType 属性, 可以自定义图片和视频选择的来源 +## 1.4.24(2023-01-11) +- uni-data-select 修复 当where变化时,数据不会自动更新的问题 +- uni-datetime-picker 修复 多次加载组件造成内存占用的 bug +- uni-datetime-picker 修复 vue3 下 i18n 国际化初始值不正确的 bug +- uni-easyinput 修复 props 中背景颜色无默认值的bug +- uni-list 修复 uni-list-chat 在vue3下跳转报错的bug +- uni-list 修复 uni-list-chat avatar属性 值为本地路径时错误的问题 +- uni-list 修复 uni-list-chat avatar属性 在腾讯云版uniCloud下错误的问题 +- uni-list 修复 uni-list-chat note属性 支持:“草稿”字样功能 文本少1位的问题 +- uni-list 修复 uni-list-item 的 customStyle 属性 padding值在 H5端 无效的bug +- uni-list 修复 uni-list-item 的 customStyle 属性 padding值在nvue(vue2)下无效的bug +- uni-list uni-list-chat 新增 avatar 支持 fileId +- uni-list uni-list 新增属性 render-reverse 详情参考:[https://uniapp.dcloud.net.cn/component/list.html](https://uniapp.dcloud.net.cn/component/list.html) +- uni-list uni-list-chat note属性 支持:“草稿”字样 加红显示 详情参考uni-im:[https://ext.dcloud.net.cn/plugin?name=uni-im](https://ext.dcloud.net.cn/plugin?name=uni-im) +- uni-list uni-list-item 新增属性 customStyle 支持设置padding、backgroundColor +- uni-popup 修复 nvue 下 v-show 报错 +## 1.4.23(2022-10-25) +- uni-datetime-picker 修复,支付宝小程序样式错乱,[详情](https://github.com/dcloudio/uni-app/issues/3861) + +- uni-nav-bar 修复 条件编译错误的bug +- uni-nav-bar 修复 nvue 环境 fixed 为 true 的情况下,无法置顶的 bug +## 1.4.22(2022-09-19) +- 优化 部分组件适配 uni-scss 主题色 +- uni-badge 修复 当 text 超过 max-num 时,badge 的宽度计算是根据 text 的长度计算,更改为 css 计算实际展示宽度,详见:[https://ask.dcloud.net.cn/question/150473](https://ask.dcloud.net.cn/question/150473) +- uni-calendar 修复 表头年月切换,导致改变当前日期为选择月1号,且未触发change事件 +- uni-data-select 修复 微信小程序下拉框出现后选择会点击到蒙板后面的输入框 +- uni-data-select 修复 点击的位置不准确 +- uni-data-select 新增 支持 disabled 属性 +- uni-datetime-picker 修复,反向选择日期范围,日期显示异常,[详情](https://ask.dcloud.net.cn/question/153401?item_id=212892&rf=false) +- uni-datetime-picker 修复 close事件无效的 bug +- uni-datetime-picker 修复 移动端 maskClick 无效的 bug,详见:[https://ask.dcloud.net.cn/question/140824?item_id=209458&rf=false](https://ask.dcloud.net.cn/question/140824?item_id=209458&rf=false) +- uni-fab 修复 小程序端由于 style 使用了对象导致报错,[详情](https://ask.dcloud.net.cn/question/152790?item_id=211778&rf=false) +- uni-fab 修复 nvue 环境下,具有 tabBar 时,fab 组件下部位置无法正常获取 --window-bottom 的bug,详见:[https://ask.dcloud.net.cn/question/110638?notification_id=826310](https://ask.dcloud.net.cn/question/110638?notification_id=826310) +- uni-forms 优化 根据 rules 自动添加 required 的问题 +- uni-forms 修复 item 未设置 require 属性,rules 设置 require 后,星号也显示的 bug,详见:[https://ask.dcloud.net.cn/question/151540](https://ask.dcloud.net.cn/question/151540) +- uni-nav-bar 修复 nvue 环境下 fixed 为 true 的情况下,无法置顶的 bug +- uni-notice-bar 新增 属性 fontSize,可修改文字大小。 +- uni-pagination 修复,未对主题色设置默认色,导致未引入 uni-scss 变量文件报错。 +- uni-pagination 修复,未对移动端当前页文字做主题色适配。 +- uni-pagination 修复 es 语言 i18n 错误 +## 1.4.21(2022-09-19) +- 修复,安装时未导入 uni-data-select 和 uni-tooltip 的问题。 +## 1.4.20(2022-07-25) +- uni-section 新增组件 +- uni-forms 修复 model 需要校验的值没有声明对应字段时,导致第一次不触发校验的bug + +## 1.4.19(2022-07-07) +- uni-data-picker 优化 pc端图标位置不正确的问题 +- uni-data-select 修复 pc端宽度异常的bug +## 1.4.18(2022-07-06) +- uni-forms 【重要】组件逻辑重构,部分用法旧版本不兼容,请注意兼容问题 +- uni-forms 【重要】组件使用 Provide/Inject 方式注入依赖,提供了自定义表单组件调用 uni-forms 校验表单的能力 +- uni-forms 新增 更多表单示例 +- uni-forms 新增 model 属性,等同于原 value/modelValue 属性,旧属性即将废弃 +- uni-forms 新增 validateTrigger 属性的 blur 值,仅 uni-easyinput 生效 +- uni-forms 新增 onFieldChange 方法,可以对子表单进行校验,可替代binddata方法 +- uni-forms 新增 子表单的 setRules 方法,配合自定义校验函数使用 +- uni-forms 新增 uni-forms-item 的 setRules 方法,配置动态表单使用可动态更新校验规则 +- uni-forms 修复 由 1.4.0 引发的 label 插槽不生效的bug +- uni-forms 修复 子组件找不到 setValue 报错的bug +- uni-forms 修复 uni-data-picker 在 uni-forms-item 中报错的bug +- uni-forms 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug +- uni-forms 修复 表单校验顺序无序问题 +- uni-forms 优化 子表单组件uni-datetime-picker、uni-data-select、uni-data-picker的显示样式 +- uni-forms 优化 动态表单校验方式,废弃拼接name的方式 +- uni-breadcrumb 修复 微信小程序 separator 不显示问题 +- uni-data-checkbox 优化 在 uni-forms 中的依赖注入方式 +- uni-data-picker 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug +- uni-data-picker 优化 显示样式 +- uni-data-select 优化 显示样式 +- uni-datetime-picker 修复 日历顶部年月及底部确认未国际化 bug +- uni-datetime-picker 优化 组件样式,调整了组件图标大小、高度、颜色等,与uni-ui风格保持一致 +- uni-easyinput 新增 在 uni-forms 1.4.0 中使用可以在 blur 时校验内容 +- uni-easyinput 新增 clear 事件,点击右侧叉号图标触发 +- uni-easyinput 新增 change 事件 ,仅在输入框失去焦点或用户按下回车时触发 +- uni-easyinput 优化 组件样式,组件获取焦点时高亮显示,图标颜色调整等 +- uni-easyinput 优化 clearable 显示策略 +- uni-file-picker 修复 在uni-forms下样式不生效的bug +- uni-nav-bar 修复 组件示例中插槽用法无法显示内容的bug +- uni-swipe-action 修复 vue3 下使用组件不能正常运行的Bug +- uni-swipe-action 修复 h5端点击click触发两次的Bug +- uni-table 修复 微信小程序存在无使用组件的问题 +## 1.4.17(2022-06-30) +- 支持 ios 安全区 +## 1.4.16(2022-06-06) +- uni-breadcrumb 新增 支持 uni.scss 修改颜色 +- uni-data-select 修复 localdata 赋值不生效的 bug +- uni-data-select 新增 支持选项禁用(数据选项设置 disabled: true 即禁用) +- uni-data-select 修复 当 value 为 0 时选择不生效的 bug +- uni-easyinput 修复 关闭图标某些情况下无法取消的bug +- uni-fav 新增 stat 属性 ,是否开启uni统计功能 +- uni-goods-nav 新增 stat属性,是否开启uni统计功能 +- uni-group 新增 stat属性,是否开启uni统计功能 +- uni-nav-bar 新增 stat 属性 ,可开启统计 title 上报 ,仅使用了title 属性且项目开启了uni统计生效 +- uni-search-bar 新增 readonly 属性,组件只读 +- uni-swipe-action 修复 isPC 找不到的Bug +- uni-swipe-action 修复 在 nvue 下 disabled 失效的bug +- uni-tooltip 修复 content 为空时仍然弹出的bug +## 1.4.15(2022-05-07) +- uni-data-picker 修复 字节小程序 本地数据无法选择下一级的Bug +- uni-data-select 新增 记住上次的选项(仅 collection 存在时有效) +- uni-search-bar 修复 vue3 input 事件不生效的bug +- uni-search-bar 修复 多余代码导致的bug +- uni-tooltip 更新 text 属性变更为 content +- uni-tooltip 更新 移除 width 属性 +- uni-tooltip 修复 组件根 text 嵌套组件 warning +## 1.4.14(2022-04-18) +- uni-datetime-picker 修复 Vue3 下动态赋值,单选类型未响应的 bug +- uni-easyinput 修复 默认值不生效的bug +## 1.4.13(2022-04-02) +- uni-calendar 修复 条件编译 nvue 不支持的 css 样式 +- uni-calendar 修复 startDate、 endDate 属性失效的 bug +- uni-data-picker 修复 nvue 不支持的 v-show 的 bug +- uni-data-picker 修复 条件编译 nvue 不支持的 css 样式 +- uni-datetime-picker 修复 Vue3 下动态赋值未响应的 bug +- uni-easyinput 修复 value不能为0的bug +- uni-popup 修复 弹出层内部无法滚动的bug +- uni-popup 修复 小程序中高度错误的bug +- uni-popup 修复 快速调用open出现问题的Bug +- uni-rate 修复 条件判断 `NaN` 错误的 bug +- uni-swipe-action 修复 按钮字体大小不能设置的bug +- uni-swipe-action 修复 h5和app端下报el错误的bug +- uni-swipe-action 修复 HBuilderX 1.4.X 版本中,h5和app端下报错的bug +## 1.4.12(2022-02-19) +- uni-collapse 修复 初始化的时候 ,open 属性失效的bug +- uni-data-checkbox 修复 multiple 为 true 时,v-model 的值为 null 报错的 bug +- uni-icons 优化 size 属性可以传入不带单位的字符串数值 +- uni-icons 优化 size 支持其他单位 +- uni-nav-bar 新增 left-width/right-width属性 ,可修改左右两侧的宽度 +- uni-popup 修复 safeArea 属性不能设置为false的bug +## 1.4.11(2022-01-21) +- uni-collapse 修复 微信小程序resize后组件收起的bug +- uni-countdown 修复 在微信小程序中样式不生效的bug +- uni-countdown 新增 update 方法 ,在动态更新时间后,刷新组件 +- uni-load-more 新增 showText属性 ,是否显示文本 +- uni-load-more 修复 nvue 平台下不显示文本的bug +- uni-load-more 修复 微信小程序平台样式选择器报警告的问题 +- uni-nav-bar 修复 在vue下,标题不垂直居中的bug +- uni-nav-bar 修复 height 属性类型错误 +- uni-nav-bar 新增 height 属性,可修改组件高度 +- uni-nav-bar 新增 dark 属性可可开启暗黑模式 +- uni-nav-bar 优化 标题字数过多显示省略号 +- uni-nav-bar 优化 插槽,插入内容可完全覆盖 +- uni-popup 修复 isMaskClick 失效的bug +- uni-popup 新增 cancelText \ confirmText 属性 ,可自定义文本 +- uni-popup 新增 maskBackgroundColor 属性 ,可以修改蒙版颜色 +- uni-popup 优化 maskClick属性 更新为 isMaskClick ,解决微信小程序警告的问题 + +## 1.4.10(2022-01-17) +- uni-card 修复 在vue页面下略缩图显示不正常的bug +- uni-datetime-picker 修复 clear-icon 属性在小程序平台不生效的 bug +- uni-datetime-picker 修复 日期范围选在小程序平台,必须多点击一次才能取消选中状态的 bug +- uni-fab 更新 组件依赖 +- +- uni-icons 修复 nvue 有些图标不显示的bug,兼容老版本图标 +- uni-icons 优化 示例可复制图标名称 +- uni-nav-bar 修复 color 属性不生效的bug +- uni-popup 修复 设置 safeArea 属性不生效的bug +- uni-popup 优化 组件示例 +- uni-popup 修复 vuedoc 文字错误 +## 1.4.9(2021-11-23) +- uni-ui 修复 vue3中某些scss变量无法找到的问题 +- uni-combox 优化 label、label-width 属性 +- uni-data-picker 修复 由上个版本引发的map、v-model等属性不生效的bug +- uni-file-picker 修复 参数为对象的情况下,url在某些情况显示错误的bug +- uni-icons 优化 兼容旧组件 type 值 +- uni-list 修复 在vue3中to属性在发行应用的时候报错的bug +- uni-scss 修复 vue3中scss语法兼容问题 +- uni-transition 修复 init 方法初始化问题 +## 1.4.8(2021-11-19) +- uni-fab 修复 阴影颜色不正确的bug +## 1.4.7(2021-11-19) +- uni-ui 新增 支持国际化 +- uni-ui 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- uni-ui 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-ui](https://uniapp.dcloud.io/component/uniui/uni-ui) +- uni-badge 修改 size 属性默认值调整为 small +- uni-badge 修改 type 属性,默认值调整为 error,info 替换 default +- uni-badge 修复 在字节小程序上样式不生效的 bug +- uni-calendar 修复 弹出层被 tabbar 遮盖 bug +- uni-card 重构插槽的用法 ,header 替换为 title +- uni-card 新增 actions 插槽 +- uni-card 新增 cover 封面图属性和插槽 +- uni-card 新增 padding 内容默认内边距离 +- uni-card 新增 margin 卡片默认外边距离 +- uni-card 新增 spacing 卡片默认内边距 +- uni-card 新增 shadow 卡片阴影属性 +- uni-card 取消 mode 属性,可使用组合插槽代替 +- uni-card 取消 note 属性 ,使用actions插槽代替 +- uni-collapse 优化 show-arrow 属性默认为true +- uni-collapse 新增 show-arrow 属性,控制是否显示右侧箭头 +- uni-countdown 新增 font-size 支持自定义字体大小 +- uni-data-checkbox 修复 在uni-forms中 modelValue 中不存在当前字段,当前字段必填写也不参与校验的问题 +- uni-data-checkbox 修复 单选 list 模式下 ,icon 为 left 时,选中图标不显示的问题 +- uni-data-checkbox 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 +- uni-dateformat 优化 默认时间不再是当前时间,而是显示'-'字符 +- uni-datetime-picker 修复 hide-second 在移动端的 bug +- uni-datetime-picker 修复 单选赋默认值时,赋值日期未高亮的 bug +- uni-datetime-picker 修复 赋默认值时,移动端未正确显示时间的 bug +- uni-datetime-picker 新增 hide-second 属性,支持只使用时分,隐藏秒 +- uni-datetime-picker 优化 取消选中时(范围选)直接开始下一次选择, 避免多点一次 +- uni-datetime-picker 优化 移动端支持清除按钮,同时支持通过 ref 调用组件的 clear 方法 +- uni-datetime-picker 优化 调整字号大小,美化日历界面 +- uni-datetime-picker 优化 范围选择器在 pc 端过宽的问题 +- uni-datetime-picker 新增 支持作为 uni-forms 子组件相关功能 +- uni-datetime-picker 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的 bug +- uni-datetime-picker 修复 type 属性动态赋值无效的 bug +- uni-datetime-picker 修复 ‘确认’按钮被 tabbar 遮盖 bug +- uni-datetime-picker 修复 组件未赋值时范围选左、右日历相同的 bug +- uni-datetime-picker 修复 范围选未正确显示当前值的 bug +- uni-datetime-picker 修复 h5 平台(移动端)报错 'cale' of undefined 的 bug +- uni-easyinput 修复 在 uni-forms 的动态表单中默认值校验不通过的 bug +- uni-easyinput 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 +- uni-file-picker 新增 参数中返回 fileID 字段 +- uni-file-picker 修复 腾讯云传入fileID 不能回显的bug +- uni-file-picker 修复 选择图片后,不能放大的问题 +- uni-file-picker 修复 由于 0.2.11 版本引起的不能回显图片的Bug +- uni-file-picker 新增 clearFiles(index) 方法,可以手动删除指定文件 +- uni-file-picker 修复 v-model 值设为 null 报错的Bug +- uni-file-picker 修复 return-type="object" 时,无法删除文件的Bug +- uni-file-picker 修复 auto-upload 属性失效的Bug +- uni-forms 修复 label 插槽不生效的bug +- uni-forms 修复 没有添加校验规则的字段依然报错的Bug +- uni-forms 修复 重置表单错误信息无法清除的问题 +- uni-forms 修复 表单验证只生效一次的问题 +- uni-icons 新增 更多图标 +- uni-icons 优化 自定义图标使用方式 +- uni-link 修复 在 nvue 下不显示的 bug +- uni-pagination 修复 current 、value 属性未监听,导致高亮样式失效的 bug +- uni-rate 优化 默认值修改为 0 颗星 +- uni-search-bar 修复 value 属性与 modelValue 属性不兼容的Bug +- uni-swipe-action 新增 close-all 方法,关闭所有已打开的组件 +- uni-swipe-action 新增 resize() 方法,在非微信小程序、h5、app-vue端出现不能滑动的问题的时候,重置组件 +- uni-swipe-action 修复 app 端偶尔出现类似 Page[x][-x,xx;-x,xx,x,x-x] 的问题 +- uni-swipe-action 优化 微信小程序、h5、app-vue 滑动逻辑,避免出现动态新增组件后不能滑动的问题 +- uni-tag 新增 提供组件设计资源,组件样式调整 +- uni-tag 移除 插槽 +- uni-tag 移除 type 属性的 royal 选项 +- uni-tag type 不是 default 时,size 为 small 字体大小显示不正确 +## 1.4.2(2021-08-20) +- 新增 uni-ui 组件支持国际化 i18n +- uni-collapse 优化 show-arrow 属性默认为true +- uni-collapse 新增 show-arrow 属性,控制是否显示右侧箭头 +- uni-data-checkbox 修复 单选 list 模式下 ,icon 为 left 时,选中图标不显示的问题 +- uni-easyinput 修复 在 uni-forms 的动态表单中默认值校验不通过的 bug +- uni-file-picker 修复 由于 0.2.11 版本引起的不能回显图片的Bug +- uni-file-picker 新增 clearFiles(index) 方法,可以手动删除指定文件 +- uni-file-picker 修复 v-model 值设为 null 报错的Bug +- uni-swipe-action 新增 close-all 方法,关闭所有已打开的组件 +- uni-swipe-action 新增 resize() 方法,在非微信小程序、h5、app-vue端出现不能滑动的问题的时候,重置组件 +- uni-swipe-action 修复 app 端偶尔出现类似 Page[x][-x,xx;-x,xx,x,x-x] 的问题 +- uni-swipe-action 优化 微信小程序、h5、app-vue 滑动逻辑,避免出现动态新增组件后不能滑动的问题 +## 1.4.0(2021-08-13) +- uni-calendar 修复 弹出层被 tabbar 遮盖 bug +- uni-data-checkbox 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 +- uni-dateformat 调整 默认时间不再是当前时间,而是显示'-'字符 +- uni-datetime-picker 新增 适配 vue3 +- uni-datetime-picker 新增 支持作为 uni-forms 子组件相关功能 +- uni-datetime-picker 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的 bug +- uni-datetime-picker 修复 type 属性动态赋值无效的 bug +- uni-datetime-picker 修复 ‘确认’按钮被 tabbar 遮盖 bug +- uni-datetime-picker 修复 组件未赋值时范围选左、右日历相同的 bug +- uni-datetime-picker 修复 范围选未正确显示当前值的 bug +- uni-datetime-picker 修复 h5 平台(移动端)报错 'cale' of undefined 的 bug +- uni-easyinput 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 +- uni-file-picker 修复 return-type="object" 时,无法删除文件的Bug +- uni-file-picker 修复 auto-upload 属性失效的Bug +- uni-forms 修复 没有添加校验规则的字段依然报错的Bug +- uni-forms 修复 重置表单错误信息无法清除的问题 +- uni-forms 优化 组件文档 +- uni-forms 修复 表单验证只生效一次的问题 +- uni-tag type 不是 default 时,size 为 small 字体大小显示不正确 +## 1.3.9(2021-08-02) +- uni-datetime-picker 新增 return-type 属性支持返回 date 日期对象 +- uni-file-picker 修复 fileExtname属性不指定值报错的Bug +- uni-file-picker 修复 在某种场景下图片不回显的Bug +- uni-link 支持自定义插槽 +## 1.3.8(2021-07-31) +- uni-ui 组件兼容 vue3 +- uni-collapse 修复 由1.2.0版本引起的 change 事件返回 undefined 的Bug +- uni-collapse 优化 组件示例 +- uni-collapse 新增 组件折叠动画 +- uni-collapse 新增 value\v-model 属性 ,动态修改面板折叠状态 +- uni-collapse 新增 title 插槽 ,可定义面板标题 +- uni-collapse 新增 border 属性 ,显示隐藏面板内容分隔线 +- uni-collapse 新增 title-border 属性 ,显示隐藏面板标题分隔线 +- uni-collapse 修复 resize 方法失效的Bug +- uni-collapse 修复 change 事件返回参数不正确的Bug +- uni-collapse 优化 H5、App 平台自动更具内容更新高度,无需调用 reszie() 方法 +- uni-data-checkbox 优化 在uni-forms组件,与label不对齐的问题 +- uni-data-checkbox 修复 单选默认值为0不能选中的Bug +- uni-easyinput 优化 errorMessage 属性支持 Boolean 类型 +- uni-file-picker 修复 return-type为object下,返回值不正确的Bug +- uni-file-picker 修复(重要) H5 平台下如果和uni-forms组件一同使用导致页面卡死的问题 +- uni-file-picker 优化 h5平台下上传文件导致页面卡死的问题 +- uni-forms 修复 vue2 下条件编译导致destroyed生命周期失效的Bug +- uni-forms 修复 1.2.1 引起的示例在小程序平台报错的Bug +- uni-forms 修复 动态校验表单,默认值为空的情况下校验失效的Bug +- uni-forms 修复 不指定name属性时,运行报错的Bug +- uni-forms 优化 label默认宽度从65调整至70,使required为true且四字时不换行 +- uni-forms 优化 组件示例,新增动态校验示例代码 +- uni-forms 优化 组件文档,使用方式更清晰 +- uni-list 修复 与其他组件嵌套使用时,点击失效的Bug +- uni-swipe-action 修复 跨页面修改组件数据 ,导致不能滑动的问题 +## 1.3.7(2021-07-16) +- uni-ui 兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +- uni-datetime-picker 修复 单选日期类型,初始赋值后不在当前日历的 bug +- uni-datetime-picker 新增 clearIcon 属性,显示框的清空按钮可配置显示隐藏(仅 pc 有效) +- uni-datetime-picker 优化 移动端移除显示框的清空按钮,无实际用途 +- uni-datetime-picker 修复 组件赋值为空,界面未更新的 bug +- uni-datetime-picker 修复 start 和 end 不能动态赋值的 bug +- uni-datetime-picker 修复 范围选类型,用户选择后再次选择右侧日历(结束日期)显示不正确的 bug +## 1.3.6(2021-07-09) +- uni-data-checkbox 优化 删除无用日志 +- uni-data-checkbox 修复 由 0.1.9 引起的非 nvue 端图标不显示的问题 +- uni-data-checkbox 修复 nvue 黑框样式问题 +- uni-datetime-picker 修复 范围选择不能动态赋值的 bug +- uni-datetime-picker 修复 范围选择的初始时间在一个月内时,造成无法选择的bug +- uni-datetime-picker 优化 弹出层在超出视窗边缘定位不准确的问题 +- uni-datetime-picker 修复 范围起始点样式的背景色与今日样式的字体前景色融合,导致日期字体看不清的 bug +- uni-datetime-picker 优化 弹出层在超出视窗边缘被遮盖的问题 +- uni-datetime-picker 新增 maskClick 事件 +- uni-datetime-picker 修复 特殊情况日历 rpx 布局错误的 bug,rpx -> px +- uni-datetime-picker 修复 范围选择时清空返回值不合理的bug,['', ''] -> [] +- uni-datetime-picker 新增 日期时间显示框支持插槽 +- uni-file-picker 修复 sourceType 缺少默认值导致 ios 无法选择文件 +- uni-file-picker 优化 解耦与uniCloud的强绑定关系 ,如不绑定服务空间,默认autoUpload为false且不可更改 +- uni-table 新增 uni-th 支持 date 日期筛选范围 +- uni-table 新增 uni-th 支持 range 筛选范围 +- uni-table 新增 uni-th 筛选功能 +## 1.3.5(2021-07-02) +- uni-card 优化 图文卡片无图片加载时,提供占位图标 +- uni-card 新增 header 插槽,自定义卡片头部( 图文卡片 mode="style" 时,不支持) +- uni-card 修复 thumbnail 不存在仍然占位的 bug +- uni-data-checkbox 修复 selectedTextColor 属性不生效的Bug +- uni-datetime-picker 优化 添加 uni-icons 依赖 +- uni-easyinput 修复 confirmType 属性(仅 type="text" 生效)导致多行文本框无法换行的 bug +- uni-file-picker 修复 由 0.0.10 版本引发的 returnType 属性失效的问题 +- uni-file-picker 优化 文件上传后进度条消失时机 +- uni-file-picker 修复 在uni-forms 中,删除文件 ,获取的值不对的Bug +- uni-forms 修复 pattern 属性在微信小程序平台无效的问题 +## 1.3.4(2021-06-25) +- uni-badge 优化 示例项目 +- uni-countdown 修复 uni-countdown 重复赋值跳两秒的 bug +- uni-easyinput 修复 passwordIcon 属性拼写错误的 bug +- uni-forms 修复 validate-trigger属性为submit且err-show-type属性为toast时不能弹出的Bug +- uni-forms 修复 只写setRules方法而导致校验不生效的Bug +- uni-forms 修复 由上个办法引发的错误提示文字错位的Bug +- uni-forms 修复 不设置 label 属性 ,无法设置label插槽的问题 +- uni-forms 修复 不设置label属性,label-width属性不生效的bug +- uni-forms 修复 setRules 方法与rules属性冲突的问题 +- uni-link 新增 download 属性,H5平台下载文件名 +- uni-popup 新增 mask-click 遮罩层点击事件 +- uni-popup 修复 nvue 平台中间弹出后,点击内容,再点击遮罩无法关闭的Bug +- uni-tag 修复 uni-tag 在字节跳动小程序上 css 类名编译错误的 bug +## 1.3.3(2021-06-18) +- uni-easyinput 新增 passwordIcon 属性,当type=password时是否显示小眼睛图标 +- uni-easyinput 修复 confirmType 属性不生效的问题 +- uni-easyinput 修复 disabled 状态可清出内容的 bug +- uni-file-picker 修复 删除文件时无法触发 v-model 的Bug +- uni-popup 修复 H5平台中间弹出后,点击内容,再点击遮罩无法关闭的Bug +- uni-popup 修复 错误的 watch 字段 +- uni-popup 修复 safeArea 属性不生效的问题 +- uni-popup 修复 点击内容,再点击遮罩无法关闭的Bug +## 1.3.2(2021-06-04) +- uni-data-checkbox 新增 map 属性,可以方便映射text/value属性 +- uni-data-checkbox 修复 不关联服务空间的情况下组件报错的Bug +- uni-data-picker 修复 上个版本引出的本地数据无法选择带有children的2级节点 +- uni-forms 修复 动态删减数据导致报错的问题 +- uni-forms 新增 modelValue 属性 ,value 即将废弃 +- uni-forms 新增 uni-forms-item 可以设置单独的 rules +- uni-forms 新增 validate 事件增加 keepitem 参数,可以选择那些字段不过滤 +- uni-forms 优化 submit 事件重命名为 validate +- uni-data-picker 修复 无法加载云端数据的问题 +- uni-data-picker 修复 v-model无效问题 +- uni-data-picker 修复 loaddata 为空数据组时加载时间过长问题 +- uni-datetime-picker 修复 图标在小程序上不显示的 bug +- uni-datetime-picker 优化 重命名引用组件,避免潜在组件命名冲突 +- uni-datetime-picker 优化 代码目录扁平化 +- uni-tag 修复 未定义 sass 变量 "$uni-color-royal" 的bug +## 1.3.1(2021-05-14) +- uni-badge 新增 uni-badge 的 absolute 属性,支持定位 +- uni-badge 新增 uni-badge 的 offset 属性,支持定位偏移 +- uni-badge 新增 uni-badge 的 is-dot 属性,支持仅显示有一个小点 +- uni-badge 新增 uni-badge 的 max-num 属性,支持自定义封顶的数字值,超过 99 显示99+ +- uni-badge 优化 uni-badge 属性 custom-style, 支持以对象形式自定义样式 +- uni-badge 修复 uni-badge 在 App 端,数字小于10时不是圆形的bug +- uni-badge 修复 uni-badge 在父元素不是 flex 布局时,宽度缩小的bug +- uni-badge 新增 uni-badge 属性 custom-style, 支持自定义样式 +- uni-datetime-picker 修复 ios 下不识别 '-' 日期格式的 bug +- uni-datetime-picker 优化 pc 下弹出层添加边框和阴影 +- uni-datetime-picker 修复 在 admin 中获取弹出层定位错误的bug +- uni-datetime-picker 修复 type 属性向下兼容,默认值从 date 变更为 datetime +- uni-datetime-picker 支持日历形式的日期+时间的范围选择 +- uni-steps 修复 uni-steps 横向布局时,多行文字高度不合理的 bug +- uni-countdown 修复 uni-countdown 不能控制倒计时的 bug +- uni-tag 修复 royal 类型无效的bug +- uni-tag 修复 uni-tag 宽度不自适应的bug +- uni-tag 新增 uni-tag 支持属性 custom-style 自定义样式 +- uni-link 新增 href 属性支持 tel:|mailto: +- uni-popup 修复 组件内放置 input 、textarea 组件,无法聚焦的问题 +- uni-popup 新增 type 属性的 left\right 值,支持左右弹出 +- uni-popup 新增 open(String:type) 方法参数 ,可以省略 type 属性 ,直接传入类型打开指定弹窗 +- uni-popup 新增 backgroundColor 属性,可定义主窗口背景色,默认不显示背景色 +- uni-popup 新增 safeArea 属性,是否适配底部安全区 +- uni-popup 修复 App\h5\微信小程序底部安全区占位不对的Bug +- uni-popup 修复 App 端弹出等待的Bug +- uni-popup 优化 提升低配设备性能,优化动画卡顿问题 +- uni-popup 优化 更简单的组件自定义方式 +- uni-table 修复 示例项目缺少组件的Bug +- uni-forms 修复 自定义检验器失效的问题 +- uni-title 修复 示例项目缺少组件的Bug +- uni-transition 修复 示例项目缺少组件的Bug +- uni-swiper-dot 修复 示例项目缺少组件的Bug +- uni-ui 新增 组件示例地址 +## 1.3.0(2021-04-23) +- uni-combox 优化 添加依赖 uni-icons, 导入后自动下载依赖 +- uni-data-picker 修复 非树形数据有 where 属性查询报错的问题 +- uni-fav 优化 添加依赖 uni-icons, 导入后自动下载依赖 +- uni-goods-nav 优化 添加依赖 uni-icons, 导入后自动下载依赖 +- uni-nav-bar 优化 添加依赖 uni-icons, 导入后自动下载依赖 +- uni-notice-bar 优化 添加依赖 uni-icons, 导入后自动下载依赖 +- uni-number-box 修复 uni-number-box 浮点数运算不精确的 bug +- uni-number-box 修复 uni-number-box change 事件触发不正确的 bug +- uni-number-box 新增 uni-number-box v-model 双向绑定 +- uni-rate 修复 布局变化后 uni-rate 星星计算不准确的 bug +- uni-rate 优化 添加依赖 uni-icons, 导入 uni-rate 自动下载依赖 +- uni-search-bar 优化 添加依赖 uni-icons, 导入后自动下载依赖 +- uni-steps 优化 添加依赖 uni-icons, 导入后自动下载依赖 +- uni-transition 新增 通过方法自定义动画 +- uni-transition 新增 custom-class 非 NVUE 平台支持自定义 class 定制样式 +- uni-transition 优化 动画触发逻辑,使动画更流畅 +- uni-transition 优化 支持单独的动画类型 +- uni-transition 优化 文档示例 +## 1.2.13(2021-04-16) +- uni-ui 新增 uni-data-picker 支持云端非树形表结构数据 +- uni-ui 修复 uni-data-checkbox nvue 下无法选中的问题 +- uni-ui 修复 uni-data-picker 根节点 parent_field 字段等于null时选择界面错乱问题 +- uni-ui 修复 uni-file-picker 选择的文件非 file-extname 字段指定的扩展名报错的Bug +- uni-ui 修复 uni-swipe-action 报错 nv_navigator is not defined 的bug +- uni-ui 修复 uni-load-more 在首页使用时,h5 平台报 'uni is not defined' 的 bug +- uni-ui 优化 uni-file-picker file-extname 字段支持字符串写法,多个扩展名需要用逗号分隔 +- uni-ui 优化 uni-pagination PC 和 移动端适配不同的 ui +- uni-ui 更新 uni-file-picker 组件示例 +- uni-ui 修复 uni-nav-bar 当 fixed 属性为 true 时铺不满屏幕的 bug +- uni-ui 新增 uni-search-bar 的 focus 事件 +- uni-ui 修复 uni-rate 属性 margin 值为 string 组件失效的 bug +- uni-data-picker 修复 本地数据概率无法回显时问题 +- uni-table 新增 sortable 属性,是否开启单列排序 +- uni-table 优化 表格多选逻辑 +## 1.2.12(2021-03-23) +- uni-ui 新增 uni-datetime-picker 的 hide-second 属性、border 属性; +- uni-ui 修复 uni-datetime-picker 选择跟显示的日期不一样的 bug, +- uni-ui 修复 uni-datetime-picker change事件触发2次的 bug +- uni-ui 修复 uni-datetime-picker 分、秒 end 范围错误的 bug +- uni-ui 新增 uni-tr selectable 属性,用于 type=selection 时,设置某行是否可由全选按钮控制 +- uni-ui 新增 uni-data-checkbox 新增 disabled属性,支持nvue +- uni-ui 优化 uni-data-checkbox 无选项时提示“暂无数据” +- uni-ui 优化 uni-data-checkbox 默认颜色显示 +- uni-ui 新增 uni-link href 属性支持 tel:|mailto: +- uni-ui 新增 uni-table 示例demo +- uni-ui 修复 uni-data-picker 微信小程序某些情况下无法选择的问题,事件无法触发的问题 +- uni-ui 修复 uni-nav-bar easycom 下,找不到 uni-status-bar 的bug +- uni-ui 修复 uni-easyinput 示例在 qq 小程序上的bug +- uni-ui 修复 uni-forms 动态显示uni-forms-item的情况下,submit 方法获取值错误的Bug +- uni-ui 调整 cli 项目 建议使用 easycom 方式引用组件,如使用按需引用,需手动维护组件内部引用 + +## 1.2.11(2021-02-24) +- 调整为uni_modules目录规范 +- uni-data-picker 新增 数据驱动的picker选择器 +- uni-file-picker 新增 文件选择上传 +- uni-row 新增 栅格系统 +- uni-data-checkbox 优化 支持 nvue +- uni-forms 修复 偶发性获取表单值错误的Bug +- uni-forms 修复 校验 uni-data-picker value 为 0 时,返回值错误的Bug +- uni-forms 修复 uni-forms-item 组件隐藏时依然触发校验的bug +- uni-forms 优化 实时校验 +- uni-forms 优化 兼容nvue页面 +- uni-easyinput 优化 兼容nvue页面 +- uni-group 优化 兼容nvue页面 +- uni-popup 优化 组件适配 PC +- uni-fab 优化 适配 PC +- uni-swiper-dot 优化 适配 PC +- uni-rate 优化 适配 PC +- uni-notice-bar 优化 适配 PC +- uni-indexed-list 优化 适配 PC +- uni-combox 优化 适配 PC +- uni-transition 优化 适配 PC +- uni-nav-bar 优化 适配 PC +- uni-swipe-action 优化 适配 PC diff --git a/im-uniapp/uni_modules/uni-ui/components/uni-ui/uni-ui.vue b/im-uniapp/uni_modules/uni-ui/components/uni-ui/uni-ui.vue new file mode 100644 index 0000000..0970892 --- /dev/null +++ b/im-uniapp/uni_modules/uni-ui/components/uni-ui/uni-ui.vue @@ -0,0 +1,7 @@ + + + diff --git a/im-uniapp/uni_modules/uni-ui/package.json b/im-uniapp/uni_modules/uni-ui/package.json new file mode 100644 index 0000000..857ed51 --- /dev/null +++ b/im-uniapp/uni_modules/uni-ui/package.json @@ -0,0 +1,129 @@ +{ + "id": "uni-ui", + "displayName": "uni-ui", + "version": "1.4.27", + "description": "uni-ui 是基于uni-app的、全端兼容的、高性能UI框架", + "keywords": [ + "uni-ui", + "uniui", + "UI组件库", + "ui框架", + "ui库" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.2.10" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-badge", + "uni-calendar", + "uni-card", + "uni-collapse", + "uni-combox", + "uni-countdown", + "uni-data-checkbox", + "uni-data-picker", + "uni-data-select", + "uni-dateformat", + "uni-datetime-picker", + "uni-drawer", + "uni-easyinput", + "uni-fab", + "uni-fav", + "uni-file-picker", + "uni-forms", + "uni-goods-nav", + "uni-grid", + "uni-group", + "uni-icons", + "uni-indexed-list", + "uni-link", + "uni-list", + "uni-load-more", + "uni-nav-bar", + "uni-notice-bar", + "uni-number-box", + "uni-pagination", + "uni-popup", + "uni-rate", + "uni-row", + "uni-search-bar", + "uni-section", + "uni-segmented-control", + "uni-steps", + "uni-swipe-action", + "uni-swiper-dot", + "uni-table", + "uni-tag", + "uni-title", + "uni-tooltip", + "uni-transition" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uni-ui/readme.md b/im-uniapp/uni_modules/uni-ui/readme.md new file mode 100644 index 0000000..23f8456 --- /dev/null +++ b/im-uniapp/uni_modules/uni-ui/readme.md @@ -0,0 +1,247 @@ +> 当前插件不包含示例页面 ,如需示例请在 HBuiderX 中新建 `hello uni-app > 扩展组件` 中查看 +> +> 代码示例地址 :[https://ext.dcloud.net.cn/plugin?id=4941](https://ext.dcloud.net.cn/plugin?id=4941) +> +> 组件演示地址:[https://hellouniapp.dcloud.net.cn](https://hellouniapp.dcloud.net.cn/pages/extUI/badge/badge) +> +> 组件文档地址:[https://uniapp.dcloud.io/component/uniui/uni-ui](https://uniapp.dcloud.io/component/uniui/uni-ui) + +# uni-ui 介绍 + +## uni-ui产品特点 + +### 1. 高性能 + +目前为止,在小程序和混合app领域,暂时还没有比 `uni-ui` 更高性能的框架。 +- 自动差量更新数据 + +虽然uni-app支持小程序自定义组件,所有小程序的ui库都可以用。但小程序自定义组件的ui库都需要使用setData手动更新数据,在大数据量时、或高频更新数据时,很容易产生性能问题。 + +而 `uni-ui` 属于vue组件,uni-app引擎底层自动diff更新数据。当然其实插件市场里众多vue组件都具备这个特点。 +- 优化逻辑层和视图层通讯折损 + +非H5,不管是小程序还是App,不管是app的webview渲染还是原生渲染,全都是逻辑层和视图层分离的。这里就有一个逻辑层和视图层通讯的折损问题。 +比如在视图层拖动一个可跟手的组件,由于通讯的损耗,用js监听很难做到实时跟手。 + +这时就需要使用css动画以及平台底层提供的wxs、bindingx等技术。不过这些技术都比较复杂,所以 `uni-ui` 里做了封装,在需要跟手式操作的ui组件,比如swiperaction列表项左滑菜单,就在底层使用了这些技术,实现了高性能的交互体验 +- 背景停止 + +很多ui组件是会一直动的,比如轮播图、跑马灯。即便这个窗体被新窗体挡住,它在背景层仍然在消耗着硬件资源。在Android的webview版本为chrome66以上,背景操作ui会引发很严重的性能问题,造成前台界面明显卡顿。 + +而 `uni-ui` 的组件,会自动判断自己的显示状态,在组件不再可见时,不会再消耗硬件资源。 + +### 2. 全端 + + `uni-ui` 的组件都是多端自适应的,底层会抹平很多小程序平台的差异或bug。 + +比如导航栏navbar组件,会自动处理不同端的状态栏。 +比如swiperaction组件,在app和微信小程序上会使用交互体验更好的wxs技术,但在不支持wxs的其他小程序端会使用js模拟类似效果。 + + `uni-ui` 还支持nvue原生渲染,[详见](https://github.com/dcloudio/uni-ui/tree/nvue-uni-ui) + +未来 `uni-ui` 还会支持pc等大屏设备。 + +### 3. 与uni统计自动集成实现免打点 + +uni统计是优秀的多端统计平台,见[tongji.dcloud.net.cn](https://tongji.dcloud.net.cn)。 + +除了一张报表看全端,它的另一个重要特点是免打点。 +比如使用 `uni-ui` 的navbar标题栏、收藏、购物车等组件,均可实现自动打点,统计页面标题等各种行为数据。 +当然你也可以关闭uni统计,这不是强制的。 + +### 4. 主题扩展 + + `uni-ui` 支持[uni.scss](https://uniapp.dcloud.io/collocation/uni-scss),可以方便的切换App的风格。 + +ui是一种需求非常发散的产品,DCloud官方也无意用 `uni-ui` 压制第三方ui插件的空间,但官方有义务在性能和多端方面提供一个开源的标杆给大家。 + +我们欢迎更多优秀的ui组件出现,也欢迎更多人贡献 `uni-ui` 的主题风格,满足更多用户的需求。 + + +## 快速开始 + +uni-ui支持 HBuilderX直接新建项目模板、npm安装和单独导入个别组件等多种使用方式 + +### 在HBuilderX 新建uni-app项目的模板中,选择uni-ui模板 +![HBuilderX内创建uni-ui项目](https://img.cdn.aliyun.dcloud.net.cn/uni-app/doc/create-uni-ui-project.jpg) + +由于uni-app独特的[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)技术,可以免引用、注册,直接使用各种符合规则的vue组件。 + +在代码区键入`u`,拉出各种内置或uni-ui的组件列表,选择其中一个,即可使用该组件。 + +光标放在组件名称上,按F1,可以查阅组件的文档。 + +![uni-ui代码块](https://img.cdn.aliyun.dcloud.net.cn/uni-app/doc/uni-ui-snippet.jpg) + +### 通过 uni_modules 单独安装组件 +如果你没有创建uni-ui项目模板,也可以在你的工程里,通过 uni_modules 单独安装需要的某个组件。下表为uni-ui的扩展组件清单,点击每个组件在详情页面可以导入组件到项目下,导入后直接使用即可,无需import和注册。 + +|组件名|组件说明| +|---|---| +|uni-badge|[数字角标](https://ext.dcloud.net.cn/plugin?name=uni-badge)| +|uni-calendar|[日历](https://ext.dcloud.net.cn/plugin?name=uni-calendar)| +|uni-card|[卡片](https://ext.dcloud.net.cn/plugin?name=uni-card)| +|uni-collapse|[折叠面板](https://ext.dcloud.net.cn/plugin?name=uni-collapse)| +|uni-combox|[组合框](https://ext.dcloud.net.cn/plugin?name=uni-combox)| +|uni-countdown|[倒计时](https://ext.dcloud.net.cn/plugin?name=uni-countdown)| +|uni-data-checkbox|[数据选择器](https://ext.dcloud.net.cn/plugin?name=uni-data-checkbox)| +|uni-data-picker|[数据驱动的picker选择器](https://ext.dcloud.net.cn/plugin?name=uni-data-picker)| +|uni-dateformat|[日期格式化](https://ext.dcloud.net.cn/plugin?name=uni-dateformat)| +|uni-datetime-picker|[日期选择器](https://ext.dcloud.net.cn/plugin?name=uni-datetime-picker)| +|uni-drawer|[抽屉](https://ext.dcloud.net.cn/plugin?name=uni-drawer)| +|uni-easyinput|[增强输入框](https://ext.dcloud.net.cn/plugin?name=uni-easyinput)| +|uni-fab|[悬浮按钮](https://ext.dcloud.net.cn/plugin?name=uni-fab)| +|uni-fav|[收藏按钮](https://ext.dcloud.net.cn/plugin?name=uni-fav)| +|uni-file-picker|[文件选择上传](https://ext.dcloud.net.cn/plugin?name=uni-file-picker)| +|uni-forms|[表单](https://ext.dcloud.net.cn/plugin?name=uni-forms)| +|uni-goods-nav|[商品导航](https://ext.dcloud.net.cn/plugin?name=uni-goods-nav)| +|uni-grid|[宫格](https://ext.dcloud.net.cn/plugin?name=uni-grid)| +|uni-group|[分组](https://ext.dcloud.net.cn/plugin?name=uni-group)| +|uni-icons|[图标](https://ext.dcloud.net.cn/plugin?name=uni-icons)| +|uni-indexed-list|[索引列表](https://ext.dcloud.net.cn/plugin?name=uni-indexed-list)| +|uni-link|[超链接](https://ext.dcloud.net.cn/plugin?name=uni-link)| +|uni-list|[列表](https://ext.dcloud.net.cn/plugin?name=uni-list)| +|uni-load-more|[加载更多](https://ext.dcloud.net.cn/plugin?name=uni-load-more)| +|uni-nav-bar|[自定义导航栏](https://ext.dcloud.net.cn/plugin?name=uni-nav-bar)| +|uni-notice-bar|[通告栏](https://ext.dcloud.net.cn/plugin?name=uni-notice-bar)| +|uni-number-box|[数字输入框](https://ext.dcloud.net.cn/plugin?name=uni-number-box)| +|uni-pagination|[分页器](https://ext.dcloud.net.cn/plugin?name=uni-pagination)| +|uni-popup|[弹出层](https://ext.dcloud.net.cn/plugin?name=uni-popup)| +|uni-rate|[评分](https://ext.dcloud.net.cn/plugin?name=uni-rate)| +|uni-row|[布局-行](https://ext.dcloud.net.cn/plugin?name=uni-row)| +|uni-search-bar|[搜索栏](https://ext.dcloud.net.cn/plugin?name=uni-search-bar)| +|uni-segmented-control|[分段器](https://ext.dcloud.net.cn/plugin?name=uni-segmented-control)| +|uni-steps|[步骤条](https://ext.dcloud.net.cn/plugin?name=uni-steps)| +|uni-swipe-action|[滑动操作](https://ext.dcloud.net.cn/plugin?name=uni-swipe-action)| +|uni-swiper-dot|[轮播图指示点](https://ext.dcloud.net.cn/plugin?name=uni-swiper-dot)| +|uni-table|[表格](https://ext.dcloud.net.cn/plugin?name=uni-table)| +|uni-tag|[标签](https://ext.dcloud.net.cn/plugin?name=uni-tag)| +|uni-title|[章节标题](https://ext.dcloud.net.cn/plugin?name=uni-title)| +|uni-transition|[过渡动画](https://ext.dcloud.net.cn/plugin?name=uni-transition)| + + +使用 `uni_modules` 方式安装组件库,可以直接通过插件市场导入,通过右键菜单快速更新组件,不需要引用、注册,直接在页面中使用 `uni-ui` 组件。[点击安装 uni-ui 组件库](https://ext.dcloud.net.cn/plugin?id=55) + +**注意:下载最新的组件目前仅支持 uni_modules ,非 uni_modules 版本最高支持到组件的1.2.10版本** + +如不能升级到 `uni_modules` 版本,可以使用 `uni_modules` 安装好对应组件,将组件拷贝到对应目录。 + +例如需更新 `uni-list`和`uni-badge` ,将 `uni_modules>uni-list>components`和`uni_modules>uni-badege>components`下所有目录拷贝到如下目录即可: + + +**目录示例** + +```json +┌─components 组件目录 +│ ├─uni-list list 列表目录 +│ │ └─uni-list.vue list 组件文件 +│ ├─uni-list-item list-item 列表目录 +│ │ └─uni-list-item.vue list 组件文件 +│ ├─uni-badge badge 角标目录 +│ │ └─uni-badge.vue badge 组件文件 +│ └─ //.... 更多组件文件 +├─pages 业务页面文件存放的目录 +│ ├─index +│ │ └─index.vue index示例页面 +├─main.js Vue初始化入口文件 +├─App.vue 应用配置,用来配置App全局样式以及监听 应用生命周期 +├─manifest.json 配置应用名称、appid、logo、版本等打包信息,详见 +└─pages.json 配置页 + +``` + +### 通过 `uni_modules` 导入全部组件 +如果想一次把所有uni-ui组件导入到项目中,只需要导入一个 `uni-ui` 组件即可 [点击去导入](https://ext.dcloud.net.cn/plugin?id=55)。 + +如果没有自动导入其他组件,可以在 uni-ui 组件目录上右键选择 `安装三方插件依赖` 即可。 + + + +### npm安装 +在 `vue-cli` 项目中可以使用 `npm` 安装 `uni-ui` 库 ,或者直接在 `HBuilderX` 项目中使用 `npm` 。 + +> **注意** +> cli 项目默认是不编译 `node_modules` 下的组件的,导致条件编译等功能失效 ,导致组件异常 +> 需要在根目录创建 `vue.config.js` 文件 ,增加 `@dcloudio/uni-ui` 包的编译即可正常 +> ```javascript +> // vue.config.js +> module.exports = { +> transpileDependencies:['@dcloudio/uni-ui'] +> } +> ``` + + + +**准备 sass** + +`vue-cli` 项目请先安装 sass 及 sass-loader,如在 HBuliderX 中使用,可跳过此步。 + +- 安装 sass +``` + npm i sass -D 或 yarn add sass -D +``` + +- 安装 sass-loader +``` +npm i sass-loader@10.1.1 -D 或 yarn add sass-loader@10.1.1 -D +``` + +> 如果 `node` 版本小于 16 ,sass-loader 请使用低于 @11.0.0 的版本,[sass-loader@11.0.0 不支持 vue@2.6.12 ](https://stackoverflow.com/questions/66082397/typeerror-this-getoptions-is-not-a-function) +> 如果 `node` 版本大于 16 , `sass-loader` 建议使用 `v8.x` 版本 + +**安装 uni-ui** + +``` +npm i @dcloudio/uni-ui 或 yarn add @dcloudio/uni-ui +``` + + + +**配置easycom** + +使用 `npm` 安装好 `uni-ui` 之后,需要配置 `easycom` 规则,让 `npm` 安装的组件支持 `easycom` + +打开项目根目录下的 `pages.json` 并添加 `easycom` 节点: + +```javascript +// pages.json +{ + "easycom": { + "autoscan": true, + "custom": { + // uni-ui 规则如下配置 + "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" + } + }, + + // 其他内容 + pages:[ + // ... + ] +} + +``` + +在 ``template`` 中使用组件: + +```html + + + +``` + + **注意** + - uni-ui 现在只推荐使用 `easycom` ,如自己引用组件,可能会出现组件找不到的问题 + - 使用 npm 安装的组件,默认情况下 babel-loader 会忽略所有 node_modules 中的文件 ,导致条件编译失效,需要通过配置 `vue.config.js` 解决: + ```javascript + // 在根目录创建 vue.config.js 文件,并配置如下 + module.exports = { + transpileDependencies: ['@dcloudio/uni-ui'] + } + ``` + - uni-ui 是uni-app内置组件的扩展。注意与web开发不同,uni-ui不包括基础组件,它是基础组件的补充。web开发中有的开发者习惯用一个ui库完成所有开发,但在uni-app体系中,推荐开发者首先使用性能更高的基础组件,然后按需引入必要的扩展组件。 + - `uni-ui` 不支持使用 `Vue.use()` 的方式安装 + + +### 贡献代码 +在使用 `uni-ui` 中,如遇到无法解决的问题,请提 [Issues](https://github.com/dcloudio/uni-ui/issues) 给我们,假如您有更好的点子或更好的实现方式,也欢迎给我们提交 [PR](https://github.com/dcloudio/uni-ui/pulls) \ No newline at end of file diff --git a/im-uniapp/uni_modules/uview-plus/LICENSE b/im-uniapp/uni_modules/uview-plus/LICENSE new file mode 100644 index 0000000..8e39ead --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 www.uviewui.com + +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. \ No newline at end of file diff --git a/im-uniapp/uni_modules/uview-plus/README.md b/im-uniapp/uni_modules/uview-plus/README.md new file mode 100644 index 0000000..427a3ee --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/README.md @@ -0,0 +1,64 @@ +

+ logo +

+

uview-plus 3.0

+

多平台快速开发的UI框架

+ +[![stars](https://img.shields.io/github/stars/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus) +[![forks](https://img.shields.io/github/forks/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus) +[![issues](https://img.shields.io/github/issues/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus/issues) +[![release](https://img.shields.io/github/v/release/ijry/uview-plus?style=flat-square)](https://gitee.com/jry/uview-plus/releases) +[![license](https://img.shields.io/github/license/ijry/uview-plus?style=flat-square)](https://en.wikipedia.org/wiki/MIT_License) + +## 说明 + +uview-plus,是uni-app全面兼容vue3/nvue的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水。uview-plus是基于uView2.x移植的支持vue3的版本,感谢uView。 + +## [官方文档:https://uview-plus.jiangruyi.com](https://uview-plus.jiangruyi.com) + + +## 预览 + +您可以通过**微信**扫码,查看最佳的演示效果。 +
+
+ + +## 链接 + +- [官方文档](https://uview-plus.jiangruyi.com) +- [更新日志](https://uview-plus.jiangruyi.com/components/changelog.html) +- [升级指南](https://uview-plus.jiangruyi.com/components/changeGuide.html) +- [关于我们](https://uview-plus.jiangruyi.com/cooperation/about.html) + +## 交流反馈 + +欢迎加入我们的QQ群交流反馈:[点此跳转](https://uview-plus.jiangruyi.com/components/addQQGroup.html) + +## 关于PR + +> 我们非常乐意接受各位的优质PR,但在此之前我希望您了解uview-plus是一个需要兼容多个平台的(小程序、h5、ios app、android app)包括nvue页面、vue页面。 +> 所以希望在您修复bug并提交之前尽可能的去这些平台测试一下兼容性。最好能携带测试截图以方便审核。非常感谢! + +## 安装 + +#### **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?name=uview-plus](https://ext.dcloud.net.cn/plugin?name=uview-plus) + +请通过[官网安装文档](https://uview-plus.jiangruyi.com/components/install.html)了解更详细的内容 + +## 快速上手 + +请通过[快速上手](https://uview-plus.jiangruyi.com/components/quickstart.html)了解更详细的内容 + +## 使用方法 +配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。 + +```html + +``` + +## 版权信息 +uview-plus遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uview-plus应用到您的产品中。 + diff --git a/im-uniapp/uni_modules/uview-plus/changelog.md b/im-uniapp/uni_modules/uview-plus/changelog.md new file mode 100644 index 0000000..1d40799 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/changelog.md @@ -0,0 +1,242 @@ +## 3.3.5(2024-07-10) +picker组件支持hasInput模式 +## 3.3.4(2024-07-07) +fix: input组件双向绑定问题 #419 + +lazy-load完善emit + +优化通用小程序分享 + +## 3.3.2(2024-06-27) +fix: 在Nvue环境中编译,出现大量警告 #406 +## 3.3.1(2024-06-27) +u-button组件报错,找不到button mixins #407 +## 3.3.0(2024-06-27) +feat: checkbox支持label设置slot + +feat: modal增加customClass + +feat: navbar、popup、tabs、text支持customClass + +fix: cell组建缺少flex布局 + +fix: 修复微信小程序真机调试时快速输入出现文本回退问题 + +feat: tag增加默认slot + +公共mixin改造为按需导入语法 + +refactor: 组件props混入mixin改造为按需导入语法 + +fix: u-tabbar 安卓手机点击按钮变蓝问题 #396 + +feat: upload组建增加extension属性 + +fix: upload组件参数mode添加left + +fix: 修复阴影在非nvue时白色背景色不显示 + +## 3.2.24(2024-06-11) +fix: 修复时间选择器confirm事件触发时机导致2次才会触发v-model更新 +## 3.2.23(2024-05-30) +fix: #378 H5 u-input 在表单中初始值为空也会触发一次 formValidate(this,"change")事件导致进入页面直接校验了一次 + +fix: #373 搜索组件up-search的@clear事件无效 + +fix: #372 ActionSheet 组件的取消按钮触发区域太小 + +## 3.2.22(2024-05-13) +上传组件支持微信小程序预览视频 + +修复折叠面板右侧箭头不显示 + +修复uxp2px + +## 3.2.21(2024-05-10) +fix: loading-icon修复flex布局 +## 3.2.20(2024-05-10) +修复瀑布流大小写#355 +## 3.2.19(2024-05-10) +去除意外的文件引入 +## 3.2.18(2024-05-09) +fix: 349 popup 组件设置 zIndex 属性后,组件渲染异常# +feat: 搜索框增加adjustPosition属性 +fix: #331增加u-action-sheet__cancel +优化mixin兼容性 +feat: #326 up-list增加下拉刷新功能 +fix: #319 优化up-tabs参数与定义匹配 +fix: index-list组件微信小程序端使用自定义导航栏异常 +fix: #285 pickerimmediateChange 写死为true +fix: #111 u-scroll-list组件,隐藏指示器后报错, 提示找不到ref +list增加微信小程序防抖配置 + +## 3.2.17(2024-05-08) +fix: 支付宝小程序二维码渲染 +## 3.2.16(2024-05-06) +修复tabs中,当前激活样式的undefined bug + +fix: #341u-code 倒计时没结束前退出,再次进入结束后退出界面,再次进入重新开始倒计时bug + +受到uni-app内置text样式影响修复 + +## 3.2.15(2024-04-28) +优化时间选择器hasInput模式初始化值 +## 3.2.14(2024-04-24) +去除pleaseSetTranspileDependencies + +http采用useStore + +## 3.2.13(2024-04-22) +修复modal标题样式 + +优化日期选择器hasInput模式宽度 + +## 3.2.12(2024-04-22) +修复color应用 +## 3.2.11(2024-04-18) +修复import化带来的问题 +## 3.2.10(2024-04-17) +完善input清空事件App端失效的兼容性 + +修复日历组件二次打开后当前月份显示不正确 + +## 3.2.9(2024-04-16) +组件内uni.$u用法改为import引入 + +规范化及兼容性增强 + +## 3.2.8(2024-04-15) +修复up-tag语法错 +## 3.2.7(2024-04-15) +修复下拉菜单背景色在支付宝小程序无效 + +setConfig改为浅拷贝解决无法用import导入代替uni.$u.props设置 + +## 3.2.6(2024-04-14) +修复某些情况下滑动单元格默认右侧按钮是展开的问题 +## 3.2.5(2024-04-13) +调整分段器尺寸及修复窗口大小改变时重新计算尺寸 + +多个组件支持cursor-pointer增强PC端体验 + +## 3.2.4(2024-04-12) +初步支持typescript +## 3.2.3(2024-04-12) +fix: 修复square属性在小程序下无效问题 + +fix:修复lastIndex异常导致的column异常问题 + +fix: alipayapp picker style + +feat(button): 添加用户同意隐私协议事件回调 + +fix: input switch password + +fix: 修复u-code组件keepRuning失效问题 + +feat: form-item添加labelPosition属性 + +新增dropdown组件 + +分段器支持内部current值 + +优化cell和action-sheet视觉大小 + +修复tabs文字换行 + +## 3.2.2(2024-04-11) +修复换行符问题 +## 3.2.1(2024-04-11) +修复演示H5二维码 + +fix: #270 ReadMore 展开阅读更多内容变化兼容 + +fix: #238Calendar组件maxDate修改为不能小于minDate + +checkbox支持独立使用 + +修复popup中在微信小程序中真机调试滚动失效 + +## 3.2.0(2024-04-10) +修复轮播图在nvue显示 +修复疑似u-slider名称被占用导致slider在App下不显示 +解决微信小程序提示 Some selectors are not allowed in component wxss +示例中u-前缀统一为up- +增加瀑布流与图片懒加载组件 +fix: #308修复tag组件缺失iconColor参数 +fix: #297使用grid布局解决目前编译为抖音小程序无法开启virtualHost +## 3.1.52(2024-04-07) +工具类方法调用import化改造 +新增up-copy复制组件 +## 3.1.51(2024-04-07) +优化时间选择器自带输入框格式化显示 +防止按钮文字换行 +修复订单列表模板滑动 +增加u-qrcode二维码组件 +## 3.1.49(2024-03-27) +日期时间组件支持自带输入框 +fix: popup弹窗滚动穿透问题 +fix: 修复小程序numberbox bug +## 3.1.48(2024-03-18) +fix:[plugin:uni:pre-css] Unbalanced delimiter found in string +## 3.1.47(2024-03-18) +fix: setConfig设置组件默认参数无效问题 +fix: 修复自定义图标无效问题 +feat: 增加u-form-item单独设置规则变量 +fix:#293小程序是自定义导航栏的时候即传了customNavHeight的时候会出现跳转偏移的情况 + +## 3.1.46(2024-01-29) +beforeUnmount +## 3.1.45(2024-01-24) +fix: #262ext组件为超链接的情况下size属性不生效 +fix: #263最新版本3.1.42中微信小程序u-swipe-action-item报错 +fix: #224最新版本3.1.42中微信小程序u-swipe-action-item报错 +fix: #263支持支付宝小程序 +fix: #261u-input在直接修改v-model的绑定值时,每隔一次会无法出发change事件 +优化折叠面板兼容微信小程序 +## 3.1.42(2024-01-15) +修复u-number-box默认值0时在小程序不显示值 +优化u-code的timer判断 +优化支付宝小程序下textarea字数统计兼容 +优化u-calendar +## 3.1.41(2023-11-18) +#215优化u-cell图标容器间距问题 +## 3.1.40(2023-11-16) +修复u-slider双向绑定 +## 3.1.39(2023-11-10) +修复头条小程序不支持env(safe-area-inset-bottom) +优化#201u-grid 指定列数导致闪烁 +#193IndexList 索引列表 高度错误 +其他优化 +## 3.1.38(2023-10-08) +修复u-slider +## 3.1.37(2023-09-13) +完善emits定义及修复code-input双向数据绑定 +## 3.1.36(2023-08-08) +修复富文本事件名称大小写 +## 3.1.35(2023-08-02) +修复编译到支付宝小程序u-form报错 +## 3.1.34(2023-07-27) +修复App打包uni.$u.mpMixin方式sdk暂时不支持导致报错 +## 3.1.33(2023-07-13) +修复弹窗进入动画、模板页面样式等 +## 3.1.31(2023-07-11) +修复dayjs引用 +## 3.0.8(2022-07-12) +修复u-tag默认宽度撑满容器 +## 3.0.7(2022-07-12) +修复u-navbar自定义插槽演示示例 +## 3.0.6(2022-07-11) +修复u-image缺少emits申明 +## 3.0.5(2022-07-11) +修复u-upload缺少emits申明 +## 3.0.4(2022-07-10) +修复u-textarea/u-input/u-datetime-picker/u-number-box/u-radio-group/u-switch/u-rate在vue3下数据绑定 +## 3.0.3(2022-07-09) +启用自建演示二维码 +## 3.0.2(2022-07-09) +修复dayjs/clipboard等导致打包报错 +## 3.0.1(2022-07-09) +增加插件市场地址 +## 3.0.0(2022-07-09) +# uview-plus(vue3)初步发布 diff --git a/im-uniapp/uni_modules/uview-plus/components/u--form/u--form.vue b/im-uniapp/uni_modules/uview-plus/components/u--form/u--form.vue new file mode 100644 index 0000000..78e9dc4 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u--form/u--form.vue @@ -0,0 +1,80 @@ + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u--image/u--image.vue b/im-uniapp/uni_modules/uview-plus/components/u--image/u--image.vue new file mode 100644 index 0000000..5916065 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u--image/u--image.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/im-uniapp/uni_modules/uview-plus/components/u--input/u--input.vue b/im-uniapp/uni_modules/uview-plus/components/u--input/u--input.vue new file mode 100644 index 0000000..2a8933f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u--input/u--input.vue @@ -0,0 +1,74 @@ + + + \ No newline at end of file diff --git a/im-uniapp/uni_modules/uview-plus/components/u--text/u--text.vue b/im-uniapp/uni_modules/uview-plus/components/u--text/u--text.vue new file mode 100644 index 0000000..9cdcf7b --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u--text/u--text.vue @@ -0,0 +1,45 @@ + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u--textarea/u--textarea.vue b/im-uniapp/uni_modules/uview-plus/components/u--textarea/u--textarea.vue new file mode 100644 index 0000000..becd23d --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u--textarea/u--textarea.vue @@ -0,0 +1,47 @@ + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-action-sheet/props.js b/im-uniapp/uni_modules/uview-plus/components/u-action-sheet/props.js new file mode 100644 index 0000000..2f365f0 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-action-sheet/props.js @@ -0,0 +1,57 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' + +export const props = defineMixin({ + props: { + // 操作菜单是否展示 (默认false) + show: { + type: Boolean, + default: () => defProps.actionSheet.show + }, + // 标题 + title: { + type: String, + default: () => defProps.actionSheet.title + }, + // 选项上方的描述信息 + description: { + type: String, + default: () => defProps.actionSheet.description + }, + // 数据 + actions: { + type: Array, + default: () => defProps.actionSheet.actions + }, + // 取消按钮的文字,不为空时显示按钮 + cancelText: { + type: String, + default: () => defProps.actionSheet.cancelText + }, + // 点击某个菜单项时是否关闭弹窗 + closeOnClickAction: { + type: Boolean, + default: () => defProps.actionSheet.closeOnClickAction + }, + // 处理底部安全区(默认true) + safeAreaInsetBottom: { + type: Boolean, + default: () => defProps.actionSheet.safeAreaInsetBottom + }, + // 小程序的打开方式 + openType: { + type: String, + default: () => defProps.actionSheet.openType + }, + // 点击遮罩是否允许关闭 (默认true) + closeOnClickOverlay: { + type: Boolean, + default: () => defProps.actionSheet.closeOnClickOverlay + }, + // 圆角值 + round: { + type: [Boolean, String, Number], + default: () => defProps.actionSheet.round + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-action-sheet/u-action-sheet.vue b/im-uniapp/uni_modules/uview-plus/components/u-action-sheet/u-action-sheet.vue new file mode 100644 index 0000000..3c28340 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-action-sheet/u-action-sheet.vue @@ -0,0 +1,281 @@ + + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-album/props.js b/im-uniapp/uni_modules/uview-plus/components/u-album/props.js new file mode 100644 index 0000000..eb0f181 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-album/props.js @@ -0,0 +1,61 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 图片地址,Array|Array形式 + urls: { + type: Array, + default: () => defProps.album.urls + }, + // 指定从数组的对象元素中读取哪个属性作为图片地址 + keyName: { + type: String, + default: () => defProps.album.keyName + }, + // 单图时,图片长边的长度 + singleSize: { + type: [String, Number], + default: () => defProps.album.singleSize + }, + // 多图时,图片边长 + multipleSize: { + type: [String, Number], + default: () => defProps.album.multipleSize + }, + // 多图时,图片水平和垂直之间的间隔 + space: { + type: [String, Number], + default: () => defProps.album.space + }, + // 单图时,图片缩放裁剪的模式 + singleMode: { + type: String, + default: () => defProps.album.singleMode + }, + // 多图时,图片缩放裁剪的模式 + multipleMode: { + type: String, + default: () => defProps.album.multipleMode + }, + // 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量 + maxCount: { + type: [String, Number], + default: () => defProps.album.maxCount + }, + // 是否可以预览图片 + previewFullImage: { + type: Boolean, + default: () => defProps.album.previewFullImage + }, + // 每行展示图片数量,如设置,singleSize和multipleSize将会无效 + rowCount: { + type: [String, Number], + default: () => defProps.album.rowCount + }, + // 超出maxCount时是否显示查看更多的提示 + showMore: { + type: Boolean, + default: () => defProps.album.showMore + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-album/u-album.vue b/im-uniapp/uni_modules/uview-plus/components/u-album/u-album.vue new file mode 100644 index 0000000..4904019 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-album/u-album.vue @@ -0,0 +1,264 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/uni_modules/uview-plus/components/u-alert/props.js b/im-uniapp/uni_modules/uview-plus/components/u-alert/props.js new file mode 100644 index 0000000..9f150b5 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-alert/props.js @@ -0,0 +1,46 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 显示文字 + title: { + type: String, + default: () => defProps.alert.title + }, + // 主题,success/warning/info/error + type: { + type: String, + default: () => defProps.alert.type + }, + // 辅助性文字 + description: { + type: String, + default: () => defProps.alert.description + }, + // 是否可关闭 + closable: { + type: Boolean, + default: () => defProps.alert.closable + }, + // 是否显示图标 + showIcon: { + type: Boolean, + default: () => defProps.alert.showIcon + }, + // 浅或深色调,light-浅色,dark-深色 + effect: { + type: String, + default: () => defProps.alert.effect + }, + // 文字是否居中 + center: { + type: Boolean, + default: () => defProps.alert.center + }, + // 字体大小 + fontSize: { + type: [String, Number], + default: () => defProps.alert.fontSize + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-alert/u-alert.vue b/im-uniapp/uni_modules/uview-plus/components/u-alert/u-alert.vue new file mode 100644 index 0000000..dea66b1 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-alert/u-alert.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-avatar-group/props.js b/im-uniapp/uni_modules/uview-plus/components/u-avatar-group/props.js new file mode 100644 index 0000000..96ed871 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-avatar-group/props.js @@ -0,0 +1,54 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 头像图片组 + urls: { + type: Array, + default: () => defProps.avatarGroup.urls + }, + // 最多展示的头像数量 + maxCount: { + type: [String, Number], + default: () => defProps.avatarGroup.maxCount + }, + // 头像形状 + shape: { + type: String, + default: () => defProps.avatarGroup.shape + }, + // 图片裁剪模式 + mode: { + type: String, + default: () => defProps.avatarGroup.mode + }, + // 超出maxCount时是否显示查看更多的提示 + showMore: { + type: Boolean, + default: () => defProps.avatarGroup.showMore + }, + // 头像大小 + size: { + type: [String, Number], + default: () => defProps.avatarGroup.size + }, + // 指定从数组的对象元素中读取哪个属性作为图片地址 + keyName: { + type: String, + default: () => defProps.avatarGroup.keyName + }, + // 头像之间的遮挡比例 + gap: { + type: [String, Number], + validator(value) { + return value >= 0 && value <= 1 + }, + default: () => defProps.avatarGroup.gap + }, + // 需额外显示的值 + extraValue: { + type: [Number, String], + default: () => defProps.avatarGroup.extraValue + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-avatar-group/u-avatar-group.vue b/im-uniapp/uni_modules/uview-plus/components/u-avatar-group/u-avatar-group.vue new file mode 100644 index 0000000..fdfeaad --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-avatar-group/u-avatar-group.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-avatar/props.js b/im-uniapp/uni_modules/uview-plus/components/u-avatar/props.js new file mode 100644 index 0000000..8fa15ac --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-avatar/props.js @@ -0,0 +1,81 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +import test from '../../libs/function/test'; +export const props = defineMixin({ + props: { + // 头像图片路径(不能为相对路径) + src: { + type: String, + default: () => defProps.avatar.src + }, + // 头像形状,circle-圆形,square-方形 + shape: { + type: String, + default: () => defProps.avatar.shape + }, + // 头像尺寸 + size: { + type: [String, Number], + default: () => defProps.avatar.size + }, + // 裁剪模式 + mode: { + type: String, + default: () => defProps.avatar.mode + }, + // 显示的文字 + text: { + type: String, + default: () => defProps.avatar.text + }, + // 背景色 + bgColor: { + type: String, + default: () => defProps.avatar.bgColor + }, + // 文字颜色 + color: { + type: String, + default: () => defProps.avatar.color + }, + // 文字大小 + fontSize: { + type: [String, Number], + default: () => defProps.avatar.fontSize + }, + // 显示的图标 + icon: { + type: String, + default: () => defProps.avatar.icon + }, + // 显示小程序头像,只对百度,微信,QQ小程序有效 + mpAvatar: { + type: Boolean, + default: () => defProps.avatar.mpAvatar + }, + // 是否使用随机背景色 + randomBgColor: { + type: Boolean, + default: () => defProps.avatar.randomBgColor + }, + // 加载失败的默认头像(组件有内置默认图片) + defaultUrl: { + type: String, + default: () => defProps.avatar.defaultUrl + }, + // 如果配置了randomBgColor为true,且配置了此值,则从默认的背景色数组中取出对应索引的颜色值,取值0-19之间 + colorIndex: { + type: [String, Number], + // 校验参数规则,索引在0-19之间 + validator(n) { + return test.range(n, [0, 19]) || n === '' + }, + default: () => defProps.avatar.colorIndex + }, + // 组件标识符 + name: { + type: String, + default: () => defProps.avatar.name + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-avatar/u-avatar.vue b/im-uniapp/uni_modules/uview-plus/components/u-avatar/u-avatar.vue new file mode 100644 index 0000000..71c0b22 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-avatar/u-avatar.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-back-top/props.js b/im-uniapp/uni_modules/uview-plus/components/u-back-top/props.js new file mode 100644 index 0000000..b714641 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-back-top/props.js @@ -0,0 +1,56 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 返回顶部的形状,circle-圆形,square-方形 + mode: { + type: String, + default: () => defProps.backtop.mode + }, + // 自定义图标 + icon: { + type: String, + default: () => defProps.backtop.icon + }, + // 提示文字 + text: { + type: String, + default: () => defProps.backtop.text + }, + // 返回顶部滚动时间 + duration: { + type: [String, Number], + default: () => defProps.backtop.duration + }, + // 滚动距离 + scrollTop: { + type: [String, Number], + default: () => defProps.backtop.scrollTop + }, + // 距离顶部多少距离显示,单位px + top: { + type: [String, Number], + default: () => defProps.backtop.top + }, + // 返回顶部按钮到底部的距离,单位px + bottom: { + type: [String, Number], + default: () => defProps.backtop.bottom + }, + // 返回顶部按钮到右边的距离,单位px + right: { + type: [String, Number], + default: () => defProps.backtop.right + }, + // 层级 + zIndex: { + type: [String, Number], + default: () => defProps.backtop.zIndex + }, + // 图标的样式,对象形式 + iconStyle: { + type: Object, + default: () => defProps.backtop.iconStyle + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-back-top/u-back-top.vue b/im-uniapp/uni_modules/uview-plus/components/u-back-top/u-back-top.vue new file mode 100644 index 0000000..fcdb045 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-back-top/u-back-top.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-badge/props.js b/im-uniapp/uni_modules/uview-plus/components/u-badge/props.js new file mode 100644 index 0000000..5ed924b --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-badge/props.js @@ -0,0 +1,79 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 是否显示圆点 + isDot: { + type: Boolean, + default: () => defProps.badge.isDot + }, + // 显示的内容 + value: { + type: [Number, String], + default: () => defProps.badge.value + }, + // 显示的内容 + modelValue: { + type: [Number, String], + default: () => defProps.badge.modelValue + }, + // 是否显示 + show: { + type: Boolean, + default: () => defProps.badge.show + }, + // 最大值,超过最大值会显示 '{max}+' + max: { + type: [Number, String], + default: () => defProps.badge.max + }, + // 主题类型,error|warning|success|primary + type: { + type: String, + default: () => defProps.badge.type + }, + // 当数值为 0 时,是否展示 Badge + showZero: { + type: Boolean, + default: () => defProps.badge.showZero + }, + // 背景颜色,优先级比type高,如设置,type参数会失效 + bgColor: { + type: [String, null], + default: () => defProps.badge.bgColor + }, + // 字体颜色 + color: { + type: [String, null], + default: () => defProps.badge.color + }, + // 徽标形状,circle-四角均为圆角,horn-左下角为直角 + shape: { + type: String, + default: () => defProps.badge.shape + }, + // 设置数字的显示方式,overflow|ellipsis|limit + // overflow会根据max字段判断,超出显示`${max}+` + // ellipsis会根据max判断,超出显示`${max}...` + // limit会依据1000作为判断条件,超出1000,显示`${value/1000}K`,比如2.2k、3.34w,最多保留2位小数 + numberType: { + type: String, + default: () => defProps.badge.numberType + }, + // 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,absolute为true时有效 + offset: { + type: Array, + default: () => defProps.badge.offset + }, + // 是否反转背景和字体颜色 + inverted: { + type: Boolean, + default: () => defProps.badge.inverted + }, + // 是否绝对定位 + absolute: { + type: Boolean, + default: () => defProps.badge.absolute + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-badge/u-badge.vue b/im-uniapp/uni_modules/uview-plus/components/u-badge/u-badge.vue new file mode 100644 index 0000000..ecc843f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-badge/u-badge.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-button/nvue.scss b/im-uniapp/uni_modules/uview-plus/components/u-button/nvue.scss new file mode 100644 index 0000000..490db7d --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-button/nvue.scss @@ -0,0 +1,46 @@ +$u-button-active-opacity:0.75 !default; +$u-button-loading-text-margin-left:4px !default; +$u-button-text-color: #FFFFFF !default; +$u-button-text-plain-error-color:$u-error !default; +$u-button-text-plain-warning-color:$u-warning !default; +$u-button-text-plain-success-color:$u-success !default; +$u-button-text-plain-info-color:$u-info !default; +$u-button-text-plain-primary-color:$u-primary !default; +.u-button { + &--active { + opacity: $u-button-active-opacity; + } + + &--active--plain { + background-color: rgb(217, 217, 217); + } + + &__loading-text { + margin-left:$u-button-loading-text-margin-left; + } + + &__text, + &__loading-text { + color:$u-button-text-color; + } + + &__text--plain--error { + color:$u-button-text-plain-error-color; + } + + &__text--plain--warning { + color:$u-button-text-plain-warning-color; + } + + &__text--plain--success{ + color:$u-button-text-plain-success-color; + } + + &__text--plain--info { + color:$u-button-text-plain-info-color; + } + + &__text--plain--primary { + color:$u-button-text-plain-primary-color; + } +} \ No newline at end of file diff --git a/im-uniapp/uni_modules/uview-plus/components/u-button/props.js b/im-uniapp/uni_modules/uview-plus/components/u-button/props.js new file mode 100644 index 0000000..d01f0ab --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-button/props.js @@ -0,0 +1,154 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 是否细边框 + hairline: { + type: Boolean, + default: () => defProps.button.hairline + }, + // 按钮的预置样式,info,primary,error,warning,success + type: { + type: String, + default: () => defProps.button.type + }, + // 按钮尺寸,large,normal,small,mini + size: { + type: String, + default: () => defProps.button.size + }, + // 按钮形状,circle(两边为半圆),square(带圆角) + shape: { + type: String, + default: () => defProps.button.shape + }, + // 按钮是否镂空 + plain: { + type: Boolean, + default: () => defProps.button.plain + }, + // 是否禁止状态 + disabled: { + type: Boolean, + default: () => defProps.button.disabled + }, + // 是否加载中 + loading: { + type: Boolean, + default: () => defProps.button.loading + }, + // 加载中提示文字 + loadingText: { + type: [String, Number], + default: () => defProps.button.loadingText + }, + // 加载状态图标类型 + loadingMode: { + type: String, + default: () => defProps.button.loadingMode + }, + // 加载图标大小 + loadingSize: { + type: [String, Number], + default: () => defProps.button.loadingSize + }, + // 开放能力,具体请看uniapp稳定关于button组件部分说明 + // https://uniapp.dcloud.io/component/button + openType: { + type: String, + default: () => defProps.button.openType + }, + // 用于 组件,点击分别会触发 组件的 submit/reset 事件 + // 取值为submit(提交表单),reset(重置表单) + formType: { + type: String, + default: () => defProps.button.formType + }, + // 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 + // 只微信小程序、QQ小程序有效 + appParameter: { + type: String, + default: () => defProps.button.appParameter + }, + // 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效 + hoverStopPropagation: { + type: Boolean, + default: () => defProps.button.hoverStopPropagation + }, + // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效 + lang: { + type: String, + default: () => defProps.button.lang + }, + // 会话来源,open-type="contact"时有效。只微信小程序有效 + sessionFrom: { + type: String, + default: () => defProps.button.sessionFrom + }, + // 会话内消息卡片标题,open-type="contact"时有效 + // 默认当前标题,只微信小程序有效 + sendMessageTitle: { + type: String, + default: () => defProps.button.sendMessageTitle + }, + // 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效 + // 默认当前分享路径,只微信小程序有效 + sendMessagePath: { + type: String, + default: () => defProps.button.sendMessagePath + }, + // 会话内消息卡片图片,open-type="contact"时有效 + // 默认当前页面截图,只微信小程序有效 + sendMessageImg: { + type: String, + default: () => defProps.button.sendMessageImg + }, + // 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示, + // 用户点击后可以快速发送小程序消息,open-type="contact"时有效 + showMessageCard: { + type: Boolean, + default: () => defProps.button.showMessageCard + }, + // 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取 + dataName: { + type: String, + default: () => defProps.button.dataName + }, + // 节流,一定时间内只能触发一次 + throttleTime: { + type: [String, Number], + default: () => defProps.button.throttleTime + }, + // 按住后多久出现点击态,单位毫秒 + hoverStartTime: { + type: [String, Number], + default: () => defProps.button.hoverStartTime + }, + // 手指松开后点击态保留时间,单位毫秒 + hoverStayTime: { + type: [String, Number], + default: () => defProps.button.hoverStayTime + }, + // 按钮文字,之所以通过props传入,是因为slot传入的话 + // nvue中无法控制文字的样式 + text: { + type: [String, Number], + default: () => defProps.button.text + }, + // 按钮图标 + icon: { + type: String, + default: () => defProps.button.icon + }, + // 按钮图标 + iconColor: { + type: String, + default: () => defProps.button.icon + }, + // 按钮颜色,支持传入linear-gradient渐变色 + color: { + type: String, + default: () => defProps.button.color + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-button/u-button.vue b/im-uniapp/uni_modules/uview-plus/components/u-button/u-button.vue new file mode 100644 index 0000000..77f5568 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-button/u-button.vue @@ -0,0 +1,503 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-button/vue.scss b/im-uniapp/uni_modules/uview-plus/components/u-button/vue.scss new file mode 100644 index 0000000..ceb6cea --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-button/vue.scss @@ -0,0 +1,81 @@ +// nvue下hover-class无效 +$u-button-before-top:50% !default; +$u-button-before-left:50% !default; +$u-button-before-width:100% !default; +$u-button-before-height:100% !default; +$u-button-before-transform:translate(-50%, -50%) !default; +$u-button-before-opacity:0 !default; +$u-button-before-background-color:#000 !default; +$u-button-before-border-color:#000 !default; +$u-button-active-before-opacity:.15 !default; +$u-button-icon-margin-left:4px !default; +$u-button-plain-u-button-info-color:$u-info; +$u-button-plain-u-button-success-color:$u-success; +$u-button-plain-u-button-error-color:$u-error; +$u-button-plain-u-button-warning-color:$u-error; + +.u-button { + width: 100%; + white-space: nowrap; + + &__text { + white-space: nowrap; + line-height: 1; + } + + &:before { + position: absolute; + top:$u-button-before-top; + left:$u-button-before-left; + width:$u-button-before-width; + height:$u-button-before-height; + border: inherit; + border-radius: inherit; + transform:$u-button-before-transform; + opacity:$u-button-before-opacity; + content: " "; + background-color:$u-button-before-background-color; + border-color:$u-button-before-border-color; + } + + &--active { + &:before { + opacity: .15 + } + } + + &__icon+&__text:not(:empty), + &__loading-text { + margin-left:$u-button-icon-margin-left; + } + + &--plain { + &.u-button--primary { + color: $u-primary; + } + } + + &--plain { + &.u-button--info { + color:$u-button-plain-u-button-info-color; + } + } + + &--plain { + &.u-button--success { + color:$u-button-plain-u-button-success-color; + } + } + + &--plain { + &.u-button--error { + color:$u-button-plain-u-button-error-color; + } + } + + &--plain { + &.u-button--warning { + color:$u-button-plain-u-button-warning-color; + } + } +} diff --git a/im-uniapp/uni_modules/uview-plus/components/u-calendar/header.vue b/im-uniapp/uni_modules/uview-plus/components/u-calendar/header.vue new file mode 100644 index 0000000..ccfe18b --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-calendar/header.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-calendar/month.vue b/im-uniapp/uni_modules/uview-plus/components/u-calendar/month.vue new file mode 100644 index 0000000..6062388 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-calendar/month.vue @@ -0,0 +1,585 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-calendar/props.js b/im-uniapp/uni_modules/uview-plus/components/u-calendar/props.js new file mode 100644 index 0000000..df990fd --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-calendar/props.js @@ -0,0 +1,147 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' + +export const props = defineMixin({ + props: { + // 日历顶部标题 + title: { + type: String, + default: () => defProps.calendar.title + }, + // 是否显示标题 + showTitle: { + type: Boolean, + default: () => defProps.calendar.showTitle + }, + // 是否显示副标题 + showSubtitle: { + type: Boolean, + default: () => defProps.calendar.showSubtitle + }, + // 日期类型选择,single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围 + mode: { + type: String, + default: () => defProps.calendar.mode + }, + // mode=range时,第一个日期底部的提示文字 + startText: { + type: String, + default: () => defProps.calendar.startText + }, + // mode=range时,最后一个日期底部的提示文字 + endText: { + type: String, + default: () => defProps.calendar.endText + }, + // 自定义列表 + customList: { + type: Array, + default: () => defProps.calendar.customList + }, + // 主题色,对底部按钮和选中日期有效 + color: { + type: String, + default: () => defProps.calendar.color + }, + // 最小的可选日期 + minDate: { + type: [String, Number], + default: () => defProps.calendar.minDate + }, + // 最大可选日期 + maxDate: { + type: [String, Number], + default: () => defProps.calendar.maxDate + }, + // 默认选中的日期,mode为multiple或range是必须为数组格式 + defaultDate: { + type: [Array, String, Date, null], + default: () => defProps.calendar.defaultDate + }, + // mode=multiple时,最多可选多少个日期 + maxCount: { + type: [String, Number], + default: () => defProps.calendar.maxCount + }, + // 日期行高 + rowHeight: { + type: [String, Number], + default: () => defProps.calendar.rowHeight + }, + // 日期格式化函数 + formatter: { + type: [Function, null], + default: () => defProps.calendar.formatter + }, + // 是否显示农历 + showLunar: { + type: Boolean, + default: () => defProps.calendar.showLunar + }, + // 是否显示月份背景色 + showMark: { + type: Boolean, + default: () => defProps.calendar.showMark + }, + // 确定按钮的文字 + confirmText: { + type: String, + default: () => defProps.calendar.confirmText + }, + // 确认按钮处于禁用状态时的文字 + confirmDisabledText: { + type: String, + default: () => defProps.calendar.confirmDisabledText + }, + // 是否显示日历弹窗 + show: { + type: Boolean, + default: () => defProps.calendar.show + }, + // 是否允许点击遮罩关闭日历 + closeOnClickOverlay: { + type: Boolean, + default: () => defProps.calendar.closeOnClickOverlay + }, + // 是否为只读状态,只读状态下禁止选择日期 + readonly: { + type: Boolean, + default: () => defProps.calendar.readonly + }, + // 是否展示确认按钮 + showConfirm: { + type: Boolean, + default: () => defProps.calendar.showConfirm + }, + // 日期区间最多可选天数,默认无限制,mode = range时有效 + maxRange: { + type: [Number, String], + default: () => defProps.calendar.maxRange + }, + // 范围选择超过最多可选天数时的提示文案,mode = range时有效 + rangePrompt: { + type: String, + default: () => defProps.calendar.rangePrompt + }, + // 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 + showRangePrompt: { + type: Boolean, + default: () => defProps.calendar.showRangePrompt + }, + // 是否允许日期范围的起止时间为同一天,mode = range时有效 + allowSameDay: { + type: Boolean, + default: () => defProps.calendar.allowSameDay + }, + // 圆角值 + round: { + type: [Boolean, String, Number], + default: () => defProps.calendar.round + }, + // 最多展示月份数量 + monthNum: { + type: [Number, String], + default: 3 + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-calendar/u-calendar.vue b/im-uniapp/uni_modules/uview-plus/components/u-calendar/u-calendar.vue new file mode 100644 index 0000000..c8f77ff --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-calendar/u-calendar.vue @@ -0,0 +1,409 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-calendar/util.js b/im-uniapp/uni_modules/uview-plus/components/u-calendar/util.js new file mode 100644 index 0000000..18762c1 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-calendar/util.js @@ -0,0 +1,86 @@ +import dayjs from 'dayjs/esm/index' +export default { + methods: { + // 设置月份数据 + setMonth() { + // 月初是周几 + const day = dayjs(this.date).date(1).day() + const start = day == 0 ? 6 : day - 1 + + // 本月天数 + const days = dayjs(this.date).endOf('month').format('D') + + // 上个月天数 + const prevDays = dayjs(this.date).endOf('month').subtract(1, 'month').format('D') + + // 日期数据 + const arr = [] + // 清空表格 + this.month = [] + + // 添加上月数据 + arr.push( + ...new Array(start).fill(1).map((e, i) => { + const day = prevDays - start + i + 1 + + return { + value: day, + disabled: true, + date: dayjs(this.date).subtract(1, 'month').date(day).format('YYYY-MM-DD') + } + }) + ) + + // 添加本月数据 + arr.push( + ...new Array(days - 0).fill(1).map((e, i) => { + const day = i + 1 + + return { + value: day, + date: dayjs(this.date).date(day).format('YYYY-MM-DD') + } + }) + ) + + // 添加下个月 + arr.push( + ...new Array(42 - days - start).fill(1).map((e, i) => { + const day = i + 1 + + return { + value: day, + disabled: true, + date: dayjs(this.date).add(1, 'month').date(day).format('YYYY-MM-DD') + } + }) + ) + + // 分割数组 + for (let n = 0; n < arr.length; n += 7) { + this.month.push( + arr.slice(n, n + 7).map((e, i) => { + e.index = i + n + + // 自定义信息 + const custom = this.customList.find((c) => c.date == e.date) + + // 农历 + if (this.lunar) { + const { + IDayCn, + IMonthCn + } = this.getLunar(e.date) + e.lunar = IDayCn == '初一' ? IMonthCn : IDayCn + } + + return { + ...e, + ...custom + } + }) + ) + } + } + } +} diff --git a/im-uniapp/uni_modules/uview-plus/components/u-car-keyboard/props.js b/im-uniapp/uni_modules/uview-plus/components/u-car-keyboard/props.js new file mode 100644 index 0000000..7cca0ae --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-car-keyboard/props.js @@ -0,0 +1,17 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' + +export const props = defineMixin({ + props: { + // 是否打乱键盘按键的顺序 + random: { + type: Boolean, + default: false + }, + // 输入一个中文后,是否自动切换到英文 + autoChange: { + type: Boolean, + default: false + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-car-keyboard/u-car-keyboard.vue b/im-uniapp/uni_modules/uview-plus/components/u-car-keyboard/u-car-keyboard.vue new file mode 100644 index 0000000..347885e --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-car-keyboard/u-car-keyboard.vue @@ -0,0 +1,315 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-cell-group/props.js b/im-uniapp/uni_modules/uview-plus/components/u-cell-group/props.js new file mode 100644 index 0000000..59c2012 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-cell-group/props.js @@ -0,0 +1,16 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 分组标题 + title: { + type: String, + default: () => defProps.cellGroup.title + }, + // 是否显示外边框 + border: { + type: Boolean, + default: () => defProps.cellGroup.border + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-cell-group/u-cell-group.vue b/im-uniapp/uni_modules/uview-plus/components/u-cell-group/u-cell-group.vue new file mode 100644 index 0000000..40ed2dd --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-cell-group/u-cell-group.vue @@ -0,0 +1,67 @@ + + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-cell/props.js b/im-uniapp/uni_modules/uview-plus/components/u-cell/props.js new file mode 100644 index 0000000..73f5210 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-cell/props.js @@ -0,0 +1,112 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 标题 + title: { + type: [String, Number], + default: () => defProps.cell.title + }, + // 标题下方的描述信息 + label: { + type: [String, Number], + default: () => defProps.cell.label + }, + // 右侧的内容 + value: { + type: [String, Number], + default: () => defProps.cell.value + }, + // 左侧图标名称,或者图片链接(本地文件建议使用绝对地址) + icon: { + type: String, + default: () => defProps.cell.icon + }, + // 是否禁用cell + disabled: { + type: Boolean, + default: () => defProps.cell.disabled + }, + // 是否显示下边框 + border: { + type: Boolean, + default: () => defProps.cell.border + }, + // 内容是否垂直居中(主要是针对右侧的value部分) + center: { + type: Boolean, + default: () => defProps.cell.center + }, + // 点击后跳转的URL地址 + url: { + type: String, + default: () => defProps.cell.url + }, + // 链接跳转的方式,内部使用的是uView封装的route方法,可能会进行拦截操作 + linkType: { + type: String, + default: () => defProps.cell.linkType + }, + // 是否开启点击反馈(表现为点击时加上灰色背景) + clickable: { + type: Boolean, + default: () => defProps.cell.clickable + }, + // 是否展示右侧箭头并开启点击反馈 + isLink: { + type: Boolean, + default: () => defProps.cell.isLink + }, + // 是否显示表单状态下的必填星号(此组件可能会内嵌入input组件) + required: { + type: Boolean, + default: () => defProps.cell.required + }, + // 右侧的图标箭头 + rightIcon: { + type: String, + default: () => defProps.cell.rightIcon + }, + // 右侧箭头的方向,可选值为:left,up,down + arrowDirection: { + type: String, + default: () => defProps.cell.arrowDirection + }, + // 左侧图标样式 + iconStyle: { + type: [Object, String], + default: () => { + return defProps.cell.iconStyle + } + }, + // 右侧箭头图标的样式 + rightIconStyle: { + type: [Object, String], + default: () => { + return defProps.cell.rightIconStyle + } + }, + // 标题的样式 + titleStyle: { + type: [Object, String], + default: () => { + return defProps.cell.titleStyle + } + }, + // 单位元的大小,可选值为large + size: { + type: String, + default: () => defProps.cell.size + }, + // 点击cell是否阻止事件传播 + stop: { + type: Boolean, + default: () => defProps.cell.stop + }, + // 标识符,cell被点击时返回 + name: { + type: [Number, String], + default: () => defProps.cell.name + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-cell/u-cell.vue b/im-uniapp/uni_modules/uview-plus/components/u-cell/u-cell.vue new file mode 100644 index 0000000..66f48be --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-cell/u-cell.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-checkbox-group/props.js b/im-uniapp/uni_modules/uview-plus/components/u-checkbox-group/props.js new file mode 100644 index 0000000..222d184 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-checkbox-group/props.js @@ -0,0 +1,93 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' + +export const props = defineMixin({ + props: { + // 标识符 + name: { + type: String, + default: () => defProps.checkboxGroup.name + }, + // #ifdef VUE3 + // 绑定的值 + modelValue: { + type: Array, + default: () => defProps.checkboxGroup.value + }, + // #endif + // #ifdef VUE2 + // 绑定的值 + value: { + type: Array, + default: () => defProps.checkboxGroup.value + }, + // #endif + // 形状,circle-圆形,square-方形 + shape: { + type: String, + default: () => defProps.checkboxGroup.shape + }, + // 是否禁用全部checkbox + disabled: { + type: Boolean, + default: () => defProps.checkboxGroup.disabled + }, + + // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值 + activeColor: { + type: String, + default: () => defProps.checkboxGroup.activeColor + }, + // 未选中的颜色 + inactiveColor: { + type: String, + default: () => defProps.checkboxGroup.inactiveColor + }, + + // 整个组件的尺寸,默认px + size: { + type: [String, Number], + default: () => defProps.checkboxGroup.size + }, + // 布局方式,row-横向,column-纵向 + placement: { + type: String, + default: () => defProps.checkboxGroup.placement + }, + // label的字体大小,px单位 + labelSize: { + type: [String, Number], + default: () => defProps.checkboxGroup.labelSize + }, + // label的字体颜色 + labelColor: { + type: [String], + default: () => defProps.checkboxGroup.labelColor + }, + // 是否禁止点击文本操作 + labelDisabled: { + type: Boolean, + default: () => defProps.checkboxGroup.labelDisabled + }, + // 图标颜色 + iconColor: { + type: String, + default: () => defProps.checkboxGroup.iconColor + }, + // 图标的大小,单位px + iconSize: { + type: [String, Number], + default: () => defProps.checkboxGroup.iconSize + }, + // 勾选图标的对齐方式,left-左边,right-右边 + iconPlacement: { + type: String, + default: () => defProps.checkboxGroup.iconPlacement + }, + // 竖向配列时,是否显示下划线 + borderBottom: { + type: Boolean, + default: () => defProps.checkboxGroup.borderBottom + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-checkbox-group/u-checkbox-group.vue b/im-uniapp/uni_modules/uview-plus/components/u-checkbox-group/u-checkbox-group.vue new file mode 100644 index 0000000..0640d85 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-checkbox-group/u-checkbox-group.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-checkbox/props.js b/im-uniapp/uni_modules/uview-plus/components/u-checkbox/props.js new file mode 100644 index 0000000..66d8150 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-checkbox/props.js @@ -0,0 +1,76 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // checkbox的名称 + name: { + type: [String, Number, Boolean], + default: () => defProps.checkbox.name + }, + // 形状,square为方形,circle为圆型 + shape: { + type: String, + default: () => defProps.checkbox.shape + }, + // 整体的大小 + size: { + type: [String, Number], + default: () => defProps.checkbox.size + }, + // 是否默认选中 + checked: { + type: Boolean, + default: () => defProps.checkbox.checked + }, + // 是否禁用 + disabled: { + type: [String, Boolean], + default: () => defProps.checkbox.disabled + }, + // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值 + activeColor: { + type: String, + default: () => defProps.checkbox.activeColor + }, + // 未选中的颜色 + inactiveColor: { + type: String, + default: () => defProps.checkbox.inactiveColor + }, + // 图标的大小,单位px + iconSize: { + type: [String, Number], + default: () => defProps.checkbox.iconSize + }, + // 图标颜色 + iconColor: { + type: String, + default: () => defProps.checkbox.iconColor + }, + // label提示文字,因为nvue下,直接slot进来的文字,由于特殊的结构,无法修改样式 + label: { + type: [String, Number], + default: () => defProps.checkbox.label + }, + // label的字体大小,px单位 + labelSize: { + type: [String, Number], + default: () => defProps.checkbox.labelSize + }, + // label的颜色 + labelColor: { + type: String, + default: () => defProps.checkbox.labelColor + }, + // 是否禁止点击提示语选中复选框 + labelDisabled: { + type: [String, Boolean], + default: () => defProps.checkbox.labelDisabled + }, + // 是否独立使用 + usedAlone: { + type: [Boolean], + default: () => false + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-checkbox/u-checkbox.vue b/im-uniapp/uni_modules/uview-plus/components/u-checkbox/u-checkbox.vue new file mode 100644 index 0000000..0abee51 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-checkbox/u-checkbox.vue @@ -0,0 +1,376 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-circle-progress/props.js b/im-uniapp/uni_modules/uview-plus/components/u-circle-progress/props.js new file mode 100644 index 0000000..ff1eeaa --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-circle-progress/props.js @@ -0,0 +1,10 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + percentage: { + type: [String, Number], + default: () => defProps.circleProgress.percentage + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-circle-progress/u-circle-progress.vue b/im-uniapp/uni_modules/uview-plus/components/u-circle-progress/u-circle-progress.vue new file mode 100644 index 0000000..6dd5c93 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-circle-progress/u-circle-progress.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-code-input/props.js b/im-uniapp/uni_modules/uview-plus/components/u-code-input/props.js new file mode 100644 index 0000000..0a4b1e8 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-code-input/props.js @@ -0,0 +1,90 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 键盘弹起时,是否自动上推页面 + adjustPosition: { + type: Boolean, + default: () => defProps.codeInput.adjustPosition + }, + // 最大输入长度 + maxlength: { + type: [String, Number], + default: () => defProps.codeInput.maxlength + }, + // 是否用圆点填充 + dot: { + type: Boolean, + default: () => defProps.codeInput.dot + }, + // 显示模式,box-盒子模式,line-底部横线模式 + mode: { + type: String, + default: () => defProps.codeInput.mode + }, + // 是否细边框 + hairline: { + type: Boolean, + default: () => defProps.codeInput.hairline + }, + // 字符间的距离 + space: { + type: [String, Number], + default: () => defProps.codeInput.space + }, + // #ifdef VUE3 + // 预置值 + modelValue: { + type: [String, Number], + default: () => defProps.codeInput.value + }, + // #endif + // #ifdef VUE2 + // 预置值 + value: { + type: [String, Number], + default: () => defProps.codeInput.value + }, + // #endif + // 是否自动获取焦点 + focus: { + type: Boolean, + default: () => defProps.codeInput.focus + }, + // 字体是否加粗 + bold: { + type: Boolean, + default: () => defProps.codeInput.bold + }, + // 字体颜色 + color: { + type: String, + default: () => defProps.codeInput.color + }, + // 字体大小 + fontSize: { + type: [String, Number], + default: () => defProps.codeInput.fontSize + }, + // 输入框的大小,宽等于高 + size: { + type: [String, Number], + default: () => defProps.codeInput.size + }, + // 是否隐藏原生键盘,如果想用自定义键盘的话,需设置此参数为true + disabledKeyboard: { + type: Boolean, + default: () => defProps.codeInput.disabledKeyboard + }, + // 边框和线条颜色 + borderColor: { + type: String, + default: () => defProps.codeInput.borderColor + }, + // 是否禁止输入"."符号 + disabledDot: { + type: Boolean, + default: () => defProps.codeInput.disabledDot + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-code-input/u-code-input.vue b/im-uniapp/uni_modules/uview-plus/components/u-code-input/u-code-input.vue new file mode 100644 index 0000000..9504dda --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-code-input/u-code-input.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-code/props.js b/im-uniapp/uni_modules/uview-plus/components/u-code/props.js new file mode 100644 index 0000000..60977b5 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-code/props.js @@ -0,0 +1,36 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 倒计时总秒数 + seconds: { + type: [String, Number], + default: () => defProps.code.seconds + }, + // 尚未开始时提示 + startText: { + type: String, + default: () => defProps.code.startText + }, + // 正在倒计时中的提示 + changeText: { + type: String, + default: () => defProps.code.changeText + }, + // 倒计时结束时的提示 + endText: { + type: String, + default: () => defProps.code.endText + }, + // 是否在H5刷新或各端返回再进入时继续倒计时 + keepRunning: { + type: Boolean, + default: () => defProps.code.keepRunning + }, + // 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了 + uniqueKey: { + type: String, + default: () => defProps.code.uniqueKey + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-code/u-code.vue b/im-uniapp/uni_modules/uview-plus/components/u-code/u-code.vue new file mode 100644 index 0000000..c0fc992 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-code/u-code.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-col/props.js b/im-uniapp/uni_modules/uview-plus/components/u-col/props.js new file mode 100644 index 0000000..b28decb --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-col/props.js @@ -0,0 +1,31 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 占父容器宽度的多少等分,总分为12份 + span: { + type: [String, Number], + default: () => defProps.col.span + }, + // 指定栅格左侧的间隔数(总12栏) + offset: { + type: [String, Number], + default: () => defProps.col.offset + }, + // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`) + justify: { + type: String, + default: () => defProps.col.justify + }, + // 垂直对齐方式,可选值为top、center、bottom、stretch + align: { + type: String, + default: () => defProps.col.align + }, + // 文字对齐方式 + textAlign: { + type: String, + default: () => defProps.col.textAlign + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-col/u-col.vue b/im-uniapp/uni_modules/uview-plus/components/u-col/u-col.vue new file mode 100644 index 0000000..57a12d6 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-col/u-col.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-collapse-item/props.js b/im-uniapp/uni_modules/uview-plus/components/u-collapse-item/props.js new file mode 100644 index 0000000..fdc923b --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-collapse-item/props.js @@ -0,0 +1,61 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 标题 + title: { + type: String, + default: () => defProps.collapseItem.title + }, + // 标题右侧内容 + value: { + type: String, + default: () => defProps.collapseItem.value + }, + // 标题下方的描述信息 + label: { + type: String, + default: () => defProps.collapseItem.label + }, + // 是否禁用折叠面板 + disabled: { + type: Boolean, + default: () => defProps.collapseItem.disabled + }, + // 是否展示右侧箭头并开启点击反馈 + isLink: { + type: Boolean, + default: () => defProps.collapseItem.isLink + }, + // 是否开启点击反馈 + clickable: { + type: Boolean, + default: () => defProps.collapseItem.clickable + }, + // 是否显示内边框 + border: { + type: Boolean, + default: () => defProps.collapseItem.border + }, + // 标题的对齐方式 + align: { + type: String, + default: () => defProps.collapseItem.align + }, + // 唯一标识符 + name: { + type: [String, Number], + default: () => defProps.collapseItem.name + }, + // 标题左侧图片,可为绝对路径的图片或内置图标 + icon: { + type: String, + default: () => defProps.collapseItem.icon + }, + // 面板展开收起的过渡时间,单位ms + duration: { + type: Number, + default: () => defProps.collapseItem.duration + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-collapse-item/u-collapse-item.vue b/im-uniapp/uni_modules/uview-plus/components/u-collapse-item/u-collapse-item.vue new file mode 100644 index 0000000..7fa4b69 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-collapse-item/u-collapse-item.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-collapse/props.js b/im-uniapp/uni_modules/uview-plus/components/u-collapse/props.js new file mode 100644 index 0000000..7712603 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-collapse/props.js @@ -0,0 +1,21 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 当前展开面板的name,非手风琴模式:[],手风琴模式:string | number + value: { + type: [String, Number, Array, null], + default: () => defProps.collapse.value + }, + // 是否手风琴模式 + accordion: { + type: Boolean, + default: () => defProps.collapse.accordion + }, + // 是否显示外边框 + border: { + type: Boolean, + default: () => defProps.collapse.border + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-collapse/u-collapse.vue b/im-uniapp/uni_modules/uview-plus/components/u-collapse/u-collapse.vue new file mode 100644 index 0000000..f7ec7f7 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-collapse/u-collapse.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-column-notice/props.js b/im-uniapp/uni_modules/uview-plus/components/u-column-notice/props.js new file mode 100644 index 0000000..ae1a335 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-column-notice/props.js @@ -0,0 +1,57 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 显示的内容,字符串 + text: { + type: [Array], + default: () => defProps.columnNotice.text + }, + // 是否显示左侧的音量图标 + icon: { + type: String, + default: () => defProps.columnNotice.icon + }, + // 通告模式,link-显示右箭头,closable-显示右侧关闭图标 + mode: { + type: String, + default: () => defProps.columnNotice.mode + }, + // 文字颜色,各图标也会使用文字颜色 + color: { + type: String, + default: () => defProps.columnNotice.color + }, + // 背景颜色 + bgColor: { + type: String, + default: () => defProps.columnNotice.bgColor + }, + // 字体大小,单位px + fontSize: { + type: [String, Number], + default: () => defProps.columnNotice.fontSize + }, + // 水平滚动时的滚动速度,即每秒滚动多少px(px),这有利于控制文字无论多少时,都能有一个恒定的速度 + speed: { + type: [String, Number], + default: () => defProps.columnNotice.speed + }, + // direction = row时,是否使用步进形式滚动 + step: { + type: Boolean, + default: () => defProps.columnNotice.step + }, + // 滚动一个周期的时间长,单位ms + duration: { + type: [String, Number], + default: () => defProps.columnNotice.duration + }, + // 是否禁止用手滑动切换 + // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序 + disableTouch: { + type: Boolean, + default: () => defProps.columnNotice.disableTouch + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-column-notice/u-column-notice.vue b/im-uniapp/uni_modules/uview-plus/components/u-column-notice/u-column-notice.vue new file mode 100644 index 0000000..5e2507f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-column-notice/u-column-notice.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-copy/u-copy.vue b/im-uniapp/uni_modules/uview-plus/components/u-copy/u-copy.vue new file mode 100644 index 0000000..7e63ded --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-copy/u-copy.vue @@ -0,0 +1,70 @@ + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-count-down/props.js b/im-uniapp/uni_modules/uview-plus/components/u-count-down/props.js new file mode 100644 index 0000000..6dbf1c2 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-count-down/props.js @@ -0,0 +1,26 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 倒计时时长,单位ms + time: { + type: [String, Number], + default: () => defProps.countDown.time + }, + // 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 + format: { + type: String, + default: () => defProps.countDown.format + }, + // 是否自动开始倒计时 + autoStart: { + type: Boolean, + default: () => defProps.countDown.autoStart + }, + // 是否展示毫秒倒计时 + millisecond: { + type: Boolean, + default: () => defProps.countDown.millisecond + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-count-down/u-count-down.vue b/im-uniapp/uni_modules/uview-plus/components/u-count-down/u-count-down.vue new file mode 100644 index 0000000..5720757 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-count-down/u-count-down.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-count-down/utils.js b/im-uniapp/uni_modules/uview-plus/components/u-count-down/utils.js new file mode 100644 index 0000000..8c75005 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-count-down/utils.js @@ -0,0 +1,62 @@ +// 补0,如1 -> 01 +function padZero(num, targetLength = 2) { + let str = `${num}` + while (str.length < targetLength) { + str = `0${str}` + } + return str +} +const SECOND = 1000 +const MINUTE = 60 * SECOND +const HOUR = 60 * MINUTE +const DAY = 24 * HOUR +export function parseTimeData(time) { + const days = Math.floor(time / DAY) + const hours = Math.floor((time % DAY) / HOUR) + const minutes = Math.floor((time % HOUR) / MINUTE) + const seconds = Math.floor((time % MINUTE) / SECOND) + const milliseconds = Math.floor(time % SECOND) + return { + days, + hours, + minutes, + seconds, + milliseconds + } +} +export function parseFormat(format, timeData) { + let { + days, + hours, + minutes, + seconds, + milliseconds + } = timeData + // 如果格式化字符串中不存在DD(天),则将天的时间转为小时中去 + if (format.indexOf('DD') === -1) { + hours += days * 24 + } else { + // 对天补0 + format = format.replace('DD', padZero(days)) + } + // 其他同理于DD的格式化处理方式 + if (format.indexOf('HH') === -1) { + minutes += hours * 60 + } else { + format = format.replace('HH', padZero(hours)) + } + if (format.indexOf('mm') === -1) { + seconds += minutes * 60 + } else { + format = format.replace('mm', padZero(minutes)) + } + if (format.indexOf('ss') === -1) { + milliseconds += seconds * 1000 + } else { + format = format.replace('ss', padZero(seconds)) + } + return format.replace('SSS', padZero(milliseconds, 3)) +} +export function isSameSecond(time1, time2) { + return Math.floor(time1 / 1000) === Math.floor(time2 / 1000) +} diff --git a/im-uniapp/uni_modules/uview-plus/components/u-count-to/props.js b/im-uniapp/uni_modules/uview-plus/components/u-count-to/props.js new file mode 100644 index 0000000..0b97c1f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-count-to/props.js @@ -0,0 +1,61 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 开始的数值,默认从0增长到某一个数 + startVal: { + type: [String, Number], + default: () => defProps.countTo.startVal + }, + // 要滚动的目标数值,必须 + endVal: { + type: [String, Number], + default: () => defProps.countTo.endVal + }, + // 滚动到目标数值的动画持续时间,单位为毫秒(ms) + duration: { + type: [String, Number], + default: () => defProps.countTo.duration + }, + // 设置数值后是否自动开始滚动 + autoplay: { + type: Boolean, + default: () => defProps.countTo.autoplay + }, + // 要显示的小数位数 + decimals: { + type: [String, Number], + default: () => defProps.countTo.decimals + }, + // 是否在即将到达目标数值的时候,使用缓慢滚动的效果 + useEasing: { + type: Boolean, + default: () => defProps.countTo.useEasing + }, + // 十进制分割 + decimal: { + type: [String, Number], + default: () => defProps.countTo.decimal + }, + // 字体颜色 + color: { + type: String, + default: () => defProps.countTo.color + }, + // 字体大小 + fontSize: { + type: [String, Number], + default: () => defProps.countTo.fontSize + }, + // 是否加粗字体 + bold: { + type: Boolean, + default: () => defProps.countTo.bold + }, + // 千位分隔符,类似金额的分割(¥23,321.05中的",") + separator: { + type: String, + default: () => defProps.countTo.separator + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-count-to/u-count-to.vue b/im-uniapp/uni_modules/uview-plus/components/u-count-to/u-count-to.vue new file mode 100644 index 0000000..83ab13d --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-count-to/u-count-to.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-datetime-picker/props.js b/im-uniapp/uni_modules/uview-plus/components/u-datetime-picker/props.js new file mode 100644 index 0000000..97c6e3a --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-datetime-picker/props.js @@ -0,0 +1,145 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 是否显示input + hasInput: { + type: Boolean, + default: () => false + }, + placeholder: { + type: String, + default: () => '请选择' + }, + format: { + type: String, + default: () => '' + }, + // 是否打开组件 + show: { + type: Boolean, + default: () => defProps.datetimePicker.show + }, + // 弹出的方向,可选值为 top bottom right left center + popupMode: { + type: String, + default: () => defProps.picker.popupMode + }, + // 是否展示顶部的操作栏 + showToolbar: { + type: Boolean, + default: () => defProps.datetimePicker.showToolbar + }, + // #ifdef VUE2 + // 绑定值 + value: { + type: [String, Number], + default: () => defProps.datetimePicker.value + }, + // #endif + // #ifdef VUE3 + // 绑定值 + modelValue: { + type: [String, Number], + default: () => defProps.datetimePicker.value + }, + // #endif + // 顶部标题 + title: { + type: String, + default: () => defProps.datetimePicker.title + }, + // 展示格式,mode=date为日期选择,mode=time为时间选择,mode=year-month为年月选择,mode=datetime为日期时间选择 + mode: { + type: String, + default: () => defProps.datetimePicker.mode + }, + // 可选的最大时间 + maxDate: { + type: Number, + // 最大默认值为后10年 + default: () => defProps.datetimePicker.maxDate + }, + // 可选的最小时间 + minDate: { + type: Number, + // 最小默认值为前10年 + default: () => defProps.datetimePicker.minDate + }, + // 可选的最小小时,仅mode=time有效 + minHour: { + type: Number, + default: () => defProps.datetimePicker.minHour + }, + // 可选的最大小时,仅mode=time有效 + maxHour: { + type: Number, + default: () => defProps.datetimePicker.maxHour + }, + // 可选的最小分钟,仅mode=time有效 + minMinute: { + type: Number, + default: () => defProps.datetimePicker.minMinute + }, + // 可选的最大分钟,仅mode=time有效 + maxMinute: { + type: Number, + default: () => defProps.datetimePicker.maxMinute + }, + // 选项过滤函数 + filter: { + type: [Function, null], + default: () => defProps.datetimePicker.filter + }, + // 选项格式化函数 + formatter: { + type: [Function, null], + default: () => defProps.datetimePicker.formatter + }, + // 是否显示加载中状态 + loading: { + type: Boolean, + default: () => defProps.datetimePicker.loading + }, + // 各列中,单个选项的高度 + itemHeight: { + type: [String, Number], + default: () => defProps.datetimePicker.itemHeight + }, + // 取消按钮的文字 + cancelText: { + type: String, + default: () => defProps.datetimePicker.cancelText + }, + // 确认按钮的文字 + confirmText: { + type: String, + default: () => defProps.datetimePicker.confirmText + }, + // 取消按钮的颜色 + cancelColor: { + type: String, + default: () => defProps.datetimePicker.cancelColor + }, + // 确认按钮的颜色 + confirmColor: { + type: String, + default: () => defProps.datetimePicker.confirmColor + }, + // 每列中可见选项的数量 + visibleItemCount: { + type: [String, Number], + default: () => defProps.datetimePicker.visibleItemCount + }, + // 是否允许点击遮罩关闭选择器 + closeOnClickOverlay: { + type: Boolean, + default: () => defProps.datetimePicker.closeOnClickOverlay + }, + // 各列的默认索引 + defaultIndex: { + type: Array, + default: () => defProps.datetimePicker.defaultIndex + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-datetime-picker/u-datetime-picker.vue b/im-uniapp/uni_modules/uview-plus/components/u-datetime-picker/u-datetime-picker.vue new file mode 100644 index 0000000..ca33e86 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-datetime-picker/u-datetime-picker.vue @@ -0,0 +1,455 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-divider/props.js b/im-uniapp/uni_modules/uview-plus/components/u-divider/props.js new file mode 100644 index 0000000..ec3fd8f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-divider/props.js @@ -0,0 +1,46 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 是否虚线 + dashed: { + type: Boolean, + default: () => defProps.divider.dashed + }, + // 是否细线 + hairline: { + type: Boolean, + default: () => defProps.divider.hairline + }, + // 是否以点替代文字,优先于text字段起作用 + dot: { + type: Boolean, + default: () => defProps.divider.dot + }, + // 内容文本的位置,left-左边,center-中间,right-右边 + textPosition: { + type: String, + default: () => defProps.divider.textPosition + }, + // 文本内容 + text: { + type: [String, Number], + default: () => defProps.divider.text + }, + // 文本大小 + textSize: { + type: [String, Number], + default: () => defProps.divider.textSize + }, + // 文本颜色 + textColor: { + type: String, + default: () => defProps.divider.textColor + }, + // 线条颜色 + lineColor: { + type: String, + default: () => defProps.divider.lineColor + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-divider/u-divider.vue b/im-uniapp/uni_modules/uview-plus/components/u-divider/u-divider.vue new file mode 100644 index 0000000..47dbf00 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-divider/u-divider.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-dropdown-item/props.js b/im-uniapp/uni_modules/uview-plus/components/u-dropdown-item/props.js new file mode 100644 index 0000000..e5e0265 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-dropdown-item/props.js @@ -0,0 +1,47 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // #ifdef VUE3 + // 当前选中项的value值 + modelValue: { + type: [Number, String, Array], + default: '' + }, + // #endif + // #ifdef VUE2 + // 当前选中项的value值 + value: { + type: [Number, String, Array], + default: '' + }, + // #endif + // 菜单项标题 + title: { + type: [String, Number], + default: '' + }, + // 选项数据,如果传入了默认slot,此参数无效 + options: { + type: Array, + default() { + return [] + } + }, + // 是否禁用此菜单项 + disabled: { + type: Boolean, + default: false + }, + // 下拉弹窗的高度 + height: { + type: [Number, String], + default: 'auto' + }, + // 点击遮罩是否可以收起弹窗 + closeOnClickOverlay: { + type: Boolean, + default: true + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-dropdown-item/u-dropdown-item.vue b/im-uniapp/uni_modules/uview-plus/components/u-dropdown-item/u-dropdown-item.vue new file mode 100644 index 0000000..ec15a06 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-dropdown-item/u-dropdown-item.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-dropdown/props.js b/im-uniapp/uni_modules/uview-plus/components/u-dropdown/props.js new file mode 100644 index 0000000..4614b96 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-dropdown/props.js @@ -0,0 +1,61 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 菜单标题和选项的激活态颜色 + activeColor: { + type: String, + default: '#2979ff' + }, + // 菜单标题和选项的未激活态颜色 + inactiveColor: { + type: String, + default: '#606266' + }, + // 点击遮罩是否关闭菜单 + closeOnClickMask: { + type: Boolean, + default: true + }, + // 点击当前激活项标题是否关闭菜单 + closeOnClickSelf: { + type: Boolean, + default: true + }, + // 过渡时间 + duration: { + type: [Number, String], + default: 300 + }, + // 标题菜单的高度 + height: { + type: [Number, String], + default: 40 + }, + // 是否显示下边框 + borderBottom: { + type: Boolean, + default: false + }, + // 标题的字体大小 + titleSize: { + type: [Number, String], + default: 14 + }, + // 下拉出来的内容部分的圆角值 + borderRadius: { + type: [Number, String], + default: 0 + }, + // 菜单右侧的icon图标 + menuIcon: { + type: String, + default: 'arrow-down' + }, + // 菜单右侧图标的大小 + menuIconSize: { + type: [Number, String], + default: 14 + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-dropdown/u-dropdown.vue b/im-uniapp/uni_modules/uview-plus/components/u-dropdown/u-dropdown.vue new file mode 100644 index 0000000..46479a3 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-dropdown/u-dropdown.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-empty/props.js b/im-uniapp/uni_modules/uview-plus/components/u-empty/props.js new file mode 100644 index 0000000..40edd11 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-empty/props.js @@ -0,0 +1,61 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 内置图标名称,或图片路径,建议绝对路径 + icon: { + type: String, + default: () => defProps.empty.icon + }, + // 提示文字 + text: { + type: String, + default: () => defProps.empty.text + }, + // 文字颜色 + textColor: { + type: String, + default: () => defProps.empty.textColor + }, + // 文字大小 + textSize: { + type: [String, Number], + default: () => defProps.empty.textSize + }, + // 图标的颜色 + iconColor: { + type: String, + default: () => defProps.empty.iconColor + }, + // 图标的大小 + iconSize: { + type: [String, Number], + default: () => defProps.empty.iconSize + }, + // 选择预置的图标类型 + mode: { + type: String, + default: () => defProps.empty.mode + }, + // 图标宽度,单位px + width: { + type: [String, Number], + default: () => defProps.empty.width + }, + // 图标高度,单位px + height: { + type: [String, Number], + default: () => defProps.empty.height + }, + // 是否显示组件 + show: { + type: Boolean, + default: () => defProps.empty.show + }, + // 组件距离上一个元素之间的距离,默认px单位 + marginTop: { + type: [String, Number], + default: () => defProps.empty.marginTop + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-empty/u-empty.vue b/im-uniapp/uni_modules/uview-plus/components/u-empty/u-empty.vue new file mode 100644 index 0000000..f83677f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-empty/u-empty.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-form-item/props.js b/im-uniapp/uni_modules/uview-plus/components/u-form-item/props.js new file mode 100644 index 0000000..9402d37 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-form-item/props.js @@ -0,0 +1,55 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // input的label提示语 + label: { + type: String, + default: () => defProps.formItem.label + }, + // 绑定的值 + prop: { + type: String, + default: () => defProps.formItem.prop + }, + // 绑定的规则 + rule: { + type: String, + default: () => defProps.formItem.rule + }, + // 是否显示表单域的下划线边框 + borderBottom: { + type: [String, Boolean], + default: () => defProps.formItem.borderBottom + }, + // label的位置,left-左边,top-上边 + labelPosition: { + type: String, + default: () => defProps.formItem.labelPosition + }, + // label的宽度,单位px + labelWidth: { + type: [String, Number], + default: () => defProps.formItem.labelWidth + }, + // 右侧图标 + rightIcon: { + type: String, + default: () => defProps.formItem.rightIcon + }, + // 左侧图标 + leftIcon: { + type: String, + default: () => defProps.formItem.leftIcon + }, + // 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置 + required: { + type: Boolean, + default: () => defProps.formItem.required + }, + leftIconStyle: { + type: [String, Object], + default: () => defProps.formItem.leftIconStyle, + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-form-item/u-form-item.vue b/im-uniapp/uni_modules/uview-plus/components/u-form-item/u-form-item.vue new file mode 100644 index 0000000..1151814 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-form-item/u-form-item.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-form/props.js b/im-uniapp/uni_modules/uview-plus/components/u-form/props.js new file mode 100644 index 0000000..b80d623 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-form/props.js @@ -0,0 +1,47 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 当前form的需要验证字段的集合 + model: { + type: Object, + default: () => defProps.form.model + }, + // 验证规则 + rules: { + type: [Object, Function, Array], + default: () => defProps.form.rules + }, + // 有错误时的提示方式,message-提示信息,toast-进行toast提示 + // border-bottom-下边框呈现红色,none-无提示 + errorType: { + type: String, + default: () => defProps.form.errorType + }, + // 是否显示表单域的下划线边框 + borderBottom: { + type: Boolean, + default: () => defProps.form.borderBottom + }, + // label的位置,left-左边,top-上边 + labelPosition: { + type: String, + default: () => defProps.form.labelPosition + }, + // label的宽度,单位px + labelWidth: { + type: [String, Number], + default: () => defProps.form.labelWidth + }, + // lable字体的对齐方式 + labelAlign: { + type: String, + default: () => defProps.form.labelAlign + }, + // lable的样式,对象形式 + labelStyle: { + type: Object, + default: () => defProps.form.labelStyle + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-form/u-form.vue b/im-uniapp/uni_modules/uview-plus/components/u-form/u-form.vue new file mode 100644 index 0000000..373d1ff --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-form/u-form.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-gap/props.js b/im-uniapp/uni_modules/uview-plus/components/u-gap/props.js new file mode 100644 index 0000000..77b1d7c --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-gap/props.js @@ -0,0 +1,26 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 背景颜色(默认transparent) + bgColor: { + type: String, + default: () => defProps.gap.bgColor + }, + // 分割槽高度,单位px(默认30) + height: { + type: [String, Number], + default: () => defProps.gap.height + }, + // 与上一个组件的距离 + marginTop: { + type: [String, Number], + default: () => defProps.gap.marginTop + }, + // 与下一个组件的距离 + marginBottom: { + type: [String, Number], + default: () => defProps.gap.marginBottom + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-gap/u-gap.vue b/im-uniapp/uni_modules/uview-plus/components/u-gap/u-gap.vue new file mode 100644 index 0000000..36863a5 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-gap/u-gap.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-grid-item/props.js b/im-uniapp/uni_modules/uview-plus/components/u-grid-item/props.js new file mode 100644 index 0000000..55f957c --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-grid-item/props.js @@ -0,0 +1,16 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 宫格的name + name: { + type: [String, Number, null], + default: () => defProps.gridItem.name + }, + // 背景颜色 + bgColor: { + type: String, + default: () => defProps.gridItem.bgColor + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-grid-item/u-grid-item.vue b/im-uniapp/uni_modules/uview-plus/components/u-grid-item/u-grid-item.vue new file mode 100644 index 0000000..89197aa --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-grid-item/u-grid-item.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-grid/props.js b/im-uniapp/uni_modules/uview-plus/components/u-grid/props.js new file mode 100644 index 0000000..3d99844 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-grid/props.js @@ -0,0 +1,21 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 分成几列 + col: { + type: [String, Number], + default: () => defProps.grid.col + }, + // 是否显示边框 + border: { + type: Boolean, + default: () => defProps.grid.border + }, + // 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右 + align: { + type: String, + default: () => defProps.grid.align + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-grid/u-grid.vue b/im-uniapp/uni_modules/uview-plus/components/u-grid/u-grid.vue new file mode 100644 index 0000000..a650504 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-grid/u-grid.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-icon/icons.js b/im-uniapp/uni_modules/uview-plus/components/u-icon/icons.js new file mode 100644 index 0000000..f4d0fe2 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-icon/icons.js @@ -0,0 +1,214 @@ +export default { + 'uicon-level': '\ue693', + 'uicon-column-line': '\ue68e', + 'uicon-checkbox-mark': '\ue807', + 'uicon-folder': '\ue7f5', + 'uicon-movie': '\ue7f6', + 'uicon-star-fill': '\ue669', + 'uicon-star': '\ue65f', + 'uicon-phone-fill': '\ue64f', + 'uicon-phone': '\ue622', + 'uicon-apple-fill': '\ue881', + 'uicon-chrome-circle-fill': '\ue885', + 'uicon-backspace': '\ue67b', + 'uicon-attach': '\ue632', + 'uicon-cut': '\ue948', + 'uicon-empty-car': '\ue602', + 'uicon-empty-coupon': '\ue682', + 'uicon-empty-address': '\ue646', + 'uicon-empty-favor': '\ue67c', + 'uicon-empty-permission': '\ue686', + 'uicon-empty-news': '\ue687', + 'uicon-empty-search': '\ue664', + 'uicon-github-circle-fill': '\ue887', + 'uicon-rmb': '\ue608', + 'uicon-person-delete-fill': '\ue66a', + 'uicon-reload': '\ue788', + 'uicon-order': '\ue68f', + 'uicon-server-man': '\ue6bc', + 'uicon-search': '\ue62a', + 'uicon-fingerprint': '\ue955', + 'uicon-more-dot-fill': '\ue630', + 'uicon-scan': '\ue662', + 'uicon-share-square': '\ue60b', + 'uicon-map': '\ue61d', + 'uicon-map-fill': '\ue64e', + 'uicon-tags': '\ue629', + 'uicon-tags-fill': '\ue651', + 'uicon-bookmark-fill': '\ue63b', + 'uicon-bookmark': '\ue60a', + 'uicon-eye': '\ue613', + 'uicon-eye-fill': '\ue641', + 'uicon-mic': '\ue64a', + 'uicon-mic-off': '\ue649', + 'uicon-calendar': '\ue66e', + 'uicon-calendar-fill': '\ue634', + 'uicon-trash': '\ue623', + 'uicon-trash-fill': '\ue658', + 'uicon-play-left': '\ue66d', + 'uicon-play-right': '\ue610', + 'uicon-minus': '\ue618', + 'uicon-plus': '\ue62d', + 'uicon-info': '\ue653', + 'uicon-info-circle': '\ue7d2', + 'uicon-info-circle-fill': '\ue64b', + 'uicon-question': '\ue715', + 'uicon-error': '\ue6d3', + 'uicon-close': '\ue685', + 'uicon-checkmark': '\ue6a8', + 'uicon-android-circle-fill': '\ue67e', + 'uicon-android-fill': '\ue67d', + 'uicon-ie': '\ue87b', + 'uicon-IE-circle-fill': '\ue889', + 'uicon-google': '\ue87a', + 'uicon-google-circle-fill': '\ue88a', + 'uicon-setting-fill': '\ue872', + 'uicon-setting': '\ue61f', + 'uicon-minus-square-fill': '\ue855', + 'uicon-plus-square-fill': '\ue856', + 'uicon-heart': '\ue7df', + 'uicon-heart-fill': '\ue851', + 'uicon-camera': '\ue7d7', + 'uicon-camera-fill': '\ue870', + 'uicon-more-circle': '\ue63e', + 'uicon-more-circle-fill': '\ue645', + 'uicon-chat': '\ue620', + 'uicon-chat-fill': '\ue61e', + 'uicon-bag-fill': '\ue617', + 'uicon-bag': '\ue619', + 'uicon-error-circle-fill': '\ue62c', + 'uicon-error-circle': '\ue624', + 'uicon-close-circle': '\ue63f', + 'uicon-close-circle-fill': '\ue637', + 'uicon-checkmark-circle': '\ue63d', + 'uicon-checkmark-circle-fill': '\ue635', + 'uicon-question-circle-fill': '\ue666', + 'uicon-question-circle': '\ue625', + 'uicon-share': '\ue631', + 'uicon-share-fill': '\ue65e', + 'uicon-shopping-cart': '\ue621', + 'uicon-shopping-cart-fill': '\ue65d', + 'uicon-bell': '\ue609', + 'uicon-bell-fill': '\ue640', + 'uicon-list': '\ue650', + 'uicon-list-dot': '\ue616', + 'uicon-zhihu': '\ue6ba', + 'uicon-zhihu-circle-fill': '\ue709', + 'uicon-zhifubao': '\ue6b9', + 'uicon-zhifubao-circle-fill': '\ue6b8', + 'uicon-weixin-circle-fill': '\ue6b1', + 'uicon-weixin-fill': '\ue6b2', + 'uicon-twitter-circle-fill': '\ue6ab', + 'uicon-twitter': '\ue6aa', + 'uicon-taobao-circle-fill': '\ue6a7', + 'uicon-taobao': '\ue6a6', + 'uicon-weibo-circle-fill': '\ue6a5', + 'uicon-weibo': '\ue6a4', + 'uicon-qq-fill': '\ue6a1', + 'uicon-qq-circle-fill': '\ue6a0', + 'uicon-moments-circel-fill': '\ue69a', + 'uicon-moments': '\ue69b', + 'uicon-qzone': '\ue695', + 'uicon-qzone-circle-fill': '\ue696', + 'uicon-baidu-circle-fill': '\ue680', + 'uicon-baidu': '\ue681', + 'uicon-facebook-circle-fill': '\ue68a', + 'uicon-facebook': '\ue689', + 'uicon-car': '\ue60c', + 'uicon-car-fill': '\ue636', + 'uicon-warning-fill': '\ue64d', + 'uicon-warning': '\ue694', + 'uicon-clock-fill': '\ue638', + 'uicon-clock': '\ue60f', + 'uicon-edit-pen': '\ue612', + 'uicon-edit-pen-fill': '\ue66b', + 'uicon-email': '\ue611', + 'uicon-email-fill': '\ue642', + 'uicon-minus-circle': '\ue61b', + 'uicon-minus-circle-fill': '\ue652', + 'uicon-plus-circle': '\ue62e', + 'uicon-plus-circle-fill': '\ue661', + 'uicon-file-text': '\ue663', + 'uicon-file-text-fill': '\ue665', + 'uicon-pushpin': '\ue7e3', + 'uicon-pushpin-fill': '\ue86e', + 'uicon-grid': '\ue673', + 'uicon-grid-fill': '\ue678', + 'uicon-play-circle': '\ue647', + 'uicon-play-circle-fill': '\ue655', + 'uicon-pause-circle-fill': '\ue654', + 'uicon-pause': '\ue8fa', + 'uicon-pause-circle': '\ue643', + 'uicon-eye-off': '\ue648', + 'uicon-eye-off-outline': '\ue62b', + 'uicon-gift-fill': '\ue65c', + 'uicon-gift': '\ue65b', + 'uicon-rmb-circle-fill': '\ue657', + 'uicon-rmb-circle': '\ue677', + 'uicon-kefu-ermai': '\ue656', + 'uicon-server-fill': '\ue751', + 'uicon-coupon-fill': '\ue8c4', + 'uicon-coupon': '\ue8ae', + 'uicon-integral': '\ue704', + 'uicon-integral-fill': '\ue703', + 'uicon-home-fill': '\ue964', + 'uicon-home': '\ue965', + 'uicon-hourglass-half-fill': '\ue966', + 'uicon-hourglass': '\ue967', + 'uicon-account': '\ue628', + 'uicon-plus-people-fill': '\ue626', + 'uicon-minus-people-fill': '\ue615', + 'uicon-account-fill': '\ue614', + 'uicon-thumb-down-fill': '\ue726', + 'uicon-thumb-down': '\ue727', + 'uicon-thumb-up': '\ue733', + 'uicon-thumb-up-fill': '\ue72f', + 'uicon-lock-fill': '\ue979', + 'uicon-lock-open': '\ue973', + 'uicon-lock-opened-fill': '\ue974', + 'uicon-lock': '\ue97a', + 'uicon-red-packet-fill': '\ue690', + 'uicon-photo-fill': '\ue98b', + 'uicon-photo': '\ue98d', + 'uicon-volume-off-fill': '\ue659', + 'uicon-volume-off': '\ue644', + 'uicon-volume-fill': '\ue670', + 'uicon-volume': '\ue633', + 'uicon-red-packet': '\ue691', + 'uicon-download': '\ue63c', + 'uicon-arrow-up-fill': '\ue6b0', + 'uicon-arrow-down-fill': '\ue600', + 'uicon-play-left-fill': '\ue675', + 'uicon-play-right-fill': '\ue676', + 'uicon-rewind-left-fill': '\ue679', + 'uicon-rewind-right-fill': '\ue67a', + 'uicon-arrow-downward': '\ue604', + 'uicon-arrow-leftward': '\ue601', + 'uicon-arrow-rightward': '\ue603', + 'uicon-arrow-upward': '\ue607', + 'uicon-arrow-down': '\ue60d', + 'uicon-arrow-right': '\ue605', + 'uicon-arrow-left': '\ue60e', + 'uicon-arrow-up': '\ue606', + 'uicon-skip-back-left': '\ue674', + 'uicon-skip-forward-right': '\ue672', + 'uicon-rewind-right': '\ue66f', + 'uicon-rewind-left': '\ue671', + 'uicon-arrow-right-double': '\ue68d', + 'uicon-arrow-left-double': '\ue68c', + 'uicon-wifi-off': '\ue668', + 'uicon-wifi': '\ue667', + 'uicon-empty-data': '\ue62f', + 'uicon-empty-history': '\ue684', + 'uicon-empty-list': '\ue68b', + 'uicon-empty-page': '\ue627', + 'uicon-empty-order': '\ue639', + 'uicon-man': '\ue697', + 'uicon-woman': '\ue69c', + 'uicon-man-add': '\ue61c', + 'uicon-man-add-fill': '\ue64c', + 'uicon-man-delete': '\ue61a', + 'uicon-man-delete-fill': '\ue66a', + 'uicon-zh': '\ue70a', + 'uicon-en': '\ue692' +} diff --git a/im-uniapp/uni_modules/uview-plus/components/u-icon/props.js b/im-uniapp/uni_modules/uview-plus/components/u-icon/props.js new file mode 100644 index 0000000..d5bdfdb --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-icon/props.js @@ -0,0 +1,91 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 图标类名 + name: { + type: String, + default: () => defProps.icon.name + }, + // 图标颜色,可接受主题色 + color: { + type: String, + default: () => defProps.icon.color + }, + // 字体大小,单位px + size: { + type: [String, Number], + default: () => defProps.icon.size + }, + // 是否显示粗体 + bold: { + type: Boolean, + default: () => defProps.icon.bold + }, + // 点击图标的时候传递事件出去的index(用于区分点击了哪一个) + index: { + type: [String, Number], + default: () => defProps.icon.index + }, + // 触摸图标时的类名 + hoverClass: { + type: String, + default: () => defProps.icon.hoverClass + }, + // 自定义扩展前缀,方便用户扩展自己的图标库 + customPrefix: { + type: String, + default: () => defProps.icon.customPrefix + }, + // 图标右边或者下面的文字 + label: { + type: [String, Number], + default: () => defProps.icon.label + }, + // label的位置,只能右边或者下边 + labelPos: { + type: String, + default: () => defProps.icon.labelPos + }, + // label的大小 + labelSize: { + type: [String, Number], + default: () => defProps.icon.labelSize + }, + // label的颜色 + labelColor: { + type: String, + default: () => defProps.icon.labelColor + }, + // label与图标的距离 + space: { + type: [String, Number], + default: () => defProps.icon.space + }, + // 图片的mode + imgMode: { + type: String, + default: () => defProps.icon.imgMode + }, + // 用于显示图片小图标时,图片的宽度 + width: { + type: [String, Number], + default: () => defProps.icon.width + }, + // 用于显示图片小图标时,图片的高度 + height: { + type: [String, Number], + default: () => defProps.icon.height + }, + // 用于解决某些情况下,让图标垂直居中的用途 + top: { + type: [String, Number], + default: () => defProps.icon.top + }, + // 是否阻止事件传播 + stop: { + type: Boolean, + default: () => defProps.icon.stop + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-icon/u-icon.vue b/im-uniapp/uni_modules/uview-plus/components/u-icon/u-icon.vue new file mode 100644 index 0000000..ce3b52a --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-icon/u-icon.vue @@ -0,0 +1,242 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-image/props.js b/im-uniapp/uni_modules/uview-plus/components/u-image/props.js new file mode 100644 index 0000000..71bc6db --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-image/props.js @@ -0,0 +1,86 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 图片地址 + src: { + type: String, + default: () => defProps.image.src + }, + // 裁剪模式 + mode: { + type: String, + default: () => defProps.image.mode + }, + // 宽度,单位任意 + width: { + type: [String, Number], + default: () => defProps.image.width + }, + // 高度,单位任意 + height: { + type: [String, Number], + default: () => defProps.image.height + }, + // 图片形状,circle-圆形,square-方形 + shape: { + type: String, + default: () => defProps.image.shape + }, + // 圆角,单位任意 + radius: { + type: [String, Number], + default: () => defProps.image.radius + }, + // 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序 + lazyLoad: { + type: Boolean, + default: () => defProps.image.lazyLoad + }, + // 开启长按图片显示识别微信小程序码菜单 + showMenuByLongpress: { + type: Boolean, + default: () => defProps.image.showMenuByLongpress + }, + // 加载中的图标,或者小图片 + loadingIcon: { + type: String, + default: () => defProps.image.loadingIcon + }, + // 加载失败的图标,或者小图片 + errorIcon: { + type: String, + default: () => defProps.image.errorIcon + }, + // 是否显示加载中的图标或者自定义的slot + showLoading: { + type: Boolean, + default: () => defProps.image.showLoading + }, + // 是否显示加载错误的图标或者自定义的slot + showError: { + type: Boolean, + default: () => defProps.image.showError + }, + // 是否需要淡入效果 + fade: { + type: Boolean, + default: () => defProps.image.fade + }, + // 只支持网络资源,只对微信小程序有效 + webp: { + type: Boolean, + default: () => defProps.image.webp + }, + // 过渡时间,单位ms + duration: { + type: [String, Number], + default: () => defProps.image.duration + }, + // 背景颜色,用于深色页面加载图片时,为了和背景色融合 + bgColor: { + type: String, + default: () => defProps.image.bgColor + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-image/u-image.vue b/im-uniapp/uni_modules/uview-plus/components/u-image/u-image.vue new file mode 100644 index 0000000..63936f6 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-image/u-image.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-index-anchor/props.js b/im-uniapp/uni_modules/uview-plus/components/u-index-anchor/props.js new file mode 100644 index 0000000..f07e407 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-index-anchor/props.js @@ -0,0 +1,31 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 列表锚点文本内容 + text: { + type: [String, Number], + default: () => defProps.indexAnchor.text + }, + // 列表锚点文字颜色 + color: { + type: String, + default: () => defProps.indexAnchor.color + }, + // 列表锚点文字大小,单位默认px + size: { + type: [String, Number], + default: () => defProps.indexAnchor.size + }, + // 列表锚点背景颜色 + bgColor: { + type: String, + default: () => defProps.indexAnchor.bgColor + }, + // 列表锚点高度,单位默认px + height: { + type: [String, Number], + default: () => defProps.indexAnchor.height + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-index-anchor/u-index-anchor.vue b/im-uniapp/uni_modules/uview-plus/components/u-index-anchor/u-index-anchor.vue new file mode 100644 index 0000000..91c43e9 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-index-anchor/u-index-anchor.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-index-item/props.js b/im-uniapp/uni_modules/uview-plus/components/u-index-item/props.js new file mode 100644 index 0000000..3461f1e --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-index-item/props.js @@ -0,0 +1,8 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' + +export const props = defineMixin({ + props: { + + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-index-item/u-index-item.vue b/im-uniapp/uni_modules/uview-plus/components/u-index-item/u-index-item.vue new file mode 100644 index 0000000..13cc30f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-index-item/u-index-item.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-index-list/props.js b/im-uniapp/uni_modules/uview-plus/components/u-index-list/props.js new file mode 100644 index 0000000..9acd678 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-index-list/props.js @@ -0,0 +1,31 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 右边锚点非激活的颜色 + inactiveColor: { + type: String, + default: () => defProps.indexList.inactiveColor + }, + // 右边锚点激活的颜色 + activeColor: { + type: String, + default: () => defProps.indexList.activeColor + }, + // 索引字符列表,数组形式 + indexList: { + type: Array, + default: () => defProps.indexList.indexList + }, + // 是否开启锚点自动吸顶 + sticky: { + type: Boolean, + default: () => defProps.indexList.sticky + }, + // 自定义导航栏的高度 + customNavHeight: { + type: [String, Number], + default: () => defProps.indexList.customNavHeight + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-index-list/u-index-list.vue b/im-uniapp/uni_modules/uview-plus/components/u-index-list/u-index-list.vue new file mode 100644 index 0000000..c55eafb --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-index-list/u-index-list.vue @@ -0,0 +1,447 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-input/props.js b/im-uniapp/uni_modules/uview-plus/components/u-input/props.js new file mode 100644 index 0000000..e5e343e --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-input/props.js @@ -0,0 +1,198 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' + +export const props = defineMixin({ + props: { + // #ifdef VUE3 + // 绑定的值 + modelValue: { + type: [String, Number], + default: () => defProps.input.value + }, + // #endif + // #ifdef VUE2 + // 绑定的值 + value: { + type: [String, Number], + default: () => defProps.input.value + }, + // #endif + // number-数字输入键盘,app-vue下可以输入浮点数,app-nvue和小程序平台下只能输入整数 + // idcard-身份证输入键盘,微信、支付宝、百度、QQ小程序 + // digit-带小数点的数字键盘,App的nvue页面、微信、支付宝、百度、头条、QQ小程序 + // text-文本输入键盘 + type: { + type: String, + default: () => defProps.input.type + }, + // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true, + // 兼容性:微信小程序、百度小程序、字节跳动小程序、QQ小程序 + fixed: { + type: Boolean, + default: () => defProps.input.fixed + }, + // 是否禁用输入框 + disabled: { + type: Boolean, + default: () => defProps.input.disabled + }, + // 禁用状态时的背景色 + disabledColor: { + type: String, + default: () => defProps.input.disabledColor + }, + // 是否显示清除控件 + clearable: { + type: Boolean, + default: () => defProps.input.clearable + }, + // 是否密码类型 + password: { + type: Boolean, + default: () => defProps.input.password + }, + // 最大输入长度,设置为 -1 的时候不限制最大长度 + maxlength: { + type: [String, Number], + default: () => defProps.input.maxlength + }, + // 输入框为空时的占位符 + placeholder: { + type: String, + default: () => defProps.input.placeholder + }, + // 指定placeholder的样式类,注意页面或组件的style中写了scoped时,需要在类名前写/deep/ + placeholderClass: { + type: String, + default: () => defProps.input.placeholderClass + }, + // 指定placeholder的样式 + placeholderStyle: { + type: [String, Object], + default: () => defProps.input.placeholderStyle + }, + // 是否显示输入字数统计,只在 type ="text"或type ="textarea"时有效 + showWordLimit: { + type: Boolean, + default: () => defProps.input.showWordLimit + }, + // 设置右下角按钮的文字,有效值:send|search|next|go|done,兼容性详见uni-app文档 + // https://uniapp.dcloud.io/component/input + // https://uniapp.dcloud.io/component/textarea + confirmType: { + type: String, + default: () => defProps.input.confirmType + }, + // 点击键盘右下角按钮时是否保持键盘不收起,H5无效 + confirmHold: { + type: Boolean, + default: () => defProps.input.confirmHold + }, + // focus时,点击页面的时候不收起键盘,微信小程序有效 + holdKeyboard: { + type: Boolean, + default: () => defProps.input.holdKeyboard + }, + // 自动获取焦点 + // 在 H5 平台能否聚焦以及软键盘是否跟随弹出,取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点 + focus: { + type: Boolean, + default: () => defProps.input.focus + }, + // 键盘收起时,是否自动失去焦点,目前仅App3.0.0+有效 + autoBlur: { + type: Boolean, + default: () => defProps.input.autoBlur + }, + // 是否去掉 iOS 下的默认内边距,仅微信小程序,且type=textarea时有效 + disableDefaultPadding: { + type: Boolean, + default: () => defProps.input.disableDefaultPadding + }, + // 指定focus时光标的位置 + cursor: { + type: [String, Number], + default: () => defProps.input.cursor + }, + // 输入框聚焦时底部与键盘的距离 + cursorSpacing: { + type: [String, Number], + default: () => defProps.input.cursorSpacing + }, + // 光标起始位置,自动聚集时有效,需与selection-end搭配使用 + selectionStart: { + type: [String, Number], + default: () => defProps.input.selectionStart + }, + // 光标结束位置,自动聚集时有效,需与selection-start搭配使用 + selectionEnd: { + type: [String, Number], + default: () => defProps.input.selectionEnd + }, + // 键盘弹起时,是否自动上推页面 + adjustPosition: { + type: Boolean, + default: () => defProps.input.adjustPosition + }, + // 输入框内容对齐方式,可选值为:left|center|right + inputAlign: { + type: String, + default: () => defProps.input.inputAlign + }, + // 输入框字体的大小 + fontSize: { + type: [String, Number], + default: () => defProps.input.fontSize + }, + // 输入框字体颜色 + color: { + type: String, + default: () => defProps.input.color + }, + // 输入框前置图标 + prefixIcon: { + type: String, + default: () => defProps.input.prefixIcon + }, + // 前置图标样式,对象或字符串 + prefixIconStyle: { + type: [String, Object], + default: () => defProps.input.prefixIconStyle + }, + // 输入框后置图标 + suffixIcon: { + type: String, + default: () => defProps.input.suffixIcon + }, + // 后置图标样式,对象或字符串 + suffixIconStyle: { + type: [String, Object], + default: () => defProps.input.suffixIconStyle + }, + // 边框类型,surround-四周边框,bottom-底部边框,none-无边框 + border: { + type: String, + default: () => defProps.input.border + }, + // 是否只读,与disabled不同之处在于disabled会置灰组件,而readonly则不会 + readonly: { + type: Boolean, + default: () => defProps.input.readonly + }, + // 输入框形状,circle-圆形,square-方形 + shape: { + type: String, + default: () => defProps.input.shape + }, + // 用于处理或者过滤输入框内容的方法 + formatter: { + type: [Function, null], + default: () => defProps.input.formatter + }, + // 是否忽略组件内对文本合成系统事件的处理 + ignoreCompositionEvent: { + type: Boolean, + default: true + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-input/u-input.vue b/im-uniapp/uni_modules/uview-plus/components/u-input/u-input.vue new file mode 100644 index 0000000..c4c18dc --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-input/u-input.vue @@ -0,0 +1,386 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-keyboard/props.js b/im-uniapp/uni_modules/uview-plus/components/u-keyboard/props.js new file mode 100644 index 0000000..8c5d8ae --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-keyboard/props.js @@ -0,0 +1,86 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 键盘的类型,number-数字键盘,card-身份证键盘,car-车牌号键盘 + mode: { + type: String, + default: () => defProps.keyboard.mode + }, + // 是否显示键盘的"."符号 + dotDisabled: { + type: Boolean, + default: () => defProps.keyboard.dotDisabled + }, + // 是否显示顶部工具条 + tooltip: { + type: Boolean, + default: () => defProps.keyboard.tooltip + }, + // 是否显示工具条中间的提示 + showTips: { + type: Boolean, + default: () => defProps.keyboard.showTips + }, + // 工具条中间的提示文字 + tips: { + type: String, + default: () => defProps.keyboard.tips + }, + // 是否显示工具条左边的"取消"按钮 + showCancel: { + type: Boolean, + default: () => defProps.keyboard.showCancel + }, + // 是否显示工具条右边的"完成"按钮 + showConfirm: { + type: Boolean, + default: () => defProps.keyboard.showConfirm + }, + // 是否打乱键盘按键的顺序 + random: { + type: Boolean, + default: () => defProps.keyboard.random + }, + // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距 + safeAreaInsetBottom: { + type: Boolean, + default: () => defProps.keyboard.safeAreaInsetBottom + }, + // 是否允许通过点击遮罩关闭键盘 + closeOnClickOverlay: { + type: Boolean, + default: () => defProps.keyboard.closeOnClickOverlay + }, + // 控制键盘的弹出与收起 + show: { + type: Boolean, + default: () => defProps.keyboard.show + }, + // 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩 + overlay: { + type: Boolean, + default: () => defProps.keyboard.overlay + }, + // z-index值 + zIndex: { + type: [String, Number], + default: () => defProps.keyboard.zIndex + }, + // 取消按钮的文字 + cancelText: { + type: String, + default: () => defProps.keyboard.cancelText + }, + // 确认按钮的文字 + confirmText: { + type: String, + default: () => defProps.keyboard.confirmText + }, + // 输入一个中文后,是否自动切换到英文 + autoChange: { + type: Boolean, + default: () => defProps.keyboard.autoChange + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-keyboard/u-keyboard.vue b/im-uniapp/uni_modules/uview-plus/components/u-keyboard/u-keyboard.vue new file mode 100644 index 0000000..868f0c5 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-keyboard/u-keyboard.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-lazy-load/u-lazy-load.vue b/im-uniapp/uni_modules/uview-plus/components/u-lazy-load/u-lazy-load.vue new file mode 100644 index 0000000..7618781 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-lazy-load/u-lazy-load.vue @@ -0,0 +1,251 @@ + + + + + \ No newline at end of file diff --git a/im-uniapp/uni_modules/uview-plus/components/u-line-progress/props.js b/im-uniapp/uni_modules/uview-plus/components/u-line-progress/props.js new file mode 100644 index 0000000..3e03e61 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-line-progress/props.js @@ -0,0 +1,30 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 激活部分的颜色 + activeColor: { + type: String, + default: () => defProps.lineProgress.activeColor + }, + inactiveColor: { + type: String, + default: () => defProps.lineProgress.color + }, + // 进度百分比,数值 + percentage: { + type: [String, Number], + default: () => defProps.lineProgress.inactiveColor + }, + // 是否在进度条内部显示百分比的值 + showText: { + type: Boolean, + default: () => defProps.lineProgress.showText + }, + // 进度条的高度,单位px + height: { + type: [String, Number], + default: () => defProps.lineProgress.height + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-line-progress/u-line-progress.vue b/im-uniapp/uni_modules/uview-plus/components/u-line-progress/u-line-progress.vue new file mode 100644 index 0000000..577f877 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-line-progress/u-line-progress.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-line/props.js b/im-uniapp/uni_modules/uview-plus/components/u-line/props.js new file mode 100644 index 0000000..6428d3f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-line/props.js @@ -0,0 +1,35 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + color: { + type: String, + default: () => defProps.line.color + }, + // 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带px单位的值等 + length: { + type: [String, Number], + default: () => defProps.line.length + }, + // 线条方向,col-竖向,row-横向 + direction: { + type: String, + default: () => defProps.line.direction + }, + // 是否显示细边框 + hairline: { + type: Boolean, + default: () => defProps.line.hairline + }, + // 线条与上下左右元素的间距,字符串形式,如"30px"、"20px 30px" + margin: { + type: [String, Number], + default: () => defProps.line.margin + }, + // 是否虚线,true-虚线,false-实线 + dashed: { + type: Boolean, + default: () => defProps.line.dashed + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-line/u-line.vue b/im-uniapp/uni_modules/uview-plus/components/u-line/u-line.vue new file mode 100644 index 0000000..4acc0a0 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-line/u-line.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-link/props.js b/im-uniapp/uni_modules/uview-plus/components/u-link/props.js new file mode 100644 index 0000000..d96d824 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-link/props.js @@ -0,0 +1,41 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 文字颜色 + color: { + type: String, + default: () => defProps.link.color + }, + // 字体大小,单位px + fontSize: { + type: [String, Number], + default: () => defProps.link.fontSize + }, + // 是否显示下划线 + underLine: { + type: Boolean, + default: () => defProps.link.underLine + }, + // 要跳转的链接 + href: { + type: String, + default: () => defProps.link.href + }, + // 小程序中复制到粘贴板的提示语 + mpTips: { + type: String, + default: () => defProps.link.mpTips + }, + // 下划线颜色 + lineColor: { + type: String, + default: () => defProps.link.lineColor + }, + // 超链接的问题,不使用slot形式传入,是因为nvue下无法修改颜色 + text: { + type: String, + default: () => defProps.link.text + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-link/u-link.vue b/im-uniapp/uni_modules/uview-plus/components/u-link/u-link.vue new file mode 100644 index 0000000..5103893 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-link/u-link.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-list-item/props.js b/im-uniapp/uni_modules/uview-plus/components/u-list-item/props.js new file mode 100644 index 0000000..edaa4dc --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-list-item/props.js @@ -0,0 +1,11 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 用于滚动到指定item + anchor: { + type: [String, Number], + default: () => defProps.listItem.anchor + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-list-item/u-list-item.vue b/im-uniapp/uni_modules/uview-plus/components/u-list-item/u-list-item.vue new file mode 100644 index 0000000..ae30b08 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-list-item/u-list-item.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-list/props.js b/im-uniapp/uni_modules/uview-plus/components/u-list/props.js new file mode 100644 index 0000000..9fca829 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-list/props.js @@ -0,0 +1,101 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 控制是否出现滚动条,仅nvue有效 + showScrollbar: { + type: Boolean, + default: () => defProps.list.showScrollbar + }, + // 距底部多少时触发scrolltolower事件 + lowerThreshold: { + type: [String, Number], + default: () => defProps.list.lowerThreshold + }, + // 距顶部多少时触发scrolltoupper事件,非nvue有效 + upperThreshold: { + type: [String, Number], + default: () => defProps.list.upperThreshold + }, + // 设置竖向滚动条位置 + scrollTop: { + type: [String, Number], + default: () => defProps.list.scrollTop + }, + // 控制 onscroll 事件触发的频率,仅nvue有效 + offsetAccuracy: { + type: [String, Number], + default: () => defProps.list.offsetAccuracy + }, + // 启用 flexbox 布局。开启后,当前节点声明了display: flex就会成为flex container,并作用于其孩子节点,仅微信小程序有效 + enableFlex: { + type: Boolean, + default: () => defProps.list.enableFlex + }, + // 是否按分页模式显示List,默认值false + pagingEnabled: { + type: Boolean, + default: () => defProps.list.pagingEnabled + }, + // 是否允许List滚动 + scrollable: { + type: Boolean, + default: () => defProps.list.scrollable + }, + // 值应为某子元素id(id不能以数字开头) + scrollIntoView: { + type: String, + default: () => defProps.list.scrollIntoView + }, + // 在设置滚动条位置时使用动画过渡 + scrollWithAnimation: { + type: Boolean, + default: () => defProps.list.scrollWithAnimation + }, + // iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只对微信小程序有效 + enableBackToTop: { + type: Boolean, + default: () => defProps.list.enableBackToTop + }, + // 列表的高度 + height: { + type: [String, Number], + default: () => defProps.list.height + }, + // 列表宽度 + width: { + type: [String, Number], + default: () => defProps.list.width + }, + // 列表前后预渲染的屏数,1代表一个屏幕的高度,1.5代表1个半屏幕高度 + preLoadScreen: { + type: [String, Number], + default: () => defProps.list.preLoadScreen + }, + // 开启自定义下拉刷新 + refresherEnabled: { + type: Boolean, + default: () => false + }, + // 设置自定义下拉刷新阈值 + refresherThreshold: { + type: Number, + default: () => 45 + }, + // 设置自定义下拉刷新默认样式,支持设置 black,white,none,none 表示不使用默认样式 + refresherDefaultStyle: { + type: String, + default: () => 'black' + }, + // 设置自定义下拉刷新区域背景颜色 + refresherBackground: { + type: String, + default: () => '#FFF' + }, + // 设置当前下拉刷新状态,true 表示下拉刷新已经被触发,false 表示下拉刷新未被触发 + refresherTriggered: { + type: Boolean, + default: () => false + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-list/u-list.vue b/im-uniapp/uni_modules/uview-plus/components/u-list/u-list.vue new file mode 100644 index 0000000..1473b3e --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-list/u-list.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-loading-icon/props.js b/im-uniapp/uni_modules/uview-plus/components/u-loading-icon/props.js new file mode 100644 index 0000000..6a6abf9 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-loading-icon/props.js @@ -0,0 +1,61 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 是否显示组件 + show: { + type: Boolean, + default: () => defProps.loadingIcon.show + }, + // 颜色 + color: { + type: String, + default: () => defProps.loadingIcon.color + }, + // 提示文字颜色 + textColor: { + type: String, + default: () => defProps.loadingIcon.textColor + }, + // 文字和图标是否垂直排列 + vertical: { + type: Boolean, + default: () => defProps.loadingIcon.vertical + }, + // 模式选择,circle-圆形,spinner-花朵形,semicircle-半圆形 + mode: { + type: String, + default: () => defProps.loadingIcon.mode + }, + // 图标大小,单位默认px + size: { + type: [String, Number], + default: () => defProps.loadingIcon.size + }, + // 文字大小 + textSize: { + type: [String, Number], + default: () => defProps.loadingIcon.textSize + }, + // 文字内容 + text: { + type: [String, Number], + default: () => defProps.loadingIcon.text + }, + // 动画模式 + timingFunction: { + type: String, + default: () => defProps.loadingIcon.timingFunction + }, + // 动画执行周期时间 + duration: { + type: [String, Number], + default: () => defProps.loadingIcon.duration + }, + // mode=circle时的暗边颜色 + inactiveColor: { + type: String, + default: () => defProps.loadingIcon.inactiveColor + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-loading-icon/u-loading-icon.vue b/im-uniapp/uni_modules/uview-plus/components/u-loading-icon/u-loading-icon.vue new file mode 100644 index 0000000..f00f777 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-loading-icon/u-loading-icon.vue @@ -0,0 +1,347 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-loading-page/props.js b/im-uniapp/uni_modules/uview-plus/components/u-loading-page/props.js new file mode 100644 index 0000000..3a0ab3f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-loading-page/props.js @@ -0,0 +1,52 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' + +export const props = defineMixin({ + props: { + // 提示内容 + loadingText: { + type: [String, Number], + default: () => defProps.loadingPage.loadingText + }, + // 文字上方用于替换loading动画的图片 + image: { + type: String, + default: () => defProps.loadingPage.image + }, + // 加载动画的模式,circle-圆形,spinner-花朵形,semicircle-半圆形 + loadingMode: { + type: String, + default: () => defProps.loadingPage.loadingMode + }, + // 是否加载中 + loading: { + type: Boolean, + default: () => defProps.loadingPage.loading + }, + // 背景色 + bgColor: { + type: String, + default: () => defProps.loadingPage.bgColor + }, + // 文字颜色 + color: { + type: String, + default: () => defProps.loadingPage.color + }, + // 文字大小 + fontSize: { + type: [String, Number], + default: () => defProps.loadingPage.fontSize + }, + // 图标大小 + iconSize: { + type: [String, Number], + default: () => defProps.loadingPage.fontSize + }, + // 加载中图标的颜色,只能rgb或者十六进制颜色值 + loadingColor: { + type: String, + default: () => defProps.loadingPage.loadingColor + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-loading-page/u-loading-page.vue b/im-uniapp/uni_modules/uview-plus/components/u-loading-page/u-loading-page.vue new file mode 100644 index 0000000..302304d --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-loading-page/u-loading-page.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-loadmore/props.js b/im-uniapp/uni_modules/uview-plus/components/u-loadmore/props.js new file mode 100644 index 0000000..fb78e39 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-loadmore/props.js @@ -0,0 +1,96 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 组件状态,loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态 + status: { + type: String, + default: () => defProps.loadmore.status + }, + // 组件背景色 + bgColor: { + type: String, + default: () => defProps.loadmore.bgColor + }, + // 是否显示加载中的图标 + icon: { + type: Boolean, + default: () => defProps.loadmore.icon + }, + // 字体大小 + fontSize: { + type: [String, Number], + default: () => defProps.loadmore.fontSize + }, + // 图标大小 + iconSize: { + type: [String, Number], + default: () => defProps.loadmore.iconSize + }, + // 字体颜色 + color: { + type: String, + default: () => defProps.loadmore.color + }, + // 加载中状态的图标,spinner-花朵状图标,circle-圆圈状,semicircle-半圆 + loadingIcon: { + type: String, + default: () => defProps.loadmore.loadingIcon + }, + // 加载前的提示语 + loadmoreText: { + type: String, + default: () => defProps.loadmore.loadmoreText + }, + // 加载中提示语 + loadingText: { + type: String, + default: () => defProps.loadmore.loadingText + }, + // 没有更多的提示语 + nomoreText: { + type: String, + default: () => defProps.loadmore.nomoreText + }, + // 在“没有更多”状态下,是否显示粗点 + isDot: { + type: Boolean, + default: () => defProps.loadmore.isDot + }, + // 加载中图标的颜色 + iconColor: { + type: String, + default: () => defProps.loadmore.iconColor + }, + // 上边距 + marginTop: { + type: [String, Number], + default: () => defProps.loadmore.marginTop + }, + // 下边距 + marginBottom: { + type: [String, Number], + default: () => defProps.loadmore.marginBottom + }, + // 高度,单位px + height: { + type: [String, Number], + default: () => defProps.loadmore.height + }, + // 是否显示左边分割线 + line: { + type: Boolean, + default: () => defProps.loadmore.line + }, + // 线条颜色 + lineColor: { + type: String, + default: () => defProps.loadmore.lineColor + }, + // 是否虚线,true-虚线,false-实线 + dashed: { + type: Boolean, + default: () => defProps.loadmore.dashed + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-loadmore/u-loadmore.vue b/im-uniapp/uni_modules/uview-plus/components/u-loadmore/u-loadmore.vue new file mode 100644 index 0000000..62c99c0 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-loadmore/u-loadmore.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-message-input/u-message-input.vue b/im-uniapp/uni_modules/uview-plus/components/u-message-input/u-message-input.vue new file mode 100644 index 0000000..0da5f1c --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-message-input/u-message-input.vue @@ -0,0 +1,318 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-modal/props.js b/im-uniapp/uni_modules/uview-plus/components/u-modal/props.js new file mode 100644 index 0000000..22dbdfd --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-modal/props.js @@ -0,0 +1,86 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 是否展示modal + show: { + type: Boolean, + default: () => defProps.modal.show + }, + // 标题 + title: { + type: [String], + default: () => defProps.modal.title + }, + // 弹窗内容 + content: { + type: String, + default: () => defProps.modal.content + }, + // 确认文案 + confirmText: { + type: String, + default: () => defProps.modal.confirmText + }, + // 取消文案 + cancelText: { + type: String, + default: () => defProps.modal.cancelText + }, + // 是否显示确认按钮 + showConfirmButton: { + type: Boolean, + default: () => defProps.modal.showConfirmButton + }, + // 是否显示取消按钮 + showCancelButton: { + type: Boolean, + default: () => defProps.modal.showCancelButton + }, + // 确认按钮颜色 + confirmColor: { + type: String, + default: () => defProps.modal.confirmColor + }, + // 取消文字颜色 + cancelColor: { + type: String, + default: () => defProps.modal.cancelColor + }, + // 对调确认和取消的位置 + buttonReverse: { + type: Boolean, + default: () => defProps.modal.buttonReverse + }, + // 是否开启缩放效果 + zoom: { + type: Boolean, + default: () => defProps.modal.zoom + }, + // 是否异步关闭,只对确定按钮有效 + asyncClose: { + type: Boolean, + default: () => defProps.modal.asyncClose + }, + // 是否允许点击遮罩关闭modal + closeOnClickOverlay: { + type: Boolean, + default: () => defProps.modal.closeOnClickOverlay + }, + // 给一个负的margin-top,往上偏移,避免和键盘重合的情况 + negativeTop: { + type: [String, Number], + default: () => defProps.modal.negativeTop + }, + // modal宽度,不支持百分比,可以数值,px,rpx单位 + width: { + type: [String, Number], + default: () => defProps.modal.width + }, + // 确认按钮的样式,circle-圆形,square-方形,如设置,将不会显示取消按钮 + confirmButtonShape: { + type: String, + default: () => defProps.modal.confirmButtonShape + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-modal/u-modal.vue b/im-uniapp/uni_modules/uview-plus/components/u-modal/u-modal.vue new file mode 100644 index 0000000..f9ab38f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-modal/u-modal.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-navbar/props.js b/im-uniapp/uni_modules/uview-plus/components/u-navbar/props.js new file mode 100644 index 0000000..2f20b02 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-navbar/props.js @@ -0,0 +1,87 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' + +export const props = defineMixin({ + props: { + // 是否开启顶部安全区适配 + safeAreaInsetTop: { + type: Boolean, + default: () => defProps.navbar.safeAreaInsetTop + }, + // 固定在顶部时,是否生成一个等高元素,以防止塌陷 + placeholder: { + type: Boolean, + default: () => defProps.navbar.placeholder + }, + // 是否固定在顶部 + fixed: { + type: Boolean, + default: () => defProps.navbar.fixed + }, + // 是否显示下边框 + border: { + type: Boolean, + default: () => defProps.navbar.border + }, + // 左边的图标 + leftIcon: { + type: String, + default: () => defProps.navbar.leftIcon + }, + // 左边的提示文字 + leftText: { + type: String, + default: () => defProps.navbar.leftText + }, + // 左右的提示文字 + rightText: { + type: String, + default: () => defProps.navbar.rightText + }, + // 右边的图标 + rightIcon: { + type: String, + default: () => defProps.navbar.rightIcon + }, + // 标题 + title: { + type: [String, Number], + default: () => defProps.navbar.title + }, + // 背景颜色 + bgColor: { + type: String, + default: () => defProps.navbar.bgColor + }, + // 标题的宽度 + titleWidth: { + type: [String, Number], + default: () => defProps.navbar.titleWidth + }, + // 导航栏高度 + height: { + type: [String, Number], + default: () => defProps.navbar.height + }, + // 左侧返回图标的大小 + leftIconSize: { + type: [String, Number], + default: () => defProps.navbar.leftIconSize + }, + // 左侧返回图标的颜色 + leftIconColor: { + type: String, + default: () => defProps.navbar.leftIconColor + }, + // 点击左侧区域(返回图标),是否自动返回上一页 + autoBack: { + type: Boolean, + default: () => defProps.navbar.autoBack + }, + // 标题的样式,对象或字符串 + titleStyle: { + type: [String, Object], + default: () => defProps.navbar.titleStyle + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-navbar/u-navbar.vue b/im-uniapp/uni_modules/uview-plus/components/u-navbar/u-navbar.vue new file mode 100644 index 0000000..6d6cdba --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-navbar/u-navbar.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-no-network/props.js b/im-uniapp/uni_modules/uview-plus/components/u-no-network/props.js new file mode 100644 index 0000000..1091cf5 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-no-network/props.js @@ -0,0 +1,21 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 页面文字提示 + tips: { + type: String, + default: () => defProps.noNetwork.tips + }, + // 一个z-index值,用于设置没有网络这个组件的层次,因为页面可能会有其他定位的元素层级过高,导致此组件被覆盖 + zIndex: { + type: [String, Number], + default: () => defProps.noNetwork.zIndex + }, + // image 没有网络的图片提示 + image: { + type: String, + default: () => defProps.noNetwork.image + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-no-network/u-no-network.vue b/im-uniapp/uni_modules/uview-plus/components/u-no-network/u-no-network.vue new file mode 100644 index 0000000..54539a5 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-no-network/u-no-network.vue @@ -0,0 +1,223 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-notice-bar/props.js b/im-uniapp/uni_modules/uview-plus/components/u-notice-bar/props.js new file mode 100644 index 0000000..cc2a53f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-notice-bar/props.js @@ -0,0 +1,72 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 显示的内容,数组 + text: { + type: [Array, String], + default: () => defProps.noticeBar.text + }, + // 通告滚动模式,row-横向滚动,column-竖向滚动 + direction: { + type: String, + default: () => defProps.noticeBar.direction + }, + // direction = row时,是否使用步进形式滚动 + step: { + type: Boolean, + default: () => defProps.noticeBar.step + }, + // 是否显示左侧的音量图标 + icon: { + type: String, + default: () => defProps.noticeBar.icon + }, + // 通告模式,link-显示右箭头,closable-显示右侧关闭图标 + mode: { + type: String, + default: () => defProps.noticeBar.mode + }, + // 文字颜色,各图标也会使用文字颜色 + color: { + type: String, + default: () => defProps.noticeBar.color + }, + // 背景颜色 + bgColor: { + type: String, + default: () => defProps.noticeBar.bgColor + }, + // 水平滚动时的滚动速度,即每秒滚动多少px(px),这有利于控制文字无论多少时,都能有一个恒定的速度 + speed: { + type: [String, Number], + default: () => defProps.noticeBar.speed + }, + // 字体大小 + fontSize: { + type: [String, Number], + default: () => defProps.noticeBar.fontSize + }, + // 滚动一个周期的时间长,单位ms + duration: { + type: [String, Number], + default: () => defProps.noticeBar.duration + }, + // 是否禁止用手滑动切换 + // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序 + disableTouch: { + type: Boolean, + default: () => defProps.noticeBar.disableTouch + }, + // 跳转的页面路径 + url: { + type: String, + default: () => defProps.noticeBar.url + }, + // 页面跳转的类型 + linkType: { + type: String, + default: () => defProps.noticeBar.linkType + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-notice-bar/u-notice-bar.vue b/im-uniapp/uni_modules/uview-plus/components/u-notice-bar/u-notice-bar.vue new file mode 100644 index 0000000..548d07f --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-notice-bar/u-notice-bar.vue @@ -0,0 +1,105 @@ + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-notify/props.js b/im-uniapp/uni_modules/uview-plus/components/u-notify/props.js new file mode 100644 index 0000000..fdb01b4 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-notify/props.js @@ -0,0 +1,51 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 到顶部的距离 + top: { + type: [String, Number], + default: () => defProps.notify.top + }, + // 是否展示组件 + // show: { + // type: Boolean, + // default: () => defProps.notify.show + // }, + // type主题,primary,success,warning,error + type: { + type: String, + default: () => defProps.notify.type + }, + // 字体颜色 + color: { + type: String, + default: () => defProps.notify.color + }, + // 背景颜色 + bgColor: { + type: String, + default: () => defProps.notify.bgColor + }, + // 展示的文字内容 + message: { + type: String, + default: () => defProps.notify.message + }, + // 展示时长,为0时不消失,单位ms + duration: { + type: [String, Number], + default: () => defProps.notify.duration + }, + // 字体大小 + fontSize: { + type: [String, Number], + default: () => defProps.notify.fontSize + }, + // 是否留出顶部安全距离(状态栏高度) + safeAreaInsetTop: { + type: Boolean, + default: () => defProps.notify.safeAreaInsetTop + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-notify/u-notify.vue b/im-uniapp/uni_modules/uview-plus/components/u-notify/u-notify.vue new file mode 100644 index 0000000..adab25a --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-notify/u-notify.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-number-box/props.js b/im-uniapp/uni_modules/uview-plus/components/u-number-box/props.js new file mode 100644 index 0000000..1c5564e --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-number-box/props.js @@ -0,0 +1,120 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 步进器标识符,在change回调返回 + name: { + type: [String, Number], + default: () => defProps.numberBox.name + }, + // #ifdef VUE2 + // 用于双向绑定的值,初始化时设置设为默认min值(最小值) + value: { + type: [String, Number], + default: () => defProps.numberBox.value + }, + // #endif + // #ifdef VUE3 + // 用于双向绑定的值,初始化时设置设为默认min值(最小值) + modelValue: { + type: [String, Number], + default: () => defProps.numberBox.value + }, + // #endif + // 最小值 + min: { + type: [String, Number], + default: () => defProps.numberBox.min + }, + // 最大值 + max: { + type: [String, Number], + default: () => defProps.numberBox.max + }, + // 加减的步长,可为小数 + step: { + type: [String, Number], + default: () => defProps.numberBox.step + }, + // 是否只允许输入整数 + integer: { + type: Boolean, + default: () => defProps.numberBox.integer + }, + // 是否禁用,包括输入框,加减按钮 + disabled: { + type: Boolean, + default: () => defProps.numberBox.disabled + }, + // 是否禁用输入框 + disabledInput: { + type: Boolean, + default: () => defProps.numberBox.disabledInput + }, + // 是否开启异步变更,开启后需要手动控制输入值 + asyncChange: { + type: Boolean, + default: () => defProps.numberBox.asyncChange + }, + // 输入框宽度,单位为px + inputWidth: { + type: [String, Number], + default: () => defProps.numberBox.inputWidth + }, + // 是否显示减少按钮 + showMinus: { + type: Boolean, + default: () => defProps.numberBox.showMinus + }, + // 是否显示增加按钮 + showPlus: { + type: Boolean, + default: () => defProps.numberBox.showPlus + }, + // 显示的小数位数 + decimalLength: { + type: [String, Number, null], + default: () => defProps.numberBox.decimalLength + }, + // 是否开启长按加减手势 + longPress: { + type: Boolean, + default: () => defProps.numberBox.longPress + }, + // 输入框文字和加减按钮图标的颜色 + color: { + type: String, + default: () => defProps.numberBox.color + }, + // 按钮大小,宽高等于此值,单位px,输入框高度和此值保持一致 + buttonSize: { + type: [String, Number], + default: () => defProps.numberBox.buttonSize + }, + // 输入框和按钮的背景颜色 + bgColor: { + type: String, + default: () => defProps.numberBox.bgColor + }, + // 指定光标于键盘的距离,避免键盘遮挡输入框,单位px + cursorSpacing: { + type: [String, Number], + default: () => defProps.numberBox.cursorSpacing + }, + // 是否禁用增加按钮 + disablePlus: { + type: Boolean, + default: () => defProps.numberBox.disablePlus + }, + // 是否禁用减少按钮 + disableMinus: { + type: Boolean, + default: () => defProps.numberBox.disableMinus + }, + // 加减按钮图标的样式 + iconStyle: { + type: [Object, String], + default: () => defProps.numberBox.iconStyle + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-number-box/u-number-box.vue b/im-uniapp/uni_modules/uview-plus/components/u-number-box/u-number-box.vue new file mode 100644 index 0000000..2371bbc --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-number-box/u-number-box.vue @@ -0,0 +1,465 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-number-keyboard/props.js b/im-uniapp/uni_modules/uview-plus/components/u-number-keyboard/props.js new file mode 100644 index 0000000..d853858 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-number-keyboard/props.js @@ -0,0 +1,21 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 键盘的类型,number-数字键盘,card-身份证键盘 + mode: { + type: String, + default: () => defProps.numberKeyboard.value + }, + // 是否显示键盘的"."符号 + dotDisabled: { + type: Boolean, + default: () => defProps.numberKeyboard.dotDisabled + }, + // 是否打乱键盘按键的顺序 + random: { + type: Boolean, + default: () => defProps.numberKeyboard.random + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-number-keyboard/u-number-keyboard.vue b/im-uniapp/uni_modules/uview-plus/components/u-number-keyboard/u-number-keyboard.vue new file mode 100644 index 0000000..a978fd0 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-number-keyboard/u-number-keyboard.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-overlay/props.js b/im-uniapp/uni_modules/uview-plus/components/u-overlay/props.js new file mode 100644 index 0000000..39f16f3 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-overlay/props.js @@ -0,0 +1,26 @@ +import { defineMixin } from '../../libs/vue' +import defProps from '../../libs/config/props.js' +export const props = defineMixin({ + props: { + // 是否显示遮罩 + show: { + type: Boolean, + default: () => defProps.overlay.show + }, + // 层级z-index + zIndex: { + type: [String, Number], + default: () => defProps.overlay.zIndex + }, + // 遮罩的过渡时间,单位为ms + duration: { + type: [String, Number], + default: () => defProps.overlay.duration + }, + // 不透明度值,当做rgba的第四个参数 + opacity: { + type: [String, Number], + default: () => defProps.overlay.opacity + } + } +}) diff --git a/im-uniapp/uni_modules/uview-plus/components/u-overlay/u-overlay.vue b/im-uniapp/uni_modules/uview-plus/components/u-overlay/u-overlay.vue new file mode 100644 index 0000000..d804750 --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-overlay/u-overlay.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/im-uniapp/uni_modules/uview-plus/components/u-parse/node/node.vue b/im-uniapp/uni_modules/uview-plus/components/u-parse/node/node.vue new file mode 100644 index 0000000..06035de --- /dev/null +++ b/im-uniapp/uni_modules/uview-plus/components/u-parse/node/node.vue @@ -0,0 +1,576 @@ +