committed by
GitHub
26 changed files with 517 additions and 14 deletions
@ -1,15 +1,98 @@ |
|||||
<script lang="ts" setup> |
<script lang="ts" setup> |
||||
import { Page } from '@vben/common-ui'; |
import type { BindItem } from '@abp/account'; |
||||
|
|
||||
import { MySetting } from '@abp/account'; |
import { defineAsyncComponent, ref } from 'vue'; |
||||
|
|
||||
|
import { Page, useVbenModal } from '@vben/common-ui'; |
||||
|
import { useRefresh } from '@vben/hooks'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { MySetting, useExternalLoginsApi } from '@abp/account'; |
||||
|
import { message, Modal } from 'ant-design-vue'; |
||||
|
|
||||
defineOptions({ |
defineOptions({ |
||||
name: 'Vben5AccountMySettings', |
name: 'Vben5AccountMySettings', |
||||
}); |
}); |
||||
|
|
||||
|
const { bindWorkWeixinApi, getExternalLoginsApi, removeExternalLoginApi } = |
||||
|
useExternalLoginsApi(); |
||||
|
const { refresh } = useRefresh(); |
||||
|
|
||||
|
const [WechatWorkUserBindModal, weComBindModal] = useVbenModal({ |
||||
|
connectedComponent: defineAsyncComponent(async () => { |
||||
|
const component = await import('@abp/wechat'); |
||||
|
return component.WechatWorkUserBinder; |
||||
|
}), |
||||
|
}); |
||||
|
const externalLogins = ref<BindItem[]>([]); |
||||
|
|
||||
|
async function onBindWorkWeixin(code: string) { |
||||
|
weComBindModal.setState({ submitting: true }); |
||||
|
try { |
||||
|
await bindWorkWeixinApi({ code }); |
||||
|
weComBindModal.close(); |
||||
|
message.success($t('AbpAccount.BindSuccessfully')); |
||||
|
refresh(); |
||||
|
} finally { |
||||
|
weComBindModal.setState({ submitting: false }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onRemoveBind(provider: string, key: string) { |
||||
|
Modal.confirm({ |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
centered: true, |
||||
|
content: $t('AbpAccount.CancelBindWarningMessage'), |
||||
|
async onOk() { |
||||
|
await removeExternalLoginApi({ |
||||
|
loginProvider: provider, |
||||
|
providerKey: key, |
||||
|
}); |
||||
|
message.success($t('AbpAccount.CancelBindSuccessfully')); |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onBind(provider: string) { |
||||
|
switch (provider.toLocaleLowerCase()) { |
||||
|
case 'workweixin': { |
||||
|
weComBindModal.open(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onClick(provider: string, key?: string) { |
||||
|
if (key) { |
||||
|
await onRemoveBind(provider, key); |
||||
|
return; |
||||
|
} |
||||
|
onBind(provider); |
||||
|
} |
||||
|
|
||||
|
async function onInit() { |
||||
|
const res = await getExternalLoginsApi(); |
||||
|
externalLogins.value = res.externalLogins.map((item) => { |
||||
|
const userLogin = res.userLogins.find((x) => x.loginProvider === item.name); |
||||
|
return { |
||||
|
title: item.displayName, |
||||
|
description: userLogin?.providerKey ?? $t('AbpAccount.UnBind'), |
||||
|
buttons: [ |
||||
|
{ |
||||
|
title: userLogin?.providerKey |
||||
|
? $t('AbpAccount.CancelBind') |
||||
|
: $t('AbpAccount.Bind'), |
||||
|
type: 'link', |
||||
|
click: () => onClick(item.name, userLogin?.providerKey), |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<Page> |
<Page> |
||||
<MySetting /> |
<MySetting :bind-items="externalLogins" @on-bind-init="onInit" /> |
||||
|
<WechatWorkUserBindModal @on-login="onBindWorkWeixin" /> |
||||
</Page> |
</Page> |
||||
</template> |
</template> |
||||
|
|||||
@ -1,4 +1,5 @@ |
|||||
export { useAccountApi } from './useAccountApi'; |
export { useAccountApi } from './useAccountApi'; |
||||
|
export { useExternalLoginsApi } from './useExternalLoginsApi'; |
||||
export { useMySessionApi } from './useMySessionApi'; |
export { useMySessionApi } from './useMySessionApi'; |
||||
export { useProfileApi } from './useProfileApi'; |
export { useProfileApi } from './useProfileApi'; |
||||
export { useScanQrCodeApi } from './useScanQrCodeApi'; |
export { useScanQrCodeApi } from './useScanQrCodeApi'; |
||||
|
|||||
@ -0,0 +1,58 @@ |
|||||
|
import type { |
||||
|
ExternalLoginResultDto, |
||||
|
RemoveExternalLoginInput, |
||||
|
WorkWeixinLoginBindInput, |
||||
|
} from '../types/external-logins'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useExternalLoginsApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
/** |
||||
|
* 绑定企业微信 |
||||
|
* @param input 绑定参数 |
||||
|
* @returns { Promise<void> } |
||||
|
*/ |
||||
|
async function bindWorkWeixinApi( |
||||
|
input: WorkWeixinLoginBindInput, |
||||
|
): Promise<void> { |
||||
|
return await request(`/api/account/oauth/work-weixin/bind`, { |
||||
|
method: 'POST', |
||||
|
data: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取外部登录提供者列表 |
||||
|
* @returns 外部登录提供者列表 |
||||
|
*/ |
||||
|
async function getExternalLoginsApi(): Promise<ExternalLoginResultDto> { |
||||
|
return await request<ExternalLoginResultDto>( |
||||
|
`/api/account/external-logins`, |
||||
|
{ |
||||
|
method: 'GET', |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 移除外部登录提供者 |
||||
|
* @returns { Promise<void> } |
||||
|
*/ |
||||
|
async function removeExternalLoginApi( |
||||
|
input: RemoveExternalLoginInput, |
||||
|
): Promise<void> { |
||||
|
return await request(`/api/account/external-logins/remove`, { |
||||
|
method: 'DELETE', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
bindWorkWeixinApi, |
||||
|
getExternalLoginsApi, |
||||
|
removeExternalLoginApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
import type { ButtonType } from 'ant-design-vue/lib/button'; |
||||
|
|
||||
|
interface BindButton { |
||||
|
click: () => Promise<void> | void; |
||||
|
title: string; |
||||
|
type?: ButtonType; |
||||
|
} |
||||
|
|
||||
|
interface BindItem { |
||||
|
buttons?: BindButton[]; |
||||
|
description?: string; |
||||
|
enable?: boolean; |
||||
|
slot?: string; |
||||
|
title: string; |
||||
|
} |
||||
|
|
||||
|
export type { BindItem }; |
||||
@ -0,0 +1,32 @@ |
|||||
|
interface UserLoginInfoDto { |
||||
|
loginProvider: string; |
||||
|
providerDisplayName: string; |
||||
|
providerKey: string; |
||||
|
} |
||||
|
|
||||
|
interface ExternalLoginInfoDto { |
||||
|
displayName: string; |
||||
|
name: string; |
||||
|
} |
||||
|
|
||||
|
interface WorkWeixinLoginBindInput { |
||||
|
code: string; |
||||
|
} |
||||
|
|
||||
|
interface ExternalLoginResultDto { |
||||
|
externalLogins: ExternalLoginInfoDto[]; |
||||
|
userLogins: UserLoginInfoDto[]; |
||||
|
} |
||||
|
|
||||
|
interface RemoveExternalLoginInput { |
||||
|
loginProvider: string; |
||||
|
providerKey: string; |
||||
|
} |
||||
|
|
||||
|
export type { |
||||
|
ExternalLoginInfoDto, |
||||
|
ExternalLoginResultDto, |
||||
|
RemoveExternalLoginInput, |
||||
|
UserLoginInfoDto, |
||||
|
WorkWeixinLoginBindInput, |
||||
|
}; |
||||
@ -1,4 +1,6 @@ |
|||||
export * from './account'; |
export * from './account'; |
||||
|
export * from './bind'; |
||||
|
export * from './external-logins'; |
||||
export * from './profile'; |
export * from './profile'; |
||||
export * from './token'; |
export * from './token'; |
||||
export * from './user'; |
export * from './user'; |
||||
|
|||||
@ -0,0 +1,42 @@ |
|||||
|
const hexList: string[] = []; |
||||
|
for (let i = 0; i <= 15; i++) { |
||||
|
hexList[i] = i.toString(16); |
||||
|
} |
||||
|
|
||||
|
export function buildUUID(): string { |
||||
|
let uuid = ''; |
||||
|
for (let i = 1; i <= 36; i++) { |
||||
|
switch (i) { |
||||
|
case 9: |
||||
|
case 14: |
||||
|
case 19: |
||||
|
case 24: { |
||||
|
uuid += '-'; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case 15: { |
||||
|
uuid += 4; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case 20: { |
||||
|
uuid += hexList[(Math.random() * 4) | 8]; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
default: { |
||||
|
uuid += hexList[Math.trunc(Math.random() * 16)]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return uuid.replaceAll('-', ''); |
||||
|
} |
||||
|
|
||||
|
let unique = 0; |
||||
|
export function buildShortUUID(prefix = ''): string { |
||||
|
const time = Date.now(); |
||||
|
const random = Math.floor(Math.random() * 1_000_000_000); |
||||
|
unique++; |
||||
|
return `${prefix}_${random}${unique}${String(time)}`; |
||||
|
} |
||||
@ -1,3 +1,4 @@ |
|||||
export { default as SettingDefinitionTable } from './definitions/SettingDefinitionTable.vue'; |
export { default as SettingDefinitionTable } from './definitions/SettingDefinitionTable.vue'; |
||||
|
export { default as SettingForm } from './settings/SettingForm.vue'; |
||||
export { default as SystemSetting } from './settings/SystemSetting.vue'; |
export { default as SystemSetting } from './settings/SystemSetting.vue'; |
||||
export { default as UserSetting } from './settings/UserSetting.vue'; |
export { default as UserSetting } from './settings/UserSetting.vue'; |
||||
|
|||||
@ -0,0 +1,40 @@ |
|||||
|
{ |
||||
|
"name": "@abp/wechat", |
||||
|
"version": "9.2.0", |
||||
|
"homepage": "https://github.com/colinin/abp-next-admin", |
||||
|
"bugs": "https://github.com/colinin/abp-next-admin/issues", |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "git+https://github.com/colinin/abp-next-admin.git", |
||||
|
"directory": "packages/@abp/wechat" |
||||
|
}, |
||||
|
"license": "MIT", |
||||
|
"type": "module", |
||||
|
"sideEffects": [ |
||||
|
"**/*.css" |
||||
|
], |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./src/index.ts", |
||||
|
"default": "./src/index.ts" |
||||
|
} |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@abp/core": "workspace:*", |
||||
|
"@abp/features": "workspace:*", |
||||
|
"@abp/request": "workspace:*", |
||||
|
"@abp/settings": "workspace:*", |
||||
|
"@abp/ui": "workspace:*", |
||||
|
"@ant-design/icons-vue": "catalog:", |
||||
|
"@vben/access": "workspace:*", |
||||
|
"@vben/common-ui": "workspace:*", |
||||
|
"@vben/hooks": "workspace:*", |
||||
|
"@vben/icons": "workspace:*", |
||||
|
"@vben/layouts": "workspace:*", |
||||
|
"@vben/locales": "workspace:*", |
||||
|
"@wecom/jssdk": "catalog:", |
||||
|
"ant-design-vue": "catalog:", |
||||
|
"dayjs": "catalog:", |
||||
|
"vue": "catalog:*" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export { userWorkWeixinJsSdkApi } from './userWorkWeixinJsSdkApi'; |
||||
|
export { useWechatSettingsApi } from './useWechatSettingsApi'; |
||||
@ -0,0 +1,40 @@ |
|||||
|
import type { ListResultDto } from '@abp/core'; |
||||
|
import type { SettingGroup } from '@abp/settings'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useWechatSettingsApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
/** |
||||
|
* 获取全局设置 |
||||
|
* @returns 设置数据传输对象列表 |
||||
|
*/ |
||||
|
function getGlobalSettingsApi(): Promise<ListResultDto<SettingGroup>> { |
||||
|
return request<ListResultDto<SettingGroup>>( |
||||
|
`/api/wechat/setting-management/by-global`, |
||||
|
{ |
||||
|
method: 'GET', |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取租户设置 |
||||
|
* @returns 设置数据传输对象列表 |
||||
|
*/ |
||||
|
function getTenantSettingsApi(): Promise<ListResultDto<SettingGroup>> { |
||||
|
return request<ListResultDto<SettingGroup>>( |
||||
|
`/api/wechat/setting-management/by-current-tenant`, |
||||
|
{ |
||||
|
method: 'GET', |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getGlobalSettingsApi, |
||||
|
getTenantSettingsApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import type { AgentConfigDto } from '../types/js-sdk'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function userWorkWeixinJsSdkApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
/** |
||||
|
* 获取企业微信应用配置 |
||||
|
* @returns 企业微信应用配置Dto |
||||
|
*/ |
||||
|
function getAgentConfigApi(): Promise<AgentConfigDto> { |
||||
|
return request<AgentConfigDto>(`/api/wechat/work/jssdk/agent-config`, { |
||||
|
method: 'GET', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getAgentConfigApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { useTemplateRef } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
|
||||
|
import { buildUUID } from '@abp/core'; |
||||
|
import { WWLoginRedirectType, WWLoginType } from '@wecom/jssdk'; |
||||
|
import * as ww from '@wecom/jssdk'; |
||||
|
|
||||
|
import { userWorkWeixinJsSdkApi } from '../../api/userWorkWeixinJsSdkApi'; |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
/** |
||||
|
* 用户扫码登录成功回调事件 |
||||
|
* @params code 企业微信授权码 |
||||
|
*/ |
||||
|
(event: 'onLogin', code: string): void; |
||||
|
}>(); |
||||
|
const wxLoginRef = useTemplateRef<Element>('wxLogin'); |
||||
|
|
||||
|
const { getAgentConfigApi } = userWorkWeixinJsSdkApi(); |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
onOpenChange(isOpen) { |
||||
|
if (isOpen) { |
||||
|
setTimeout(onInitLogin, 200); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
async function onInitLogin() { |
||||
|
try { |
||||
|
modalApi.setState({ loading: true }); |
||||
|
const agentConfig = await getAgentConfigApi(); |
||||
|
ww.createWWLoginPanel({ |
||||
|
el: wxLoginRef.value!, |
||||
|
params: { |
||||
|
login_type: WWLoginType.corpApp, |
||||
|
appid: agentConfig.corpId, |
||||
|
agentid: agentConfig.agentId, |
||||
|
// TODO: 是否应改为可配置式? 企业微信仅允许配置一个回调地址, 生产环境应配合反向代理服务器. |
||||
|
redirect_uri: window.location.href, |
||||
|
state: buildUUID(), |
||||
|
redirect_type: WWLoginRedirectType.callback, |
||||
|
}, |
||||
|
onLoginSuccess(res) { |
||||
|
emits('onLogin', res.code); |
||||
|
}, |
||||
|
}); |
||||
|
} finally { |
||||
|
modalApi.setState({ loading: false }); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal :title="$t('AbpAccountOAuth.OAuth:WorkWeixin')"> |
||||
|
<div ref="wxLogin"></div> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,2 @@ |
|||||
|
export { default as WechatWorkUserBinder } from './bind-user/index.vue'; |
||||
|
export { default as WechatSettings } from './settings/index.vue'; |
||||
@ -0,0 +1,37 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { SettingsUpdateInput } from '@abp/settings'; |
||||
|
|
||||
|
import { useAbpStore } from '@abp/core'; |
||||
|
import { SettingForm, useSettingsApi } from '@abp/settings'; |
||||
|
|
||||
|
import { useWechatSettingsApi } from '../../api/useWechatSettingsApi'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'MaterialInspectSettings', |
||||
|
}); |
||||
|
|
||||
|
const abpStore = useAbpStore(); |
||||
|
const { getGlobalSettingsApi, getTenantSettingsApi } = useWechatSettingsApi(); |
||||
|
const { setGlobalSettingsApi, setTenantSettingsApi } = useSettingsApi(); |
||||
|
|
||||
|
async function onGet() { |
||||
|
const getSettingsApi = abpStore.application?.currentTenant.isAvailable |
||||
|
? getTenantSettingsApi |
||||
|
: getGlobalSettingsApi; |
||||
|
const { items } = await getSettingsApi(); |
||||
|
return items; |
||||
|
} |
||||
|
|
||||
|
async function onSubmit(input: SettingsUpdateInput) { |
||||
|
const setSettingsApi = abpStore.application?.currentTenant.isAvailable |
||||
|
? setTenantSettingsApi |
||||
|
: setGlobalSettingsApi; |
||||
|
await setSettingsApi(input); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<SettingForm :get-api="onGet" :submit-api="onSubmit" /> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,2 @@ |
|||||
|
export * from './api'; |
||||
|
export * from './components'; |
||||
@ -0,0 +1,6 @@ |
|||||
|
interface AgentConfigDto { |
||||
|
agentId: string; |
||||
|
corpId: string; |
||||
|
} |
||||
|
|
||||
|
export type { AgentConfigDto }; |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/tsconfig", |
||||
|
"extends": "@vben/tsconfig/web.json", |
||||
|
"include": ["src"], |
||||
|
"exclude": ["node_modules"] |
||||
|
} |
||||
Loading…
Reference in new issue