UNPKG

@ithinkdt/cloud

Version:

iThinkDT Cloud API

641 lines (593 loc) 28.9 kB
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 } }, }, }) }