@ithinkdt/cloud
Version:
iThinkDT Cloud API
641 lines (593 loc) • 28.9 kB
JavaScript
import { computedAsync, promiseTimeout } from '@vueuse/core'
import { createMD5 } from 'hash-wasm'
import { h, nextTick } from 'vue'
import { useApp } from '@ithinkdt/app/use-app'
import { merge } from '@ithinkdt/common/object'
import { request as request0 } from '@ithinkdt/common/request'
import { walkTree } from '@ithinkdt/common/tree'
export * from './request.js'
const fetchIcon = async (id, sysServerName, request) => {
const blob = await request.get(`${sysServerName}/pub/logo/${id}`, {}, { responseType: 'response' })
const text = await blob.text()
try {
const e = JSON.parse(text)
console.debug(`[cloud] 获取图标失败,id: ${id}`, e)
return
} catch {
return text
}
}
async function getModuleResources(token, lang, moduleCode, appCode, contentCode, sysServerName, request) {
const list = await request.get(`${sysServerName}/cli/i18n/list`, { moduleCode, appCode, lang, contentCode }, {
headers: { Authorization: `Bearer ${token}` },
})
return Object.fromEntries(list.map(it => [it.contentCode, it.text]))
}
async function getResources(moduleCode, contentCode, appCode, sysServerName, request) {
const list = await request.get(`${sysServerName}/cli/i18n/list`, { moduleCode, contentCode, appCode })
return Object.fromEntries(list.map(it => [it.lang, it.text || '']))
}
function deleteResources(moduleCode, contentCode, appCode, sysServerName, request) {
return request.delete(`${sysServerName}/cer/i18n/key`, { appCode, moduleCode, contentCode })
}
async function saveResources(moduleCode, contentCode, resources, appCode, sysServerName, request) {
await deleteResources(moduleCode, contentCode, appCode, sysServerName, request)
const saves = Object.entries(resources)
.filter(([_lang, text]) => text?.trim())
.map(([lang, text]) => ({
appCode, moduleCode, contentCode, lang, text,
}))
if (saves.length === 0) return
return request.post(
`${sysServerName}/cer/i18n/save/batch`,
saves,
{ requestType: 'json' },
)
}
function getAllApps(sysServerName, request) {
return request.get(`${sysServerName}/pub/app/all`)
.then(res => res.filter(app => app.status === 0))
.catch((error) => {
console.debug(`[cloud] 未能获取到前端应用信息 (${error.message}),使用默认配置。`)
return []
})
}
function getAppName(appCode, lang, sysServerName, request) {
return request.get(`${sysServerName}/pub/i18n/text/appName`, { appCode, lang })
}
function getLocale(appCode, lang, sysServerName, request) {
return request.get(`${sysServerName}/pub/i18n/front/${appCode}`, { lang })
}
export function cloud(sysServerName, { request = request0 } = {}) {
return {
getAllApps: () => getAllApps(sysServerName, request),
getAppName: (appCode, lang) => getAppName(appCode, lang, sysServerName, request),
getIconData: id => fetchIcon(id, sysServerName, request),
getI18nResources: (appCode, lang) => getLocale(appCode, lang, sysServerName, request),
getFrontConfig: (appCode, cached, yidaServerName) => _getAppConfig(cached, appCode, yidaServerName, sysServerName, request),
authenticator: (appCode, lang) => {
const MODULE_PATH_SPLIT = ' | '
return {
login: (username, password) => {
return request.post(`${sysServerName}/login`, { username, password }, {
requestType: 'json',
redirect: 'error',
credentials: 'omit',
})
},
logout: async (token) => {
try {
return await request.get(`${sysServerName}/logout`, {}, {
redirect: 'error',
credentials: 'omit',
headers: { Authorization: `Bearer ${token}` },
})
} catch (error) {
console.warn('[cloud] 调用登出接口错误', error)
} finally {
sessionStorage.clear()
}
},
changePwd: (token, curPassword, newPassword) => {
return request.post(`${sysServerName}/cli/user/password/change`, { curPassword, newPassword }, {
requestType: 'urlencoded',
headers: {
Authorization: `Bearer ${token}`,
},
})
},
getUserInfo: async (token) => {
const user = await request.get(`${sysServerName}/cli/userinfo`, {}, {
headers: { Authorization: `Bearer ${token}` },
})
return ({ ...user, nickname: user.personName })
},
refreshAuth: async (token) => {
token = await request.get(`${sysServerName}/cli/login/refresh`, {}, {
headers: { Authorization: `Bearer ${token}` },
})
sessionStorage.clear()
return token
},
getUserModules: async (token) => {
let [modules, resources] = await Promise.all([
request.get(
`${sysServerName}/cli/modules`,
{ appCode },
{ headers: { Authorization: `Bearer ${token}` } },
),
getModuleResources(token, lang, 'menu', appCode, undefined, sysServerName, request),
])
const moduleTypeMap = {
'01': 'menu',
'02': 'lowcode',
'03': 'federate',
'04': 'external',
}
const map = (data, onlyAction = false) => {
return data
.map((m) => {
if (m.parentId === '0') m.asMenu = true
const name = resources[m.moduleNameEn] ?? m.moduleName ?? m.text
const type = m.asMenu ? (m.moduleUrl?.trim() ? 'view' : 'group') : 'action'
const obj = { key: m.id, name, type, visible: m.visiable, moduleKey: m.moduleNameEn }
if (type === 'action') {
if (m.visiable === false) return
Object.assign(obj, {
operation: m.moduleUrl,
visible: false,
})
} else {
if (onlyAction) return
Object.assign(obj, {
icon: m.icon,
children: map(m.children, type === 'view'),
})
if (type === 'view') {
Object.assign(obj, {
isLowcode: false,
isRemote: false,
isExternal: false,
namespace: undefined,
})
const viewType = moduleTypeMap[m.moduleType ?? '01']
const [path, data] = m.moduleUrl?.trim().split(MODULE_PATH_SPLIT) ?? []
switch (viewType) {
case 'menu': {
Object.assign(obj, {
path,
})
break
}
case 'federate': {
Object.assign(obj, {
path,
isRemote: true,
remoteEntry: decodeURIComponent(data),
namespace: '',
})
break
}
case 'lowcode': {
Object.assign(obj, {
path,
isLowcode: true,
lcKey: decodeURIComponent(data),
namespace: 'ns-cloud-yida',
})
break
}
case 'external': {
Object.assign(obj, {
path,
isExternal: true,
externalLink: decodeURIComponent(data),
externalEmbed: true,
})
break
}
default: {
console.warn(
`[cloud] 未知的模块类型 ${m.moduleType},模块 ID ${m.id}`,
)
}
}
}
}
return obj
})
.filter(it => it !== undefined)
?? []
}
modules = map(modules ?? [])
if (appCode) {
return modules[0]?.children ?? []
}
return modules
},
}
},
}
}
export default cloud
export function useCloud({ request = request0, mcServerName, sysServerName, fileServerName } = {}) {
const app = useApp()
const base64Prefix = 'data:image/svg+xml;base64,'
const getIconRender = (id) => {
const img = computedAsync(() => fetchIcon(id, sysServerName, request))
return (placeholder) => {
if (!img.value) return placeholder
if (img.value.startsWith(base64Prefix)) {
const text = atob(img.value.slice(base64Prefix.length))
if (!text.includes('height="1em"')) {
return h('img', {
src: img.value,
style: {
height: '1em',
},
})
}
return h('span', {
innerHTML: text,
style: {
lineHeight: '0',
},
})
}
return h('img', {
src: img.value,
style: {
height: '1em',
},
})
}
}
const listMessages = (params) => {
return request.post(`${mcServerName}/cli/mc/msgInternal/page`, params, { requestType: 'json' })
}
const notification = {
getUnreadCount: () => {
return listMessages({ status: 0, pageSize: 0, pageNumber: 1 }).then(page => page.total)
},
getPage: (type, pageNumber, pageSize, searchText) => {
return listMessages({
status: type === 'unread' ? 0 : undefined,
searchText,
pageSize,
pageNumber,
}).then((data) => {
for (const it of data.records) {
it.key = it.id
it.status = it.status ? 'read' : 'unread'
const content = it.content
it.content = () => h('div', { innerHTML: content })
it.date = it.createDate
}
return data
})
},
markRead: (ids) => {
return request.post(`${mcServerName}/cli/mc/msgInternal/read`, ids, { requestType: 'json' })
},
markDelete: (ids) => {
return request.post(`${mcServerName}/cli/mc/msgInternal/deletes`, ids, { requestType: 'json' })
},
}
const $dictResources = {}
async function getDictByTypes(types, lang = app.language, appCode = app.appCode) {
const key = `${appCode}$@$${lang}`
if (!$dictResources[key]) {
$dictResources[key] = getModuleResources(app.auth.token, lang, 'dict', appCode, undefined, sysServerName, request)
}
types = types.map(it => `${appCode}.${it}`)
const api
= types.length > 1
? `${sysServerName}/cli/dict/value/getByTypes?dictTypes=${encodeURIComponent(types.join(','))}`
: `${sysServerName}/cli/dict/value/getByType?dictType=${encodeURIComponent(types[0])}`
let [dicts, resources] = await Promise.all([
request.get(api),
$dictResources[key],
])
if (types.length === 1) {
dicts = {
[types[0]]: dicts,
}
}
return Object.fromEntries(
Object.entries(dicts).map(([type, data = []]) => {
const dictType = type.slice(appCode.length + 1)
return [
dictType,
data.map(dict => ({
...dict,
value: dict.dictKey,
label: resources[`${dictType}.${dict.dictKey}`] ?? dict.dictValue,
})),
]
}),
)
}
function getSysParams(group) {
return request.post(`${sysServerName}/cli/busConfig/getByGroup/${group}`)
}
function uploadFile(file, params, onUploadProgress, chunk = false) {
const fd = new FormData()
for (const [k, v] of Object.entries(params || {})) {
if (v !== undefined) fd.append(k, v)
}
fd.append('file', file)
return request
.post(
chunk ? `${fileServerName}/cli/file/transfer/chunk/upload` : `${fileServerName}/cli/file/transfer/upload`,
fd,
{ onUploadProgress },
)
.then((data) => {
return data[0]?.id
})
}
const previewFileUrl = (fileId, credentials = true) =>
`${request.baseUrl}${fileServerName}${credentials ? '/cli' : '/pub'}/file/transfer/preview?id=${fileId}`
+ (credentials ? `&access_token=${encodeURIComponent(`Bearer ${app.auth.token}`)}` : '')
const downloadFileUrl = (fileId, credentials = true) =>
`${request.baseUrl}${fileServerName}${credentials ? '/cli' : '/pub'}/file/transfer/download?id=${fileId}`
+ (credentials ? `&access_token=${encodeURIComponent(`Bearer ${app.auth.token}`)}` : '')
const getUserNames = (usernames) => {
return request
.get(`${sysServerName}/cli/user/name`, { usernames: usernames?.join(',') ?? '' })
}
const userCache = new Set()
let delay, req, resolve
const getUserNamesDelayed = async () => {
let req2
if (delay && userCache.size >= 50) {
resolve()
await promiseTimeout(0)
}
if (delay) {
req2 = req
} else {
delay = new Promise((resolve0) => {
resolve = resolve0
nextTick(resolve0)
}).finally(() => {
delay = undefined
})
req = req2 = delay.then(() => {
const $ret = getUserNames([...userCache])
userCache.clear()
return $ret
})
}
return req2
}
return {
changeTenant: tenantId => request.get(`${sysServerName}/cli/login/changeTenant`, { toTenantId: tenantId }),
getIconRender,
getIconData: id => fetchIcon(id, sysServerName, request),
renderLogo: getIconRender(app.appLogo),
notification,
getDictByTypes,
getSysParams,
getModuleResources: (lang, moduleCode, appCode, contentCode) => getModuleResources(app.auth.token, lang, moduleCode, appCode, contentCode, sysServerName, request),
getResources: (moduleCode, contentCode, appCode) => getResources(moduleCode, contentCode, appCode, sysServerName, request),
deleteResources: (moduleCode, contentCode, appCode) => deleteResources(moduleCode, contentCode, appCode, sysServerName, request),
saveResources: (moduleCode, contentCode, resources, appCode) => saveResources(moduleCode, contentCode, resources, appCode, sysServerName, request),
previewFileUrl,
downloadFileUrl,
getFileInfos: fileIds =>
request.get(`${fileServerName}/cli/file/transfer/fileinfo`, { ids: fileIds })
.then(data =>
fileIds.map((id) => {
const has = !!data[id]
const it = data[id] || {}
it.id = id
it.name = has ? it.fileName : 'UNKNOWN'
it.size = it.fileSize
it.type = it.fileType
it.url = has ? downloadFileUrl(id, it.accessRight !== 'PUBLIC') : undefined
it.thumbnailUrl = has ? previewFileUrl(id, it.accessRight !== 'PUBLIC') : undefined
it.status = has ? 'finished' : 'error'
return it
}),
),
uploadFile: async (file, { accessRight = 'INTERNAL', filePath = '/', fileName = file.name, onUploadProgress }) => {
const chunkSize = 20 * 1024 * 1024
const hasher = await createMD5()
const readFile = async (file) => {
hasher.init()
const chunkNumber = Math.floor(file.size / chunkSize)
for (let i = 0; i <= chunkNumber; i++) {
const chunk = file.slice(
chunkSize * i,
Math.min(chunkSize * (i + 1), file.size),
)
const view = new Uint8Array(await chunk.arrayBuffer())
hasher.update(view)
}
const hash = hasher.digest()
return hash
}
const hash = await readFile(file)
const quickUploadFile = await request.post(`${fileServerName}/cli/file/transfer/quickupload`, {
identifier: hash,
accessRight,
filename: fileName,
filePath,
relativePath: '',
})
if (quickUploadFile) {
return quickUploadFile.id
}
if (file.size > chunkSize) {
// 大于 20M 使用分片上传,每片 10M
const chunkSize = 10 * 1024 * 1024
const chunks = Math.ceil(file.size / chunkSize)
const uploaded = Array.from({ length: chunks }, () => 0)
return Promise.all(Array.from({ length: chunks }, (_, index) => {
const start = index * chunkSize
const end = Math.min(file.size, start + chunkSize)
const chunk = file.slice(start, end)
const params = {
accessRight, filePath, filename: fileName, identifier: hash,
chunkNumber: index, chunkSize, totalChunks: chunks, totalSize: file.size, currentChunkSize: chunk.size,
}
const onUploadProgress2 = (percent) => {
uploaded[index] = percent * chunk.size / 100
onUploadProgress?.(Math.ceil((uploaded.reduce((a, b) => a + b, 0) / file.size) * 100))
}
return uploadFile(chunk, params, onUploadProgress2, true)
})).then(res => res[0])
}
return uploadFile(file, { accessRight, filePath, filename: fileName, identifier: hash }, onUploadProgress)
},
fileRemove: (fileId) => {
return request.post(`${fileServerName}/cli/file/transfer/delete`, { id: fileId }, { requestType: 'urlencoded' })
},
getServerTimestamp: () => {
return request.get(`${sysServerName}/cli/timestamp/now`)
},
getUsersByUsername(usernames) {
if (!usernames || usernames.length === 0) {
return request
.get(`${sysServerName}/cli/user/list`, { name: '%' })
.then((users) => {
return users.map(u => ({ ...u, username: u.username, nickname: u.personName }))
})
}
userCache.add(...usernames)
return getUserNamesDelayed()
.then((data) => {
return usernames.map(username => ({ username, nickname: data[username] || username }))
})
},
getOrgsByCode: async (codes) => {
if (codes) {
const [res, namet] = await Promise.all([
request.get(`${sysServerName}/cli/org/name`, { codes: codes?.join(',') || '' })
.then(data => Object.entries(data).map(([code, name]) => ({ code, name }))),
getModuleResources(app.auth.token, app.language, 'userGroup', 'ALL', undefined, sysServerName, request),
])
for (const org of res) {
org.name = namet[org.code] || org.name
}
return res
}
const [res, namet] = await Promise.all([
request.get(`${sysServerName}/cli/org/tree`, { codes: codes?.join(',') || '' }),
getModuleResources(app.auth.token, app.language, 'userGroup', 'ALL', undefined, sysServerName, request),
])
walkTree(res, (node) => {
node.name = namet[node.code] || node.text
})
return res
},
getUserGroups: async () => {
const [groups, namet] = await Promise.all([
request.post(`${sysServerName}/cli/group/list`, {}, { requestType: 'json' }),
getModuleResources(app.auth.token, app.language, 'userGroup', 'ALL', undefined, sysServerName, request),
])
for (const g of groups) {
g.name = namet[g.code] || g.name
}
return groups
},
getUsersByGroup: (code) => {
return request.get(`${sysServerName}/cli/group/user/get`, { code })
.then(data => data.map(u => ({ ...u, username: u.username, nickname: u.personName })))
},
getUsersByOrg: org => request.get(`${sysServerName}/cli/user/org`, { orgCode: org })
.then(data => data.map(u => ({ ...u, username: u.username, nickname: u.personName }))),
}
}
async function _getAppConfig(cached, appCode, yidaServerName, sysServerName, request) {
let result
if (cached && (result = sessionStorage.getItem(`${typeof cached === 'string' ? cached : 'app/'}config@[${appCode}]`))) {
result = JSON.parse(result)
} else {
const [
{
FRONT_LOGO_PLACEMENT, FRONT_MULTI_TAB, FRONT_I18N, FRONT_LANGUAGES, FRONT_LOCALE, FRONT_MENU_ACCORDION, FRONT_MENU_PLACEMENT, FRONT_YIDA_ENTRY,
FRONT_APPEARANCE, FRONT_THEME_LIGHT, FRONT_THEME_DARK, FRONT_WATERMARK, FRONT_FOOTER, FRONT_REQUEST_TIMEOUT, FRONT_MSG_INTERVAL, FRONT_TENANT,
...config
},
personalize = {},
] = await Promise.all([
request.get(`${sysServerName}/pub/busConfig/getFrontConfig`)
.catch((error) => {
console.debug(`[cloud] 未能获取到前端配置 (${error.message}),使用默认配置。`)
})
.then((configs = []) => Object.fromEntries(configs.map(c => [c.configKey, c.configValue]))),
yidaServerName
? request.get(`${yidaServerName}/pub/yida/app/personalize/${appCode}`).catch((error) => {
console.debug(`[cloud] 未能获取到前端自定义配置 (${error.message}),使用默认配置。`)
})
: {},
])
result = {
...config,
tenant: FRONT_TENANT === 'true',
yidaEntry: FRONT_YIDA_ENTRY?.trim() || undefined,
logoPlacement: personalize.logoPlacement ?? (FRONT_LOGO_PLACEMENT || 'sidebar'),
multiTab: personalize.multiTab ?? FRONT_MULTI_TAB === 'true',
i18n: FRONT_I18N === 'true',
supportLanguages: JSON.parse(FRONT_LANGUAGES ?? '[{ "value": "zh_CN", "label": "中文" }]'),
defaultLanguage: FRONT_LOCALE || undefined,
menuAccordion: personalize.menuAccordion ?? FRONT_MENU_ACCORDION === 'true',
menuPlacement: personalize.menuPlacement ?? (FRONT_MENU_PLACEMENT || 'sidebar'),
appearance: personalize.appearance ?? FRONT_APPEARANCE === 'true',
theme: {
light: JSON.parse(FRONT_THEME_LIGHT ?? '{}'),
dark: merge(
{},
JSON.parse(FRONT_THEME_LIGHT ?? '{}'),
JSON.parse(FRONT_THEME_DARK ?? '{}'),
),
},
footer: FRONT_FOOTER.trim() || undefined,
timeout: Number(FRONT_REQUEST_TIMEOUT) || undefined,
msgInterval: Number(FRONT_MSG_INTERVAL) || undefined,
watermark: personalize.watermark ?? FRONT_WATERMARK === 'true',
}
if (cached) sessionStorage.setItem(`${typeof cached === 'string' ? cached : 'app/'}config@[${appCode}]`, JSON.stringify(result))
}
return Object.assign({}, result, {
watermark:
result.watermark
? new Function('user', `return (${result.FRONT_WATERMARK_RENDER})`)
: false,
sso:
result.FRONT_SSO_ENABLED === 'false'
? false
: {
selfLogin: result.FRONT_LOCAL_LOGIN === 'true',
getLoginUrl: (redirect) => {
return new Function('redirect', 'appCode', `return \`${result.FRONT_SSO_LOGIN_URL}\``)(
encodeURIComponent(redirect || ''),
encodeURIComponent(appCode || ''),
)
},
getLogoutUrl: async (redirect, username, token) => {
return new Function(
'redirect',
'appCode',
'username',
'token',
`return \`${result.FRONT_SSO_LOGOUT_URL}\``,
)(
encodeURIComponent(redirect || ''),
encodeURIComponent(appCode || ''),
encodeURIComponent(username || ''),
encodeURIComponent(token || ''),
)
},
parseUrl: (fromUrl) => {
const url = new URL(fromUrl)
const query = url.searchParams
const token = query.get('token') ?? undefined
query.delete('token')
return { token, search: query.toString(), hash: url.hash }
},
},
})
}