feat: 增加 License 导入界面 (#4295)

This commit is contained in:
ssongliu 2024-03-25 12:20:07 +08:00 committed by GitHub
parent 33309d2d93
commit ba6991bfb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 600 additions and 9 deletions

View file

@ -47,10 +47,14 @@ func WithDetail(Key string, detail interface{}, err error) BusinessError {
}
func WithErr(Key string, err error) BusinessError {
paramMap := map[string]interface{}{}
if err != nil {
paramMap["err"] = err
}
return BusinessError{
Msg: Key,
Detail: "",
Err: err,
Msg: Key,
Map: paramMap,
Err: err,
}
}

View file

@ -43,6 +43,9 @@ var (
ErrNameIsExist = "ErrNameIsExist"
ErrDemoEnvironment = "ErrDemoEnvironment"
ErrCmdIllegal = "ErrCmdIllegal"
ErrXpackNotFound = "ErrXpackNotFound"
ErrXpackNotActive = "ErrXpackNotActive"
ErrXpackOutOfDate = "ErrXpackOutOfDate"
)
// app
@ -150,3 +153,12 @@ var (
var (
ErrNotExistUser = "ErrNotExistUser"
)
// license
var (
ErrLicense = "ErrLicense"
ErrLicenseCheckInLocal = "ErrLicenseCheckInLocal"
ErrLicenseCheckInRemote = "ErrLicenseCheckInRemote"
ErrLicenseSave = "ErrLicenseSave"
ErrLicenseSync = "ErrLicenseSync"
)

View file

@ -171,4 +171,14 @@ ErrBanAction: "Setting failed, the current {{ .name }} service is unavailable, p
ErrScope: "Modification of this configuration is not supported"
ErrStateChange: "State modification failed"
ErrRuleExist: "Rule is Exist"
ErrRuleNotExist: "Rule is not Exist"
ErrRuleNotExist: "Rule is not Exist"
#license
ErrLicense: "License format error, please re-import!"
ErrLicenseCheckInLocal: "Local license check failed, error {{ .err }}, please try again!"
ErrLicenseCheckInRemote: "Remote license check failed, error {{ .err }}, please try again!"
ErrLicenseSave: "Failed to save license information, error {{ .err }}, please try again!"
ErrLicenseSync: "Failed to synchronize license information, error {{ .err }}, please try again!"
ErrXpackNotFound: "This section is an Xpack feature, please first import the License in Panel Settings-License interface"
ErrXpackNotActive: "This section is an Xpack feature, please first synchronize the License status in Panel Settings-License interface"
ErrXpackOutOfDate: "The current License has expired, please re-import the License in Panel Settings-License interface"

View file

@ -173,3 +173,13 @@ ErrScope: "不支援修改此配置"
ErrStateChange: "狀態修改失敗"
ErrRuleExist: "規則名稱已存在"
ErrRuleNotExist: "規則不存在"
#license
ErrLicense: "License 格式錯誤,請重新匯入!"
ErrLicenseCheckInLocal: "License 本地校驗失敗,錯誤 {{ .err }},請重試!"
ErrLicenseCheckInRemote: "License 遠程校驗失敗,錯誤 {{ .err }},請重試!"
ErrLicenseSave: "License 資訊保存失敗,錯誤 {{ .err }},請重試!"
ErrLicenseSync: "License 資訊同步失敗,錯誤 {{ .err }},請重試!"
ErrXpackNotFound: "該部分為 Xpack 功能,請先在 面板設置-許可證 界面匯入 License"
ErrXpackNotActive: "該部分為 Xpack 功能,請先在 面板設置-許可證 界面同步 License 狀態"
ErrXpackOutOfDate: "當前 License 已過期,請重新在 面板設置-許可證 界面匯入 License"

View file

@ -171,4 +171,15 @@ ErrBanAction: "设置失败,当前 {{ .name }} 服务不可用,请检查后
ErrScope: "不支持修改此配置"
ErrStateChange: "状态修改失败"
ErrRuleExist: "规则名称已存在"
ErrRuleNotExist: "规则不存在"
ErrRuleNotExist: "规则不存在"
#license
ErrLicense: "License 格式错误,请重新导入!"
ErrLicenseCheckInLocal: "License 本地校验失败,错误 {{ .err }},请重试!"
ErrLicenseCheckInRemote: "License 远程校验失败,错误 {{ .err }},请重试!"
ErrLicenseSave: "License 信息保存失败,错误 {{ .err }},请重试!"
ErrLicenseSync: "License 信息同步失败,错误 {{ .err }},请重试!"
ErrXpackNotFound: "该部分为 Xpack 功能,请先在 面板设置-许可证 界面导入 License"
ErrXpackNotActive: "该部分为 Xpack 功能,请先在 面板设置-许可证 界面同步 License 状态"
ErrXpackOutOfDate: "当前 License 已过期,请重新在 面板设置-许可证 界面导入 License"

View file

@ -73,6 +73,11 @@ class RequestHttp {
});
return Promise.reject(data);
}
if (data.code == ResultEnum.ERRXPACK) {
globalStore.isProductPro = false;
window.location.reload();
return Promise.reject(data);
}
if (data.code == ResultEnum.ERRGLOBALLOADDING) {
globalStore.setGlobalLoading(true);
globalStore.setLoadingText(data.message);

View file

@ -140,4 +140,12 @@ export namespace Setting {
latestVersion: string;
releaseNote: string;
}
export interface License {
licenseName: string;
assigneeName: string;
productPro: string;
trial: boolean;
status: string;
}
}

View file

@ -6,6 +6,18 @@ import { Backup } from '../interface/backup';
import { Setting } from '../interface/setting';
import { TimeoutEnum } from '@/enums/http-enum';
export const UploadFileData = (params: FormData) => {
return http.upload('/licenses/upload', params);
};
export const getLicense = () => {
return http.get<Setting.License>(`/licenses/get`);
};
export const syncLicense = () => {
return http.post(`/licenses/sync`);
};
export const getSettingInfo = () => {
return http.post<Setting.SettingInfo>(`/settings/search`);
};

View file

@ -14,7 +14,6 @@
</el-button>
<el-divider direction="vertical" />
</span>
<span class="version">{{ $t('setting.currentVersion') + version }}</span>
<el-badge is-dot class="item" v-if="version !== 'Waiting' && globalStore.hasNewVersion">
<el-button type="primary" link @click="onLoadUpgradeInfo">
<span>{{ $t('setting.hasNewVersion') }}</span>

View file

@ -9,6 +9,7 @@ export enum ResultEnum {
ERRGLOBALLOADDING = 407,
ERRIP = 408,
ERRDOMAIN = 409,
ERRXPACK = 410,
TIMEOUT = 20000,
TYPE = 'success',
}

View file

@ -1449,6 +1449,34 @@ const message = {
forum: 'Forum Help',
doc2: 'User Manual',
currentVersion: 'Version',
license: 'License',
},
license: {
community: 'Community Edition',
pro: 'Professional Edition',
trial: 'Trial Version',
office: 'Official Version',
authorizationId: 'Subscription Authorization ID',
authorizedUser: 'Authorized User',
expiresAt: 'Expiration Time',
productName: 'Product Name',
productStatus: 'Product Status',
lost01: 'Lost * 1',
lost02: 'Lost * 2',
Enable: 'Enabled',
Disable: 'Disabled',
lostHelper:
'The license needs to be regularly synchronized for availability. Please ensure normal access to the external network. After three losses of contact, the license binding will be released.',
quickUpdate: 'Quick Update',
import: 'Import',
importLicense: 'Import License',
importHelper: 'Click or drag the License file here',
technicalAdvice: 'Technical Advice',
advice: 'Consultation',
indefinitePeriod: 'Indefinite Period',
levelUpPro: 'Upgrade to Professional Edition',
knowMorePro: 'Learn More about Professional Edition',
},
clean: {
scan: 'Start Scanning',

View file

@ -1350,6 +1350,32 @@ const message = {
forum: '論壇求助',
doc2: '使用手冊',
currentVersion: '當前運行版本',
license: '許可證',
},
license: {
community: '社區版',
pro: '專業版',
trial: '試用版',
office: '正式版本',
authorizationId: '訂閱授權 ID',
authorizedUser: '被授權方',
productName: '產品名稱',
productStatus: '產品狀態',
lost01: '失聯 * 1',
lost02: '失聯 * 2',
Enable: '已啟用',
Disable: '未啟用',
lostHelper: 'License 需要定時同步是否可用請確保正常外網訪問失聯三次後將解除 License 綁定',
quickUpdate: '快速更新',
import: '導入',
importLicense: '導入 License',
importHelper: '點擊或將 License 文件拖拽到此處',
technicalAdvice: '技術諮詢',
advice: '諮詢',
indefinitePeriod: '無限期',
levelUpPro: '升級專業版',
knowMorePro: '了解更多專業版信息',
},
clean: {
scan: '開始掃描',

View file

@ -1351,6 +1351,36 @@ const message = {
forum: '论坛求助',
doc2: '使用手册',
currentVersion: '当前运行版本',
license: '许可证',
},
license: {
community: '社区版',
pro: '专业版',
trial: '实验版',
office: '正式版',
authorizationId: '订阅授权 ID',
authorizedUser: '被授权方',
expiresAt: '到期时间',
productName: '产品名称',
productStatus: '产品状态',
lost01: '失联 * 1',
lost02: '失联 * 2',
Enable: '已激活',
Disable: '未激活',
lostHelper: 'License 需要定时同步是否可用请保证正常外网访问失联三次后将解除 License 绑定',
quickUpdate: '快速更新',
import: '导入',
importLicense: '导入 License',
importHelper: '点击或将 License 文件拖拽到此处',
technicalAdvice: '技术咨询',
advice: '咨询',
indefinitePeriod: '无限期',
levelUpPro: '升级专业版',
knowMorePro: '了解更多专业版信息',
noLicense: '该部分为 Xpack 功能请先在 面板设置-许可证 界面导入 License',
goImport: '去导入',
closeAlert: '当前页面可在面板设置中关闭显示',
},
clean: {
scan: '开始扫描',

View file

@ -23,7 +23,7 @@ import { MenuStore } from '@/store/modules/menu';
import { DeviceType } from '@/enums/app';
import { useI18n } from 'vue-i18n';
import { useTheme } from '@/hooks/use-theme';
import { getSettingInfo, getSystemAvailable } from '@/api/modules/setting';
import { getLicense, getSettingInfo, getSystemAvailable } from '@/api/modules/setting';
useResize();
const menuStore = MenuStore();
@ -73,6 +73,11 @@ const loadDataFromDB = async () => {
switchDark();
};
const loadProductProFromDB = async () => {
const res = await getLicense();
globalStore.isProductPro = res.data.status === 'Enable';
};
const updateDarkMode = async (event: MediaQueryListEvent) => {
const res = await getSettingInfo();
if (res.data.theme !== 'auto') {
@ -110,6 +115,7 @@ onBeforeUnmount(() => {
onMounted(() => {
loadStatus();
loadDataFromDB();
loadProductProFromDB();
const mqList = window.matchMedia('(prefers-color-scheme: dark)');
if (mqList.addEventListener) {

View file

@ -37,6 +37,16 @@ const settingRouter = {
activeMenu: 'Setting',
},
},
{
path: 'license',
name: 'License',
component: () => import('@/views/setting/license/index.vue'),
hidden: true,
meta: {
requiresAuth: true,
activeMenu: 'Setting',
},
},
{
path: 'about',
name: 'About',

View file

@ -36,6 +36,8 @@ export const GlobalStore = defineStore({
currentDB: '',
showEntranceWarn: true,
defaultNetwork: 'all',
isProductPro: false,
}),
getters: {},
actions: {

View file

@ -31,6 +31,8 @@ export interface GlobalState {
currentDB: string;
showEntranceWarn: boolean;
defaultNetwork: string;
isProductPro: boolean;
}
export interface MenuState {

View file

@ -7,7 +7,41 @@
path: '/',
},
]"
/>
>
<template #route-button>
<div class="router-button">
<span class="version" v-if="show">
{{ $t('license.community') }}
</span>
<span class="version" v-else>{{ $t('license.pro') }}</span>
<template v-if="show">
<el-divider direction="vertical" />
<el-button link type="primary" @click="dialogFormVisible = true">
{{ $t('license.levelUpPro') }}
</el-button>
</template>
</div>
</template>
</RouterButton>
<el-dialog v-model="dialogFormVisible" :title="$t('license.levelUpPro')" width="500">
<div style="text-align: center; margin-top: 20px">
<div style="justify-self: center">
<img style="width: 80px" src="@/assets/images/1panel-logo-light.png" />
</div>
<h3>{{ $t('setting.description') }}</h3>
<el-button type="primary" plain @click="toUpload">
{{ $t('license.importLicense') }}
</el-button>
<div style="margin-top: 10px">
<el-link @click="toHalo">
<span>{{ $t('license.knowMorePro') }}</span>
</el-link>
</div>
</div>
</el-dialog>
<el-alert
v-if="!isSafety && globalStore.showEntranceWarn"
style="margin-top: 20px"
@ -218,6 +252,8 @@
</CardWithHeader>
</el-col>
</el-row>
<Upload ref="uploadRef" @search="search()" />
</div>
</template>
@ -233,8 +269,9 @@ import { dateFormatForSecond, computeSize } from '@/utils/util';
import { useRouter } from 'vue-router';
import { loadBaseInfo, loadCurrentInfo } from '@/api/modules/dashboard';
import { getIOOptions, getNetworkOptions } from '@/api/modules/monitor';
import { getSettingInfo, loadUpgradeInfo } from '@/api/modules/setting';
import { getLicense, getSettingInfo, loadUpgradeInfo } from '@/api/modules/setting';
import { GlobalStore } from '@/store';
import Upload from '@/views/setting/license/upload/index.vue';
const router = useRouter();
const globalStore = GlobalStore();
@ -258,6 +295,23 @@ const timeNetDatas = ref<Array<string>>([]);
const ioOptions = ref();
const netOptions = ref();
const dialogFormVisible = ref(false);
const uploadRef = ref();
const loading = ref();
const show = ref(null);
const license = reactive({
licenseName: '',
trial: true,
expiresAt: '',
assigneeName: '',
productName: '',
status: '',
});
const searchInfo = reactive({
ioOption: 'all',
netOption: 'all',
@ -538,7 +592,30 @@ const onBlur = () => {
isActive.value = false;
};
const search = async () => {
loading.value = true;
await getLicense()
.then((res) => {
loading.value = false;
license.status = res.data.status;
show.value = license.status !== 'Enable';
})
.catch(() => {
show.value = true;
loading.value = false;
});
};
const toHalo = () => {
window.open('https://halo.test.lxware.cn/', '_blank', 'noopener,noreferrer');
};
const toUpload = () => {
uploadRef.value.acceptParams();
};
onMounted(() => {
search();
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
loadSafeStatus();
@ -607,4 +684,22 @@ onBeforeUnmount(() => {
margin-bottom: 10px;
}
}
.version {
font-size: 14px;
color: #858585;
text-decoration: none;
letter-spacing: 0.5px;
}
.system-link {
margin-left: 15px;
.svg-icon {
font-size: 7px;
}
span {
line-height: 20px;
}
}
</style>

View file

@ -27,6 +27,10 @@ const buttons = [
label: i18n.global.t('setting.snapshot'),
path: '/settings/snapshot',
},
{
label: i18n.global.t('setting.license'),
path: '/settings/license',
},
{
label: i18n.global.t('setting.about'),
path: '/settings/about',

View file

@ -0,0 +1,209 @@
<template>
<div>
<LayoutContent v-loading="loading" :title="$t('setting.license')" :divider="true">
<template #main>
<el-row :gutter="20" class="mt-5; mb-10">
<el-col :xs="24" :sm="24" :md="15" :lg="15" :xl="15">
<div class="descriptions" v-if="hasLicense()">
<el-descriptions :column="1" direction="horizontal" size="large" border>
<el-descriptions-item :label="$t('license.authorizationId')">
{{ license.licenseName || '-' }}
<el-button type="primary" class="ml-3" plain @click="onSync" size="small">
{{ $t('commons.button.sync') }}
</el-button>
</el-descriptions-item>
<el-descriptions-item :label="$t('license.authorizedUser')">
{{ license.assigneeName || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="$t('license.expiresAt')">
{{ license.expiresAt || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="$t('license.productName')">
{{ license.productName || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="$t('license.productStatus')">
<div v-if="license.status">
<el-tooltip
v-if="license.status.indexOf('lost') !== -1"
:content="$t('license.lostHelper')"
>
<el-tag type="info">
{{ $t('license.' + license.status) }}
</el-tag>
</el-tooltip>
<el-tag v-else>{{ $t('license.' + license.status) }}</el-tag>
</div>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
</div>
<CardWithHeader :header="$t('home.overview')" height="160px" v-if="!hasLicense()">
<template #body>
<div class="h-overview">
<el-row>
<el-col :span="6">
<span>{{ $t('setting.license') }}</span>
</el-col>
<el-col :span="6">
<span>{{ $t('license.community') }}</span>
</el-col>
</el-row>
</div>
</template>
</CardWithHeader>
</el-col>
<el-col :xs="24" :sm="24" :md="9" :lg="9" :xl="9">
<CardWithHeader :header="$t('license.quickUpdate')" height="160px">
<template #body>
<div class="h-app-card">
<el-row :gutter="10">
<el-col :span="15">
<div class="h-app-content">{{ $t('license.importLicense') }}</div>
</el-col>
<el-col :span="5">
<el-button type="primary" plain round size="small" @click="toUpload">
{{ $t('license.import') }}
</el-button>
</el-col>
</el-row>
</div>
<div class="h-app-card">
<el-row :gutter="10">
<el-col :span="15">
<div class="h-app-content">{{ $t('license.technicalAdvice') }}</div>
</el-col>
<el-col :span="5">
<el-button type="primary" plain round size="small" @click="toHalo()">
{{ $t('license.advice') }}
</el-button>
</el-col>
</el-row>
</div>
</template>
</CardWithHeader>
</el-col>
</el-row>
</template>
</LayoutContent>
<Upload ref="uploadRef" @search="search()" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { getLicense, syncLicense } from '@/api/modules/setting';
import CardWithHeader from '@/components/card-with-header/index.vue';
import Upload from '@/views/setting/license/upload/index.vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store';
const loading = ref();
const uploadRef = ref();
const globalStore = GlobalStore();
const license = reactive({
licenseName: '',
trial: true,
expiresAt: '',
assigneeName: '',
productName: '',
status: '',
});
const toHalo = () => {
window.open('https://halo.test.lxware.cn/', '_blank', 'noopener,noreferrer');
};
const onSync = async () => {
loading.value = true;
await syncLicense()
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
};
const timestampToDate = (timestamp: number) => {
const date = new Date(timestamp * 1000);
const y = date.getFullYear();
let m: string | number = date.getMonth() + 1;
m = m < 10 ? `0${String(m)}` : m;
let d: string | number = date.getDate();
d = d < 10 ? `0${String(d)}` : d;
let h: string | number = date.getHours();
h = h < 10 ? `0${String(h)}` : h;
let minute: string | number = date.getMinutes();
minute = minute < 10 ? `0${String(minute)}` : minute;
let second: string | number = date.getSeconds();
second = second < 10 ? `0${String(second)}` : second;
return `${y}-${m}-${d} ${h}:${minute}:${second}`;
};
const search = async () => {
loading.value = true;
await getLicense()
.then((res) => {
loading.value = false;
license.status = res.data.status;
globalStore.isProductPro = res.data.status === 'Enable';
if (res.data.status !== 'Enable') {
return;
}
license.licenseName = res.data.licenseName;
license.assigneeName = res.data.assigneeName;
license.trial = res.data.trial;
if (res.data.productPro) {
license.productName = 'product-1panel-pro';
license.expiresAt =
res.data.productPro === '0'
? i18n.global.t('license.indefinitePeriod')
: timestampToDate(Number(res.data.productPro));
}
})
.catch(() => {
loading.value = false;
});
};
const hasLicense = () => {
return license.status === 'Enable';
};
const toUpload = () => {
uploadRef.value.acceptParams();
};
onMounted(() => {
search();
});
</script>
<style scoped lang="scss">
.h-app-card {
padding: 10px 15px;
margin-right: 10px;
line-height: 18px;
.h-app-content {
padding-left: 15px;
.h-app-desc {
span {
font-weight: 400;
font-size: 12px;
color: var(--el-text-color-regular);
}
}
}
&:hover {
background-color: rgba(0, 94, 235, 0.03);
}
}
</style>

View file

@ -0,0 +1,107 @@
<template>
<el-drawer
v-model="open"
:before-close="handleClose"
size="30%"
:destroy-on-close="true"
:close-on-click-modal="false"
>
<template #header>
<DrawerHeader :header="$t('license.importLicense')" :back="handleClose" />
</template>
<div v-loading="loading">
<el-upload
action="#"
:auto-upload="false"
ref="uploadRef"
class="upload-demo"
drag
:limit="1"
:on-change="fileOnChange"
:on-exceed="handleExceed"
v-model:file-list="uploaderFiles"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
{{ $t('license.importHelper') }}
</div>
</el-upload>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :disabled="loading || uploaderFiles.length == 0">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
import { UploadFileData } from '@/api/modules/setting';
import i18n from '@/lang';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
const loading = ref(false);
const open = ref(false);
const em = defineEmits(['search']);
const uploadRef = ref<UploadInstance>();
const uploaderFiles = ref<UploadFiles>([]);
const handleClose = () => {
open.value = false;
uploadRef.value!.clearFiles();
em('search');
};
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
uploaderFiles.value = uploadFiles;
};
const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
uploadRef.value!.handleStart(file);
};
const submit = async () => {
if (uploaderFiles.value.length !== 1) {
return;
}
const file = uploaderFiles.value[0];
const formData = new FormData();
formData.append('file', file.raw);
loading.value = true;
await UploadFileData(formData)
.then(async () => {
loading.value = false;
uploadRef.value!.clearFiles();
uploaderFiles.value = [];
em('search');
open.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
window.location.reload();
})
.catch(() => {
loading.value = false;
uploadRef.value!.clearFiles();
uploaderFiles.value = [];
});
};
const acceptParams = () => {
uploaderFiles.value = [];
uploadRef.value?.clearFiles();
open.value = true;
};
defineExpose({ acceptParams });
</script>