Compare commits

...

371 Commits
5.6.3 ... dev

Author SHA1 Message Date
Pan Qiancheng 11f978b24c fix: 修复auth组件内一处不需要的判断,对aspect内查询state新增了forUpdate 2026-01-22 16:00:02 +08:00
Xu Chang b8654da5fb 5.11.2-dev 2026-01-21 10:09:26 +08:00
Xu Chang 3fae4dd855 5.11.1-pub 2026-01-21 10:07:20 +08:00
Pan Qiancheng 8660c6a1e3 feat: 修复了一点小bug 2026-01-19 15:09:24 +08:00
Pan Qiancheng 7312545eda feat: oauthUser新增索引,用于查询第三方认证的用户信息 2026-01-16 16:51:29 +08:00
Pan Qiancheng cf63a53109 feat: 添加一些assert 2026-01-15 12:05:47 +08:00
Pan Qiancheng 9027a40479 feat: 将获取分片信息延迟到上传时,采用批量获取的形式,并且不再记录uploadUrl 2026-01-14 15:38:55 +08:00
Xu Chang 5c3a02aa95 5.11.1-dev 2026-01-09 16:04:14 +08:00
Xu Chang b1f8300bbb 5.11.0-pub 2026-01-09 16:02:51 +08:00
wkj 10b4e8dbd8 fix: makeException少处理了OakIncompleteConfig 2026-01-09 11:02:02 +08:00
wkj 0ca7f1b020 feat: loginByAccount 只有开启邮箱、手机号再加上密码登录,默认只能账号加密码登录 2026-01-07 13:40:03 +08:00
Pan Qiancheng 0315b11ae2 fix: 修复两处i18n问题 2026-01-06 18:08:42 +08:00
Pan Qiancheng 98ad339316 feat: 新编译器支持i18n检查,修复部分i18n问题 2026-01-06 17:52:59 +08:00
Pan Qiancheng b6a118efc5 feat: build.js 2026-01-04 16:51:47 +08:00
Pan Qiancheng 7862282214 fix: oak-ignore 2026-01-04 16:25:17 +08:00
Pan Qiancheng 79a98e2d8f fix: 忽略两处检查 2026-01-04 15:39:38 +08:00
Pan Qiancheng e92f26f50c fix: 一处未await错误 2026-01-04 14:52:28 +08:00
Pan Qiancheng 9c5fb29259 fix: cos的状态码判断错误 2026-01-04 14:08:51 +08:00
Pan Qiancheng 9025b7d8b4 feat: 使用编译脚本,并适配了新的编译器改动 2026-01-01 21:13:25 +08:00
Pan Qiancheng 44288172f5 feat: 修改模板,适配新的登录组件 2025-12-31 17:48:21 +08:00
lxy 570f6e8df5 merge passport2 2025-12-31 16:21:35 +08:00
Pan Qiancheng 337782a54b feat: 添加一些log 2025-12-30 23:29:51 +08:00
Pan Qiancheng f585d154ea feat: 升级SQL 2025-12-29 17:52:27 +08:00
Pan Qiancheng fdf948d3ee fix: 修复watcher检查分片文件的部分问题 2025-12-29 17:35:53 +08:00
Pan Qiancheng 14d593cc89 feat: 新增aspect:presignFile用于私有桶的预签名下载 2025-12-29 16:53:42 +08:00
Pan Qiancheng 3fa730cfb1 feat: cos-backend添加presignFile 2025-12-29 16:41:41 +08:00
Pan Qiancheng ab9789eb4c fix: 如果启用了分片上传,则不产生uploadMeta 2025-12-29 09:09:35 +08:00
Pan Qiancheng 2871934bcd fix: 如果先前的分片已经被取消,则后续就不需要上传了(整个任务实际被取消) 2025-12-27 00:06:15 +08:00
Pan Qiancheng df9855906d fix: 分片预签名过期时间3天 2025-12-26 23:44:17 +08:00
Pan Qiancheng 066d834d8d fix: projection不需要每次都查询分片信息,导致过多的数据量 2025-12-26 22:01:07 +08:00
Pan Qiancheng a52a2874c6 feat: 检查分片文件的watcher, 修复请求超时问题 2025-12-26 18:30:35 +08:00
Pan Qiancheng 5543806a63 fix: 一处错误的导入 2025-12-26 17:59:15 +08:00
Pan Qiancheng f3d782fcbc fix: 小程序不检查10M限制,默认超时时间设置为1小时 2025-12-26 17:33:37 +08:00
Pan Qiancheng 95b506b0eb feat: 适配新uploadFn,并修复小程序端使用PUT的错误判断 2025-12-26 17:07:55 +08:00
Pan Qiancheng 56b07dc4cb fix: 改为由后端判断分片上传情况 2025-12-26 16:22:32 +08:00
Pan Qiancheng 147dc5eb12 fix: 判断DomException,新增分片数量的检查 2025-12-26 14:42:20 +08:00
Pan Qiancheng 347e1ab360 fix: 类型声明 2025-12-26 14:10:54 +08:00
Pan Qiancheng 336f0b1aa1 fix: 添加小程序端不得超过10m的判断,分片信息可以传入upload组件 2025-12-26 14:09:57 +08:00
Pan Qiancheng 10077f150e feat: 新增S3实现分片上传等功能 2025-12-26 13:30:07 +08:00
Pan Qiancheng d5a09546dd feat: 重构文件上传部分逻辑,实现分片上传以及取消上传等功能 2025-12-26 12:31:33 +08:00
wkj ffcac7f843 feat:1、weichatLogin实体增加router,表示扫码跳转的目标路由, 通过aspect默认传入wechatLogin/confirm;2、修正微信公众号扫码登录和绑定时,createWechatUserAndReturnTokenId不需要传入wechatLoginId 2025-12-25 14:57:32 +08:00
lxy 4227430080 fix: 微信绑定查找wechatUser增加appliaction判断 2025-12-24 18:09:37 +08:00
Pan Qiancheng 1e5697a28e fix: 更新oauth相关数据的组件优化,允许未提供refreshEndpoint 2025-12-24 17:27:14 +08:00
Pan Qiancheng a3ec5fc808 fix: 修复oauth-endpoint相关问题,并允许重复注册处理器 2025-12-24 15:13:16 +08:00
Pan Qiancheng ba434da707 fix: 授权结果页的错误信息显示 2025-12-24 13:45:30 +08:00
Pan Qiancheng 0e05a3ad4e fix: 修复oauth-app和provider的systemId填充问题,将oauth认证页未登录回调以及oauth结果页面的按钮回调,全部交给上层应用处理 2025-12-24 12:26:47 +08:00
Pan Qiancheng 7c35f695ab fix: passport不存在导致的login组件加载报错 2025-12-23 17:44:01 +08:00
Pan Qiancheng 35f8bfcc0b feat: feature-token支持传入忽略的Exception列表,并在刷新token时若出现该异常则不强制退出登录 2025-12-23 15:31:30 +08:00
wkj 40d69bf0f4 feat: 绑定微信时,判断wechatUser是否已绑定,抛一个合法异常 2025-12-23 13:18:46 +08:00
wkj d347e19ede feat: 补上元素key 2025-12-23 10:49:58 +08:00
wkj 97fd741a3b refactor: syncMessageTemplate函数名改为syncWechatTemplate 2025-12-23 10:00:09 +08:00
wkj 6aff242687 feat: 打开微信模版列表弹出框,增加微信模版消息内容显示 2025-12-23 09:44:40 +08:00
wkj 257f7669fc feat: 短信注入函数registSms改为registerSms,项目引用需适配,短信模版列表支持传入pageIndex和pageSize 2025-12-22 19:05:12 +08:00
wkj edbeb63cc1 feat: 修改message restriction声明,扩充对不同渠道发送通知时 是否支持router跳转 2025-12-22 17:42:27 +08:00
Pan Qiancheng 5aa78b89b4 fix: passport/password组件内useEffect无限递归,applicationPassport组件内Table未指定rowKey,web环境下不存在text标签 2025-12-20 12:05:36 +08:00
Xu Chang 7c2fba3191 5.10.9-dev 2025-12-16 13:55:16 +08:00
Xu Chang ab6616526d 5.10.8-pub-2 2025-12-16 13:54:03 +08:00
Xu Chang 67cf576ddc 5.10.8-pub 2025-12-16 13:47:37 +08:00
wkj ae1d1cf70a fix: 新建article时 ,上传文件extraFile缺失entityId 2025-12-15 18:18:04 +08:00
wkj 21348fff43 fix: 1、更新wechatUser的refreshToken未加上filter条件;2、获取公众号用户信息,更新下wechatUser的nickname和avatar 2025-12-12 15:47:15 +08:00
Xu Chang 157dbfd88a 5.10.8-dev 2025-12-11 10:39:14 +08:00
Xu Chang 660b25a7d8 5.10.7-pub 2025-12-11 10:03:13 +08:00
wkj fbfeb210bd fix: 扫码绑定公众号,code换取相关token需更新wechatUser上 2025-12-10 10:17:38 +08:00
wkj a5f7af8ec1 fix: 绑定微信公众号传入user不存在 2025-12-09 19:16:35 +08:00
wkj 1b61055ad4 feat: 移除废弃文件 2025-12-09 15:24:13 +08:00
wkj 4e555c90e2 feat: 微信扫码中间页即wechatQrCode/scan路径支持配置 2025-12-09 15:09:58 +08:00
wkj 1b90173e16 feat: wechatLogin confirm页面使用微信授权api 2025-12-09 12:19:04 +08:00
wkj 365900b750 feat: 生成微信二维码时,移除开发环境mock数据,使用微信测试号进行api调用 2025-12-09 11:05:01 +08:00
wkj f2ada8fbba fix: 微信消息创建sessionMessage缺失id 2025-12-08 18:04:59 +08:00
wkj 4b30f6c838 feat:微信消息创建session出错时,打印下error输出 2025-12-08 17:57:12 +08:00
wkj 8c8717433c feat: 微信消息创建session出错时,打印下error输出 2025-12-08 17:52:18 +08:00
QCQCQC@Debian ef871317a6 build 2025-12-05 19:08:01 +08:00
wkj 16eb5de960 fix: 验证码发送失败,使captcha过期 2025-12-05 19:05:53 +08:00
Pan Qiancheng 24396a8782 feat: 导出oauth用户信息处理器的注入点 2025-12-04 11:08:09 +08:00
Pan Qiancheng 5af3a8f9db fix: 简化了多余的数据查询 2025-12-04 10:47:40 +08:00
Pan Qiancheng af88199ba7 feat: 修改了Message的注入点类型定义 2025-12-04 09:11:32 +08:00
Pan Qiancheng 78eb039a04 feat: 提供notification发送和发送失败处理的注入点 2025-12-03 21:51:32 +08:00
Pan Qiancheng 5ff8f1ed53 Merge branch 'dev' of https://gitea.51mars.com/Oak-Team/oak-general-business into dev 2025-12-03 18:43:09 +08:00
Pan Qiancheng 5fbc76c93f feat: 重构消息发送部分,支持消息类型处理器注入 2025-12-03 18:43:06 +08:00
Pan Qiancheng f57dacfe5d feat: upload组件支持计算md5(通过组件参数开启) 2025-12-03 18:42:42 +08:00
Xu Chang 2ed82709b8 5.10.7-dev 2025-12-03 11:11:01 +08:00
Xu Chang 27687c5b2f 5.10.6 2025-12-03 11:09:14 +08:00
lxy 799246c21b Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-12-03 10:58:21 +08:00
lxy 3cf3b4da09 feat: message的select checker getSystemId增加true 2025-12-03 10:58:15 +08:00
Pan Qiancheng d8d78edde1 fix: 删除一处不需要的文件 2025-12-03 10:42:48 +08:00
Pan Qiancheng ee57c1e932 build 2025-12-02 18:05:37 +08:00
Xu Chang 20708057da 5.10.6-dev 2025-12-02 15:28:24 +08:00
Xu Chang 696278927a 5.10.5-pub 2025-12-02 15:25:51 +08:00
Pan Qiancheng 6a49d6f5af feat: 重构了cos相关的调用方法,并在前端的COS中传入了cache 2025-12-02 14:10:02 +08:00
Pan Qiancheng 68b3f93e9f feat: 在CosBackend的相关方法中加入context参数并适配现有代码 2025-12-02 11:16:19 +08:00
Xu Chang 3995ff52f8 5.10.5-dev 2025-12-02 10:15:01 +08:00
Xu Chang d34c91bf07 5.10.4-pub 2025-12-02 10:08:01 +08:00
Pan Qiancheng e8b3a5878e feat: 切分registry为backend与frontend,并移除了wechatPublicTag相关不再被使用的注册函数 2025-12-01 18:35:16 +08:00
wkj b9783aa588 fix: extraFile触发器创建动作priority改为9 2025-12-01 12:42:01 +08:00
lxy 3aec26e06f feat: endopints/wechat 中callback改为异步 2025-11-27 15:30:44 +08:00
wkj 7cb2479eab feat: extraFile projection加上extra2 2025-11-27 15:24:17 +08:00
wkj ca958c60f0 feat: extraFile触发器创建动作加上 priority: 99 2025-11-27 15:05:22 +08:00
lxy 20030cc9ad fix: applicationPassport checker中pipeline使用错误修正 2025-11-21 14:02:53 +08:00
lxy 67341a64df fix: i18n 2025-11-20 10:39:03 +08:00
lxy b839318c5c refactor: 调整密码登录异常顺序 2025-11-20 10:38:05 +08:00
Xu Chang acf99448a5 稍微适配了domain中权限相关exception的改动 2025-11-19 17:32:41 +08:00
lxy 63dca8d683 feat: 登录配置中对未支持的登录方式增加提示 2025-11-19 15:25:46 +08:00
lxy 126d43a90b fix: 方法名修正 2025-11-19 14:18:09 +08:00
lxy aeb3dee4c2 feat: 获取登录方式接口优化,排除password类型 2025-11-18 12:08:15 +08:00
lxy e89661326e upgrade: v5.11.0,升级SQL 2025-11-18 10:36:12 +08:00
lxy 818504690b style: 密码登录提示文字调整 2025-11-18 10:06:18 +08:00
lxy 9751ee5e10 feat: 支持小程序扫码授权登录网页 2025-11-17 17:44:23 +08:00
lxy 169562994b feat: 小程序支持账号注册 2025-11-14 17:19:46 +08:00
lxy a1ed3c7ca7 feat: 小程序登录样式调整适配密码登录变化 2025-11-14 11:27:51 +08:00
lxy 3f690e6725 feat: 增加判断oauth登录时是否开启相应applicationPassport 2025-11-14 10:28:01 +08:00
lxy d7d302f329 feat: 新增用户账号注册组件;登录组件适配账号登录 2025-11-13 16:12:55 +08:00
lxy b93ae46894 feat: 新增账号登录与oauth授权登录 2025-11-12 15:09:10 +08:00
lxy 5b702a0d7d feat: 密码登录方式调整至系统配置管理下 2025-11-12 14:50:35 +08:00
Xu Chang 5fb1988b0d 5.10.4-dev 2025-11-12 14:00:40 +08:00
Xu Chang f26eb01807 5.10.3 2025-11-12 13:58:24 +08:00
Xu Chang 62dd001be3 build 2025-11-12 12:58:56 +08:00
Xu Chang 20cc101f88 userRelationUpsert页面的分享逻辑不成立,通过回调给上层处理 2025-11-12 12:57:08 +08:00
Pan Qiancheng ccdd86923f build2 2025-11-07 15:27:29 +08:00
Pan Qiancheng cd1bf2ce21 build 2025-11-07 14:40:57 +08:00
Pan Qiancheng e2f9a49d76 fix: 修改提示文本 2025-11-07 09:47:44 +08:00
Pan Qiancheng 5f0c366135 fix: token不存在时刷新token抛出未登录异常 2025-11-07 08:31:15 +08:00
Xu Chang ca06b24276 5.10.3-dev 2025-11-06 18:11:43 +08:00
Xu Chang 7e05611da0 5.10.2-pub 2025-11-06 18:09:22 +08:00
lxy 0c86dc99e1 feat: extrafile/upload组件小程序上传图片数量限制 2025-11-06 16:53:20 +08:00
lxy 75a303ed95 feat: 删除mobile前删除关联的已禁用token 2025-11-06 15:54:08 +08:00
lxy 212df98c60 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-11-06 15:09:22 +08:00
Xu Chang a4040a397b Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-11-06 15:09:21 +08:00
lxy 077278d220 fix: 个人信息手机号更新后未显示修正 2025-11-06 15:09:18 +08:00
Xu Chang a9f0493316 user/login组件去掉默认的loginMode 2025-11-06 15:09:14 +08:00
lxy 21b170bdc5 feat: 小程序用户头像昵称获取调整 2025-11-05 15:18:09 +08:00
Xu Chang e9cb9809f1 5.10.2-dev 2025-11-04 14:25:40 +08:00
Xu Chang 4675e4c0ea 5.10.1-pub 2025-11-04 14:23:18 +08:00
Xu Chang 84e64b9d66 5.10.0-publish 2025-11-04 14:22:48 +08:00
Xu Chang 8fb96b1687 build 2025-11-04 14:18:18 +08:00
Xu Chang e64ce53334 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-11-04 14:11:22 +08:00
Xu Chang 5f33319f75 merge 2025-11-04 14:11:18 +08:00
lxy d4ceb97260 fix: 文章更新保存前判断修正 2025-11-03 15:56:46 +08:00
lxy f66fc76c01 fix: article/upsert在点击保存按钮时更新article的content 2025-11-03 10:50:00 +08:00
lxy 706ea27664 fix: 文档管理编辑器调整 2025-11-03 10:46:06 +08:00
lxy b1b31fa0e7 fix: 修复application创建时config中未赋type 2025-10-31 17:16:08 +08:00
Xu Chang 83f0879d0c 给WechatMpConfig增加了getPhone的配置 2025-10-31 16:57:46 +08:00
Pan Qiancheng 39ae0d19eb fix: 修复两处login的错误 2025-10-31 16:28:50 +08:00
wkj 0cfaa9f93f feat: 5.10.1版本sql 2025-10-31 12:19:11 +08:00
wkj 2bebe90f66 build: 编译 2025-10-31 12:18:05 +08:00
Pan Qiancheng f9123d81cc Merge branch 'merge_oauth2_extrafile_dev' of https://gitea.51mars.com/Oak-Team/oak-general-business into merge_oauth2_extrafile_dev 2025-10-28 13:03:32 +08:00
Pan Qiancheng 502c028eb1 合并到5.10.4的升级目录中 2025-10-28 13:01:43 +08:00
Pan Qiancheng d034741ba4 fix: 迁移SQL 2025-10-28 13:01:43 +08:00
Pan Qiancheng a2dfa1d1f3 build 2025-10-28 13:01:43 +08:00
Pan Qiancheng 44c969635f feat: 在accessToken的endpoint和oauth的aspect中支持了PKCE相关支持 2025-10-28 13:01:43 +08:00
Pan Qiancheng b6aea50539 fix: 在创建操作时阻止用户重新生成、密钥 2025-10-28 13:01:43 +08:00
Pan Qiancheng 0f90d5c360 feat: 修改实体,将provider的type定义改为string,以实现自定义的类型注入。
feat: 在OAuthApplication中新增强制PKCE参数
fix: 修复了oauth相关upsert组件的部分问题
2025-10-28 13:01:37 +08:00
Pan Qiancheng 629cf2e9d5 合并到5.10.4的升级目录中 2025-10-28 12:59:46 +08:00
Pan Qiancheng 8e79f42e2c fix: 迁移SQL 2025-10-28 11:29:19 +08:00
Pan Qiancheng 3ed9055c15 build 2025-10-28 11:21:30 +08:00
Pan Qiancheng 989c3c66ad feat: 在accessToken的endpoint和oauth的aspect中支持了PKCE相关支持 2025-10-28 11:07:37 +08:00
Pan Qiancheng 27db58a6f5 fix: 在创建操作时阻止用户重新生成、密钥 2025-10-28 10:47:10 +08:00
Pan Qiancheng e4295f5a4f feat: 修改实体,将provider的type定义改为string,以实现自定义的类型注入。
feat: 在OAuthApplication中新增强制PKCE参数
fix: 修复了oauth相关upsert组件的部分问题
2025-10-28 10:38:29 +08:00
wkj 8367447700 Merge branch 'merge_oauth2_extrafile_dev' into dev 2025-10-27 13:40:35 +08:00
Pan Qiancheng ded4eded1f fix: 对origin为空的情况新增了容错处理 2025-10-27 12:04:46 +08:00
wkj 16e347b7e3 修正cos文件下upload参数申明 2025-10-27 10:47:03 +08:00
wkj 840348d084 Merge branch 'merge_oauth2_extrafile_dev' into dev 2025-10-27 10:14:29 +08:00
wkj 964973daae upgrade: package.json version 5.10.1 2025-10-27 10:07:44 +08:00
wkj 116feb8a80 platform 和 system 文件下,panel 组件中props增加tabs传入 2025-10-27 10:04:01 +08:00
pqcqaq fcc9389812 fix: 修复一处if判断的笔误 2025-10-25 10:17:01 +08:00
pqcqaq 4e94e86d20 fix: 样式调整 2025-10-25 00:38:32 +08:00
Pan Qiancheng 4f41a93dd5 upgrade: 修改版本号,提供了升级SQL 2025-10-24 17:41:23 +08:00
Pan Qiancheng 75296485f1 fix: 新增是否启用pathStyle 2025-10-24 17:27:10 +08:00
Pan Qiancheng 0271a93e31 build 2025-10-24 17:16:29 +08:00
Pan Qiancheng 2c786b3d09 fix: 修复了由新增参数导致的s3 upload实现调用错误 2025-10-24 17:05:43 +08:00
Pan Qiancheng c2f14287b4 merge oauth2 and extrafile rebase from dev 2025-10-24 17:03:35 +08:00
Pan Qiancheng 670e4525cd feat: build 2025-10-24 16:58:36 +08:00
Pan Qiancheng 5c06cf0a7b fix: 移除了所有extraFile相关组件的origin默认值,修复了在trigger中不支持的selection写法导致的崩溃 2025-10-24 16:58:33 +08:00
Pan Qiancheng 7beeb8afeb feat: 支持了application, system,platform的defaultOrigin配置 2025-10-24 16:57:58 +08:00
Pan Qiancheng 021276346d fix: 因编译器暂不支持嵌套引用类型,故重新在entity内声明CosOrigin(以后适配) 2025-10-24 16:57:56 +08:00
Pan Qiancheng 4e005cec60 fix: 赋值 2025-10-24 16:57:39 +08:00
Pan Qiancheng 859d09cf13 fix: 优化了查询逻辑,如果提供origin则不需要再去查询数据库 2025-10-24 16:57:39 +08:00
Pan Qiancheng f4c80b052c feat: 在application, system, platform中新增了defaultOrigin的配置,并在extrafile的create-before trigger中获取配置并应用 2025-10-24 16:57:39 +08:00
Pan Qiancheng 6b29b03750 feat: 新增我的授权历史组件 2025-10-24 16:57:39 +08:00
Pan Qiancheng c28bca6385 feat: 修复oauthUserAuth中的revoke逻辑,在撤销时还需要将unused的所用的code都标记为已使用 2025-10-24 16:57:39 +08:00
Pan Qiancheng fda2b1be6b feat: 修复revoke端口相关逻辑 2025-10-24 16:57:39 +08:00
Pan Qiancheng 2199178421 feat: build 2025-10-24 16:57:39 +08:00
Pan Qiancheng 9ba261793b feat: 如果是在某一应用下反复授权则直接允许,并修改实体添加了更多认真状态 2025-10-24 16:57:36 +08:00
Pan Qiancheng 4d8e3d15ff feat: 实现派发方的revokr逻辑 2025-10-24 16:57:25 +08:00
Pan Qiancheng d8c358f463 fix: 获取token之后要回填到userAuth记录中 2025-10-24 16:57:10 +08:00
Pan Qiancheng 6e6b8eb3b9 feat: 复制几个固定页面到模板中 2025-10-24 16:57:10 +08:00
Pan Qiancheng cd7ab8a1d1 feat: build 2025-10-24 16:57:08 +08:00
Pan Qiancheng a0dec2b277 feat: oauth相关aspect实现 && 给其他aspect定义加上了注释 2025-10-24 16:56:16 +08:00
Pan Qiancheng 67fbd49c26 feat: oauth配置相关增删改查组件实现 2025-10-24 16:56:16 +08:00
Pan Qiancheng 91ff162908 feat: 定期刷新要过期的oauth授权用户 2025-10-24 16:56:16 +08:00
Pan Qiancheng f1e7894e48 feat: 新增oauth用户信息注册点工具包 2025-10-24 16:56:16 +08:00
Pan Qiancheng d7b29e0d0e feat: oauth相关trigger实现 2025-10-24 16:56:16 +08:00
Pan Qiancheng e5821fb8a7 feat: 更新oauth相关实体定义 2025-10-24 16:56:16 +08:00
Pan Qiancheng f824463a2a feat: 获取OAuth Token、获取OAuth用户信息、刷新OAuth令牌
endpoint实现
2025-10-24 16:56:16 +08:00
Pan Qiancheng 7baa7f808c feat: 修改了oauth服务相关的实体定义 2025-10-24 16:56:16 +08:00
Pan Qiancheng 476b68ab1c feat: 在feature/token中新增loginByOAuth方法,build 2025-10-24 16:56:13 +08:00
Pan Qiancheng f10208e5d5 feat: 导出所需函数供项目中使用 2025-10-24 16:52:55 +08:00
Pan Qiancheng 9299a2ba66 feat: 新增oauth相关的实体定义及类型定义 2025-10-24 16:52:55 +08:00
Xu Chang b1c33ebd9c 5.9.4-dev 2025-10-21 17:22:38 +08:00
Xu Chang 495b928dde 5.9.3-pub 2025-10-21 17:21:37 +08:00
Xu Chang 8b96a31bfb template中的data适配新的config结构 2025-10-21 17:17:58 +08:00
xzf 7453b8dcae features/extraFile.ts 更改Upload函数的引用文件 2025-10-21 11:56:45 +08:00
xzf e2627fd93b 去除QrCode多余的传参 2025-10-21 11:39:26 +08:00
xzf 9d86b9caa1 去除没必要的引用 2025-10-21 11:38:57 +08:00
xzf 95bae42ad8 ts类型报错 2025-10-21 11:38:39 +08:00
xzf a6966ae02c extraFile支持中断文件上传 2025-10-21 11:38:13 +08:00
Pan Qiancheng 5d5210ac7d build 2025-10-16 14:44:39 +08:00
Pan Qiancheng 88482b96c1 feat: 修改S3配置的类型,并实现系统管理页面中S3相关的账号和cos配置 2025-10-16 14:44:38 +08:00
lxy 6ce0cf80aa 补充5.8.1相关sql 2025-10-16 11:50:49 +08:00
Xu Chang 0da6a3ebb2 5.9.3-dev 2025-10-16 10:58:30 +08:00
Xu Chang 74f8d5eeec 5.9.2-pub 2025-10-16 10:44:33 +08:00
Xu Chang 92d7d95ca9 build 2025-10-16 10:42:42 +08:00
Xu Chang a948027e6f 5.9.2-dev 2025-10-16 10:13:18 +08:00
Xu Chang 8c0d0f93d9 5.9.1-pub 2025-10-16 09:33:37 +08:00
Xu Chang e42ebf0707 5.8.2-pub 2025-10-16 09:27:30 +08:00
Xu Chang fae809e4bd user/info的小更新 2025-10-16 09:27:19 +08:00
Xu Chang 32fe168559 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-10-16 08:32:00 +08:00
Xu Chang 60d8c5d147 user/info的一些更新 2025-10-15 16:55:02 +08:00
Pan Qiancheng 2376af227f build 2025-10-15 16:28:44 +08:00
Pan Qiancheng 0cb03c1e4b feat: 支持了S3协议的上传 2025-10-15 16:28:35 +08:00
lxy 0cce61fea1 changePassword按钮disabled调整 2025-10-14 14:25:57 +08:00
lxy a4ad5411a2 密码登录支持配置明文密文存储模式 2025-10-13 17:45:32 +08:00
lxy d9927261e5 文档列表点击文档后跳转至文档编辑页 2025-10-13 12:27:17 +08:00
lxy 4aa9073981 密码登录支持配置正则 2025-10-11 16:25:56 +08:00
Xu Chang 472f8bf21e 微调了userEntityGrant相关的页面细节 2025-10-11 13:15:56 +08:00
Xu Chang 11dc2ca89a 微调了userRelation/list的过滤条件,使得删除了权限的用户从列表中消失 2025-10-11 11:52:15 +08:00
Xu Chang 6c8efa9d00 更正了一堆userEntityGrant相关的bug 2025-10-10 19:17:51 +08:00
Xu Chang 0e7db712bb byUserEntityGrant的页面调整 2025-10-10 16:12:57 +08:00
Xu Chang d8be2d5323 重构了userRelation/upsert 2025-10-09 14:56:45 +08:00
Xu Chang 483226791d 适配domain的改动 2025-10-07 18:54:40 +08:00
wkj 4bf2afa45c 修正 文章分类更新名称去重判断 2025-09-26 19:03:19 +08:00
lxy 122e868b52 目录滚动observer容错 2025-09-26 17:24:14 +08:00
Xu Chang 5f9de0de10 5.8.2-dev 2025-09-26 13:09:40 +08:00
Xu Chang 38f0ae1a00 5.8.1-pub 2025-09-26 13:08:40 +08:00
wkj 7463494fce 生成二维码增加mode参数web端调整 2025-09-23 18:24:38 +08:00
lxy 0a359eb287 文档管理 复制链接修正 2025-09-23 18:13:37 +08:00
wkj d18e4cb1eb 生成二维码组件增加mode参数 2025-09-23 18:09:43 +08:00
lxy d15bd7bfaf articleMenu isArticle变化时推送订阅 2025-09-23 15:11:16 +08:00
lxy 32556a4c4a 文章目录实现页面滚动时对应目录项高亮 2025-09-22 18:23:17 +08:00
lxy fae9655cad articleMenu实体新增latestAt,新增文档管理相关组件 2025-09-19 16:24:00 +08:00
wkj ef648c8fab 调整article props声明 2025-09-15 18:30:23 +08:00
wkj 8af8487016 maskBgColor改为maskColor 2025-09-13 22:21:47 +08:00
wkj c4c78aa885 二维码组件增加 maskBgColor、maskTextColor、maskText 2025-09-13 22:10:45 +08:00
Xu Chang 2bce7a738e template/login 2025-09-13 21:18:37 +08:00
wkj 61901b302a 授权分享增加size、disabled、color、bgColor 2025-09-13 20:56:22 +08:00
wkj 256f0eefa1 程序二维码分享组件, 支持传入color,bgColor,disabled 2025-09-13 20:48:27 +08:00
wkj d89a0a27ef 程序二维码分享组件, 支持传入size 显示二维码大小 2025-09-13 17:29:48 +08:00
wkj 983d55e97a 小程序二维码分享组件, 支持传入disableDownload隐藏下载按钮 2025-09-13 16:43:58 +08:00
wkj 24f328047d 小程序链接转二维码组件i18n 2025-09-11 12:01:42 +08:00
wkj 82279251f1 fix: 删除文章分类、文章时 级联删除extraFile 2025-09-10 15:31:15 +08:00
wkj 81d02f4ee4 小程序端 支持链接转成二维码显示和二维码下载 2025-09-09 17:56:03 +08:00
lxy 75c2547407 文章目录样式调整 2025-09-08 19:34:07 +08:00
wkj d34acebeb3 下载文件 解析ReadableStream 2025-09-05 09:34:41 +08:00
lxy 6f3cea700b 调整手机号管理 2025-09-04 16:33:26 +08:00
lxy b1cd5e098a 补充userProjection中user$ref 2025-09-04 10:35:33 +08:00
lxy 1d0329521d userProjection增加usr$ref 2025-09-04 10:19:07 +08:00
Xu Chang f11e910774 merge 2025-09-03 23:05:30 +08:00
Xu Chang 89b2e229e1 5.7.10-dev 2025-09-03 23:04:55 +08:00
Xu Chang 0b783a7a85 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-09-03 23:03:46 +08:00
Xu Chang bd1c30d8d9 前端初始化时,注入了selection/operation的rewriter 2025-09-03 23:03:31 +08:00
wangwenchen c7ebbf00f6 上传文件支持进度条 参数getPercent 2025-08-28 10:55:12 +08:00
Xu Chang 5674bedb75 5.7.10-dev 2025-08-23 12:42:07 +08:00
Xu Chang 9b932f01fd Merge branch 'dev' into release 2025-08-23 12:41:08 +08:00
Xu Chang c33ac19bf2 5.7.9-pub 2025-08-23 12:41:01 +08:00
lxy c79b7ca63f captcha添加application外键,创建captcha时附上applicationId 2025-08-19 17:18:46 +08:00
lxy 130740db93 移除console.log 2025-08-08 17:02:54 +08:00
lxy f42737c5db 增加使用小程序tokenValue登录web(小程序web-view使用) 2025-08-07 13:45:44 +08:00
Xu Chang e05ccb9243 5.7.9-dev 2025-08-04 15:21:39 +08:00
Xu Chang f3aaa4ca90 Merge branch 'dev' into release 2025-08-04 15:19:38 +08:00
Xu Chang 4981a8d9b3 5.7.8-pub 2025-08-04 15:19:25 +08:00
wkj dcbd9557bb fix 阿里云获取模板列表参数 2025-08-04 14:19:38 +08:00
Xu Chang 321a32e0bf 5.7.8-dev 2025-08-02 21:32:07 +08:00
Xu Chang 18a921ec55 Merge branch 'dev' into release 2025-08-02 21:30:42 +08:00
Xu Chang 5a52956e1c 5.7.7-pub 2025-08-02 21:30:16 +08:00
Xu Chang bfcfd1a10f Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-08-02 21:18:22 +08:00
Xu Chang 3f15890d9c 完善了template中的data 2025-07-18 15:56:00 +08:00
lxy fa90f90558 适配external-sdk中创建直播流方法名变动 2025-07-14 16:47:07 +08:00
Xu Chang 700bc110d7 遗漏了一个template中的修正 2025-07-09 11:24:17 +08:00
Xu Chang 4e7d50d95e 遗漏了一个 2025-07-09 10:25:25 +08:00
Xu Chang 341027ca82 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-07-09 10:21:53 +08:00
Xu Chang f8550dc263 template中index.json中的引用要用@的形式 2025-07-09 10:21:48 +08:00
wkj 702e31e543 fix 邮件验证码过滤条件 2025-07-01 11:49:22 +08:00
wkj 6782b93e33 验证码每次发送 都产生新的 2025-06-30 21:02:00 +08:00
wkj faee5e9f3a 验证码登录成功就失效 2025-06-30 19:22:41 +08:00
wkj 641ffd4b1f application web类型增加微信支付配置 2025-06-26 18:46:54 +08:00
lxy 26bda3c197 config类型定义中qiniuLiveConfi新增配置添加? 2025-06-18 10:05:43 +08:00
Xu Chang 01609d4093 5.7.7-dev 2025-06-16 12:23:25 +08:00
Xu Chang 54201677cc Merge branch 'dev' into release 2025-06-16 12:22:33 +08:00
Xu Chang dd9558d37a 5.7.6-pub 2025-06-16 12:22:24 +08:00
wkj 71498e8381 build 2025-06-12 15:06:18 +08:00
wkj 049e1bef32 调整composeAccessPath方法,出现多域名情况 2025-06-12 15:04:37 +08:00
lxy 246db21ab6 system的config中qiniuLiveConfig类型定义修改;liveStream调用七牛云接口方法调整 2025-06-06 18:39:58 +08:00
Xu Chang bee5848640 微调了userInfo页面 2025-05-17 13:03:00 +08:00
Xu Chang c0a18c0523 5.7.6-dev 2025-05-12 15:40:46 +08:00
Xu Chang 2b51130217 Merge branch 'dev' into release 2025-05-12 15:39:54 +08:00
Xu Chang e8498d8d0e 5.7.5-pub 2025-05-12 15:39:46 +08:00
lxy 6a7bb6b41b user/info组件修改:mp使用extrafile/commit组件;当用户已身份认证时不允许更新姓名、性别、生日 2025-05-12 15:16:55 +08:00
wkj 3a2b1dd845 修正一些单词写错问题 2025-05-09 15:08:10 +08:00
wkj 80509da833 调整email ts声明 2025-05-09 10:01:11 +08:00
Xu Chang 5b69d9c6cc 5.7.5-dev 2025-05-08 16:36:00 +08:00
Xu Chang 7c5a09539a Merge branch 'dev' into release 2025-05-08 16:35:00 +08:00
Xu Chang d62395b743 5.7.4-pub 2025-05-08 16:34:49 +08:00
Xu Chang 3b34a206bf template微调,user/login增加了loginWechatMp 2025-05-08 16:33:51 +08:00
wkj 55c14a8121 build 2025-05-07 16:03:06 +08:00
qsc 24a65e8ed2 文章管理按钮加上tooltip 2025-05-07 11:48:37 +08:00
wkj 719b225227 邮箱后缀字段名调整 2025-05-07 09:39:57 +08:00
wkj 11962700ff passport、applicationPassport设置为select free 2025-05-06 17:47:29 +08:00
wkj e51dca38ca 调整提示 2025-05-06 16:54:10 +08:00
wkj 606b9e2023 邮箱支持配置后缀,当设置后缀,会检查输入的邮箱是否符合要求 2025-05-06 16:25:33 +08:00
Xu Chang dae20932a9 template中一些页面的细节 2025-05-04 20:34:14 +08:00
Xu Chang 8a9976a090 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-05-04 19:29:22 +08:00
Xu Chang 983ac16101 调整了user/info组件,在组件内创建mobile和password 2025-05-04 19:29:15 +08:00
qsc 1466a47c69 编辑文章extraFile entityId传入oakId或是自身id 2025-04-22 13:56:15 +08:00
wkj e8b952388e 移除cache.get allowMiss 2025-04-21 16:59:04 +08:00
wkj 52336cb952 删除extraFile时远端也进行删除时,getApplication置上true 2025-04-21 10:11:17 +08:00
Xu Chang 7b7b8cb39e 5.7.4-dev 2025-04-18 18:37:06 +08:00
Xu Chang 1c7b305d85 Merge branch 'dev' into release 2025-04-18 18:36:00 +08:00
Xu Chang fe5aebc09f 5.7.3-pub 2025-04-18 18:35:47 +08:00
wkj a506eb032b system projection 加上style 2025-04-16 09:39:00 +08:00
Xu Chang 82822c204d 调整了template中的src目录数据 2025-04-12 17:59:36 +08:00
Xu Chang 151458bba3 template中的改动 2025-04-12 13:55:10 +08:00
Xu Chang 3393bc5d5f Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-04-12 13:46:08 +08:00
Xu Chang 021fd49b7d 模板中增加了System对象,删除了SystemProvider对象 2025-04-12 13:46:02 +08:00
wkj 957c8fc6ed system 增加了platfromId 2025-04-10 18:18:02 +08:00
lxy b9405c265b 公众号菜单配置问题修改 2025-04-09 18:56:57 +08:00
Xu Chang 1e2a44523c 5.7.3-dev 2025-04-07 14:08:51 +08:00
wkj 6932752ecd 邮件、短信发送验证码错误时,增加error打印 2025-04-02 12:15:09 +08:00
wkj d9b3277596 移除es下废弃组件 2025-03-20 13:50:45 +08:00
wkj d35a02a02f 取出platformId 2025-03-20 12:41:11 +08:00
Xu Chang 0802f377cd Merge branch 'dev' into release 2025-03-17 14:42:34 +08:00
Xu Chang 86b9424645 5.7.2-pub 2025-03-17 14:42:26 +08:00
Xu Chang 6dc95be8ec build 2025-03-17 14:29:41 +08:00
wkj dd4c1fe94d 修正upload组件style申明 2025-03-06 17:21:52 +08:00
Xu Chang a3c2a22271 token增加了applicationId 2025-03-06 16:08:09 +08:00
Xu Chang 57f0a03654 5.7.2-dev 2025-02-27 14:42:29 +08:00
Xu Chang 945227640b Merge branch 'dev' into release 2025-02-27 14:41:18 +08:00
Xu Chang 25fae3b35f 5.7.1-pub 2025-02-27 14:41:10 +08:00
Xu Chang 80fd4a42f0 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-general-business into dev 2025-02-26 10:05:32 +08:00
Xu Chang 98c3f40f6f soaVerion长度放长 2025-02-26 10:05:25 +08:00
wkj 9de758f486 修复 token缺失applicationId,再refreshToken附上applicationId 2025-02-20 18:49:21 +08:00
wkj 8490711b91 refreshToken 根据token上applicationId查询配置 2025-02-14 14:08:38 +08:00
wkj 99019b1a6b 修正 新用户创建token时,未附上appliactionId 2025-02-13 16:16:27 +08:00
wkj 8ca59369ec refreshToken 从上下文获取getApplication 置上true 2025-02-07 18:36:56 +08:00
Xu Chang 41700a3f0d 5.7.1-dev 2025-02-05 14:48:10 +08:00
Xu Chang 9d756cb70e Merge branch 'dev' into release 2025-02-05 14:46:10 +08:00
Xu Chang 753509cca8 5.7.0-pub 2025-02-05 14:46:01 +08:00
Xu Chang 2fe9030905 5.6.5-dev 2025-02-05 14:34:39 +08:00
Xu Chang 6cf4c61898 Merge branch 'dev' into release 2025-02-05 14:33:15 +08:00
Xu Chang 356e965eef 5.6.4-pub 2025-02-05 14:33:07 +08:00
Xu Chang a6a859f103 build 2025-02-05 14:32:01 +08:00
Xu Chang a4329b140c merge 2025-01-26 16:15:09 +08:00
Xu Chang 7021b0e548 增加了application和system中的相关数据结构,用于检查和控制应用升级 2025-01-26 16:14:29 +08:00
pqcqaq ce8d87829f 部分组件的类型错误 2025-01-21 17:32:06 +08:00
pqcqaq b5889714a8 编译 2025-01-21 17:06:18 +08:00
pqcqaq 5aaec783bf Merge branch 'dev' of https://gitea.51mars.com/Oak-Team/oak-general-business into dev 2025-01-21 17:05:46 +08:00
Xu Chang f8635876c6 5.6.4-dev 2025-01-21 16:54:01 +08:00
Xu Chang 4ec6fdb46c Merge branch 'dev' into release 2025-01-21 16:50:41 +08:00
pqcqaq 43bb3ab9bb react版本>=改为^18.2.0 2025-01-21 16:25:01 +08:00
Xu Chang 0c941fc4e3 Merge branch 'dev' into release 2025-01-21 15:05:14 +08:00
Xu Chang 24731370d5 Merge branch 'dev' into release 2025-01-15 14:03:54 +08:00
Xu Chang 61a0ec0a00 Merge branch 'dev' into release 2024-12-23 17:00:52 +08:00
Xu Chang 08b432a02e Merge branch 'dev' into release 2024-12-04 10:30:17 +08:00
Xu Chang 1a9a9a9bd8 Merge branch 'dev' into release 2024-12-02 18:10:00 +08:00
Xu Chang 40df79637c Merge branch 'dev' into release 2024-11-27 19:13:58 +08:00
Xu Chang d924f889eb Merge branch 'dev' into release 2024-11-19 08:55:07 +08:00
Xu Chang 44a596ba30 Merge branch 'dev' into release 2024-11-05 09:02:18 +08:00
Xu Chang d4092b9ccd Merge branch 'dev' into release 2024-10-21 19:43:09 +08:00
Xu Chang d897e1d723 Merge branch 'dev' into release 2024-10-08 21:59:59 +08:00
Xu Chang 16fc448470 Merge branch 'dev' into release 2024-09-30 12:17:11 +08:00
Xu Chang d510a74180 merge 2024-09-20 19:51:43 +08:00
Xu Chang d548388b6c Merge branch 'dev' into release 2024-09-16 17:59:49 +08:00
Xu Chang 7605071187 merge 2024-09-14 10:45:08 +08:00
2088 changed files with 65773 additions and 11920 deletions

View File

@ -7,62 +7,159 @@ import { MaterialType } from '../types/WeChat';
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
import { WechatPublicEventData, WechatMpEventData } from 'oak-external-sdk';
export type AspectDict<ED extends EntityDict> = {
/**
* 使 token Web
* @param mpToken token
* @param env Web
* @returns Web token
*/
loginWebByMpToken: (params: {
mpToken: string;
env: WebEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param from ID
* @param to ID
*/
mergeUser: (params: {
from: string;
to: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
*/
refreshWechatPublicUserInfo: (params: {}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param code code
* @param env
* @returns
*/
getWechatMpUserPhoneNumber: (params: {
code: string;
env: WechatMpEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param mobile
* @param captcha
* @param env
*/
bindByMobile: (params: {
mobile: string;
captcha: string;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param email
* @param captcha
* @param env
*/
bindByEmail: (params: {
email: string;
captcha: string;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param mobile
* @param captcha
* @param disableRegister true
* @param env
* @returns token
*/
loginByMobile: (params: {
mobile: string;
captcha: string;
disableRegister?: boolean;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param password SHA1
* @param env
*/
verifyPassword: (params: {
password: string;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
* //
* @param account
* @param password SHA1
* @param env
* @returns token
*/
loginByAccount: (params: {
account: string;
password: string;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param email
* @param captcha
* @param disableRegister true
* @param env
* @returns token
*/
loginByEmail: (params: {
email: string;
captcha: string;
disableRegister?: boolean;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param code code
* @param env Web
* @param wechatLoginId ID
* @returns token
*/
loginWechat: ({ code, env, wechatLoginId, }: {
code: string;
env: WebEnv;
wechatLoginId?: string;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
* 使 token
* @param tokenValue token
*/
logout: (params: {
tokenValue: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
loginWechatMp: ({ code, env, }: {
/**
*
* @param code code
* @param env
* @param wechatLoginId ID
* @returns token
*/
loginWechatMp: ({ code, env, wechatLoginId, }: {
code: string;
env: WechatMpEnv;
wechatLoginId?: string;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
* APP
* @param code code
* @param env APP
* @returns token
*/
loginWechatNative: ({ code, env, }: {
code: string;
env: NativeEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param nickname
* @param avatarUrl URL
* @param encryptedData
* @param iv
* @param signature
*/
syncUserInfoWechatMp: ({ nickname, avatarUrl, encryptedData, iv, signature, }: {
nickname: string;
avatarUrl: string;
@ -70,30 +167,74 @@ export type AspectDict<ED extends EntityDict> = {
iv: string;
signature: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
* shadow
* @param id ID
* @param env
* @returns token
*/
wakeupParasite: (params: {
id: string;
env: WebEnv | WechatMpEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
* token
* @param tokenValue token
* @param env
* @param applicationId ID
* @returns token
*/
refreshToken: (params: {
tokenValue: string;
env: WebEnv | WechatMpEnv | NativeEnv;
applicationId: string;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param mobile
* @param env
* @param type login-changePassword-confirm-
* @returns ID
*/
sendCaptchaByMobile: (params: {
mobile: string;
env: WechatMpEnv | WebEnv;
type: 'login' | 'changePassword' | 'confirm';
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param email
* @param env
* @param type login-changePassword-confirm-
* @returns ID
*/
sendCaptchaByEmail: (params: {
email: string;
env: WechatMpEnv | WebEnv;
type: 'login' | 'changePassword' | 'confirm';
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param version
* @param type web/wechatMp/wechatPublic/native
* @param domain
* @param data
* @param appId ID
* @returns ID
*/
getApplication: (params: {
version: string;
type: AppType;
domain: string;
data: ED['application']['Projection'];
appId?: string;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
* JS-SDK JS
* @param url URL
* @param env Web
* @returns signaturenoncestrtimestampappId
*/
signatureJsSDK: (params: {
url: string;
env: WebEnv;
@ -103,38 +244,91 @@ export type AspectDict<ED extends EntityDict> = {
timestamp: number;
appId: string;
}>;
/**
*
* @param entity platform system
* @param entityId ID
* @param config
*/
updateConfig: (params: {
entity: 'platform' | 'system';
entityId: string;
config: Config;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param entity platform/system/application
* @param entityId ID
* @param style
*/
updateStyle: (params: {
entity: 'platform' | 'system' | 'application';
entityId: string;
style: Style;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param entity application
* @param entityId ID
* @param config
*/
updateApplicationConfig: (params: {
entity: 'application';
entityId: string;
config: EntityDict['application']['Schema']['config'];
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param userId ID
*/
switchTo: (params: {
userId: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param wechatQrCodeId ID
* @returns Base64
*/
getMpUnlimitWxaCode: (wechatQrCodeId: string, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param type login-bind-
* @param interval
* @param router
* @returns ID
*/
createWechatLogin: (params: {
type: EntityDict['wechatLogin']['Schema']['type'];
interval: number;
router: EntityDict['wechatLogin']['Schema']['router'];
qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType'];
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param wechatUserId ID
* @param captcha
* @param mobile
*/
unbindingWechat: (params: {
wechatUserId: string;
captcha?: string;
mobile?: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
* ID Web
* @param wechatLoginId ID
* @param env Web
* @returns token
*/
loginByWechat: (params: {
wechatLoginId: string;
env: WebEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
* URL
* @param url URL
* @returns
*/
getInfoByUrl: (params: {
url: string;
}) => Promise<{
@ -142,9 +336,23 @@ export type AspectDict<ED extends EntityDict> = {
publishDate: number | undefined;
imageList: string[];
}>;
/**
*
* @param userId ID
* @returns mobile-password-
*/
getChangePasswordChannels: (params: {
userId: string;
}, context: BackendRuntimeContext<ED>) => Promise<string[]>;
/**
*
* @param userId ID
* @param prevPassword 使
* @param mobile 使
* @param captcha 使
* @param newPassword
* @returns
*/
updateUserPassword: (params: {
userId: string;
prevPassword?: string;
@ -155,136 +363,427 @@ export type AspectDict<ED extends EntityDict> = {
result: string;
times?: number;
}>;
/**
*
* @param data
* @param type
* @param entity
* @param entityId ID
* @returns ID
*/
createSession: (params: {
data?: WechatPublicEventData | WechatMpEventData;
type: AppType;
entity?: string;
entityId?: string;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
*
* @param params ID
* @returns mediaId
*/
uploadWechatMedia: (params: any, context: BackendRuntimeContext<ED>) => Promise<{
mediaId: string;
}>;
/**
* 使
* @param applicationId ID
* @returns
*/
getCurrentMenu: (params: {
applicationId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @returns
*/
getMenu: (params: {
applicationId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param menuConfig
* @param id ID
*/
createMenu: (params: {
applicationId: string;
menuConfig: any;
id: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param menuConfig
* @param id ID
*/
createConditionalMenu: (params: {
applicationId: string;
menuConfig: any;
id: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param menuId ID
*/
deleteConditionalMenu: (params: {
applicationId: string;
menuId: number;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
*/
deleteMenu: (params: {
applicationId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param offset
* @param count
* @param noContent 0-1-
* @returns
*/
batchGetArticle: (params: {
applicationId: string;
offset?: number;
count: number;
noContent?: 0 | 1;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param articleId ID
* @returns
*/
getArticle: (params: {
applicationId: string;
articleId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param type image-voice-video-news-
* @param offset
* @param count
* @returns
*/
batchGetMaterialList: (params: {
applicationId: string;
type: MaterialType;
offset?: number;
count: number;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param mediaId ID
* @param isPermanent
* @returns
*/
getMaterial: (params: {
applicationId: string;
mediaId: string;
isPermanent?: boolean;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param mediaId ID
*/
deleteMaterial: (params: {
applicationId: string;
mediaId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param name
* @returns
*/
createTag: (params: {
applicationId: string;
name: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @returns
*/
getTags: (params: {
applicationId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param id ID
* @param name
*/
editTag: (params: {
applicationId: string;
id: number;
name: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param id ID
* @param wechatId ID
*/
deleteTag: (params: {
applicationId: string;
id: string;
wechatId: number;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
syncMessageTemplate: (params: {
/**
*
* @param applicationId ID
* @returns
*/
syncWechatTemplate: (params: {
applicationId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @returns
*/
getMessageType: (params: {}, content: BackendRuntimeContext<ED>) => Promise<string[]>;
/**
*
* @param applicationId ID
* @param id ID
*/
syncTag: (params: {
applicationId: string;
id: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
*/
oneKeySync: (params: {
applicationId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param tagId ID
* @returns
*/
getTagUsers: (params: {
applicationId: string;
tagId: number;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param openIdList openId
* @param tagId ID
*/
batchtagging: (params: {
applicationId: string;
openIdList: string[];
tagId: number;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param openIdList openId
* @param tagId ID
*/
batchuntagging: (params: {
applicationId: string;
openIdList: string[];
tagId: number;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param openId openId
* @returns ID
*/
getUserTags: (params: {
applicationId: string;
openId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param nextOpenId openId
* @returns
*/
getUsers: (params: {
applicationId: string;
nextOpenId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param openId openId
* @param tagIdList ID
*/
tagging: (params: {
applicationId: string;
openId: string;
tagIdList: number[];
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param openId openId
*/
syncToLocale: (params: {
applicationId: string;
openId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param applicationId ID
* @param id ID
* @param openId openId
*/
syncToWechat: (params: {
applicationId: string;
id: string;
openId: string;
}, context: BackendRuntimeContext<ED>) => Promise<any>;
/**
*
* @param systemId ID
* @param origin
*/
syncSmsTemplate: (params: {
systemId: string;
origin: EntityDict['smsTemplate']['Schema']['origin'];
pageIndex?: number;
pageSize?: number;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param applicationId ID
* @returns
*/
getApplicationPassports: (params: {
applicationId: string;
}, context: BackendRuntimeContext<ED>) => Promise<EntityDict['applicationPassport']['Schema'][]>;
/**
* ID
* @param passportIds ID
*/
removeApplicationPassportsByPIds: (params: {
passportIds: string[];
}, content: BackendRuntimeContext<ED>) => Promise<void>;
/**
* OAuth 2.0
* @param code OAuth
* @param state
* @param env
* @returns token
*/
loginByOauth: (params: {
code: string;
state: string;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
* OAuth /
* @param providerId OAuth ID
* @param userId ID
* @param type bind-login-
* @returns
*/
createOAuthState: (params: {
providerId: string;
userId?: string;
type: "bind" | "login";
}, context: BackendRuntimeContext<ED>) => Promise<string>;
/**
* OAuth 2.0
* @param response_type "code"
* @param client_id ID
* @param redirect_uri
* @param scope
* @param state
* @param action grant-deny-
* @returns URL
*/
authorize: (params: {
response_type: string;
client_id: string;
redirect_uri: string;
scope: string;
state: string;
action: "grant" | "deny";
code_challenge?: string;
code_challenge_method?: 'plain' | 'S256';
}, context: BackendRuntimeContext<ED>) => Promise<{
redirectUri: string;
}>;
/**
* OAuth
* @param client_id ID
* @returns null
*/
getOAuthClientInfo: (params: {
client_id: string;
currentUserId?: string;
}, context: BackendRuntimeContext<ED>) => Promise<{
data: EntityDict['oauthApplication']['Schema'] | null;
alreadyAuth: boolean;
}>;
/**
*
* @param avatar url
* @returns
*/
setUserAvatarFromWechat: (params: {
avatar: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param extraFileId extraFile的id
*/
mergeChunkedUpload: (params: {
extraFileId: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
/**
*
* @param params
*/
presignFile: (params: {
extraDileId: string;
method?: 'GET' | 'PUT' | 'POST' | 'DELETE';
}, context: BackendRuntimeContext<ED>) => Promise<{
url: string;
headers?: Record<string, string | string[]>;
formdata?: Record<string, any>;
}>;
/**
*
* @param params ,
*/
presignMultiPartUpload: (params: {
extraFileId: string;
from: number;
to: number;
}, context: BackendRuntimeContext<ED>) => Promise<{
partNumber: number;
uploadUrl: string;
formData: Record<string, any>;
}[]>;
/**
*
* @param loginName
* @param password
* @param context
* @returns
*/
registerUserByLoginName: (params: {
loginName: string;
password: string;
}, context: BackendRuntimeContext<ED>) => Promise<void>;
};
export default AspectDict;

View File

@ -5,6 +5,7 @@ import { WebEnv } from 'oak-domain/lib/types/Environment';
import { File } from 'formidable';
import { BRC } from '../types/RuntimeCxt';
export declare function getApplication<ED extends EntityDict>(params: {
version: string;
type: AppType;
domain: string;
data: ED['application']['Projection'];

View File

@ -2,8 +2,10 @@ import { assert } from 'oak-domain/lib/utils/assert';
import { applicationProjection } from '../types/Projection';
import WechatSDK from 'oak-external-sdk/lib/WechatSDK';
import fs from 'fs';
import { cloneDeep } from 'oak-domain/lib/utils/lodash';
import { cloneDeep, unset } from 'oak-domain/lib/utils/lodash';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { compareVersion } from 'oak-domain/lib/utils/version';
import { OakApplicationHasToUpgrade } from 'oak-domain/lib/types/Exception';
async function getApplicationByDomain(context, options) {
const { data, type, domain } = options;
let applications = await context.select('application', {
@ -39,8 +41,26 @@ async function getApplicationByDomain(context, options) {
}
return applications;
}
function checkAppVersionSafe(application, version) {
const { dangerousVersions, warningVersions, system } = application;
const { oldestVersion, platform } = system;
const { oldestVersion: pfOldestVersion } = platform || {};
const oldest = pfOldestVersion || oldestVersion;
if (oldest) {
if (compareVersion(version, oldest) < 0) {
throw new OakApplicationHasToUpgrade();
}
}
if (dangerousVersions && dangerousVersions.includes(version)) {
throw new OakApplicationHasToUpgrade();
}
unset(application, 'dangerousVersions');
if (warningVersions) {
application.warningVersions = warningVersions.filter(ele => ele === version);
}
}
export async function getApplication(params, context) {
const { type, domain, data, appId } = params;
const { type, domain, data, appId, version } = params;
// 先找指定domain的应用如果不存在再找系统下面的domain 但无论怎么样都必须一项
const applications = await getApplicationByDomain(context, {
type,
@ -51,11 +71,13 @@ export async function getApplication(params, context) {
case 'wechatMp': {
assert(applications.length === 1, `微信小程序环境下,同一个系统必须存在唯一的【${type}】应用,域名「${domain}`);
const application = applications[0];
checkAppVersionSafe(application, version);
return application.id;
}
case 'native': {
assert(applications.length === 1, `APP环境下同一个系统必须存在唯一的【${type}】应用,域名「${domain}`);
const application = applications[0];
checkAppVersionSafe(application, version);
return application.id;
}
case 'wechatPublic': {
@ -68,15 +90,18 @@ export async function getApplication(params, context) {
});
assert(webApplications.length === 1, `微信公众号环境下, 可以未配置公众号但必须存在web应用域名「${domain}`);
const application = webApplications[0];
checkAppVersionSafe(application, version);
return application.id;
}
assert(applications.length === 1, `微信公众号环境下,同一个系统必须存在唯一的【${type}】应用 或 多个${type}应用必须配置域名,域名「${domain}`);
const application = applications[0];
checkAppVersionSafe(application, version);
return application.id;
}
case 'web': {
assert(applications.length === 1, `web环境下同一个系统必须存在唯一的【${type}】应用 或 多个${type}应用必须配置域名,域名「${domain}`);
const application = applications[0];
checkAppVersionSafe(application, version);
return application.id;
}
default: {

View File

@ -12,9 +12,15 @@ export async function getApplicationPassports(params, context) {
config: 1,
},
isDefault: 1,
allowPwd: 1,
},
filter: {
applicationId,
passport: {
type: {
$ne: 'password',
}
}
}
}, {});
closeRoot();

View File

@ -16,3 +16,26 @@ export declare function uploadExtraFile<ED extends EntityDict>(params: {
context: BRC<ED>): Promise<{
success: boolean;
}>;
/**
*
*/
export declare function mergeChunkedUpload<ED extends EntityDict>(params: {
extraFileId: string;
}, context: BRC<ED>): Promise<void>;
export declare function presignFile<ED extends EntityDict>(params: {
extraFileId: string;
method?: 'GET' | 'PUT' | 'POST' | 'DELETE';
}, context: BRC<ED>): Promise<{
url: string;
headers?: Record<string, string | string[]>;
formdata?: Record<string, any>;
}>;
export declare function presignMultiPartUpload<ED extends EntityDict>(params: {
extraFileId: string;
from: number;
to: number;
}, context: BRC<ED>): Promise<{
partNumber: number;
uploadUrl: string;
formData?: Record<string, any>;
}[]>;

View File

@ -3,7 +3,8 @@ import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import fs from 'fs';
import { assert } from 'oak-domain/lib/utils/assert';
import { cloneDeep } from 'oak-domain/lib/utils/lodash';
import { applicationProjection } from '../types/Projection';
import { applicationProjection, extraFileProjection } from '../types/Projection';
import { getCosBackend } from '../utils/cos/index.backend';
// 请求链接获取标题,发布时间,图片等信息
export async function getInfoByUrl(params) {
const { url } = params;
@ -49,3 +50,99 @@ context) {
success: true,
};
}
/**
* 合并分片上传的文件
*/
export async function mergeChunkedUpload(params, context) {
const { extraFileId } = params;
assert(extraFileId, 'extraFileId不能为空');
const [extrafile] = await context.select('extraFile', {
data: {
...extraFileProjection,
application: {
...applicationProjection,
},
enableChunkedUpload: 1,
chunkInfo: 1,
},
filter: {
id: extraFileId,
}
}, { dontCollect: true });
assert(extrafile, `找不到id为${extraFileId}的extraFile记录`);
assert(extrafile.enableChunkedUpload, `extraFile ${extraFileId} 未启用分片上传功能`);
assert(extrafile.chunkInfo, `extraFile ${extraFileId} 的chunkInfo信息缺失`);
assert(!extrafile.chunkInfo.merged, `extraFile ${extraFileId} 已经合并过分片,无需重复合并`);
// 必须保证所有分片都有上传完成
const cos = getCosBackend(extrafile.origin);
const { parts } = await cos.listMultipartUploads(extrafile.application, extrafile, context);
const allPartsDone = parts.every(part => part.etag && part.size > 0);
assert(allPartsDone, `extraFile ${extraFileId} 存在未上传完成的分片,无法合并`);
await cos.mergeChunkedUpload(extrafile.application, extrafile, parts.map(part => ({
partNumber: part.partNumber,
etag: part.etag,
})), context);
// 更新chunkInfo状态
const closeRootMode = context.openRootMode();
try {
await context.operate('extraFile', {
id: await generateNewIdAsync(),
action: 'update',
data: {
chunkInfo: {
...extrafile.chunkInfo,
merged: true,
parts: parts.map(part => part.etag),
},
},
filter: {
id: extraFileId,
},
}, {});
closeRootMode();
}
catch (err) {
closeRootMode();
throw err;
}
}
export async function presignFile(params, context) {
const { extraFileId, method = 'GET' } = params;
assert(extraFileId, 'extraFileId不能为空');
const [extrafile] = await context.select('extraFile', {
data: {
...extraFileProjection,
application: {
...applicationProjection,
},
},
filter: {
id: extraFileId,
}
}, { dontCollect: true });
assert(extrafile, `找不到id为${extraFileId}的extraFile记录`);
const cos = getCosBackend(extrafile.origin);
return await cos.presignFile(method, extrafile.application, extrafile, context);
}
export async function presignMultiPartUpload(params, context) {
const { extraFileId, from, to } = params;
assert(extraFileId, 'extraFileId不能为空');
assert(from >= 1, 'from必须大于等于1');
assert(to >= from, 'to必须大于等于from');
const [extrafile] = await context.select('extraFile', {
data: {
...extraFileProjection,
application: {
...applicationProjection,
},
chunkInfo: 1,
enableChunkedUpload: 1,
},
filter: {
id: extraFileId,
}
}, { dontCollect: true });
assert(extrafile, `找不到id为${extraFileId}的extraFile记录`);
const cos = getCosBackend(extrafile.origin);
return cos.presignMultiPartUpload(extrafile.application, extrafile, from, to, context);
}

21
es/aspects/index.d.ts vendored
View File

@ -1,10 +1,10 @@
import { bindByEmail, bindByMobile, loginByAccount, loginByEmail, loginByMobile, loginWechat, loginWechatMp, loginWechatNative, syncUserInfoWechatMp, sendCaptchaByMobile, sendCaptchaByEmail, switchTo, refreshWechatPublicUserInfo, getWechatMpUserPhoneNumber, logout, loginByWechat, wakeupParasite, refreshToken, verifyPassword } from './token';
import { getInfoByUrl } from './extraFile';
import { bindByEmail, bindByMobile, loginByAccount, loginByEmail, loginByMobile, loginWechat, loginWechatMp, loginWechatNative, syncUserInfoWechatMp, sendCaptchaByMobile, sendCaptchaByEmail, switchTo, refreshWechatPublicUserInfo, getWechatMpUserPhoneNumber, logout, loginByWechat, wakeupParasite, refreshToken, verifyPassword, loginWebByMpToken, setUserAvatarFromWechat } from './token';
import { getInfoByUrl, mergeChunkedUpload, presignFile, presignMultiPartUpload } from './extraFile';
import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, getArticle, batchGetMaterialList, getMaterial, deleteMaterial } from './application';
import { updateConfig, updateApplicationConfig, updateStyle } from './config';
import { syncMessageTemplate, getMessageType } from './template';
import { syncWechatTemplate, getMessageType } from './template';
import { syncSmsTemplate } from './sms';
import { mergeUser, getChangePasswordChannels, updateUserPassword } from './user';
import { mergeUser, getChangePasswordChannels, updateUserPassword, registerUserByLoginName } from './user';
import { createWechatLogin } from './wechatLogin';
import { unbindingWechat } from './wechatUser';
import { getMpUnlimitWxaCode } from './wechatQrCode';
@ -14,6 +14,7 @@ import { createTag, getTags, editTag, deleteTag, syncTag, oneKeySync } from './w
import { getTagUsers, batchtagging, batchuntagging, getUserTags, getUsers, tagging, syncToLocale, syncToWechat } from './userWechatPublicTag';
import { wechatMpJump } from './wechatMpJump';
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
import { authorize, createOAuthState, getOAuthClientInfo, loginByOauth } from './oauth';
declare const aspectDict: {
bindByEmail: typeof bindByEmail;
bindByMobile: typeof bindByMobile;
@ -62,7 +63,7 @@ declare const aspectDict: {
getTags: typeof getTags;
editTag: typeof editTag;
deleteTag: typeof deleteTag;
syncMessageTemplate: typeof syncMessageTemplate;
syncWechatTemplate: typeof syncWechatTemplate;
getMessageType: typeof getMessageType;
syncTag: typeof syncTag;
oneKeySync: typeof oneKeySync;
@ -79,6 +80,16 @@ declare const aspectDict: {
getApplicationPassports: typeof getApplicationPassports;
removeApplicationPassportsByPIds: typeof removeApplicationPassportsByPIds;
verifyPassword: typeof verifyPassword;
loginWebByMpToken: typeof loginWebByMpToken;
loginByOauth: typeof loginByOauth;
getOAuthClientInfo: typeof getOAuthClientInfo;
createOAuthState: typeof createOAuthState;
authorize: typeof authorize;
setUserAvatarFromWechat: typeof setUserAvatarFromWechat;
mergeChunkedUpload: typeof mergeChunkedUpload;
presignFile: typeof presignFile;
presignMultiPartUpload: typeof presignMultiPartUpload;
registerUserByLoginName: typeof registerUserByLoginName;
};
export default aspectDict;
export { AspectDict } from './AspectDict';

View File

@ -1,10 +1,10 @@
import { bindByEmail, bindByMobile, loginByAccount, loginByEmail, loginByMobile, loginWechat, loginWechatMp, loginWechatNative, syncUserInfoWechatMp, sendCaptchaByMobile, sendCaptchaByEmail, switchTo, refreshWechatPublicUserInfo, getWechatMpUserPhoneNumber, logout, loginByWechat, wakeupParasite, refreshToken, verifyPassword, } from './token';
import { getInfoByUrl } from './extraFile';
import { bindByEmail, bindByMobile, loginByAccount, loginByEmail, loginByMobile, loginWechat, loginWechatMp, loginWechatNative, syncUserInfoWechatMp, sendCaptchaByMobile, sendCaptchaByEmail, switchTo, refreshWechatPublicUserInfo, getWechatMpUserPhoneNumber, logout, loginByWechat, wakeupParasite, refreshToken, verifyPassword, loginWebByMpToken, setUserAvatarFromWechat, } from './token';
import { getInfoByUrl, mergeChunkedUpload, presignFile, presignMultiPartUpload } from './extraFile';
import { getApplication, signatureJsSDK, uploadWechatMedia, batchGetArticle, getArticle, batchGetMaterialList, getMaterial, deleteMaterial, } from './application';
import { updateConfig, updateApplicationConfig, updateStyle } from './config';
import { syncMessageTemplate, getMessageType } from './template';
import { syncWechatTemplate, getMessageType } from './template';
import { syncSmsTemplate } from './sms';
import { mergeUser, getChangePasswordChannels, updateUserPassword } from './user';
import { mergeUser, getChangePasswordChannels, updateUserPassword, registerUserByLoginName } from './user';
import { createWechatLogin } from './wechatLogin';
import { unbindingWechat } from './wechatUser';
import { getMpUnlimitWxaCode } from './wechatQrCode';
@ -14,6 +14,7 @@ import { createTag, getTags, editTag, deleteTag, syncTag, oneKeySync, } from './
import { getTagUsers, batchtagging, batchuntagging, getUserTags, getUsers, tagging, syncToLocale, syncToWechat, } from './userWechatPublicTag';
import { wechatMpJump, } from './wechatMpJump';
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
import { authorize, createOAuthState, getOAuthClientInfo, loginByOauth } from './oauth';
const aspectDict = {
bindByEmail,
bindByMobile,
@ -62,7 +63,7 @@ const aspectDict = {
getTags,
editTag,
deleteTag,
syncMessageTemplate,
syncWechatTemplate,
getMessageType,
syncTag,
oneKeySync,
@ -79,5 +80,17 @@ const aspectDict = {
getApplicationPassports,
removeApplicationPassportsByPIds,
verifyPassword,
loginWebByMpToken,
// oauth
loginByOauth,
getOAuthClientInfo,
createOAuthState,
authorize,
setUserAvatarFromWechat,
// extraFile新增
mergeChunkedUpload,
presignFile,
presignMultiPartUpload,
registerUserByLoginName,
};
export default aspectDict;

32
es/aspects/oauth.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
import { BRC } from "../types/RuntimeCxt";
import { EntityDict } from "../oak-app-domain";
import { NativeEnv, WebEnv, WechatMpEnv } from "oak-domain/lib/types";
export declare function loginByOauth<ED extends EntityDict>(params: {
code: string;
state: string;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BRC<ED>): Promise<string>;
export declare function getOAuthClientInfo<ED extends EntityDict>(params: {
client_id: string;
currentUserId?: string;
}, context: BRC<ED>): Promise<{
data: Partial<ED["oauthApplication"]["Schema"]>;
alreadyAuth: boolean;
}>;
export declare function createOAuthState<ED extends EntityDict>(params: {
providerId: string;
userId?: string;
type: 'login' | 'bind';
}, context: BRC<ED>): Promise<string>;
export declare function authorize<ED extends EntityDict>(params: {
response_type: string;
client_id: string;
redirect_uri: string;
scope?: string;
state?: string;
action: 'grant' | 'deny';
code_challenge?: string;
code_challenge_method?: 'plain' | 'S256';
}, context: BRC<ED>): Promise<{
redirectUri: string;
}>;

449
es/aspects/oauth.js Normal file
View File

@ -0,0 +1,449 @@
import assert from "assert";
import { OakUserException } from "oak-domain/lib/types";
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
import { loadTokenInfo, setUpTokenAndUser } from "./token";
import { randomUUID } from "crypto";
import { processUserInfo } from "../utils/oauth";
export async function loginByOauth(params, context) {
const { code, state: stateCode, env } = params;
const closeRootMode = context.openRootMode();
const currentUserId = context.getCurrentUserId(true);
const applicationId = context.getApplicationId();
const islogginedIn = !!currentUserId;
assert(applicationId, '无法获取当前应用ID');
assert(code, 'code 参数缺失');
assert(stateCode, 'state 参数缺失');
// 验证 state 并获取 OAuth 配置
const [state] = await context.select("oauthState", {
data: {
providerId: 1,
provider: {
type: 1,
clientId: 1,
redirectUri: 1,
clientSecret: 1,
tokenEndpoint: 1,
userInfoEndpoint: 1,
ableState: 1,
autoRegister: 1,
},
usedAt: 1,
},
filter: {
state: stateCode,
},
}, { dontCollect: true, forUpdate: true }); // 这里直接加锁,防止其他人抢了
const systemId = context.getSystemId();
const [applicationPassport] = await context.select('applicationPassport', {
data: {
id: 1,
applicationId: 1,
passportId: 1,
passport: {
id: 1,
type: 1,
systemId: 1,
config: 1,
},
},
filter: {
passport: {
systemId,
type: 'oauth',
},
applicationId,
}
}, { dontCollect: true });
const allowOauth = !!(state.providerId && applicationPassport?.passport?.config?.oauthIds && applicationPassport?.passport?.config)?.oauthIds.includes(state.providerId);
if (!allowOauth) {
throw new OakUserException('error::user.loginWayDisabled');
}
assert(state, '无效的 state 参数');
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
// 如果已经使用
if (state.usedAt) {
throw new OakUserException('该授权请求已被使用,请重新发起授权请求');
}
// 更新为使用过
await context.operate("oauthState", {
id: await generateNewIdAsync(),
action: 'update',
data: {
usedAt: Date.now(),
},
filter: {
id: state.id,
}
}, { dontCollect: true });
// 使用 code 换取 access_token 并获取用户信息
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(code, state.provider);
const [existingOAuthUser] = await context.select("oauthUser", {
data: {
id: 1,
userId: 1,
providerUserId: 1,
user: {
id: 1,
userState: 1,
refId: 1,
ref: {
id: 1,
userState: 1,
}
}
},
filter: {
providerUserId: oauthUserInfo.providerUserId,
providerConfigId: state.providerId,
}
}, { dontCollect: true, forUpdate: true }); // 加锁,防止并发绑定
// 已登录的情况
if (islogginedIn) {
// 检查当前用户是否已绑定此提供商
const [currentUserBinding] = await context.select("oauthUser", {
data: {
id: 1,
},
filter: {
userId: currentUserId,
providerConfigId: state.providerId,
}
}, {});
if (currentUserBinding) {
throw new OakUserException('当前用户已绑定该 OAuth 平台账号');
}
if (existingOAuthUser) {
throw new OakUserException('该 OAuth 账号已被其他用户绑定');
}
console.log("绑定 OAuth 账号到当前用户:", currentUserId, oauthUserInfo.providerUserId);
// 创建绑定关系
await context.operate("oauthUser", {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
userId: currentUserId,
providerConfigId: state.providerId,
providerUserId: oauthUserInfo.providerUserId,
rawUserInfo: oauthUserInfo.rawData,
accessToken,
accessExpiresAt: accessTokenExp,
refreshToken,
refreshExpiresAt: refreshTokenExp,
applicationId,
stateId: state.id,
}
}, { dontCollect: true });
// 返回当前 token
const tokenValue = context.getTokenValue();
await loadTokenInfo(tokenValue, context);
closeRootMode();
return tokenValue;
// 未登录OAuth账号已存在直接登录
}
else if (existingOAuthUser) {
console.log("使用已绑定的 OAuth 账号登录:", existingOAuthUser.id);
const { user } = existingOAuthUser;
const targetUser = user?.userState === 'merged' ? user.ref : user;
const tokenValue = await setUpTokenAndUser(env, context, 'oauthUser', existingOAuthUser.id, // 使用已存在的 oauthUser ID
undefined, targetUser // 关联的用户
);
// 更新登录信息
await context.operate("oauthUser", {
id: await generateNewIdAsync(),
action: 'update',
data: {
rawUserInfo: oauthUserInfo.rawData,
accessToken,
accessExpiresAt: accessTokenExp,
refreshToken,
refreshExpiresAt: refreshTokenExp,
applicationId,
stateId: state.id,
},
filter: {
id: existingOAuthUser.id,
}
}, { dontCollect: true });
await loadTokenInfo(tokenValue, context);
closeRootMode();
return tokenValue;
}
// 未登录OAuth账号不存在创建新用户
else {
if (!state.provider.autoRegister) {
throw new OakUserException('您还没有账号,请先注册一个账号');
}
console.log("使用未绑定的 OAuth 账号登录:", oauthUserInfo.providerUserId);
const newUserId = await generateNewIdAsync();
const oauthUserCreateData = {
id: newUserId,
providerConfigId: state.providerId,
providerUserId: oauthUserInfo.providerUserId,
rawUserInfo: oauthUserInfo.rawData,
accessToken,
accessExpiresAt: accessTokenExp,
refreshToken,
refreshExpiresAt: refreshTokenExp,
applicationId,
stateId: state.id,
loadState: 'unload'
};
// 不传 user 参数,会自动创建新用户
const tokenValue = await setUpTokenAndUser(env, context, 'oauthUser', undefined, oauthUserCreateData, // 创建新的 oauthUser
undefined // 不传 user自动创建新用户
);
await context.operate("oauthUser", {
id: await generateNewIdAsync(),
action: 'loadUserInfo',
data: {},
filter: {
id: newUserId,
}
}, { dontCollect: true });
await loadTokenInfo(tokenValue, context);
closeRootMode();
return tokenValue;
}
}
export async function getOAuthClientInfo(params, context) {
const { client_id, currentUserId } = params;
const closeRootMode = context.openRootMode();
const systemId = context.getSystemId();
const applicationId = context.getApplicationId();
const [oauthApp] = await context.select("oauthApplication", {
data: {
id: 1,
name: 1,
redirectUris: 1,
description: 1,
logo: 1,
isConfidential: 1,
},
filter: {
id: client_id,
systemId: systemId,
ableState: "enabled",
}
}, { dontCollect: true });
// 如果还有正在生效的授权,说明已经授权过了
const [hasAuth] = await context.select("oauthUserAuthorization", {
data: {
id: 1,
userId: 1,
applicationId: 1,
usageState: 1,
authorizedAt: 1,
},
filter: {
// 如果 已经授权过token并且 没有被撤销
tokenId: {
$exists: true
},
token: {
revokedAt: {
$exists: false
}
},
usageState: 'granted',
code: {
// 当前应用下的认证客户端
oauthApp: {
id: client_id
},
applicationId: applicationId,
userId: currentUserId,
}
}
}, { dontCollect: true });
if (hasAuth) {
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
}
if (!oauthApp) {
throw new OakUserException('未经授权的客户端应用');
}
closeRootMode();
return {
data: oauthApp,
alreadyAuth: !!hasAuth,
};
}
export async function createOAuthState(params, context) {
const { providerId, userId, type } = params;
const closeRootMode = context.openRootMode();
const generateCode = () => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
};
const state = generateCode();
await context.operate("oauthState", {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
providerId,
userId,
type,
state
}
}, { dontCollect: true });
closeRootMode();
return state;
}
export async function authorize(params, context) {
const { response_type, client_id, redirect_uri, scope, state, action, code_challenge, code_challenge_method } = params;
if (response_type !== 'code') {
throw new OakUserException('不支持的 response_type 类型');
}
const closeRootMode = context.openRootMode();
const systemId = context.getSystemId();
const [oauthApp] = await context.select("oauthApplication", {
data: {
id: 1,
redirectUris: 1,
isConfidential: 1,
},
filter: {
id: client_id,
systemId: systemId,
}
}, { dontCollect: true });
if (!oauthApp) {
throw new OakUserException('未经授权的客户端应用');
}
// 创建授权记录
const recordId = await generateNewIdAsync();
await context.operate("oauthUserAuthorization", {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: recordId,
userId: context.getCurrentUserId(),
applicationId: oauthApp.id,
usageState: action === 'grant' ? 'unused' : 'denied',
authorizedAt: Date.now(),
}
}, { dontCollect: true });
if (action === 'deny') {
const params = new URLSearchParams();
params.set('error', 'access_denied');
params.set('error_description', '用户拒绝了授权请求');
if (state) {
params.set('state', state);
}
closeRootMode();
return {
redirectUri: `${redirect_uri}?${params.toString()}`,
};
}
if (action === 'grant') {
// 检查redirectUri 是否在注册的列表中
if (!oauthApp.redirectUris?.includes(redirect_uri)) {
console.log('不合法的重定向 URI:', redirect_uri, oauthApp.redirectUris);
throw new OakUserException('重定向 URI 不合法');
}
const code = randomUUID();
const codeId = await generateNewIdAsync();
// 存储授权码
await context.operate("oauthAuthorizationCode", {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: codeId,
code,
redirectUri: redirect_uri,
oauthAppId: oauthApp.id,
applicationId: context.getApplicationId(),
userId: context.getCurrentUserId(),
scope: scope === undefined ? [] : [scope],
expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
// PKCE 支持
codeChallenge: code_challenge,
codeChallengeMethod: code_challenge_method || 'plain',
}
}, { dontCollect: true });
// 更新记录
await context.operate("oauthUserAuthorization", {
id: await generateNewIdAsync(),
action: 'update',
data: {
codeId: codeId,
},
filter: {
id: recordId,
}
}, {});
const params = new URLSearchParams();
params.set('code', code);
if (state) {
params.set('state', state);
}
closeRootMode();
return {
redirectUri: `${redirect_uri}?${params.toString()}`,
};
}
closeRootMode();
throw new Error('unknown action');
}
const fetchOAuthUserInfo = async (code, providerConfig) => {
// 1. 使用 code 换取 access_token
const tokenResponse = await fetch(providerConfig.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: providerConfig.clientId,
client_secret: providerConfig.clientSecret,
redirect_uri: providerConfig.redirectUri,
}),
});
if (!tokenResponse.ok) {
const errorjson = await tokenResponse.json();
if (errorjson.error == "unauthorized_client") {
throw new OakUserException(`授权校验已过期,请重新发起授权请求`);
}
else if (errorjson.error == "invalid_grant") {
throw new OakUserException(`授权码无效或已过期,请重新发起授权请求`);
}
else if (errorjson.error) {
throw new OakUserException(`获取访问令牌失败: ${errorjson.error_description || errorjson.error}`);
}
throw new OakUserException(`获取访问令牌失败: ${tokenResponse.statusText}`);
}
const tokenData = await tokenResponse.json();
const accessToken = tokenData.access_token;
const refreshToken = tokenData.refresh_token;
const accessTokenExp = tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : undefined;
const refreshTokenExp = tokenData.refresh_expires_in ? Date.now() + tokenData.refresh_expires_in * 1000 : undefined;
const tokenType = tokenData.token_type;
assert(tokenType && tokenType.toLowerCase() === 'bearer', '不支持的令牌类型');
// 2. 使用 access_token 获取用户信息
const userInfoResponse = await fetch(providerConfig.userInfoEndpoint, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!userInfoResponse.ok) {
throw new OakUserException(`获取用户信息失败: ${userInfoResponse.statusText}`);
}
const userInfoData = await userInfoResponse.json();
// TODO: 用户信息中获取唯一标识,通过注入解决: utils/oauth/index.ts
const { id: providerUserId } = await processUserInfo(providerConfig.type, userInfoData);
if (!providerUserId) {
throw new OakUserException('用户信息中缺少唯一标识符');
}
return {
oauthUserInfo: {
providerUserId,
rawData: userInfoData,
},
accessToken,
refreshToken,
accessTokenExp,
refreshTokenExp,
};
};

View File

@ -85,6 +85,7 @@ export async function createSession(params, context) {
aaoe: false,
extra: data,
userId,
id: await generateNewIdAsync(),
};
if (MsgType === 'text') {
Object.assign(sessionMessage, {
@ -103,7 +104,7 @@ export async function createSession(params, context) {
origin: 'wechat',
type: 'image',
tag1: 'image',
objectId: await generateNewIdAsync(),
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,
@ -128,7 +129,7 @@ export async function createSession(params, context) {
origin: 'wechat',
type: 'video',
tag1: 'video',
objectId: await generateNewIdAsync(),
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,
@ -150,7 +151,7 @@ export async function createSession(params, context) {
origin: 'wechat',
type: 'audio',
tag1: 'audio',
objectId: await generateNewIdAsync(),
objectId: await generateNewIdAsync(), // 这个域用来标识唯一性
sort: 1000,
uploadState: 'success',
extra1: data.MediaId,

2
es/aspects/sms.d.ts vendored
View File

@ -3,4 +3,6 @@ import { BRC } from '../types/RuntimeCxt';
export declare function syncSmsTemplate<ED extends EntityDict>(params: {
origin: EntityDict['smsTemplate']['Schema']['origin'];
systemId: string;
pageIndex?: number;
pageSize?: number;
}, context: BRC<ED>): Promise<void>;

View File

@ -1,9 +1,13 @@
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { getSms } from '../utils/sms/index';
export async function syncSmsTemplate(params, context) {
const { origin, systemId } = params;
const { origin, systemId, pageIndex, pageSize } = params;
const Sms = getSms(origin);
const templateFormalData = await Sms.syncTemplate(systemId, context);
const templateFormalData = await Sms.syncTemplate({
systemId: systemId,
pageIndex,
pageSize
}, context);
const existTemplateList = await context.select('smsTemplate', {
data: {
id: 1,

View File

@ -2,7 +2,7 @@ import { EntityDict } from '../oak-app-domain';
import { BRC } from '../types/RuntimeCxt';
export declare function registerMessageType(messageType: string[]): void;
export declare function getMessageType(): Promise<string[]>;
export declare function syncMessageTemplate<ED extends EntityDict>(params: {
export declare function syncWechatTemplate<ED extends EntityDict>(params: {
applicationId: string;
}, context: BRC<ED>): Promise<{
wechatId: string;
@ -19,6 +19,6 @@ export declare function syncMessageTemplate<ED extends EntityDict>(params: {
example: string;
keywordEnumValueList: {
keywordCode: string;
enumValueList: string[];
enumValueList: Array<string>;
}[];
}[]>;

View File

@ -33,7 +33,7 @@ function analyseContent(content) {
}
return result;
}
export async function syncMessageTemplate(params, context) {
export async function syncWechatTemplate(params, context) {
const applicationId = params?.applicationId;
const [application] = await context.select('application', {
data: {

31
es/aspects/token.d.ts vendored
View File

@ -1,9 +1,20 @@
import { EntityDict } from '../oak-app-domain';
import { NativeEnv, WebEnv, WechatMpEnv } from 'oak-domain/lib/types/Environment';
import { BRC } from '../types/RuntimeCxt';
/**
* user的不同情况
* @param env
* @param context
* @param user
* @return tokenValue
*/
export declare function setUpTokenAndUser<ED extends EntityDict>(env: WebEnv | WechatMpEnv | NativeEnv, context: BRC<ED>, entity: string, // 支持更多的登录渠道使用此函数创建token
entityId?: string, // 如果是现有对象传id如果没有对象传createData
createData?: any, user?: Partial<ED['user']['Schema']>): Promise<string>;
export declare function loadTokenInfo<ED extends EntityDict>(tokenValue: string, context: BRC<ED>): Promise<Partial<ED["token"]["Schema"]>[]>;
export declare function loginByMobile<ED extends EntityDict>(params: {
mobile: string;
captcha: string;
captcha?: string;
disableRegister?: boolean;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BRC<ED>): Promise<string>;
@ -32,6 +43,9 @@ export declare function bindByEmail<ED extends EntityDict>(params: {
captcha: string;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BRC<ED>): Promise<void>;
export declare function setUserAvatarFromWechat<ED extends EntityDict>(params: {
avatar: string;
}, context: BRC<ED>): Promise<void>;
export declare function refreshWechatPublicUserInfo<ED extends EntityDict>({}: {}, context: BRC<ED>): Promise<void>;
export declare function loginByWechat<ED extends EntityDict>(params: {
wechatLoginId: string;
@ -63,9 +77,10 @@ export declare function loginWechat<ED extends EntityDict>({ code, env, wechatLo
* @param context
* @returns
*/
export declare function loginWechatMp<ED extends EntityDict>({ code, env, }: {
export declare function loginWechatMp<ED extends EntityDict>({ code, env, wechatLoginId, }: {
code: string;
env: WechatMpEnv;
wechatLoginId?: string;
}, context: BRC<ED>): Promise<string>;
/**
* wx.getUserProfile拿到的用户信息
@ -112,4 +127,16 @@ export declare function wakeupParasite<ED extends EntityDict>(params: {
export declare function refreshToken<ED extends EntityDict>(params: {
env: WebEnv | WechatMpEnv | NativeEnv;
tokenValue: string;
applicationId: string;
}, context: BRC<ED>): Promise<string>;
/**
* web-view处理token
* @param mpToken
* @param env
* @param context
* @returns
*/
export declare function loginWebByMpToken<ED extends EntityDict>(params: {
mpToken: string;
env: WebEnv;
}, context: BRC<ED>): Promise<string>;

File diff suppressed because it is too large Load Diff

View File

@ -23,3 +23,12 @@ export declare function updateUserPassword<ED extends EntityDict>(params: {
result: string;
times?: undefined;
}>;
/**
*
* @param params
* @param context
*/
export declare function registerUserByLoginName<ED extends EntityDict>(params: {
loginName: string;
password: string;
}, context: BRC<ED>): Promise<void>;

View File

@ -1,4 +1,4 @@
import { OakOperationUnpermittedException } from "oak-domain/lib/types";
import { OakOperationUnpermittedException, OakPreConditionUnsetException } from "oak-domain/lib/types";
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
import { encryptPasswordSha1 } from '../utils/password';
import { assert } from 'oak-domain/lib/utils/assert';
@ -6,7 +6,7 @@ import dayjs from 'dayjs';
export async function mergeUser(params, context, innerLogic) {
const { from, to, mergeMobile, mergeEmail, mergeWechatUser } = params;
if (!innerLogic && !context.isRoot()) {
throw new OakOperationUnpermittedException('user', { id: 'merge', action: 'merge', data: {}, filter: { id: from } }, '不允许执行mergeUser操作');
throw new OakOperationUnpermittedException('user', { id: 'merge', action: 'merge', data: {}, filter: { id: from } }, context.getCurrentUserId(), '不允许执行mergeUser操作');
}
assert(from);
assert(to);
@ -49,6 +49,28 @@ export async function mergeUser(params, context, innerLogic) {
}
}
} */
// 如果from是rootto也得赋上
const [fromUser] = await context.select('user', {
data: {
id: 1,
isRoot: 1,
},
filter: {
id: from,
}
}, { dontCollect: true });
if (fromUser.isRoot) {
await context.operate('user', {
id: await generateNewIdAsync(),
action: 'update',
data: {
isRoot: true,
},
filter: {
id: to,
},
}, {});
}
await context.operate('token', {
id: await generateNewIdAsync(),
action: 'disable',
@ -133,6 +155,7 @@ export async function getChangePasswordChannels(params, context, innerLogic) {
data: {
id: 1,
password: 1,
passwordSha1: 1,
},
filter: {
id: userId,
@ -144,19 +167,33 @@ export async function getChangePasswordChannels(params, context, innerLogic) {
if (mobileList.length > 0) {
result.push('mobile');
}
if (user.password) {
if (user.password || user.passwordSha1) {
result.push('password');
}
return result;
}
export async function updateUserPassword(params, context, innerLogic) {
const { userId, prevPassword, captcha, mobile, newPassword } = params;
const systemId = context.getSystemId();
const closeRootMode = context.openRootMode();
try {
const [system] = await context.select('system', {
data: {
id: 1,
config: 1,
},
filter: {
id: systemId,
}
}, { forUpdate: true });
assert(system);
const config = system.config?.Password;
const mode = config?.mode ?? 'all';
const [user] = await context.select('user', {
data: {
id: 1,
password: 1,
passwordSha1: 1,
},
filter: {
id: userId,
@ -216,14 +253,43 @@ export async function updateUserPassword(params, context, innerLogic) {
times: count1,
};
}
if (user.password === prevPassword) {
const allowUpdate = mode === 'sha1' ? user.passwordSha1 === prevPassword : user.password === prevPassword; //sha1密文模式判断密文是否相等
let userData = {}, changeCreateData = {};
if (mode === 'all') {
userData = {
password: newPassword,
passwordSha1: encryptPasswordSha1(newPassword),
};
changeCreateData = {
prevPassword,
newPassword,
prevPasswordSha1: encryptPasswordSha1(prevPassword),
newPasswordSha1: encryptPasswordSha1(newPassword),
};
}
else if (mode === 'plain') {
userData = {
password: newPassword,
};
changeCreateData = {
prevPassword,
newPassword,
};
}
else if (mode === 'sha1') {
userData = {
passwordSha1: newPassword,
};
changeCreateData = {
prevPasswordSha1: prevPassword,
newPasswordSha1: newPassword,
};
}
if (allowUpdate) {
await context.operate('user', {
id: await generateNewIdAsync(),
action: 'update',
data: {
password: newPassword,
passwordSha1: encryptPasswordSha1(newPassword),
},
data: userData,
filter: {
id: userId,
},
@ -236,9 +302,8 @@ export async function updateUserPassword(params, context, innerLogic) {
data: {
id: await generateNewIdAsync(),
userId,
prevPassword,
newPassword,
result: 'success',
...changeCreateData,
},
}, {
dontCollect: true,
@ -255,9 +320,8 @@ export async function updateUserPassword(params, context, innerLogic) {
data: {
id: await generateNewIdAsync(),
userId,
prevPassword,
newPassword,
result: 'fail',
...changeCreateData,
},
}, {
dontCollect: true,
@ -286,13 +350,41 @@ export async function updateUserPassword(params, context, innerLogic) {
dontCollect: true,
});
if (aliveCaptcha) {
let userData = {}, changeCreateData = {};
if (mode === 'all') {
userData = {
password: newPassword,
passwordSha1: encryptPasswordSha1(newPassword),
};
changeCreateData = {
prevPassword: user.password,
newPassword,
prevPasswordSha1: user.passwordSha1,
newPasswordSha1: encryptPasswordSha1(newPassword),
};
}
else if (mode === 'plain') {
userData = {
password: newPassword,
};
changeCreateData = {
prevPassword: user.password,
newPassword,
};
}
else if (mode === 'sha1') {
userData = {
passwordSha1: newPassword,
};
changeCreateData = {
prevPasswordSha1: user.passwordSha1,
newPasswordSha1: newPassword,
};
}
await context.operate('user', {
id: await generateNewIdAsync(),
action: 'update',
data: {
password: newPassword,
passwordSha1: encryptPasswordSha1(newPassword),
},
data: userData,
filter: {
id: userId,
},
@ -305,9 +397,8 @@ export async function updateUserPassword(params, context, innerLogic) {
data: {
id: await generateNewIdAsync(),
userId,
prevPassword: user.password,
newPassword,
result: 'success',
...changeCreateData,
},
}, {
dontCollect: true,
@ -334,3 +425,83 @@ export async function updateUserPassword(params, context, innerLogic) {
throw err;
}
}
/**
* 用户账号注册
* @param params
* @param context
*/
export async function registerUserByLoginName(params, context) {
const { loginName, password } = params;
const systemId = context.getSystemId();
const closeRootMode = context.openRootMode();
try {
// 检查loginName是否重复
const [existLoginName] = await context.select('loginName', {
data: {
id: 1,
name: 1,
},
filter: {
name: loginName,
ableState: 'enabled',
},
}, { dontCollect: true, forUpdate: true });
if (existLoginName) {
closeRootMode();
throw new OakPreConditionUnsetException('账号已存在,请重新设置');
}
// 创建user并附上密码级联创建loginName
const [system] = await context.select('system', {
data: {
id: 1,
config: 1,
},
filter: {
id: systemId,
}
}, { forUpdate: true });
assert(system);
const config = system.config?.Password;
const mode = config?.mode ?? 'all';
let passwordData = {};
if (mode === 'all') {
passwordData = {
password: password,
passwordSha1: encryptPasswordSha1(password),
};
}
else if (mode === 'plain') {
passwordData = {
password: password,
};
}
else if (mode === 'sha1') {
passwordData = {
passwordSha1: password,
};
}
const userData = {
id: await generateNewIdAsync(),
loginName$user: [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
name: loginName,
}
}
]
};
Object.assign(userData, passwordData);
await context.operate('user', {
id: await generateNewIdAsync(),
action: 'create',
data: userData,
}, {});
}
catch (err) {
closeRootMode();
throw err;
}
}

View File

@ -3,4 +3,6 @@ import { BRC } from '../types/RuntimeCxt';
export declare function createWechatLogin<ED extends EntityDict>(params: {
type: EntityDict['wechatLogin']['Schema']['type'];
interval: number;
router: EntityDict['wechatLogin']['Schema']['router'];
qrCodeType?: EntityDict['wechatLogin']['Schema']['qrCodeType'];
}, context: BRC<ED>): Promise<string>;

View File

@ -1,23 +1,37 @@
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
export async function createWechatLogin(params, context) {
const { type, interval } = params;
const { type, interval, qrCodeType = "wechatPublic", router } = params;
let userId;
if (type === 'bind') {
userId = context.getCurrentUserId();
}
const id = await generateNewIdAsync();
let _router = router;
// router为空则默认为/wechatLogin/confirm
if (!router) {
_router = {
pathname: '/wechatLogin/confirm',
props: {
oakId: id,
},
};
}
else {
_router.props = {
oakId: id,
};
}
const createData = {
id,
type,
expiresAt: Date.now() + interval,
expired: false,
qrCodeType: 'wechatPublic',
qrCodeType,
successed: false,
router: _router,
};
if (userId) {
Object.assign(createData, {
userId,
});
createData.userId = userId;
}
if (type === 'login') {
const closeRoot = context.openRootMode();

View File

@ -12,6 +12,7 @@ import { shrinkUuidTo32Bytes } from 'oak-domain/lib/utils/uuid';
* @returns
*/
export async function createWechatQrCode(options, context) {
console.warn('本接口将被封闭请直接使用operation来实现wechatQrCode的创建动作');
const { entity, entityId, tag, permanent = false, props, type: qrCodeType, } = options;
const applicationId = context.getApplicationId();
assert(applicationId);
@ -161,7 +162,7 @@ export async function createWechatQrCode(options, context) {
permanent,
url,
expired: false,
expiresAt: Date.now() + 2592000 * 1000,
expiresAt: Date.now() + 2592000 * 1000, // wecharQrCode里的过期时间都放到最大由上层关联对象来主动过期by Xc, 20230131)
props,
};
// 直接创建

View File

@ -15,7 +15,7 @@ export async function unbindingWechat(params, context) {
id: wechatUserId,
}
}, {});
assert(wechatUser.userId === userId, '查询到的wechatUser.userId与当前登录者不相同');
assert(wechatUser.userId === userId, '已绑定微信的用户与当前登录者不一致');
await context.operate('wechatUser', {
id: await generateNewIdAsync(),
action: 'update',
@ -39,6 +39,7 @@ export async function unbindingWechat(params, context) {
origin: 'mobile',
content: mobile,
code: captcha,
// TODO: 这里的type暂未确定如果需要发验证码需要再添加一个验证码类型
},
sorter: [{
$attr: {
@ -54,13 +55,13 @@ export async function unbindingWechat(params, context) {
if (captchaRow.expired) {
throw new OakUserException('验证码已经过期');
}
fn();
await fn();
}
else {
throw new OakUserException('验证码无效');
}
}
else {
fn();
await fn();
}
}

View File

@ -1,4 +1,18 @@
import { OakInputIllegalException } from 'oak-domain/lib/types';
import { checkAttributesNotNull } from 'oak-domain/lib/utils/validator';
import { isVersion } from 'oak-domain/lib/utils/version';
function checkVersion(data) {
const { dangerousVersions, warningVersions, soaVersion } = data;
if (dangerousVersions && dangerousVersions.find(ele => !isVersion(ele))) {
throw new OakInputIllegalException('application', ['dangerousVersions'], 'error::illegalVersionData');
}
if (warningVersions && warningVersions.find(ele => !isVersion(ele))) {
throw new OakInputIllegalException('application', ['warningVersions'], 'error::illegalVersionData');
}
if (soaVersion && !isVersion(soaVersion)) {
throw new OakInputIllegalException('application', ['soaVersion'], 'error::illegalVersionData');
}
}
const checkers = [
{
type: 'data',
@ -14,6 +28,7 @@ const checkers = [
};
if (data instanceof Array) {
data.forEach((ele) => {
checkVersion(ele);
checkAttributesNotNull('application', ele, [
'name',
'type',
@ -23,6 +38,7 @@ const checkers = [
});
}
else {
checkVersion(data);
checkAttributesNotNull('application', data, [
'name',
'type',
@ -33,5 +49,13 @@ const checkers = [
return;
},
},
{
type: 'data',
action: 'update',
entity: 'application',
checker(data) {
checkVersion(data);
}
}
];
export default checkers;

View File

@ -71,16 +71,17 @@ const checkers = [
checker(operation, context, option) {
const { filter } = operation;
assert(filter);
const remove = context.select('applicationPassport', {
return pipeline(() => context.select('applicationPassport', {
data: {
id: 1,
applicationId: 1,
isDefault: 1,
},
filter,
}, { forUpdate: true });
const updateDefaultFn = (id, applicationId) => {
return pipeline(() => context.select('applicationPassport', {
}, { forUpdate: true }), (removes) => {
const removeIds = removes?.map(ele => ele.id);
const applicationId = removes?.[0]?.applicationId;
return context.select('applicationPassport', {
data: {
id: 1,
applicationId: 1,
@ -88,40 +89,28 @@ const checkers = [
},
filter: {
id: {
$ne: id,
$nin: removeIds,
},
isDefault: false,
applicationId,
},
indexFrom: 0,
count: 1,
}, {}), (other) => {
if (other && other.length === 1) {
return context.operate('applicationPassport', {
id: generateNewId(),
action: 'update',
data: {
isDefault: true,
},
filter: {
id: other[0].id,
},
}, option);
}
});
};
if (remove instanceof Promise) {
return remove.then((r) => {
if (r[0]?.isDefault) {
return updateDefaultFn(r[0].id, r[0].applicationId);
}
});
}
else {
if (remove[0]?.isDefault) {
return updateDefaultFn(remove[0].id, remove[0].applicationId);
}, {});
}, (other) => {
if (other && other.length === 1) {
return context.operate('applicationPassport', {
id: generateNewId(),
action: 'update',
data: {
isDefault: true,
},
filter: {
id: other[0].id,
},
}, option);
}
}
});
}
},
];

5
es/checkers/article.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { Checker } from "oak-domain/lib/types";
import { EntityDict } from '../oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
declare const checkers: Checker<EntityDict, 'article', RuntimeCxt<EntityDict>>[];
export default checkers;

21
es/checkers/article.js Normal file
View File

@ -0,0 +1,21 @@
import { generateNewId } from 'oak-domain/lib/utils/uuid';
const checkers = [
{
// 删除文章级联删除extraFile
action: 'remove',
type: 'logical',
entity: 'article',
checker(operation, context) {
const { filter } = operation;
return context.operate('extraFile', {
id: generateNewId(),
action: 'remove',
data: {},
filter: {
article: filter,
},
}, {});
},
},
];
export default checkers;

5
es/checkers/articleMenu.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { Checker } from "oak-domain/lib/types";
import { EntityDict } from '../oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
declare const checkers: Checker<EntityDict, 'articleMenu', RuntimeCxt<EntityDict>>[];
export default checkers;

View File

@ -0,0 +1,98 @@
import { generateNewId } from 'oak-domain/lib/utils/uuid';
import { pipeline } from 'oak-domain/lib/utils/executor';
const checkers = [
{
// 删除文章分类级联删除extraFile
action: 'remove',
type: 'logical',
entity: 'articleMenu',
checker(operation, context) {
const { filter } = operation;
return context.operate('extraFile', {
id: generateNewId(),
action: 'remove',
data: {},
filter: {
articleMenu: filter,
},
}, {});
},
},
{
// 删除文章分类级联删除子文章分类
action: 'remove',
type: 'logical',
entity: 'articleMenu',
checker(operation, context) {
const { filter } = operation;
if (filter) {
return pipeline(() => context.select('articleMenu', {
data: {
id: 1,
articleMenu$parent: {
$entity: 'articleMenu',
data: {
id: 1,
}
}
},
filter,
}, {}), (articleMenus) => {
const children = articleMenus.map((ele) => ele.articleMenu$parent).flat();
const childrenIds = children?.map((ele) => ele.id);
if (childrenIds && childrenIds.length > 0) {
return context.operate("articleMenu", {
id: generateNewId(),
action: "remove",
data: {},
filter: {
id: {
$in: childrenIds,
}
},
}, {});
}
});
}
},
},
{
// 删除文章分类级联删除子文章
action: 'remove',
type: 'logical',
entity: 'articleMenu',
checker(operation, context) {
const { filter } = operation;
if (filter) {
return pipeline(() => context.select('articleMenu', {
data: {
id: 1,
article$articleMenu: {
$entity: 'article',
data: {
id: 1,
}
}
},
filter,
}, {}), (articleMenus) => {
const articles = articleMenus.map((ele) => ele.article$articleMenu).flat();
const articleIds = articles?.map((ele) => ele.id);
if (articleIds && articleIds.length > 0) {
return context.operate("article", {
id: generateNewId(),
action: "remove",
data: {},
filter: {
id: {
$in: articleIds,
}
},
}, {});
}
});
}
},
},
];
export default checkers;

View File

@ -1,2 +1,2 @@
declare const checkers: (import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "mobile", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "user", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "address", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "application", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "applicationPassport", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "token", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "message", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "parasite", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>>)[];
declare const checkers: (import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "application", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "applicationPassport", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "article", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "articleMenu", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "user", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "mobile", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "parasite", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "system", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "platform", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "address", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "token", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Checker<import("../oak-app-domain").EntityDict, "message", import("..").RuntimeCxt<import("../oak-app-domain").EntityDict>>)[];
export default checkers;

View File

@ -9,6 +9,10 @@ import wechatPublicTagChecker from './wechatPublicTag';
import messageChecker from './message';
import parasite from './parasite';
import applicationPassport from './applicationPassport';
import systems from './system';
import platforms from './platform';
import articles from './article';
import articleMenus from './articleMenu';
const checkers = [
...mobileChecker,
...addressCheckers,
@ -21,5 +25,9 @@ const checkers = [
...messageChecker,
...parasite,
...applicationPassport,
...systems,
...platforms,
...articles,
...articleMenus,
];
export default checkers;

View File

@ -5,7 +5,7 @@ const checkers = [
action: 'select',
entity: 'message',
checker: (operation, context) => {
const systemId = context.getSystemId();
const systemId = context.getSystemId(true);
if (!systemId) {
return;
}

View File

@ -11,6 +11,7 @@ const checkers = [
assert(!(data instanceof Array));
checkAttributesNotNull('parasite', data, ['expiresAt', 'tokenLifeLength']);
if (data.userId) {
// @oak-ignore 这里先不await下面再具体检查返回类型
const users2 = context.select('user', {
data: {
id: 1,

View File

@ -1,4 +1,6 @@
import { OakInputIllegalException } from 'oak-domain/lib/types';
import { checkAttributesNotNull } from 'oak-domain/lib/utils/validator';
import { isVersion } from 'oak-domain/lib/utils/version';
const checkers = [
{
type: 'data',
@ -15,11 +17,17 @@ const checkers = [
if (data instanceof Array) {
data.forEach((ele) => {
checkAttributesNotNull('platform', ele, ['name']);
if (ele.oldestVersion && !isVersion(ele.oldestVersion)) {
throw new OakInputIllegalException('system', ['oldestVersion'], 'error::illegalVersionData', 'oak-general-business');
}
setData(ele);
});
}
else {
checkAttributesNotNull('platform', data, ['name']);
if (data.oldestVersion && !isVersion(data.oldestVersion)) {
throw new OakInputIllegalException('system', ['oldestVersion'], 'error::illegalVersionData', 'oak-general-business');
}
setData(data);
}
return;

View File

@ -1,4 +1,6 @@
import { OakInputIllegalException } from 'oak-domain/lib/types';
import { checkAttributesNotNull } from 'oak-domain/lib/utils/validator';
import { isVersion } from 'oak-domain/lib/utils/version';
const checkers = [
{
type: 'data',
@ -20,15 +22,32 @@ const checkers = [
if (data instanceof Array) {
data.forEach((ele) => {
checkAttributesNotNull('system', ele, ['name', 'platformId']);
if (ele.oldestVersion && !isVersion(ele.oldestVersion)) {
throw new OakInputIllegalException('system', ['oldestVersion'], 'error::illegalVersionData', 'oak-general-business');
}
setData(ele);
});
}
else {
checkAttributesNotNull('system', data, ['name', 'platformId']);
if (data.oldestVersion && !isVersion(data.oldestVersion)) {
throw new OakInputIllegalException('system', ['oldestVersion'], 'error::illegalVersionData', 'oak-general-business');
}
setData(data);
}
return;
},
},
{
type: 'data',
action: 'update',
entity: 'system',
checker(data) {
const { oldestVersion } = data;
if (oldestVersion && !isVersion(oldestVersion)) {
throw new OakInputIllegalException('system', ['oldestVersion'], 'error::illegalVersionData');
}
}
}
];
export default checkers;

View File

@ -22,7 +22,7 @@ const checkers = [
checker: (operation, context) => {
// 只有root才能进行操作
if (!context.isRoot()) {
throw new OakOperationUnpermittedException('user', { id: 'disable', action: 'disable', data: {} });
throw new OakOperationUnpermittedException('user', { id: 'disable', action: 'disable', data: {} }, context.getCurrentUserId());
}
}
},
@ -179,7 +179,7 @@ export const UserCheckers = [
for (const attr in data) {
const rel = judgeRelation(context.getSchema(), 'user', attr);
if (rel !== 1) {
throw new OakOperationUnpermittedException('user', operation, '您不能更新他人信息');
throw new OakOperationUnpermittedException('user', operation, context.getCurrentUserId(), '您不能更新他人信息');
}
}
const result = checkFilterContains('user', context, {
@ -188,12 +188,12 @@ export const UserCheckers = [
if (result instanceof Promise) {
return result.then((r) => {
if (!r) {
throw new OakOperationUnpermittedException('user', operation, '您不能更新他人信息');
throw new OakOperationUnpermittedException('user', operation, context.getCurrentUserId(), '您不能更新他人信息');
}
});
}
if (!result) {
throw new OakOperationUnpermittedException('user', operation, '您不能更新他人信息');
throw new OakOperationUnpermittedException('user', operation, context.getCurrentUserId(), '您不能更新他人信息');
}
},
}

View File

@ -1,6 +1,7 @@
import { OakInputIllegalException, } from 'oak-domain/lib/types';
import { assert } from 'oak-domain/lib/utils/assert';
import { generateNewId } from 'oak-domain/lib/utils/uuid';
import { pipeline } from 'oak-domain/lib/utils/executor';
const checkers = [
{
type: 'data',
@ -40,20 +41,8 @@ const checkers = [
const { userEntityClaim$ueg } = data;
assert(filter.id);
assert(userEntityClaim$ueg instanceof Array);
const result = context.select('userEntityGrant', {
data: {
id: 1,
relationEntity: 1,
multiple: 1,
},
filter,
}, option);
const dealInner = (userEntityGrant) => {
const { relationEntity, multiple } = userEntityGrant;
if (!multiple) {
userEntityGrant.expired = true;
userEntityGrant.expiresAt = Date.now();
}
const { relationEntity } = userEntityGrant;
userEntityClaim$ueg.forEach((uec) => {
const { action, data } = uec;
assert(action === 'create');
@ -74,10 +63,14 @@ const checkers = [
});
return userEntityClaim$ueg.length;
};
if (result instanceof Promise) {
return result.then(([ueg]) => dealInner(ueg));
}
return dealInner(result[0]);
return pipeline(() => context.select('userEntityGrant', {
data: {
id: 1,
relationEntity: 1,
multiple: 1,
},
filter,
}, option), ([ueg]) => dealInner(ueg));
}
}
];

View File

@ -1,3 +1,2 @@
/// <reference types="wechat-miniprogram" />
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "address", true, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
export default _default;

View File

@ -1,3 +1,2 @@
/// <reference types="wechat-miniprogram" />
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "address", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
export default _default;

View File

@ -1,4 +1,3 @@
/// <reference types="@uiw/react-amap-types" />
import React from 'react';
export type PositionProps = {
loadUI: boolean;

View File

@ -1,4 +1,3 @@
/// <reference types="@uiw/react-amap-types" />
import React from 'react';
import { ModalProps } from 'antd';
import { GeolocationProps } from '@uiw/react-amap';

View File

@ -1,4 +1,3 @@
/// <reference types="@uiw/react-amap-types" />
import React from 'react';
import { MapProps, APILoaderConfig } from '@uiw/react-amap';
import './index.less';

View File

@ -0,0 +1,5 @@
import { NativeConfig, WebConfig, WechatMpConfig, WechatPublicConfig } from '../../../entities/Application';
export type AppConfig = WebConfig | WechatMpConfig | WechatPublicConfig | NativeConfig;
export type CosConfig = AppConfig['cos'];
declare const _default: any;
export default _default;

View File

@ -0,0 +1,111 @@
import { cloneDeep, set } from 'oak-domain/lib/utils/lodash';
import { generateNewId } from 'oak-domain/lib/utils/uuid';
import { isEmptyJsonObject } from '../../../utils/strings';
export default OakComponent({
isList: false,
properties: {
config: {},
entity: 'application',
entityId: '',
name: '',
},
data: {
initialConfig: {},
dirty: false,
currentConfig: {},
selections: [],
},
lifetimes: {
async ready() {
const { config } = this.props;
this.setState({
initialConfig: config,
dirty: false,
currentConfig: cloneDeep(config),
});
const systemId = this.features.application.getApplication().systemId;
const { data: [system] } = await this.features.cache.refresh("system", {
data: {
config: {
Cos: {
qiniu: 1,
ctyun: 1,
aliyun: 1,
tencent: 1,
local: 1,
s3: 1,
},
},
},
filter: {
id: systemId,
}
});
const cosConfig = system?.config?.Cos;
// 如果key存在并且不为defaultOrigin并且value的keys长度大于0则加入选择项
const selections = [];
if (cosConfig) {
for (const [key, value] of Object.entries(cosConfig)) {
if (key === 'defaultOrigin') {
continue;
}
if (value && !isEmptyJsonObject(value)) {
selections.push({
name: key,
value: key,
});
}
}
}
this.setState({
selections,
});
}
},
methods: {
setValue(path, value) {
const { currentConfig } = this.state;
const newConfig = cloneDeep(currentConfig || {});
set(newConfig, path, value);
this.setState({
currentConfig: newConfig,
dirty: true,
});
},
resetConfig() {
const { initialConfig } = this.state;
this.setState({
dirty: false,
currentConfig: cloneDeep(initialConfig),
});
},
async updateConfig() {
const { currentConfig } = this.state;
const { entity, entityId } = this.props;
if (!entityId) {
this.setMessage({
content: '缺少实体ID无法更新配置',
type: 'error',
});
return;
}
await this.features.cache.operate("application", {
id: generateNewId(),
action: 'update',
data: {
config: currentConfig,
},
filter: {
id: entityId,
}
}, {});
this.setMessage({
content: '操作成功',
type: 'success',
});
this.setState({
dirty: false,
});
},
},
});

View File

@ -0,0 +1,8 @@
{
"qiniu": "七牛云",
"ctyun": "天翼云",
"aliyun": "阿里云",
"tencent": "腾讯云",
"local": "本地存储",
"s3": "S3存储"
}

View File

@ -0,0 +1,5 @@
.contains {
margin-top: 20px;
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';
declare const Cos: (props: WebComponentProps<EntityDict, keyof EntityDict, false, {
currentConfig: EntityDict["application"]["OpSchema"]["config"];
dirty: boolean;
entity: string;
name: string;
selections: {
name: string;
value: string;
}[];
}, {
setValue: (path: string, value: any) => void;
resetConfig: () => void;
updateConfig: () => void;
}>) => React.JSX.Element;
export default Cos;

View File

@ -0,0 +1,47 @@
// @oak-ignore
import React from 'react';
import Styles from './styles.module.less';
import { Affix, Alert, Button, Select, Space, Typography } from 'antd';
const Cos = (props) => {
const { currentConfig, dirty, entity, name, selections } = props.data;
const { t, setValue, resetConfig, updateConfig } = props.methods;
return (<>
<Affix offsetTop={64}>
<Alert message={<div>
<span>
您正在更新
<Typography.Text keyboard>
{entity}
</Typography.Text>
对象
<Typography.Text keyboard>
{name}
</Typography.Text>
的COS配置请谨慎操作
</span>
</div>} type="info" showIcon action={<Space>
<Button disabled={!dirty} type="primary" danger onClick={() => resetConfig()} style={{
marginRight: 10,
}}>
{t('common::reset')}
</Button>
<Button disabled={!dirty} type="primary" onClick={() => updateConfig()}>
{t('common::action.confirm')}
</Button>
</Space>}/>
</Affix>
<div className={Styles.contains}>
<Space direction="vertical" style={{ width: '100%' }}>
<Typography.Text strong>默认COS源</Typography.Text>
<Select value={currentConfig?.cos?.defaultOrigin} onChange={(v) => {
setValue('cos.defaultOrigin', v);
}} style={{ width: '100%' }} allowClear placeholder="请选择默认COS源">
{selections.map((item) => (<Select.Option key={item.value} value={item.value}>
{t(item.name)}
</Select.Option>))}
</Select>
</Space>
</div>
</>);
};
export default Cos;

View File

@ -1,3 +1,2 @@
/// <reference types="wechat-miniprogram" />
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "application", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
export default _default;

View File

@ -1,8 +1,11 @@
export default OakComponent({
isList: false,
entity: 'application',
formData({ data }) {
formData({ data, features }) {
const { dangerousVersions, warningVersions } = data;
return {
dv: dangerousVersions ? features.locales.t('whole', { count: dangerousVersions.length }) : features.locales.t('common::unset'),
wv: warningVersions ? features.locales.t('whole', { count: warningVersions.length }) : features.locales.t('common::unset'),
...data,
oakExecutable: this.tryExecute(),
};

View File

@ -0,0 +1,3 @@
{
"whole": "共%{count}项"
}

View File

@ -8,4 +8,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'application
tabValue: 'detail';
type: EntityDict['application']['Schema']['type'];
oakExecutable: boolean;
soaVersion?: string;
dv: string;
wv: string;
}>): React.JSX.Element | undefined;

View File

@ -2,12 +2,12 @@ import React, { useState } from 'react';
import { Row, Descriptions, Typography, Button, Modal, Space } from 'antd';
import ApplicationUpsert from '../upsert';
export default function Render(props) {
const { id, name, description, type, oakFullpath, oakExecutable, oakExecuting } = props.data;
const { id, name, description, type, oakFullpath, oakDirty, oakExecuting, dv, wv, soaVersion } = props.data;
const { t, clean, execute } = props.methods;
const [open, setOpen] = useState(false);
if (id && oakFullpath) {
return (<>
<Modal open={open} width={500} onCancel={() => {
<Modal destroyOnClose open={open} width={500} onCancel={() => {
clean();
setOpen(false);
}} footer={<Space>
@ -20,7 +20,7 @@ export default function Render(props) {
<Button type="primary" onClick={async () => {
await execute();
setOpen(false);
}} disabled={oakExecutable !== true || oakExecuting}>
}} disabled={!oakDirty || oakExecuting}>
{t('common::action.confirm')}
</Button>
</Space>}>
@ -41,6 +41,15 @@ export default function Render(props) {
<Descriptions.Item label={t('application:attr.type')}>
{t(`application:v.type.${type}`)}
</Descriptions.Item>
<Descriptions.Item label={t('application:attr.soaVersion')}>
{soaVersion || t('common::unset')}
</Descriptions.Item>
<Descriptions.Item label={t('application:attr.dangerousVersions')}>
{dv}
</Descriptions.Item>
<Descriptions.Item label={t('application:attr.warningVersions')}>
{wv}
</Descriptions.Item>
<Descriptions.Item span={2}>
<Row justify="end">
<Button type="primary" onClick={() => setOpen(true)}>

View File

@ -1,3 +1,2 @@
/// <reference types="wechat-miniprogram" />
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "application", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
export default _default;

View File

@ -1,3 +1,9 @@
/// <reference types="wechat-miniprogram" />
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "application", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
import React from 'react';
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "application", false, {
tabs: {
label: React.ReactNode;
key: string;
children: React.ReactNode;
}[];
}>) => React.ReactElement;
export default _default;

View File

@ -7,10 +7,16 @@ export default OakComponent({
config: 1,
description: 1,
type: 1,
dangerousVersions: 1,
warningVersions: 1,
soaVersion: 1,
systemId: 1,
domainId: 1,
},
formData({ data }) {
return data || {};
},
properties: {
tabs: [],
},
});

View File

@ -6,5 +6,6 @@
"menu": "菜单管理",
"autoReply": "被关注回复管理",
"tag": "标签管理",
"user": "用户管理"
"user": "用户管理",
"cos": "COS配置"
}

View File

@ -8,4 +8,9 @@ export default function Render(props: WebComponentProps<EntityDict, 'application
name: string;
style: Style;
type: string;
tabs: {
label: React.ReactNode;
key: string;
children: React.ReactNode;
}[];
}>): React.JSX.Element | undefined;

View File

@ -9,15 +9,16 @@ import WechatMenu from '../../wechatMenu';
import UserWechatPublicTag from '../../userWechatPublicTag';
import WechatPublicTag from '../..//wechatPublicTag/list';
import WechatPublicAutoReply from '../../wechatPublicAutoReply';
import Cos from '../cos';
export default function Render(props) {
const { id, config, oakFullpath, name, style, type } = props.data;
const { id, config, oakFullpath, name, style, type, tabs } = props.data;
const { t, update } = props.methods;
const [tabKey, setTabKey] = useState('detail');
const items = [
{
label: <div className={Styles.tabLabel}>{t('detail')}</div>,
key: 'detail',
children: (<ApplicationDetail oakId={id} oakPath={oakFullpath}/>),
children: <ApplicationDetail oakId={id} oakPath={oakFullpath}/>,
},
{
label: <div className={Styles.tabLabel}>{t('config')}</div>,
@ -27,8 +28,15 @@ export default function Render(props) {
{
label: <div className={Styles.tabLabel}>{t('style')}</div>,
key: 'style',
children: (<StyleUpsert style={style} entity={'platform'} entityId={id} name={name}/>),
children: (<StyleUpsert style={style} entity={'application'} entityId={id} name={name}/>),
},
{
label: <div className={Styles.tabLabel}>{t('cos')}</div>,
key: 'cos',
children: (<Cos oakPath={`#application-panel-cos-${id}`} config={config} entity="application" entityId={id} name={name}>
</Cos>),
},
...(tabs || [])
];
if (type === 'wechatPublic') {
items.push({

View File

@ -1,4 +1,3 @@
/// <reference types="wechat-miniprogram" />
import { EntityDict } from '../../../oak-app-domain';
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "application", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
export default _default;

View File

@ -6,6 +6,9 @@ export default OakComponent({
name: 1,
config: 1,
description: 1,
dangerousVersions: 1,
warningVersions: 1,
soaVersion: 1,
type: 1,
systemId: 1,
domainId: 1,

View File

@ -15,6 +15,9 @@ export default function Render(props: WebComponentProps<EntityDict, 'application
$$createAt$$: number;
domainId: string;
domains: EntityDict['domain']['Schema'][];
dangerousVersions: EntityDict['application']['OpSchema']['dangerousVersions'];
warningVersions: EntityDict['application']['OpSchema']['warningVersions'];
soaVersion: string;
}, {
confirm: () => void;
getDomains: (systemId: string) => Promise<void>;

View File

@ -1,10 +1,62 @@
import React from 'react';
import { Form, Select, Input } from 'antd';
import React, { useState, useRef } from 'react';
import { Form, Flex, Tag, Tooltip, Select, Input, theme } from 'antd';
import { PlusOutlined, CloseOutlined } from '@ant-design/icons';
function renderVersions(props) {
const { versions, onChange, t } = props;
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef(null);
const tagInputStyle = {
width: 64,
height: 22,
marginInlineEnd: 8,
verticalAlign: 'top',
};
const { token } = theme.useToken();
const tagPlusStyle = {
height: 22,
background: token.colorBgContainer,
borderStyle: 'dashed',
};
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && !versions?.includes(inputValue)) {
onChange([...versions || [], inputValue]);
}
setInputVisible(false);
setInputValue('');
};
const handleClose = (removedTag) => {
const versions2 = versions.filter((tag) => tag !== removedTag);
onChange(versions2);
};
const showInput = () => {
setInputVisible(true);
};
return (<Flex gap="4px 0" wrap="wrap">
{(versions || []).map((tag, index) => {
const isLongTag = tag.length > 20;
const tagElem = (<Tag closeIcon={<CloseOutlined />} key={tag} onClose={() => handleClose(tag)}>
<span>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>);
return isLongTag ? (<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>) : (tagElem);
})}
{inputVisible ? (<Input ref={inputRef} type="text" size="small" style={tagInputStyle} value={inputValue} onChange={handleInputChange} onBlur={handleInputConfirm} onPressEnter={handleInputConfirm}/>) : (<Tag style={tagPlusStyle} icon={<PlusOutlined />} onClick={showInput}>
{t('common::action.add')}
</Tag>)}
</Flex>);
}
export default function Render(props) {
const { systemId, name, description, type, typeArr, $$createAt$$, domainId, domains, } = props.data;
const { systemId, name, description, type, typeArr, $$createAt$$, domainId, domains, dangerousVersions, warningVersions, soaVersion, } = props.data;
const { t, update, confirm, getDomains } = props.methods;
return (<Form colon={true} labelCol={{ span: 6 }} wrapperCol={{ span: 16 }}>
<Form.Item label="名称" required>
<Form.Item label={t('application:attr.name')} required>
<>
<Input onChange={(e) => {
update({
@ -13,7 +65,34 @@ export default function Render(props) {
}} value={name}/>
</>
</Form.Item>
<Form.Item label="描述">
<Form.Item label={t('application:attr.soaVersion')} required>
<>
<Input onChange={(e) => {
update({
soaVersion: e.target.value,
});
}} value={soaVersion}/>
</>
</Form.Item>
<Form.Item label={t('application:attr.dangerousVersions')}>
{renderVersions({
versions: dangerousVersions,
onChange: (v) => update({
dangerousVersions: v
}),
t,
})}
</Form.Item>
<Form.Item label={t('application:attr.warningVersions')}>
{renderVersions({
versions: warningVersions,
onChange: (v) => update({
warningVersions: v
}),
t,
})}
</Form.Item>
<Form.Item label={t('application:attr.description')}>
<>
<Input.TextArea onChange={(e) => {
update({
@ -22,7 +101,7 @@ export default function Render(props) {
}} value={description}/>
</>
</Form.Item>
<Form.Item label="应用类型" required>
<Form.Item label={t('application:attr.type')} required>
<>
<Select value={type} style={{ width: 120 }} disabled={$$createAt$$ > 1} options={typeArr.map((ele) => ({
label: t(`application:v.type.${ele.value}`),
@ -34,7 +113,7 @@ export default function Render(props) {
}}/>
</>
</Form.Item>
<Form.Item label="域名">
<Form.Item label={t('domain:name')}>
<>
<Select allowClear value={domainId} style={{ width: 120 }} options={domains?.map((ele) => ({
label: ele.url,

View File

@ -1,5 +1,7 @@
import { EntityDict } from "../../oak-app-domain";
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "applicationPassport", true, {
import { ReactComponentProps } from "oak-frontend-base";
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, true, {
systemId: string;
}>) => React.ReactElement;
export default _default;

View File

@ -20,6 +20,7 @@ export default OakComponent({
enabled: 1,
},
isDefault: 1,
allowPwd: 1,
},
properties: {
systemId: '',
@ -34,6 +35,9 @@ export default OakComponent({
},
passport: {
systemId,
type: {
$ne: 'password'
}
},
};
}
@ -83,8 +87,9 @@ export default OakComponent({
}
else {
const { disabled, disabledTip } = this.checkDisabled(a, r[0]);
const { showPwd, pwdDisabled, pwdDisabledTip } = this.checkPwd(r[0]);
const apId = await generateNewIdAsync();
Object.assign(typeRecords, { [key]: { render, pId: r[0].id, checked: false, disabled, disabledTip, apId, } });
Object.assign(typeRecords, { [key]: { render, pId: r[0].id, checked: false, disabled, disabledTip, apId, showPwd, allowPwd: undefined, pwdDisabled, pwdDisabledTip } });
}
}
Object.assign(item, { typeRecords });
@ -106,8 +111,15 @@ export default OakComponent({
if (apArray[aIdx].typeRecords[t].pId === p.id) {
apArray[aIdx].typeRecords[t].checked = true;
apArray[aIdx].typeRecords[t].apId = ap.id;
apArray[aIdx].typeRecords[t].allowPwd = ap.allowPwd;
}
}
if (t === 'loginName') {
apArray[aIdx].typeRecords[t].pwdDisabledTip = '账号登录必须使用密码方式';
}
else {
apArray[aIdx].typeRecords[t].pwdDisabled = false;
}
apArray[aIdx].defaultOptions.push({
label: this.t(`passport:v.type.${p.type}`),
value: ap.id,
@ -157,6 +169,9 @@ export default OakComponent({
filter: {
systemId,
enabled: true,
type: {
$ne: 'password',
}
},
sorter: [{
$attr: {
@ -193,36 +208,36 @@ export default OakComponent({
switch (pType) {
case 'sms':
if (!pConfig.mockSend) {
if (!pConfig.templateName || pConfig.templateName === '') {
if (!pConfig.templateName) {
return {
disabled: true,
disabledTip: '短信登录未配置验证码模板名称',
disabledTip: '手机号登录未配置验证码模板名称',
};
}
if (!pConfig.defaultOrigin) {
return {
disabled: true,
disabledTip: '短信登录未配置默认渠道',
disabledTip: '手机号登录未配置默认渠道',
};
}
}
break;
case 'email':
if (!pConfig.mockSend) {
if (!pConfig.account || pConfig.account === '') {
if (!pConfig.account) {
return {
disabled: true,
disabledTip: '邮箱登录未配置账号',
};
}
else if (!pConfig.subject || pConfig.subject === '') {
else if (!pConfig.subject) {
return {
disabled: true,
disabledTip: '邮箱登录未配置邮件主题',
};
}
else if ((!pConfig.text || pConfig.text === '' || !pConfig.text?.includes('${code}')) &&
(!pConfig.html || pConfig.html === '' || !pConfig.html?.includes('${code}'))) {
else if ((!pConfig.text || !pConfig.text?.includes('${code}')) &&
(!pConfig.html || !pConfig.html?.includes('${code}'))) {
return {
disabled: true,
disabledTip: '邮箱登录未配置邮件内容模板',
@ -231,7 +246,7 @@ export default OakComponent({
}
break;
case 'wechatPublicForWeb':
if (!pConfig.appId || pConfig.appId === '') {
if (!pConfig.appId) {
return {
disabled: true,
disabledTip: '公众号授权登录未配置appId',
@ -239,13 +254,27 @@ export default OakComponent({
}
break;
case 'wechatMpForWeb':
if (!pConfig.appId || pConfig.appId === '') {
if (!pConfig.appId) {
return {
disabled: true,
disabledTip: '小程序授权登录未配置appId',
};
}
break;
case 'oauth':
if (!(pConfig.oauthIds && pConfig.oauthIds.length > 0)) {
return {
disabled: true,
disabledTip: 'OAuth授权登录未配置oauth供应商',
};
}
break;
case 'password':
return {
disabled: true,
disabledTip: '密码登录已调整',
};
break;
default:
break;
}
@ -269,7 +298,7 @@ export default OakComponent({
}
break;
case 'wechatMp':
if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) {
return {
disabled: true,
disabledTip: '当前application不支持该登录方式',
@ -277,7 +306,7 @@ export default OakComponent({
}
break;
case 'wechatPublic':
if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) {
return {
disabled: true,
disabledTip: '当前application不支持该登录方式',
@ -285,7 +314,7 @@ export default OakComponent({
}
break;
case 'native':
if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb', 'oauth'].includes(pType)) {
return {
disabled: true,
disabledTip: '当前application不支持该登录方式',
@ -301,13 +330,25 @@ export default OakComponent({
};
},
async onCheckedChange(aId, pId, checked, apId) {
const { passports } = this.state;
const passportType = passports?.find((ele) => ele.id === pId)?.type;
if (checked) {
//create applicationPassport
this.addItem({
applicationId: aId,
passportId: pId,
isDefault: true,
});
if (passportType === 'loginName') {
this.addItem({
applicationId: aId,
passportId: pId,
isDefault: true,
allowPwd: true,
});
}
else {
this.addItem({
applicationId: aId,
passportId: pId,
isDefault: true,
});
}
}
else {
//remove id为apId的applicationPassport
@ -349,6 +390,20 @@ export default OakComponent({
}
}
return render;
}
},
checkPwd(passport) {
const { type } = passport;
let showPwd = false, pwdDisabled = undefined, pwdDisabledTip = undefined;
if (['sms', 'email', 'loginName'].includes(type)) {
showPwd = true;
pwdDisabled = true;
pwdDisabledTip = '请先启用该登录方式';
}
return {
showPwd,
pwdDisabled,
pwdDisabledTip,
};
},
}
});

View File

@ -20,6 +20,10 @@ type TypeRecord = Record<string, {
checked?: boolean;
disabled: boolean;
disabledTip: string;
showPwd: boolean;
allowPwd?: boolean;
pwdDisabled?: boolean;
pwdDisabledTip?: string;
}>;
type PassportOption = {
label: string;

View File

@ -56,8 +56,11 @@ export default function render(props) {
}} disabled={typeRecords[type].disabled} options={typeRecords[type].passportOptions} optionRender={(option) => (<Tooltip title={(option.data.disabled) ? option.data.disabledTip : ''}>
<div>{option.data.label}</div>
</Tooltip>)} style={{ width: 140 }}/>
</Tooltip>) : (<Tooltip title={typeRecords[type].disabled ? typeRecords[type].disabledTip : ''}>
<Switch disabled={typeRecords[type].disabled} checkedChildren={<CheckOutlined />} unCheckedChildren={<CloseOutlined />} checked={!!typeRecords[type].checked} onChange={(checked) => {
</Tooltip>) : (<>
<Tooltip title={typeRecords[type].disabled ? typeRecords[type].disabledTip : ''}>
<Space>
<div>启用:</div>
<Switch disabled={typeRecords[type].disabled} checkedChildren={<CheckOutlined />} unCheckedChildren={<CloseOutlined />} checked={!!typeRecords[type].checked} onChange={(checked) => {
if (!checked && checkLastOne(aId, typeRecords[type].pId)) {
showConfirm(aId, typeRecords[type].pId, typeRecords[type].apId);
}
@ -65,7 +68,20 @@ export default function render(props) {
onCheckedChange(aId, typeRecords[type].pId, checked, typeRecords[type].apId);
}
}}/>
</Tooltip>)}
</Space>
</Tooltip>
{typeRecords[type].showPwd &&
<Space>
<div>允许密码登录:</div>
<Tooltip title={typeRecords[type].pwdDisabled ? typeRecords[type].pwdDisabledTip : ''}>
<Switch size="small" disabled={typeRecords[type].pwdDisabled} checkedChildren={<CheckOutlined />} unCheckedChildren={<CloseOutlined />} checked={!!typeRecords[type].allowPwd} onChange={(checked) => {
methods.updateItem({
allowPwd: checked
}, typeRecords[type].apId);
}}/>
</Tooltip>
</Space>}
</>)}
</Space>
});
@ -94,6 +110,6 @@ export default function render(props) {
确定
</Button>
</div>
<Table columns={columns} dataSource={apArray} pagination={false} scroll={{ x: 1200 }}/>
<Table columns={columns} dataSource={apArray} pagination={false} scroll={{ x: 1200 }} rowKey="aId"/>
</>);
}

View File

@ -1,3 +1,2 @@
/// <reference types="wechat-miniprogram" />
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "article", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
export default _default;

View File

@ -7,6 +7,8 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
className: string;
scrollId: string;
tocWidth: number | "auto" | undefined;
tocHeight: number | "auto" | undefined;
tocHeight: number | undefined;
showtitle: boolean;
activeColor: string | undefined;
}>) => React.ReactElement;
export default _default;

View File

@ -1,3 +1,4 @@
import { DATA_SUBSCRIBER_KEYS } from "../../../config/constants";
export default OakComponent({
entity: 'article',
isList: false,
@ -11,7 +12,6 @@ export default OakComponent({
isArticle: 1,
},
},
data: {},
formData: function ({ data: article }) {
return {
content: article?.content,
@ -23,12 +23,29 @@ export default OakComponent({
tocFixed: true,
tocPosition: 'none',
highlightBgColor: 'none',
headerTop: 0,
headerTop: 0, //页面中吸顶部分高度
className: '',
scrollId: '',
scrollId: '', // 滚动条所在容器id不传默认body
tocWidth: undefined,
tocHeight: undefined,
showtitle: false, //大纲顶层显示文章名称
activeColor: undefined,
},
data: {
unsub: undefined,
},
lifetimes: {
async ready() {
const { oakId } = this.props;
const unsub = await this.subDataEvents([`${DATA_SUBSCRIBER_KEYS.articleUpdate}-${oakId}`]);
this.setState({
unsub,
});
},
detached() {
const { unsub } = this.state;
unsub && unsub();
}
},
lifetimes: {},
methods: {}
});

View File

@ -5,12 +5,14 @@ export default function Render(props: WebComponentProps<EntityDict, 'article', f
name?: string;
content?: string;
tocPosition: 'none' | 'left' | 'right';
highlightBgColor: string;
highlightBgColor?: string;
headerTop: number;
className?: string;
tocFixed: boolean;
tocClosed: boolean;
scrollId?: string;
tocWidth?: number | 'auto';
tocHeight?: number | 'auto';
tocWidth?: number;
tocHeight?: number | string;
showtitle: boolean;
activeColor?: string;
}, {}>): React.JSX.Element;

View File

@ -5,7 +5,7 @@ import classNames from 'classnames';
import { TocView } from '../toc/tocView';
import Styles from './web.module.less';
export default function Render(props) {
const { className, name, content, tocPosition = 'none', tocFixed, highlightBgColor = 'none', headerTop = 0, tocClosed = false, scrollId, tocWidth, tocHeight } = props.data;
const { className, name, content, tocPosition = 'none', highlightBgColor = 'none', activeColor, headerTop = 0, tocClosed = false, scrollId, tocWidth, tocHeight, showtitle } = props.data;
const editorConfig = {
readOnly: true,
autoFocus: true,
@ -40,7 +40,9 @@ export default function Render(props) {
}, [name]);
return (<div className={classNames(Styles.container, className)}>
<div className={Styles.contentContainer}>
{tocPosition === "left" ? (<TocView toc={toc} showToc={showToc} tocPosition="left" setShowToc={setShowToc} highlightBgColor={highlightBgColor} headerTop={headerTop} fixed={tocFixed} closed={tocClosed} scrollId={scrollId} tocWidth={tocWidth} tocHeight={tocHeight}/>) : null}
{tocPosition === "left" ? (<TocView toc={toc} showToc={showToc} tocPosition="left" setShowToc={setShowToc}
// highlightBgColor={highlightBgColor}
activeColor={activeColor} headerTop={headerTop} closed={tocClosed} scrollId={scrollId} tocWidth={tocWidth} tocHeight={tocHeight ? `calc(${tocHeight} - 32px)` : 'calc(100vh - 32px)'} title={showtitle ? name : undefined}/>) : null}
<div className={Styles.content}>
<div className={Styles.editorContainer}>
@ -64,7 +66,9 @@ export default function Render(props) {
</div>
</div>
</div>
{tocPosition === "right" ? (<TocView toc={toc} showToc={showToc} tocPosition="right" setShowToc={setShowToc} highlightBgColor={highlightBgColor} headerTop={headerTop} fixed={tocFixed} closed={tocClosed} scrollId={scrollId} tocWidth={tocWidth} tocHeight={tocHeight}/>) : null}
{tocPosition === "right" ? (<TocView toc={toc} showToc={showToc} tocPosition="right" setShowToc={setShowToc}
// highlightBgColor={highlightBgColor}
activeColor={activeColor} headerTop={headerTop} closed={tocClosed} scrollId={scrollId} tocWidth={tocWidth} tocHeight={tocHeight ? `calc(${tocHeight} - 32px)` : 'calc(100vh - 32px)'} title={showtitle ? name : undefined}/>) : null}
</div>
</div>);
}

13
es/components/article/editor/index.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import { EntityDict } from '../../../oak-app-domain';
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "article", false, {
articleMenuId: string;
changeIsEdit: () => void;
tocPosition: "none" | "left" | "right";
highlightBgColor: string;
onArticlePreview: (content?: string, title?: string) => void;
origin: null | EntityDict["extraFile"]["Schema"]["origin"];
scrollId: string;
height: number | "auto";
activeColor: string | undefined;
}>) => React.ReactElement;
export default _default;

View File

@ -0,0 +1,138 @@
export default OakComponent({
entity: 'article',
isList: false,
projection: {
id: 1,
name: 1,
content: 1,
articleMenu: {
id: 1,
},
},
formData: function ({ data: article, features }) {
return {
id: article?.id,
content: article?.content,
name: article?.name,
articleMenuId: article?.articleMenuId,
execuable: this.tryExecute() === true,
};
},
data: {
editor: null,
html: '',
},
properties: {
articleMenuId: '',
changeIsEdit: () => undefined,
tocPosition: 'none', //目录显示位置none为不显示目录
highlightBgColor: 'none', //点击目录时标题高亮背景色none为不显示高亮背景色
onArticlePreview: (content, title) => undefined, //预览文章
origin: null, // 默认为空,由系统决定
scrollId: '', // 滚动条所在容器id不传默认页面编辑器容器id
height: 600,
activeColor: undefined,
},
listeners: {
'editor,content'(prev, next) {
if (next.editor && next.content) {
next.editor.setHtml(next.content);
}
},
// oakId(prev, next) {
// if (prev.oakId !== next.oakId) {
// const { editor } = this.state;
// if (editor == null) return;
// editor.destroy();
// this.setEditor(null);
// }
// },
name(prev, next) {
if (prev.name !== next.name) {
window.document.title = next.name ?? '';
}
}
},
lifetimes: {
async ready() {
const { oakId, articleMenuId } = this.props;
if (this.isCreation()) {
if (articleMenuId) {
this.update({
articleMenuId,
});
const { editor } = this.state;
editor?.setHtml('');
// this.update({
// content: '',
// });
}
}
},
detached() {
const { editor } = this.state;
if (editor == null)
return;
editor.destroy();
this.setEditor(null);
},
},
methods: {
async uploadFile(extraFile, file) {
const result = await this.features.extraFile.autoUpload({
extraFile: extraFile,
file: file
});
return result;
},
setEditor(editor) {
this.setState({
editor,
});
},
async check() {
if (this.state.name &&
this.state.name.length > 0 &&
this.state.html &&
this.state.html.length > 0 &&
this.state.html !== '<p><br></p>') {
const id = this.getId();
await this.execute(undefined, {
type: 'success',
content: this.t('success'),
});
this.setId(id);
if (this.props.changeIsEdit) {
this.props.changeIsEdit();
}
}
else if (this.state.name && this.state.name.length > 0) {
this.setMessage({
content: this.t('check.no content'),
type: 'warning',
});
}
else if (this.state.html &&
this.state.html.length > 0 &&
this.state.html !== '<p><br></p>') {
this.setMessage({
content: this.t('check.no name'),
type: 'warning',
});
}
},
async reset() {
// 重置
this.clean();
},
setHtml(html) {
this.setState({
html,
});
},
gotoPreview(content, title) {
const { onArticlePreview } = this.props;
onArticlePreview && onArticlePreview(content, title);
},
},
});

View File

@ -0,0 +1,14 @@
{
"success": "保存成功",
"preview": "预览",
"save": "保存",
"exitConfirm": "您确认离开页面吗?",
"placeholder": {
"name": "请输入文章标题",
"content": "请输入文章内容..."
},
"check": {
"no name": "请填写文章标题!",
"no content": "请填写文章内容!"
}
}

29
es/components/article/editor/web.d.ts vendored Normal file
View File

@ -0,0 +1,29 @@
import React from "react";
import "@wangeditor/editor/dist/css/style.css";
import { WebComponentProps } from "oak-frontend-base";
import { EntityDict } from "./../../../oak-app-domain";
export default function Render(props: WebComponentProps<EntityDict, 'article', false, {
id: string;
name: string;
editor: any;
content?: string;
origin?: null | EntityDict['extraFile']['Schema']['origin'];
articleMenuId: string;
oakId: string;
tocPosition: 'none' | 'left' | 'right';
highlightBgColor?: string;
scrollId?: string;
tocWidth?: number;
tocHeight?: number | string;
height?: number | string;
tocClosed: boolean;
execuable: boolean;
activeColor?: string;
html: string;
}, {
setHtml: (content: string) => void;
setEditor: (editor: any) => void;
check: () => void;
uploadFile: (extraFile: EntityDict['extraFile']['CreateOperationData'], file: File) => Promise<string>;
gotoPreview: (content?: string, title?: string) => void;
}>): React.JSX.Element;

View File

@ -0,0 +1,194 @@
import React, { useState, useEffect } from "react";
import { Button, Space, Input, } from "antd";
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
import { SlateNode } from "@wangeditor/editor";
import { generateNewId } from "oak-domain/lib/utils/uuid";
import classNames from "classnames";
import Prompt from "../../../components/common/prompt";
import Style from "./web.module.less";
import { TocView } from '../toc/tocView';
import { EyeOutlined, } from "@ant-design/icons";
// 工具栏配置
const toolbarConfig = {
excludeKeys: ["fullScreen"],
}; // TS 语法
// 自定义校验图片
function customCheckImageFn(src, alt, url) {
// TS 语法
if (!src) {
return;
}
if (src.indexOf("http") !== 0) {
return "图片网址必须以 http/https 开头";
}
return true;
// 返回值有三种选择:
// 1. 返回 true ,说明检查通过,编辑器将正常插入图片
// 2. 返回一个字符串,说明检查未通过,编辑器会阻止插入。会 alert 出错误信息(即返回的字符串)
// 3. 返回 undefined即没有任何返回说明检查未通过编辑器会阻止插入。但不会提示任何信息
}
export default function Render(props) {
const { methods, data } = props;
const { t, setMessage, setEditor, check, uploadFile, update, setHtml, gotoPreview, } = methods;
const { oakId, oakFullpath, id, name, content, editor, origin, tocPosition = 'none', highlightBgColor, scrollId, tocWidth, tocHeight, height = 600, tocClosed = false, execuable, activeColor, html, oakLoading, oakExecuting, } = data;
const [articleId, setArticleId] = useState('');
const [toc, setToc] = useState([]);
const [showToc, setShowToc] = useState(false);
const containerId = scrollId || 'article-upsert-editorContainer';
useEffect(() => {
if (id) {
setArticleId(id);
}
}, [id]);
useEffect(() => {
if (tocPosition !== 'none') {
setShowToc(true);
}
}, [tocPosition]);
return (<div className={Style.container}>
<Prompt when={!id || data.oakDirty} message={t('exitConfirm')}/>
<div className={Style.top}>
<div className={Style.header}>
<div className={Style.title}>{name}</div>
<Space>
<Button onClick={() => {
gotoPreview(html, data.name);
}} icon={<EyeOutlined />}>
{t('preview')}
</Button>
<Button disabled={oakLoading || oakExecuting || !(name && name.length > 0 && html && html.length > 0 && html !== '<p><br></p>')} type="primary" onClick={() => {
update({
content: html,
});
check();
}}>
{t('save')}
</Button>
</Space>
</div>
<div className={Style.toolbar}>
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default"/>
</div>
</div>
<div className={Style.contentContainer} id='article-upsert-editorContainer'>
{tocPosition === "left" ? (<TocView toc={toc} showToc={showToc} tocPosition="left" setShowToc={setShowToc}
// highlightBgColor={highlightBgColor}
activeColor={activeColor} closed={tocClosed} scrollId={containerId} tocWidth={tocWidth} tocHeight={'calc(100% - 16px)'}/>) : null}
<div className={Style.content} style={{ maxWidth: `calc(100% - ${tocWidth || 228}px)` }}>
<div className={classNames(Style.editorContainer, {
[Style.editorExternalContainer]: !!scrollId,
})}>
<div className={Style.titleContainer}>
<Input onChange={(e) => {
if (e.target.value.trim() && e.target.value.trim() !== '') {
update({ name: e.target.value.trim() });
}
else {
update({ name: null });
}
}} value={data.name ?? ''} placeholder={t('placeholder.name')} size="large" maxLength={32} showCount className={Style.titleInput}/>
</div>
<div>
{!!articleId && <Editor defaultConfig={{
autoFocus: true,
placeholder: t('placeholder.content'),
MENU_CONF: {
checkImage: customCheckImageFn,
uploadImage: {
// 自定义上传
async customUpload(file, insertFn) {
// TS 语法
// file 即选中的文件
const { name, size, type } = file;
const extension = name.substring(name.lastIndexOf(".") + 1);
const filename = name.substring(0, name.lastIndexOf("."));
const extraFile = {
entity: "article",
entityId: oakId || articleId,
origin: origin,
type: "image",
tag1: "source",
objectId: generateNewId(),
filename,
size,
extension,
bucket: "",
id: generateNewId(),
fileType: type,
};
try {
// 自己实现上传,并得到图片 url alt href
const url = await uploadFile(extraFile, file);
// 最后插入图片
insertFn(url, extraFile.filename);
}
catch (err) {
setMessage({
type: "error",
content: err.message,
});
}
},
},
uploadVideo: {
// 自定义上传
async customUpload(file, insertFn) {
// TS 语法
// file 即选中的文件
const { name, size, type } = file;
const extension = name.substring(name.lastIndexOf(".") + 1);
const filename = name.substring(0, name.lastIndexOf("."));
const extraFile = {
entity: "article",
entityId: oakId || articleId,
origin: origin,
type: "video",
tag1: "source",
objectId: generateNewId(),
filename,
size,
extension,
bucket: "",
id: generateNewId(),
fileType: type,
};
try {
// 自己实现上传,并得到图片 url alt href
const url = await uploadFile(extraFile, file);
// 最后插入图片
insertFn(url, url + "?vframe/jpg/offset/0");
}
catch (err) {
setMessage({
type: "error",
content: err.message,
});
}
},
},
},
}} onCreated={setEditor} onChange={(editor) => {
setHtml(editor.getHtml());
const headers = editor.getElemsByTypePrefix("header");
const tocItems = headers.map((header) => {
const text = SlateNode.string(header);
const { id, type } = header;
return {
text,
level: parseInt(type.substring(6)),
id,
};
});
setToc([...tocItems]);
}} style={{
minHeight: '100%',
}} mode="default"/>}
</div>
</div>
</div>
{tocPosition === "right" ? (<TocView toc={toc} showToc={showToc} tocPosition="right" setShowToc={setShowToc} activeColor={activeColor} scrollId={containerId} tocWidth={tocWidth} tocHeight={tocHeight}/>) : null}
</div>
</div>);
}

View File

@ -0,0 +1,148 @@
.container {
width: 100%;
height: 100%;
max-height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 0px 12px 12px 12px;
overflow: hidden;
}
.top {
display: flex;
flex-direction: column;
position: sticky;
top: 0px;
z-index: 999;
box-sizing: border-box;
gap: 8px;
padding: 12px 0px;
}
.header {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px;
}
.title {
font-size: 20px;
font-weight: 600;
}
.toolbar {
width: 100%;
box-sizing: border-box;
flex: 1;
}
.contentContainer {
display: flex;
flex-direction: row;
flex: 1;
gap: 8px;
overflow-y: auto;
height: 100%;
}
.content {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
}
.editorContainer {
flex: 1;
width: 100%;
max-width: 794px;
display: flex;
flex-direction: column;
padding: 20px 50px 50px 50px;
background-color: #fff;
}
.editorExternalContainer {
overflow-y: unset;
max-height: unset;
}
.titleContainer {
padding: 5px 0;
border-bottom: 1px solid #e8e8e8;
}
.input {
border: unset !important;
box-shadow: none !important;
}
.titleInput {
border: unset !important;
box-shadow: none !important;
font-size: 18px !important;
font-weight: bold !important;
}
.input:hover {
border: unset !important;
}
.footer {
bottom: 0;
height: 50px;
}
.highlight {
background-color: var(--highlight-bg-color);
transition: all 1s ease;
}
.catalogTitle {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #DDD;
padding: 34px 6px 10px 6px;
box-sizing: border-box;
position: sticky;
top: 0;
background-color: #fff;
}
.listItem {
display: flex;
align-items: center;
justify-content: flex-start;
cursor: pointer;
line-height: 28px;
}
.listItem .icon {
opacity: 0;
margin-right: 8px;
transition: opacity 0.3s ease;
}
.listItem:hover .icon {
opacity: 1;
}
.listItem:hover .tocItem {
text-decoration: underline;
}
.iconFold {
transform: rotate(-90deg);
transform-origin: center;
}
.fold {
display: none;
}

9
es/components/article/list/index.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import { GenerateUrlFn } from "../../../types/Article";
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "article", true, {
entityId: string;
articleMenuId: string | undefined;
generateUrl: GenerateUrlFn;
empty: React.ReactNode | undefined;
menuCheck: (isArticle: boolean) => void;
}>) => React.ReactElement;
export default _default;

View File

@ -0,0 +1,124 @@
import dayjs from "dayjs";
import copy from 'copy-to-clipboard';
export default OakComponent({
entity: 'article',
isList: true,
projection: {
id: 1,
name: 1,
articleMenuId: 1,
$$updateAt$$: 1,
},
sorters: [
{
sorter: {
$attr: {
$$createAt$$: 1,
},
$direction: 'asc',
},
},
],
filters: [
{
filter() {
const { articleMenuId } = this.props;
return {
articleMenuId,
};
}
}
],
formData({ data, }) {
return {
articles: data?.map((ele) => {
return {
...ele,
updateAtStr: ele.$$updateAt$$ ? dayjs(ele.$$updateAt$$).format('YYYY-MM-DD HH:mm:ss') : '--',
};
}),
};
},
properties: {
entityId: '',
articleMenuId: '',
generateUrl: ((mode, type, id) => { }),
empty: undefined,
menuCheck: (isArticle) => undefined,
},
// data: {
// unsub: undefined as undefined | (() => void),
// },
// listeners: {
// async entityId(prev, next) {
// if (prev.entityId !== next.entityId) {
// if (prev.entityId) {
// const { unsub } = this.state;
// console.log('listener unsub');
// unsub && unsub();
// }
// if (next.entityId) {
// const unsub = await this.subDataEvents([`${DATA_SUBSCRIBER_KEYS.articleCreate}-${next.entityId}`]);
// console.log('listener sub');
// this.setState({
// unsub,
// })
// }
// }
// }
// },
// lifetimes: {
// async ready() {
// const { entityId, } = this.props;
// if (entityId) {
// const unsub = await this.subDataEvents([`${DATA_SUBSCRIBER_KEYS.articleCreate}-${entityId}`]);
// console.log('ready sub');
// this.setState({
// unsub,
// })
// }
// },
// detached() {
// const { unsub } = this.state;
// console.log('detached unsub')
// unsub && unsub();
// },
// },
methods: {
goDetail(articleId) {
const { generateUrl } = this.props;
const url = generateUrl ? generateUrl('article', 'detail', articleId) : '';
window.open(url, '_blank');
},
onCopy(articleId) {
const { generateUrl } = this.props;
const url = generateUrl ? generateUrl('article', 'copy', articleId) : '';
copy(url);
this.setMessage({
type: 'success',
content: this.t('success.copy'),
});
},
onEditor(articleId) {
const { generateUrl } = this.props;
const url = generateUrl ? generateUrl('article', 'editor', articleId) : '';
window.open(url, '_blank');
},
afterDelete() {
const { articleMenuId, menuCheck } = this.props;
if (articleMenuId && menuCheck) {
const [menu] = this.features.cache.get('articleMenu', {
data: {
id: 1,
isArticle: 1,
},
filter: {
id: articleMenuId,
}
});
const { isArticle } = menu;
menuCheck(!!isArticle);
}
}
}
});

View File

@ -0,0 +1,19 @@
{
"name": "文档",
"updateAt": "最近编辑",
"editor": "编辑文档",
"preview": "预览",
"copy": "复制链接",
"delete": "删除文档",
"removeConfirm": {
"title": "您确认要删除<%{name}>文档吗?",
"content": "删除后无法找回,相关链接将失效"
},
"success": {
"remove": "文档已删除",
"copy": "复制成功"
},
"fail": {
"remove": "删除失败"
}
}

14
es/components/article/list/web.pc.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import React from 'react';
import { WebComponentProps } from "oak-frontend-base";
import { EntityDict } from "../../../oak-app-domain";
export default function Render(props: WebComponentProps<EntityDict, 'article', true, {
articles: (EntityDict['article']['Schema'] & {
updateAtStr: string;
})[];
empty: React.ReactNode | undefined;
}, {
goDetail: (articleId: string) => void;
onCopy: (articleId: string) => void;
onEditor: (articleId?: string) => void;
afterDelete: () => void;
}>): React.JSX.Element;

View File

@ -0,0 +1,122 @@
import React from 'react';
import { Modal, Table, Button, ConfigProvider, Tooltip, Space } from 'antd';
import Styles from './web.pc.module.less';
import { CopyOutlined, DeleteOutlined, EditOutlined, ExclamationCircleFilled, EyeOutlined } from '@ant-design/icons';
import { generateNewId } from 'oak-domain/lib/utils/uuid';
import Pagination from 'oak-frontend-base/es/components/pagination';
const { confirm } = Modal;
export default function Render(props) {
const { data, methods } = props;
const { articles, oakFullpath, oakEntity, oakLoading, empty, } = data;
const { t, goDetail, onCopy, onEditor, afterDelete } = props.methods;
const IconButton = (props) => {
const { icon, onClick, disabled, tooltip, style, linkHoverBg = 'var(--oak-bg-color-container-hover)' } = props;
const Btn = (<Button disabled={disabled} type="link" icon={icon} onClick={onClick} style={style}></Button>);
return (<ConfigProvider theme={{
components: {
Button: {
fontWeight: 600,
contentFontSize: 13,
borderRadius: 4,
controlHeight: 28,
linkHoverBg,
},
},
}}>
{tooltip ? <Tooltip title={tooltip}>{Btn}</Tooltip> : Btn}
</ConfigProvider>);
};
const articleAttr = [
{
title: t('name'),
key: 'name',
dataIndex: 'name',
ellipsis: true,
width: 600,
render: (_, record) => {
return (<div className={Styles.name} onClick={() => {
onEditor(record.id);
}}>{record.name}</div>);
}
},
{
title: t('updateAt'),
key: 'updateAt',
dataIndex: 'updateAtStr',
// width: 180,
ellipsis: true,
},
{
title: t('common::operate'),
key: 'operate',
fixed: 'right',
render: (_, record) => {
return (<Space size={[8, 0]}>
<IconButton style={{ marginTop: 4 }} tooltip={t('editor')} icon={<EditOutlined />} onClick={() => {
onEditor(record.id);
}}></IconButton>
<IconButton style={{ marginTop: 4 }} tooltip={t('preview')} icon={<EyeOutlined />} onClick={() => {
goDetail(record.id);
}}></IconButton>
<IconButton style={{ marginTop: 4 }} tooltip={t('copy')} icon={<CopyOutlined />} onClick={(e) => {
e.stopPropagation();
onCopy(record.id);
}}></IconButton>
<IconButton tooltip={t('delete')} icon={<DeleteOutlined />} onClick={() => { showRemoveConfirm(record.id, record.name); }}></IconButton>
</Space>);
},
}
];
const showRemoveConfirm = (articleId, articleName) => {
confirm({
title: t('removeConfirm.title', { name: articleName }),
icon: <ExclamationCircleFilled />,
content: <div style={{ color: 'var(--oak-color-error)' }}>{t('removeConfirm.content')}</div>,
async onOk() {
try {
await methods.execute(undefined, {
type: 'success',
content: t('success.remove')
}, undefined, [
{
entity: 'article',
operation: {
id: generateNewId(),
action: 'remove',
data: {},
filter: {
id: articleId,
},
},
},
]);
afterDelete();
}
catch (err) {
methods.setMessage({
type: 'error',
content: t('fail.remove') + err?.message
});
}
},
okButtonProps: {
danger: true,
},
okText: t('common::action.confirm'),
cancelText: t('common::action.cancel')
});
};
return (<>
<Table rowKey="id" dataSource={articles} loading={oakLoading} columns={articleAttr} locale={{
emptyText: empty,
}} pagination={false}/>
<div style={{
display: "flex",
flexDirection: "row-reverse",
alignItems: "center",
}}>
<Pagination oakPath={oakFullpath} entity={oakEntity} oakAutoUnmount={true} showQuickJumper={true}/>
</div>
</>);
}

View File

@ -0,0 +1,33 @@
import .container {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
min-height: 100%;
}
.name {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
cursor: pointer;
}
.name:hover {
color: var(--oak-color-primary);
}
.btnContainer {
min-height: 180px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.help {
display: flex;
flex-direction: column;
margin-bottom: 24px;
}
}

View File

@ -7,6 +7,8 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
className: string;
scrollId: string;
tocWidth: number | "auto" | undefined;
tocHeight: number | "auto" | undefined;
tocHeight: number | undefined;
showtitle: boolean;
activeColor: string | undefined;
}>) => React.ReactElement;
export default _default;

View File

@ -28,11 +28,13 @@ export default OakComponent({
tocFixed: true,
tocPosition: 'none',
highlightBgColor: 'none',
headerTop: 0,
headerTop: 0, //页面中吸顶部分高度
className: '',
scrollId: '',
scrollId: '', // 滚动条所在容器id不传默认body
tocWidth: undefined,
tocHeight: undefined,
showtitle: false, //大纲顶层显示文章名称
activeColor: undefined,
},
methods: {},
});

View File

@ -2,14 +2,17 @@ import React from 'react';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';
export default function Render(props: WebComponentProps<EntityDict, 'article', false, {
title: string;
content?: string;
tocPosition: 'none' | 'left' | 'right';
highlightBgColor: string;
highlightBgColor?: string;
headerTop: number;
className?: string;
tocFixed: boolean;
tocClosed: boolean;
scrollId?: string;
tocWidth?: number | 'auto';
tocHeight?: number | 'auto';
tocWidth?: number;
tocHeight?: number | string;
showtitle: boolean;
activeColor?: string;
}, {}>): React.JSX.Element;

View File

@ -5,7 +5,7 @@ import classNames from 'classnames';
import { TocView } from '../toc/tocView';
import Styles from './web.module.less';
export default function Render(props) {
const { className, content, tocPosition = 'none', tocFixed, highlightBgColor = 'none', headerTop = 0, tocClosed = false, scrollId, tocWidth, tocHeight } = props.data;
const { className, title, content, tocPosition = 'none', tocFixed, highlightBgColor, activeColor, headerTop = 0, tocClosed = false, scrollId, tocWidth, tocHeight, showtitle } = props.data;
const editorConfig = {
readOnly: true,
autoFocus: true,
@ -35,7 +35,9 @@ export default function Render(props) {
}, [tocPosition]);
return (<div className={classNames(Styles.container, className)}>
<div className={Styles.contentContainer}>
{tocPosition === "left" && (<TocView toc={toc} showToc={showToc} tocPosition="left" setShowToc={setShowToc} highlightBgColor={highlightBgColor} headerTop={headerTop} fixed={tocFixed} closed={tocClosed} scrollId={scrollId} tocWidth={tocWidth} tocHeight={tocHeight}/>)}
{tocPosition === "left" && (<TocView toc={toc} showToc={showToc} tocPosition="left" setShowToc={setShowToc}
// highlightBgColor={highlightBgColor}
activeColor={activeColor} headerTop={headerTop} closed={tocClosed} scrollId={scrollId} tocWidth={tocWidth} tocHeight={tocHeight ? `calc(${tocHeight} - 32px)` : 'calc(100vh - 32px)'} title={showtitle ? title : undefined}/>)}
<div className={Styles.content}>
<div className={Styles.editorContainer}>
@ -59,7 +61,9 @@ export default function Render(props) {
</div>
</div>
</div>
{tocPosition === "right" && (<TocView toc={toc} showToc={showToc} tocPosition="right" setShowToc={setShowToc} highlightBgColor={highlightBgColor} headerTop={headerTop} fixed={tocFixed} closed={tocClosed} scrollId={scrollId} tocWidth={tocWidth} tocHeight={tocHeight}/>)}
{tocPosition === "right" && (<TocView toc={toc} showToc={showToc} tocPosition="right" setShowToc={setShowToc}
// highlightBgColor={highlightBgColor}
activeColor={activeColor} headerTop={headerTop} closed={tocClosed} scrollId={scrollId} tocWidth={tocWidth} tocHeight={tocHeight} title={showtitle ? title : undefined}/>)}
</div>
</div>);
}

View File

@ -1,4 +1,4 @@
/// <reference types="react" />
import React from "react";
export type TocItem = {
id: string;
level: number;
@ -9,11 +9,12 @@ export declare function TocView(props: {
showToc: boolean;
tocPosition: 'left' | 'right';
setShowToc: (showToc: boolean) => void;
highlightBgColor: string;
highlightBgColor?: string;
activeColor?: string;
headerTop?: number;
fixed?: boolean;
scrollId?: string;
closed?: boolean;
tocWidth?: number | 'auto';
tocHeight?: number | 'auto';
}): import("react").JSX.Element;
tocWidth?: number;
tocHeight?: number | string;
title?: string;
}): React.JSX.Element;

View File

@ -1,13 +1,16 @@
import { useEffect } from "react";
import React, { useEffect } from "react";
import { Button, Tooltip } from "antd";
import { CaretDownOutlined, CloseOutlined, MenuOutlined } from "@ant-design/icons";
import classNames from "classnames";
import Style from './tocView.module.less';
export function TocView(props) {
const { toc, showToc, tocPosition, setShowToc, highlightBgColor, headerTop = 0, scrollId, fixed = false, closed = false, tocWidth, tocHeight } = props;
const { toc, showToc, tocPosition, setShowToc, highlightBgColor, activeColor = 'var(--oak-color-primary)', headerTop = 0, scrollId, closed = false, tocWidth, tocHeight = '100vh', title } = props;
// useEffect(() => {
// document.documentElement.style.setProperty('--highlight-bg-color', highlightBgColor);
// }, [highlightBgColor]);
useEffect(() => {
document.documentElement.style.setProperty('--highlight-bg-color', highlightBgColor);
}, [highlightBgColor]);
document.documentElement.style.setProperty('--active-color', activeColor);
}, [activeColor]);
const generateTocList = (items, currentLevel = 1, parentId = null) => {
//递归生成嵌套列表
const result = [];
@ -45,34 +48,7 @@ export function TocView(props) {
ulElem?.classList.add(Style.fold);
}
}}/>
<div style={{ fontSize: '1em', fontWeight: item.level === 1 ? 'bold' : 'normal' }} className={Style.tocItem} onClick={(event) => {
//页面滚动到对应元素
const elem = document.getElementById(item.id);
const elemTop = elem?.getBoundingClientRect().top;
if (scrollId) {
const scrollContainer = document.getElementById(scrollId);
const containerTop = scrollContainer?.getBoundingClientRect().top;
scrollContainer?.scrollBy({
top: elemTop - containerTop,
behavior: 'smooth',
});
}
else {
// const containerTop = document.body.getBoundingClientRect().top;
window.scrollBy({
top: elemTop - headerTop,
behavior: 'smooth',
});
}
//添加背景色
elem?.classList.add(Style.highlight);
//移除背景色类名
setTimeout(function () {
elem?.classList.remove(Style.highlight);
}, 1000);
event.preventDefault();
event.stopPropagation();
}}>
<div style={{ fontSize: '1em', fontWeight: item.level === 1 ? 'bold' : 'normal' }} className={Style.tocItem}>
{item.text}
</div>
</li>);
@ -82,23 +58,151 @@ export function TocView(props) {
}
return result;
};
return (<div className={classNames(Style.tocContainer, {
[Style.fixed]: fixed
})} style={Object.assign({}, tocWidth ? { width: tocWidth } : {}, tocHeight ? { height: tocHeight } : {})}>
useEffect(() => {
const tocContainer = document.getElementById('tocContainer');
const tocItems = tocContainer?.querySelectorAll('li');
let sections = [];
for (const t of toc) {
const item = document.getElementById(t.id);
sections.push(item);
}
let currentHighlighted = '';
let isClick = false;
let clickTimeout = undefined;
// 滚动目录以确保指定目录项可见
function scrollToTocItem(item) {
if (tocContainer) {
const tocRect = tocContainer.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
// 检查目录项是否在可视区域内
if (itemRect.top < tocRect.top || itemRect.bottom > tocRect.bottom) {
const itemOffsetTop = item.offsetTop;
const containerHeight = tocContainer.clientHeight;
const itemHeight = item.offsetHeight;
let targetScrollTop;
if (itemRect.top < tocRect.top) {
// 目录项在可视区域上方
targetScrollTop = itemOffsetTop - 80;
}
else {
// 目录项在可视区域下方
targetScrollTop = itemOffsetTop - containerHeight + itemHeight + 10;
}
tocContainer.scrollTo({
top: targetScrollTop,
behavior: 'smooth'
});
}
}
}
// 高亮指定的目录项
function highlightTocItem(targetId, source) {
// 移除所有目录项的active样式
tocItems?.forEach(item => {
item.classList.remove(Style.active);
});
// 找到对应的目录项并添加active样式
const correspondingTocItem = document.querySelector(`#${targetId}`);
if (correspondingTocItem) {
correspondingTocItem.classList.add(Style.active);
// 滚动目录
scrollToTocItem(correspondingTocItem);
// 如果是点击触发的,设置超时
if (source === 'click') {
isClick = true;
// 设置超时1秒后恢复滚动的高亮功能
clearTimeout(clickTimeout);
clickTimeout = setTimeout(() => {
isClick = false;
}, 1000);
}
currentHighlighted = targetId;
}
}
// 点击目录项平滑滚动到对应区块并高亮
const handleClick = function (event) {
const targetSection = event.target.closest('li');
const targetId = targetSection.getAttribute('id');
if (targetSection) {
// 高亮点击的目录项
highlightTocItem(targetId, 'click');
// 平滑滚动到目标位置
const itemId = targetId.substring(3);
const elem = document.getElementById(itemId);
const elemTop = elem?.getBoundingClientRect().top;
if (scrollId) {
const scrollContainer = document.getElementById(scrollId);
const containerTop = scrollContainer?.getBoundingClientRect().top;
scrollContainer?.scrollBy({
top: elemTop - containerTop,
behavior: 'smooth',
});
}
else {
window.scrollBy({
top: elemTop - headerTop,
behavior: 'smooth',
});
}
}
};
tocItems?.forEach(item => {
item.addEventListener('click', handleClick);
});
// 创建Intersection Observer实例
const observer = new IntersectionObserver(entries => {
// 如果处于点击状态,跳过滚动检测
if (isClick)
return;
entries.forEach(entry => {
if (entry.isIntersecting) {
// 获取当前可见区块的ID
const id = `li-${entry.target.getAttribute('id')}`;
if (id && id !== currentHighlighted) {
highlightTocItem(id, 'scroll');
}
}
});
}, {
threshold: 0.5,
rootMargin: '0px 0px -75% 0px',
});
// 开始观察所有内容区块
sections?.forEach(section => {
if (section) {
observer.observe(section);
}
});
return () => {
if (tocContainer) {
tocContainer.removeEventListener('click', handleClick);
observer.disconnect();
if (clickTimeout) {
clearTimeout(clickTimeout);
}
}
};
}, [toc, showToc]);
return (<div className={Style.tocContainer} style={Object.assign({}, tocWidth ? { width: tocWidth } : {}, tocHeight ? { height: tocHeight } : {})}>
{showToc ? (<>
<div className={Style.catalogTitle}>
<div style={{ color: '#A5A5A5' }}>大纲</div>
<div style={{ color: '#A5A5A5' }}>{title ?? '大纲'}</div>
{closed ? (<CloseOutlined style={{ color: '#A5A5A5' }} onClick={() => setShowToc(false)}/>) : null}
</div>
{(toc && toc.length > 0) ? (<ul style={{ listStyleType: 'none', paddingInlineStart: '0px' }}>{generateTocList([...toc])}</ul>) : (<div style={{ display: 'flex', alignItems: 'center', color: '#B1B1B1', height: '200px' }}>
{(toc && toc.length > 0) ? (<ul id="tocContainer" style={{ listStyleType: 'none', paddingInlineStart: '0px', overflowX: 'hidden', overflowY: 'auto', height: '100%', borderLeft: tocPosition === 'right' ? '1px solid var(--oak-border-color)' : '' }}>{generateTocList([...toc])}</ul>) : (<div style={{ display: 'flex', alignItems: 'center', color: '#B1B1B1', height: '200px' }}>
<div>
对文档内容应用标题样式即可生成大纲
<div>
对文档内容应用标题样式
</div>
<div>
即可生成大纲
</div>
</div>
</div>)}
</>) : (<div className={classNames(Style.tocButton, { [Style.tocButtonRight]: tocPosition === 'right' })}>
<Tooltip title="显示大纲" placement={tocPosition === 'right' ? 'left' : 'right'}>
<Button size="small" icon={<MenuOutlined />} onClick={() => setShowToc(true)}/>
<Button size="small" icon={<MenuOutlined />} onClick={() => setShowToc(true)}/>
</Tooltip>
</div>)}
</div>);

View File

@ -1,5 +1,5 @@
.tocContainer {
position: relative;
// position: relative;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
@ -7,11 +7,11 @@
display: flex;
flex-direction: column;
min-width: 228px;
max-width: 300px;
position: sticky;
top: 16px;
}
.fixed {
position: fixed;
}
.tocButton {
display: flex;
@ -38,6 +38,10 @@
transition: all 1s ease;
}
.active {
color: var(--active-color);
}
.catalogTitle {
display: flex;
justify-content: space-between;

View File

@ -1,7 +1,7 @@
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "article", true, {
articleMenuId: string | undefined;
onChildEditArticleChange: (data: string) => void;
show: "preview" | "edit" | "doc";
show: "edit" | "doc" | "preview";
getBreadcrumbItemsByParent: (breadcrumbItems: string[]) => void;
breadcrumbItems: string[];
drawerOpen: boolean;

View File

@ -4,7 +4,7 @@ export default OakComponent({
properties: {
articleMenuId: '',
onChildEditArticleChange: (data) => undefined,
show: 'edit',
show: 'edit', // edit为编辑doc为查看preview为预览
getBreadcrumbItemsByParent: (breadcrumbItems) => undefined,
breadcrumbItems: [],
drawerOpen: false,

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import Styles from './web.pc.module.less';
import { Input, Button, Divider, Modal } from 'antd';
import { Input, Button, Divider, Modal, Tooltip } from 'antd';
import { EditOutlined, MinusOutlined, CopyOutlined } from '@ant-design/icons';
import copy from 'copy-to-clipboard';
export default function Render(props) {
@ -87,11 +87,13 @@ export default function Render(props) {
setNameEditing('');
}}/>
</div> : <>
<Button type="text" icon={<EditOutlined />} size="small" onClick={(e) => {
<Tooltip title={'编辑标题'}>
<Button type="text" icon={<EditOutlined />} size="small" onClick={(e) => {
setName(ele.name);
setNameEditing(ele.id);
e.stopPropagation();
}}/>
</Tooltip>
<div className={Styles.name}>
<div style={{ marginLeft: 4, overflow: 'hidden', width: '150px', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ele?.name}</div>
</div>
@ -99,7 +101,8 @@ export default function Render(props) {
</div>
<Divider type="vertical" style={{ height: '100%', marginTop: 4, marginBottom: 4 }}/>
<div className={Styles.control}>
<Button type="text" icon={<CopyOutlined />} size="small" onClick={(e) => {
<Tooltip title={'复制链接'}>
<Button type="text" icon={<CopyOutlined />} size="small" onClick={(e) => {
e.stopPropagation();
const url = setCopyArticleUrl(ele.id);
copy(url);
@ -108,7 +111,9 @@ export default function Render(props) {
type: 'success',
});
}}/>
<Button type="text" icon={<MinusOutlined />} size="small" onClick={(e) => {
</Tooltip>
<Tooltip title={'删除文档'}>
<Button type="text" icon={<MinusOutlined />} size="small" onClick={(e) => {
e.stopPropagation();
modal.confirm({
title: '请确认',
@ -122,6 +127,7 @@ export default function Render(props) {
}
});
}}/>
</Tooltip>
</div>
</div>
<Divider style={{ margin: 1 }}/>

View File

@ -5,8 +5,9 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
tocPosition: "none" | "left" | "right";
highlightBgColor: string;
onArticlePreview: (content?: string, title?: string) => void;
origin: string;
origin: EntityDict["extraFile"]["Schema"]["origin"] | null;
scrollId: string;
height: number | "auto";
activeColor: string | undefined;
}>) => React.ReactElement;
export default _default;

View File

@ -25,12 +25,13 @@ export default OakComponent({
properties: {
articleMenuId: '',
changeIsEdit: () => undefined,
tocPosition: 'none',
highlightBgColor: 'none',
onArticlePreview: (content, title) => undefined,
origin: 'qiniu',
scrollId: '',
tocPosition: 'none', //目录显示位置none为不显示目录
highlightBgColor: 'none', //点击目录时标题高亮背景色none为不显示高亮背景色
onArticlePreview: (content, title) => undefined, //预览文章
origin: null, // 默认为七牛云
scrollId: '', // 滚动条所在容器id不传默认页面编辑器容器id
height: 600,
activeColor: undefined,
},
listeners: {
'editor,content'(prev, next) {
@ -74,7 +75,10 @@ export default OakComponent({
},
methods: {
async uploadFile(extraFile, file) {
const result = await this.features.extraFile.autoUpload(extraFile, file);
const result = await this.features.extraFile.autoUpload({
extraFile: extraFile,
file: file
});
return result;
},
setEditor(editor) {
@ -90,8 +94,8 @@ export default OakComponent({
async check() {
if (this.state.name &&
this.state.name.length > 0 &&
this.state.content &&
this.state.content.length > 0 &&
this.state.html &&
this.state.html.length > 0 &&
this.state.html !== '<p><br></p>') {
await this.execute();
if (this.props.changeIsEdit) {
@ -104,8 +108,8 @@ export default OakComponent({
type: 'warning',
});
}
else if (this.state.content &&
this.state.content.length > 0 &&
else if (this.state.html &&
this.state.html.length > 0 &&
this.state.html !== '<p><br></p>') {
this.setMessage({
content: '请填写文章标题!',
@ -113,17 +117,10 @@ export default OakComponent({
});
}
},
async reset() {
// 重置
this.clean();
},
setHtml(html) {
this.setState({
html,
});
if (html && html !== '<p><br></p>' && this.state.oakFullpath) {
this.update({ content: html });
}
},
gotoPreview(content, title) {
const { onArticlePreview } = this.props;

View File

@ -7,16 +7,18 @@ export default function Render(props: WebComponentProps<EntityDict, 'article', f
name: string;
editor: any;
content?: string;
origin?: string;
origin?: null | EntityDict['extraFile']['Schema']['origin'];
contentTip: boolean;
articleMenuId: string;
oakId: string;
tocPosition: 'none' | 'left' | 'right';
highlightBgColor: string;
highlightBgColor?: string;
scrollId?: string;
tocWidth?: number | 'auto';
tocHeight?: number | 'auto';
height?: number | 'auto';
tocWidth?: number;
tocHeight?: number | string;
height?: number | string;
activeColor?: string;
html: string;
}, {
setHtml: (content: string) => void;
setEditor: (editor: any) => void;

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Alert, Button, Space, Input, } from "antd";
import { Button, Space, Input, } from "antd";
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
import { SlateNode } from "@wangeditor/editor";
@ -30,8 +30,8 @@ function customCheckImageFn(src, alt, url) {
}
export default function Render(props) {
const { methods, data } = props;
const { t, setEditor, check, uploadFile, update, setHtml, gotoPreview, clearContentTip, } = methods;
const { oakFullpath, id, content, editor, origin = 'qiniu', tocPosition = 'none', highlightBgColor = 'none', scrollId, tocWidth, tocHeight, height = 600 } = data;
const { t, setMessage, setEditor, check, uploadFile, update, setHtml, gotoPreview, clearContentTip, } = methods;
const { oakId, oakFullpath, id, content, editor, origin, tocPosition = 'none', highlightBgColor, activeColor, scrollId, tocWidth, tocHeight, height = 600, html, oakLoading, oakExecuting, } = data;
const [articleId, setArticleId] = useState('');
const [toc, setToc] = useState([]);
const [showToc, setShowToc] = useState(false);
@ -52,18 +52,27 @@ export default function Render(props) {
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default"/>
</div>
<div className={Style.contentContainer}>
{tocPosition === "left" ? (<TocView toc={toc} showToc={showToc} tocPosition="left" setShowToc={setShowToc} highlightBgColor={highlightBgColor} scrollId={containerId} tocWidth={tocWidth} tocHeight={tocHeight || height}/>) : null}
{tocPosition === "left" ? (<TocView toc={toc} showToc={showToc} tocPosition="left" setShowToc={setShowToc}
// highlightBgColor={highlightBgColor}
activeColor={activeColor} scrollId={containerId} tocWidth={tocWidth} tocHeight={tocHeight || (height === 'auto' ? '100vh' : height)}/>) : null}
<div className={Style.content} style={{ maxWidth: `calc(100% - ${tocWidth || 228}px)` }}>
<div id={containerId} className={classNames(Style.editorContainer, {
<div className={Style.content} style={{ maxWidth: `calc(100% - ${tocWidth || 228}px)` }}>
<div id={containerId} className={classNames(Style.editorContainer, {
[Style.editorExternalContainer]: !!scrollId,
})}>
{data.contentTip && (<Alert type="info" message={t("tips.content")} closable onClose={() => clearContentTip()}/>)}
<div className={Style.titleContainer}>
<Input onChange={(e) => update({ name: e.target.value })} value={data.name} placeholder={"请输入文章标题"} size="large" maxLength={32} suffix={`${(data.name || "").length}/32`} className={Style.titleInput}/>
</div>
<div className={Style.editorContent}>
<Editor defaultConfig={{
{/* {data.contentTip && (
<Alert
type="info"
message={t("tips.content")}
closable
onClose={() => clearContentTip()}
/>
)} */}
<div className={Style.titleContainer}>
<Input onChange={(e) => update({ name: e.target.value })} value={data.name} placeholder={"请输入文章标题"} size="large" maxLength={32} suffix={`${(data.name || "").length}/32`} className={Style.titleInput}/>
</div>
<div className={Style.editorContent}>
<Editor defaultConfig={{
autoFocus: true,
placeholder: "请输入文章内容...",
MENU_CONF: {
@ -78,7 +87,7 @@ export default function Render(props) {
const filename = name.substring(0, name.lastIndexOf("."));
const extraFile = {
entity: "article",
entityId: articleId,
entityId: oakId || articleId,
origin: origin,
type: "image",
tag1: "source",
@ -96,7 +105,12 @@ export default function Render(props) {
// 最后插入图片
insertFn(url, extraFile.filename);
}
catch (err) { }
catch (err) {
setMessage({
type: "error",
content: err.message,
});
}
},
},
uploadVideo: {
@ -109,7 +123,7 @@ export default function Render(props) {
const filename = name.substring(0, name.lastIndexOf("."));
const extraFile = {
entity: "article",
entityId: articleId,
entityId: oakId || articleId,
origin: origin,
type: "video",
tag1: "source",
@ -128,7 +142,12 @@ export default function Render(props) {
insertFn(url, url +
"?vframe/jpg/offset/0");
}
catch (err) { }
catch (err) {
setMessage({
type: "error",
content: err.message,
});
}
},
},
},
@ -148,25 +167,29 @@ export default function Render(props) {
}} style={{
minHeight: 440,
}} mode="default"/>
</div>
</div>
<div className={Style.footer}>
<Space>
<Button disabled={!data.oakDirty ||
data.oakExecuting} type="primary" onClick={() => {
check();
}}>
保存
</Button>
<Button onClick={() => {
gotoPreview(content, data.name);
}} icon={<EyeOutlined />}>
预览
</Button>
</Space>
</div>
</div>
{tocPosition === "right" ? (<TocView toc={toc} showToc={showToc} tocPosition="right" setShowToc={setShowToc} highlightBgColor={highlightBgColor} scrollId={containerId} tocWidth={tocWidth} tocHeight={tocHeight}/>) : null}
<div className={Style.footer}>
<Space>
<Button disabled={oakLoading || oakExecuting || !(data.name && data.name.length > 0 && html && html.length > 0 && html !== '<p><br></p>')} type="primary" onClick={() => {
update({
content: html,
});
check();
}}>
保存
</Button>
<Button onClick={() => {
gotoPreview(html, data.name);
}} icon={<EyeOutlined />}>
预览
</Button>
</Space>
</div>
</div>
{tocPosition === "right" ? (<TocView toc={toc} showToc={showToc} tocPosition="right" setShowToc={setShowToc}
// highlightBgColor={highlightBgColor}
activeColor={activeColor} scrollId={containerId} tocWidth={tocWidth} tocHeight={tocHeight}/>) : null}
</div>
</div>);
}

View File

@ -60,46 +60,6 @@
height: 50px;
}
.contentNumber {
display: flex;
min-height: 32px;
flex-direction: column;
justify-content: center;
color: var(--oak-text-color-secondary);
}
.tocContainer {
// padding-top: 28px;
position: relative;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
display: flex;
flex-direction: column;
min-width: 228px;
}
.tocButton {
display: flex;
flex-direction: column;
margin-top: 8px;
align-items: flex-start;
}
.tocButtonRight {
align-items: flex-end;
}
.tocItem {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
flex: 1;
}
.highlight {
background-color: var(--highlight-bg-color);
transition: all 1s ease;

View File

@ -0,0 +1,12 @@
import { EntityDict } from "../../../oak-app-domain";
import { GenerateUrlFn } from "../../../types/Article";
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, false, {
entity: string;
entityId: string;
title: string;
origin: null | EntityDict["extraFile"]["Schema"]["origin"];
menuEmpty: React.ReactNode | undefined;
articleEmpty: React.ReactNode | undefined;
generateUrl: GenerateUrlFn;
}>) => React.ReactElement;
export default _default;

Some files were not shown because too many files have changed in this diff Show More