Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ FormData;保存返回完整配置,页面以该返回值作为唯一数据来
`admin/auth/current-user.ts` 的 postCurrentUserPassword 向 `/user/current/reset_password`
提交 RSA 加密的 `{ encryptedData }`;调用方校验与加密,成功清除凭据后进入登录。

### 平台许可证

`admin/system/license.ts` 通过 GET、PUT `/license/profile` 查询和更新授权信息,
更新使用 FormData 的 `license_file` 字段。`LicenseProfile` 与 `LicenseInfo` 从 `@/api/types`
导入,查询结果直接读取 `license`。关于弹窗仅商业版本查询授权,更新入口沿用系统许可证权限;
更新成功后重新查询展示,关闭弹窗时刷新页面以应用授权与权限。

### 四类特殊资源 API

根据资源范围或业务上下文选择出的 API 对象统一命名为 `requestApi`,包括 `computed` 结果和
Expand Down
10 changes: 10 additions & 0 deletions ui/src/api/admin/auth/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { get, put } from '../core/request'
import type { LicenseProfile } from '@/api/types'

/** 获取当前平台的授权信息。 */
const getLicense = () => get<LicenseProfile>('/license/profile')

/** 上传并更新平台许可证。 */
const putLicense = (payload: FormData) => put<FormData, LicenseProfile>('/license/profile', payload)

export default { getLicense, putLicense }
1 change: 1 addition & 0 deletions ui/src/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type * from './chat-auth'
export type * from './chat-application'
export type * from './chat-user-groups'
export type * from './login'
export type * from './license'
export type * from './system-user'
export type * from './system-user-groups'
export type * from './system-workspace'
Expand Down
13 changes: 13 additions & 0 deletions ui/src/api/types/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** 平台许可证中用于关于弹窗展示的授权信息。 */
export interface LicenseInfo {
corporation?: string
expired?: string
serialNo?: string
remark?: string
}

export interface LicenseProfile {
status?: string
license?: LicenseInfo | null
message?: string
}
132 changes: 132 additions & 0 deletions ui/src/layout/avatar-dropdown/AboutDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { UploadFile, UploadInstance } from 'element-plus'
import LicenseApi from '@/api/admin/auth/license'
import type { LicenseInfo } from '@/api/types'
import LogoFull from '@/components/mk-logo/LogoFull.vue'
import { perm } from '@/permission'
import { useStore } from '@/stores/admin'

defineOptions({ name: 'AboutDialog' })

const { auth } = useStore()
const dialogVisible = ref(false)
const loading = ref(false)
const licenseInfo = ref<LicenseInfo | null>(null)
const licenseUpdated = ref(false)
const uploadRef = ref<UploadInstance>()
const copyrightYear = new Date().getFullYear()

// 版本与授权信息展示。
const editionText = computed(() => {
const edition = auth.baseProfile?.edition
if (edition === 'PE') return '专业版'
if (edition === 'EE') return '企业版'
return '社区版'
})

const expirationNotice = computed(() => {
if (!licenseInfo.value?.expired) return null
const remainingTime = new Date(licenseInfo.value.expired).getTime() - Date.now()
if (!Number.isFinite(remainingTime)) return null
if (remainingTime < 0) return { text: '已过期', expired: true }
if (remainingTime < 60000) return { text: '即将到期', expired: false }
if (remainingTime < 3600000) return { text: `${Math.floor(remainingTime / 60000)}分钟后到期`, expired: false }
if (remainingTime < 86400000) return { text: `${Math.floor(remainingTime / 3600000)}小时后到期`, expired: false }
if (remainingTime <= 7 * 86400000) return { text: `${Math.floor(remainingTime / 86400000)}天后到期`, expired: false }
return null
})

const licenseFields = computed(() => [
{ key: 'corporation', label: '授权给', value: licenseInfo.value?.corporation || '-' },
{ key: 'expired', label: '过期时间', value: licenseInfo.value?.expired || '-' },
{ key: 'edition', label: '版本', value: editionText.value },
{ key: 'version', label: '版本号', value: auth.baseProfile?.version || '-' },
{ key: 'serialNo', label: '序列号', value: licenseInfo.value?.serialNo || '-' },
{ key: 'remark', label: '备注', value: licenseInfo.value?.remark || '-' },
])

function loadLicense() {
return LicenseApi.getLicense().then((profile) => {
licenseInfo.value = profile.license ?? null
})
}

function open() {
dialogVisible.value = true
if (!auth.showXpack) return
loading.value = true
return loadLicense().finally(() => {
loading.value = false
})
}

// 更新成功后刷新授权展示,关闭弹窗时重新加载平台档案与权限。
function updateLicense(file: UploadFile) {
if (!file.raw || loading.value) return
const payload = new FormData()
payload.append('license_file', file.raw)
loading.value = true
return LicenseApi.putLicense(payload)
.then(() => {
licenseUpdated.value = true
// 上传期间已关闭弹窗时,也要应用最新授权。
if (!dialogVisible.value) {
window.location.reload()
return
}
return loadLicense()
})
.finally(() => {
loading.value = false
uploadRef.value?.clearFiles()
})
}

function handleClosed() {
if (licenseUpdated.value) window.location.reload()
licenseUpdated.value = false
licenseInfo.value = null
}

defineExpose({ open })
</script>

<template>
<MkDialog align-center v-model="dialogVisible" title="关于" :close-on-click-modal="true" :close-on-press-escape="true" @closed="handleClosed">
<div v-loading="loading" class="overflow-hidden rounded-xl border">
<div class="flex-center h-35 bg-primary/10">
<LogoFull :height="60" class="max-w-full!" />
</div>
<div class="px-8 py-6">
<dl class="space-y-4">
<template v-for="field in licenseFields" :key="field.key">
<div class="grid grid-cols-[100px_minmax(0,1fr)] gap-x-4">
<dt class="text-N600">{{ field.label }}</dt>
<dd class="m-0 wrap-anywhere">
{{ field.value }}
<span v-if="field.key === 'expired' && expirationNotice" :class="expirationNotice.expired ? 'text-danger' : 'text-warning'">
({{ expirationNotice.text }})
</span>
</dd>
</div>
</template>
</dl>
<!-- 上传并更新 License -->
<el-upload
v-if="auth.showXpack && perm.system.other.license()"
ref="uploadRef"
class="mt-6"
action="#"
:auto-upload="false"
:show-file-list="false"
:disabled="loading"
:on-change="updateLicense"
>
<el-button type="primary" plain :loading="loading" :disabled="loading">更新 License</el-button>
</el-upload>
</div>
</div>
<p class="mb-0 mt-4 text-center text-sm text-N500">2014–{{ copyrightYear }} 版权所有 © 杭州飞致云信息科技有限公司</p>
</MkDialog>
</template>
13 changes: 11 additions & 2 deletions ui/src/layout/avatar-dropdown/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import LoginApi from '@/api/admin/auth/login'
import { LOGIN_METHOD } from '@/api/enums'
import { useStore } from '@/stores'
import ChangePasswordDialog from './ChangePasswordDialog.vue'
import AboutDialog from './AboutDialog.vue'
import perm from '@/permission/index.ts'

defineOptions({ name: 'AvatarDropdown' })
Expand All @@ -27,6 +28,13 @@ function handleOpenChangePassword() {
changePasswordDialogRef.value?.open()
}

// 查看平台版本与授权信息。
const aboutDialogRef = ref<InstanceType<typeof AboutDialog>>()

function handleOpenAbout() {
aboutDialogRef.value?.open()
}

function handleLogout() {
const loginMode = user.userInfo?.source
LoginApi.postLogout().then(() => {
Expand Down Expand Up @@ -113,8 +121,8 @@ function toUrl(url?: string) {
<template #icon><MkIcon name="icon-maybe_outlined" /></template>
<span>论坛求助</span>
</MkDropdownItem>
<!-- // TODO 关于 -->
<MkDropdownItem v-if="perm.system.other.about()">
<!-- 查看版本与授权信息 -->
<MkDropdownItem @click="handleOpenAbout">
<template #icon><MkIcon name="icon_info_outlined" /></template>
<span>关于</span>
</MkDropdownItem>
Expand All @@ -127,4 +135,5 @@ function toUrl(url?: string) {
</template>
</MkDropdown>
<ChangePasswordDialog ref="changePasswordDialogRef" />
<AboutDialog ref="aboutDialogRef" />
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ function deleteNode() {
删除
</MkDropdownItem>
<div v-if="sourceName" class="border-t mt-1 px-3 py-2" @click.stop>
<div class="text-sm text-N600">来源</div>
<div class="text-sm text-N500">来源</div>
<div class="mt-1 break-all text-N600">{{ sourceName }}</div>
</div>
</MkDropdownMenu>
Expand Down
Loading