Compare commits
371 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
11f978b24c | |
|
|
b8654da5fb | |
|
|
3fae4dd855 | |
|
|
8660c6a1e3 | |
|
|
7312545eda | |
|
|
cf63a53109 | |
|
|
9027a40479 | |
|
|
5c3a02aa95 | |
|
|
b1f8300bbb | |
|
|
10b4e8dbd8 | |
|
|
0ca7f1b020 | |
|
|
0315b11ae2 | |
|
|
98ad339316 | |
|
|
b6a118efc5 | |
|
|
7862282214 | |
|
|
79a98e2d8f | |
|
|
e92f26f50c | |
|
|
9c5fb29259 | |
|
|
9025b7d8b4 | |
|
|
44288172f5 | |
|
|
570f6e8df5 | |
|
|
337782a54b | |
|
|
f585d154ea | |
|
|
fdf948d3ee | |
|
|
14d593cc89 | |
|
|
3fa730cfb1 | |
|
|
ab9789eb4c | |
|
|
2871934bcd | |
|
|
df9855906d | |
|
|
066d834d8d | |
|
|
a52a2874c6 | |
|
|
5543806a63 | |
|
|
f3d782fcbc | |
|
|
95b506b0eb | |
|
|
56b07dc4cb | |
|
|
147dc5eb12 | |
|
|
347e1ab360 | |
|
|
336f0b1aa1 | |
|
|
10077f150e | |
|
|
d5a09546dd | |
|
|
ffcac7f843 | |
|
|
4227430080 | |
|
|
1e5697a28e | |
|
|
a3ec5fc808 | |
|
|
ba434da707 | |
|
|
0e05a3ad4e | |
|
|
7c35f695ab | |
|
|
35f8bfcc0b | |
|
|
40d69bf0f4 | |
|
|
d347e19ede | |
|
|
97fd741a3b | |
|
|
6aff242687 | |
|
|
257f7669fc | |
|
|
edbeb63cc1 | |
|
|
5aa78b89b4 | |
|
|
7c2fba3191 | |
|
|
ab6616526d | |
|
|
67cf576ddc | |
|
|
ae1d1cf70a | |
|
|
21348fff43 | |
|
|
157dbfd88a | |
|
|
660b25a7d8 | |
|
|
fbfeb210bd | |
|
|
a5f7af8ec1 | |
|
|
1b61055ad4 | |
|
|
4e555c90e2 | |
|
|
1b90173e16 | |
|
|
365900b750 | |
|
|
f2ada8fbba | |
|
|
4b30f6c838 | |
|
|
8c8717433c | |
|
|
ef871317a6 | |
|
|
16eb5de960 | |
|
|
24396a8782 | |
|
|
5af3a8f9db | |
|
|
af88199ba7 | |
|
|
78eb039a04 | |
|
|
5ff8f1ed53 | |
|
|
5fbc76c93f | |
|
|
f57dacfe5d | |
|
|
2ed82709b8 | |
|
|
27687c5b2f | |
|
|
799246c21b | |
|
|
3cf3b4da09 | |
|
|
d8d78edde1 | |
|
|
ee57c1e932 | |
|
|
20708057da | |
|
|
696278927a | |
|
|
6a49d6f5af | |
|
|
68b3f93e9f | |
|
|
3995ff52f8 | |
|
|
d34c91bf07 | |
|
|
e8b3a5878e | |
|
|
b9783aa588 | |
|
|
3aec26e06f | |
|
|
7cb2479eab | |
|
|
ca958c60f0 | |
|
|
20030cc9ad | |
|
|
67341a64df | |
|
|
b839318c5c | |
|
|
acf99448a5 | |
|
|
63dca8d683 | |
|
|
126d43a90b | |
|
|
aeb3dee4c2 | |
|
|
e89661326e | |
|
|
818504690b | |
|
|
9751ee5e10 | |
|
|
169562994b | |
|
|
a1ed3c7ca7 | |
|
|
3f690e6725 | |
|
|
d7d302f329 | |
|
|
b93ae46894 | |
|
|
5b702a0d7d | |
|
|
5fb1988b0d | |
|
|
f26eb01807 | |
|
|
62dd001be3 | |
|
|
20cc101f88 | |
|
|
ccdd86923f | |
|
|
cd1bf2ce21 | |
|
|
e2f9a49d76 | |
|
|
5f0c366135 | |
|
|
ca06b24276 | |
|
|
7e05611da0 | |
|
|
0c86dc99e1 | |
|
|
75a303ed95 | |
|
|
212df98c60 | |
|
|
a4040a397b | |
|
|
077278d220 | |
|
|
a9f0493316 | |
|
|
21b170bdc5 | |
|
|
e9cb9809f1 | |
|
|
4675e4c0ea | |
|
|
84e64b9d66 | |
|
|
8fb96b1687 | |
|
|
e64ce53334 | |
|
|
5f33319f75 | |
|
|
d4ceb97260 | |
|
|
f66fc76c01 | |
|
|
706ea27664 | |
|
|
b1b31fa0e7 | |
|
|
83f0879d0c | |
|
|
39ae0d19eb | |
|
|
0cfaa9f93f | |
|
|
2bebe90f66 | |
|
|
f9123d81cc | |
|
|
502c028eb1 | |
|
|
d034741ba4 | |
|
|
a2dfa1d1f3 | |
|
|
44c969635f | |
|
|
b6aea50539 | |
|
|
0f90d5c360 | |
|
|
629cf2e9d5 | |
|
|
8e79f42e2c | |
|
|
3ed9055c15 | |
|
|
989c3c66ad | |
|
|
27db58a6f5 | |
|
|
e4295f5a4f | |
|
|
8367447700 | |
|
|
ded4eded1f | |
|
|
16e347b7e3 | |
|
|
840348d084 | |
|
|
964973daae | |
|
|
116feb8a80 | |
|
|
fcc9389812 | |
|
|
4e94e86d20 | |
|
|
4f41a93dd5 | |
|
|
75296485f1 | |
|
|
0271a93e31 | |
|
|
2c786b3d09 | |
|
|
c2f14287b4 | |
|
|
670e4525cd | |
|
|
5c06cf0a7b | |
|
|
7beeb8afeb | |
|
|
021276346d | |
|
|
4e005cec60 | |
|
|
859d09cf13 | |
|
|
f4c80b052c | |
|
|
6b29b03750 | |
|
|
c28bca6385 | |
|
|
fda2b1be6b | |
|
|
2199178421 | |
|
|
9ba261793b | |
|
|
4d8e3d15ff | |
|
|
d8c358f463 | |
|
|
6e6b8eb3b9 | |
|
|
cd7ab8a1d1 | |
|
|
a0dec2b277 | |
|
|
67fbd49c26 | |
|
|
91ff162908 | |
|
|
f1e7894e48 | |
|
|
d7b29e0d0e | |
|
|
e5821fb8a7 | |
|
|
f824463a2a | |
|
|
7baa7f808c | |
|
|
476b68ab1c | |
|
|
f10208e5d5 | |
|
|
9299a2ba66 | |
|
|
b1c33ebd9c | |
|
|
495b928dde | |
|
|
8b96a31bfb | |
|
|
7453b8dcae | |
|
|
e2627fd93b | |
|
|
9d86b9caa1 | |
|
|
95bae42ad8 | |
|
|
a6966ae02c | |
|
|
5d5210ac7d | |
|
|
88482b96c1 | |
|
|
6ce0cf80aa | |
|
|
0da6a3ebb2 | |
|
|
74f8d5eeec | |
|
|
92d7d95ca9 | |
|
|
a948027e6f | |
|
|
8c0d0f93d9 | |
|
|
e42ebf0707 | |
|
|
fae809e4bd | |
|
|
32fe168559 | |
|
|
60d8c5d147 | |
|
|
2376af227f | |
|
|
0cb03c1e4b | |
|
|
0cce61fea1 | |
|
|
a4ad5411a2 | |
|
|
d9927261e5 | |
|
|
4aa9073981 | |
|
|
472f8bf21e | |
|
|
11dc2ca89a | |
|
|
6c8efa9d00 | |
|
|
0e7db712bb | |
|
|
d8be2d5323 | |
|
|
483226791d | |
|
|
4bf2afa45c | |
|
|
122e868b52 | |
|
|
5f9de0de10 | |
|
|
38f0ae1a00 | |
|
|
7463494fce | |
|
|
0a359eb287 | |
|
|
d18e4cb1eb | |
|
|
d15bd7bfaf | |
|
|
32556a4c4a | |
|
|
fae9655cad | |
|
|
ef648c8fab | |
|
|
8af8487016 | |
|
|
c4c78aa885 | |
|
|
2bce7a738e | |
|
|
61901b302a | |
|
|
256f0eefa1 | |
|
|
d89a0a27ef | |
|
|
983d55e97a | |
|
|
24f328047d | |
|
|
82279251f1 | |
|
|
81d02f4ee4 | |
|
|
75c2547407 | |
|
|
d34acebeb3 | |
|
|
6f3cea700b | |
|
|
b1cd5e098a | |
|
|
1d0329521d | |
|
|
f11e910774 | |
|
|
89b2e229e1 | |
|
|
0b783a7a85 | |
|
|
bd1c30d8d9 | |
|
|
c7ebbf00f6 | |
|
|
5674bedb75 | |
|
|
9b932f01fd | |
|
|
c33ac19bf2 | |
|
|
c79b7ca63f | |
|
|
130740db93 | |
|
|
f42737c5db | |
|
|
e05ccb9243 | |
|
|
f3aaa4ca90 | |
|
|
4981a8d9b3 | |
|
|
dcbd9557bb | |
|
|
321a32e0bf | |
|
|
18a921ec55 | |
|
|
5a52956e1c | |
|
|
bfcfd1a10f | |
|
|
3f15890d9c | |
|
|
fa90f90558 | |
|
|
700bc110d7 | |
|
|
4e7d50d95e | |
|
|
341027ca82 | |
|
|
f8550dc263 | |
|
|
702e31e543 | |
|
|
6782b93e33 | |
|
|
faee5e9f3a | |
|
|
641ffd4b1f | |
|
|
26bda3c197 | |
|
|
01609d4093 | |
|
|
54201677cc | |
|
|
dd9558d37a | |
|
|
71498e8381 | |
|
|
049e1bef32 | |
|
|
246db21ab6 | |
|
|
bee5848640 | |
|
|
c0a18c0523 | |
|
|
2b51130217 | |
|
|
e8498d8d0e | |
|
|
6a7bb6b41b | |
|
|
3a2b1dd845 | |
|
|
80509da833 | |
|
|
5b69d9c6cc | |
|
|
7c5a09539a | |
|
|
d62395b743 | |
|
|
3b34a206bf | |
|
|
55c14a8121 | |
|
|
24a65e8ed2 | |
|
|
719b225227 | |
|
|
11962700ff | |
|
|
e51dca38ca | |
|
|
606b9e2023 | |
|
|
dae20932a9 | |
|
|
8a9976a090 | |
|
|
983ac16101 | |
|
|
1466a47c69 | |
|
|
e8b952388e | |
|
|
52336cb952 | |
|
|
7b7b8cb39e | |
|
|
1c7b305d85 | |
|
|
fe5aebc09f | |
|
|
a506eb032b | |
|
|
82822c204d | |
|
|
151458bba3 | |
|
|
3393bc5d5f | |
|
|
021fd49b7d | |
|
|
957c8fc6ed | |
|
|
b9405c265b | |
|
|
1e2a44523c | |
|
|
6932752ecd | |
|
|
d9b3277596 | |
|
|
d35a02a02f | |
|
|
0802f377cd | |
|
|
86b9424645 | |
|
|
6dc95be8ec | |
|
|
dd4c1fe94d | |
|
|
a3c2a22271 | |
|
|
57f0a03654 | |
|
|
945227640b | |
|
|
25fae3b35f | |
|
|
80fd4a42f0 | |
|
|
98c3f40f6f | |
|
|
9de758f486 | |
|
|
8490711b91 | |
|
|
99019b1a6b | |
|
|
8ca59369ec | |
|
|
41700a3f0d | |
|
|
9d756cb70e | |
|
|
753509cca8 | |
|
|
2fe9030905 | |
|
|
6cf4c61898 | |
|
|
356e965eef | |
|
|
a6a859f103 | |
|
|
a4329b140c | |
|
|
7021b0e548 | |
|
|
ce8d87829f | |
|
|
b5889714a8 | |
|
|
5aaec783bf | |
|
|
f8635876c6 | |
|
|
4ec6fdb46c | |
|
|
43bb3ab9bb | |
|
|
0c941fc4e3 | |
|
|
24731370d5 | |
|
|
61a0ec0a00 | |
|
|
08b432a02e | |
|
|
1a9a9a9bd8 | |
|
|
40df79637c | |
|
|
d924f889eb | |
|
|
44a596ba30 | |
|
|
d4092b9ccd | |
|
|
d897e1d723 | |
|
|
16fc448470 | |
|
|
d510a74180 | |
|
|
d548388b6c | |
|
|
7605071187 |
|
|
@ -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 返回签名信息(signature、noncestr、timestamp、appId)
|
||||
*/
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,15 @@ export async function getApplicationPassports(params, context) {
|
|||
config: 1,
|
||||
},
|
||||
isDefault: 1,
|
||||
allowPwd: 1,
|
||||
},
|
||||
filter: {
|
||||
applicationId,
|
||||
passport: {
|
||||
type: {
|
||||
$ne: 'password',
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
closeRoot();
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}[]>;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}>;
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}[];
|
||||
}[]>;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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是root,to也得赋上
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
// 直接创建
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(), '您不能更新他人信息');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="@uiw/react-amap-types" />
|
||||
import React from 'react';
|
||||
export type PositionProps = {
|
||||
loadUI: boolean;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="@uiw/react-amap-types" />
|
||||
import React from 'react';
|
||||
import { ModalProps } from 'antd';
|
||||
import { GeolocationProps } from '@uiw/react-amap';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="@uiw/react-amap-types" />
|
||||
import React from 'react';
|
||||
import { MapProps, APILoaderConfig } from '@uiw/react-amap';
|
||||
import './index.less';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"qiniu": "七牛云",
|
||||
"ctyun": "天翼云",
|
||||
"aliyun": "阿里云",
|
||||
"tencent": "腾讯云",
|
||||
"local": "本地存储",
|
||||
"s3": "S3存储"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
|
||||
.contains {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"whole": "共%{count}项"
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@
|
|||
"menu": "菜单管理",
|
||||
"autoReply": "被关注回复管理",
|
||||
"tag": "标签管理",
|
||||
"user": "用户管理"
|
||||
"user": "用户管理",
|
||||
"cos": "COS配置"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
</>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"success": "保存成功",
|
||||
"preview": "预览",
|
||||
"save": "保存",
|
||||
"exitConfirm": "您确认离开页面吗?",
|
||||
"placeholder": {
|
||||
"name": "请输入文章标题",
|
||||
"content": "请输入文章内容..."
|
||||
},
|
||||
"check": {
|
||||
"no name": "请填写文章标题!",
|
||||
"no content": "请填写文章内容!"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "文档",
|
||||
"updateAt": "最近编辑",
|
||||
"editor": "编辑文档",
|
||||
"preview": "预览",
|
||||
"copy": "复制链接",
|
||||
"delete": "删除文档",
|
||||
"removeConfirm": {
|
||||
"title": "您确认要删除<%{name}>文档吗?",
|
||||
"content": "删除后无法找回,相关链接将失效"
|
||||
},
|
||||
"success": {
|
||||
"remove": "文档已删除",
|
||||
"copy": "复制成功"
|
||||
},
|
||||
"fail": {
|
||||
"remove": "删除失败"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
</>);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }}/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue