feat: Add support for offline version. (#10395)

This commit is contained in:
CityFun 2025-09-17 17:49:59 +08:00 committed by wanghe-fit2cloud
parent 725d03b41e
commit 4e8c5a21f9
23 changed files with 54 additions and 13 deletions

View file

@ -14,6 +14,7 @@ type Base struct {
Mode string `mapstructure:"mode"` // xpack [ Enable / Disable ] Mode string `mapstructure:"mode"` // xpack [ Enable / Disable ]
IsDemo bool `mapstructure:"is_demo"` IsDemo bool `mapstructure:"is_demo"`
InstallDir string `mapstructure:"install_dir"` InstallDir string `mapstructure:"install_dir"`
IsOffLine bool `mapstructure:"is_offline"`
} }
type RemoteURL struct { type RemoteURL struct {

View file

@ -137,6 +137,7 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) {
res := &dto.LoginSetting{ res := &dto.LoginSetting{
IsDemo: global.CONF.Base.IsDemo, IsDemo: global.CONF.Base.IsDemo,
IsIntl: global.CONF.Base.IsIntl, IsIntl: global.CONF.Base.IsIntl,
IsOffLine: global.CONF.Base.IsOffLine,
Language: settingInfo.Language, Language: settingInfo.Language,
MenuTabs: settingInfo.MenuTabs, MenuTabs: settingInfo.MenuTabs,
PanelName: settingInfo.PanelName, PanelName: settingInfo.PanelName,

View file

@ -232,6 +232,7 @@ type AppstoreConfig struct {
type LoginSetting struct { type LoginSetting struct {
IsDemo bool `json:"isDemo"` IsDemo bool `json:"isDemo"`
IsIntl bool `json:"isIntl"` IsIntl bool `json:"isIntl"`
IsOffLine bool `json:"isOffLine"`
Language string `json:"language"` Language string `json:"language"`
MenuTabs string `json:"menuTabs"` MenuTabs string `json:"menuTabs"`
PanelName string `json:"panelName"` PanelName string `json:"panelName"`

View file

@ -38,6 +38,9 @@ func NewIUpgradeService() IUpgradeService {
} }
func (u *UpgradeService) SearchUpgrade() (*dto.UpgradeInfo, error) { func (u *UpgradeService) SearchUpgrade() (*dto.UpgradeInfo, error) {
if global.CONF.Base.IsOffLine {
return &dto.UpgradeInfo{}, nil
}
var upgrade dto.UpgradeInfo var upgrade dto.UpgradeInfo
currentVersion, err := settingRepo.Get(repo.WithByKey("SystemVersion")) currentVersion, err := settingRepo.Get(repo.WithByKey("SystemVersion"))
if err != nil { if err != nil {

View file

@ -14,6 +14,7 @@ type Base struct {
Language string `mapstructure:"language"` Language string `mapstructure:"language"`
IsDemo bool `mapstructure:"is_demo"` IsDemo bool `mapstructure:"is_demo"`
IsIntl bool `mapstructure:"is_intl"` IsIntl bool `mapstructure:"is_intl"`
IsOffLine bool `mapstructure:"is_offline"`
Version string `mapstructure:"version"` Version string `mapstructure:"version"`
InstallDir string `mapstructure:"install_dir"` InstallDir string `mapstructure:"install_dir"`
ChangeUserInfo string `mapstructure:"change_user_info"` ChangeUserInfo string `mapstructure:"change_user_info"`

View file

@ -91,6 +91,7 @@ func Init() {
global.CONF.Base.InstallDir = baseDir global.CONF.Base.InstallDir = baseDir
global.CONF.Base.IsDemo = v.GetBool("base.is_demo") global.CONF.Base.IsDemo = v.GetBool("base.is_demo")
global.CONF.Base.IsIntl = v.GetBool("base.is_intl") global.CONF.Base.IsIntl = v.GetBool("base.is_intl")
global.CONF.Base.IsOffLine = v.GetBool("base.is_offline")
global.CONF.Base.Version = version global.CONF.Base.Version = version
global.CONF.Base.Username = username global.CONF.Base.Username = username
global.CONF.Base.Password = password global.CONF.Base.Password = password

View file

@ -18,7 +18,15 @@
</span> </span>
<div class="flex flex-wrap items-center"> <div class="flex flex-wrap items-center">
<el-link underline="never" type="primary" @click="toLxware"> <el-link underline="never" type="primary" @click="toLxware">
{{ $t(!isMasterPro ? 'license.community' : 'license.pro') }} <span v-if="isMasterPro">
{{ $t('license.pro') }}
</span>
<span v-else-if="isOffLine">
{{ $t('license.offLine') }}
</span>
<span v-else>
{{ $t('license.community') }}
</span>
</el-link> </el-link>
<el-link <el-link
underline="never" underline="never"
@ -56,7 +64,7 @@ import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const { docsUrl } = storeToRefs(globalStore); const { docsUrl, isOffLine } = storeToRefs(globalStore);
const upgradeRef = ref(); const upgradeRef = ref();
const releasesRef = ref(); const releasesRef = ref();
const isMasterPro = computed(() => { const isMasterPro = computed(() => {

View file

@ -1982,6 +1982,7 @@ const message = {
backupRecoverMessage: 'Please enter the compression or decompression password (leave blank to not set)', backupRecoverMessage: 'Please enter the compression or decompression password (leave blank to not set)',
}, },
license: { license: {
offLine: 'Offline',
community: 'OSS', community: 'OSS',
oss: 'Open Source Software', oss: 'Open Source Software',
pro: 'Pro', pro: 'Pro',

View file

@ -1899,6 +1899,7 @@ const message = {
backupRecoverMessage: '圧縮または減圧パスワードを入力してください設定しないように空白のままにしてください', backupRecoverMessage: '圧縮または減圧パスワードを入力してください設定しないように空白のままにしてください',
}, },
license: { license: {
offLine: 'オフライン版',
community: '無料', community: '無料',
oss: '無料', oss: '無料',
pro: '専門', pro: '専門',

View file

@ -1870,6 +1870,7 @@ const message = {
backupRecoverMessage: '압축 또는 압축 해제 비밀번호를 입력하세요 (설정하지 않으려면 비워 두세요)', backupRecoverMessage: '압축 또는 압축 해제 비밀번호를 입력하세요 (설정하지 않으려면 비워 두세요)',
}, },
license: { license: {
offLine: '오프라인 버전',
community: 'OSS', community: 'OSS',
oss: '오픈 소스 소프트웨어', oss: '오픈 소스 소프트웨어',
pro: 'Pro', pro: 'Pro',

View file

@ -1958,6 +1958,7 @@ const message = {
'Sila masukkan kata laluan mampatan atau nyahmampatan (biarkan kosong jika tidak menetapkan)', 'Sila masukkan kata laluan mampatan atau nyahmampatan (biarkan kosong jika tidak menetapkan)',
}, },
license: { license: {
offLine: 'Versi Luar Talian',
community: 'OSS', community: 'OSS',
oss: 'Perisian Sumber Terbuka', oss: 'Perisian Sumber Terbuka',
pro: 'Pro', pro: 'Pro',

View file

@ -1946,6 +1946,7 @@ const message = {
'Por favor, insira a senha de compressão ou descompressão (deixe em branco para não definir)', 'Por favor, insira a senha de compressão ou descompressão (deixe em branco para não definir)',
}, },
license: { license: {
offLine: 'Versão Offline',
community: 'Gratuito', community: 'Gratuito',
oss: 'Open Source Software', oss: 'Open Source Software',
pro: 'Pro', pro: 'Pro',

View file

@ -1945,6 +1945,7 @@ const message = {
'Пожалуйста, введите пароль для сжатия или распаковки (оставьте пустым, чтобы не устанавливать)', 'Пожалуйста, введите пароль для сжатия или распаковки (оставьте пустым, чтобы не устанавливать)',
}, },
license: { license: {
offLine: 'Офлайн версия',
community: 'OSS', community: 'OSS',
oss: 'Open Source Software', oss: 'Open Source Software',
pro: 'Pro', pro: 'Pro',

View file

@ -1999,6 +1999,7 @@ const message = {
backupRecoverMessage: 'Lütfen sıkıştırma veya sıkıştırma açma parolasını girin (ayarlamamak için boş bırakın)', backupRecoverMessage: 'Lütfen sıkıştırma veya sıkıştırma açma parolasını girin (ayarlamamak için boş bırakın)',
}, },
license: { license: {
offLine: 'Çevrimdışı Sürüm',
community: 'OSS', community: 'OSS',
oss: 'ık Kaynak Yazılım', oss: 'ık Kaynak Yazılım',
pro: 'Pro', pro: 'Pro',

View file

@ -1855,6 +1855,7 @@ const message = {
backupRecoverMessage: '請輸入壓縮或解壓縮密碼留空則不設定', backupRecoverMessage: '請輸入壓縮或解壓縮密碼留空則不設定',
}, },
license: { license: {
offLine: '離線版',
community: '社區版', community: '社區版',
oss: '社區版', oss: '社區版',
pro: '專業版', pro: '專業版',

View file

@ -1848,6 +1848,7 @@ const message = {
backupRecoverMessage: '请输入压缩或解压缩密码留空则不设置', backupRecoverMessage: '请输入压缩或解压缩密码留空则不设置',
}, },
license: { license: {
offLine: '离线版',
community: '社区版', community: '社区版',
oss: '社区版', oss: '社区版',
pro: '专业版', pro: '专业版',

View file

@ -41,6 +41,7 @@ export interface GlobalState {
isIntl: boolean; isIntl: boolean;
productProExpires: number; productProExpires: number;
isMasterProductPro: boolean; isMasterProductPro: boolean;
isOffLine: boolean;
currentNode: string; currentNode: string;
currentNodeAddr: string; currentNodeAddr: string;

View file

@ -44,6 +44,7 @@ const GlobalStore = defineStore({
isIntl: false, isIntl: false,
productProExpires: 0, productProExpires: 0,
isMasterProductPro: false, isMasterProductPro: false,
isOffLine: false,
currentNode: 'local', currentNode: 'local',
currentNodeAddr: '', currentNodeAddr: '',

View file

@ -7,7 +7,7 @@
</template> </template>
<template #leftToolBar> <template #leftToolBar>
<el-button @click="sync" type="primary" plain :disabled="syncing"> <el-button @click="sync" type="primary" plain :disabled="syncing">
<span>{{ syncCustomAppstore ? $t('app.syncCustomApp') : $t('app.syncAppList') }}</span> <span>{{ syncCustomAppstore || isOffLine ? $t('app.syncCustomApp') : $t('app.syncAppList') }}</span>
</el-button> </el-button>
<el-button @click="syncLocal" type="primary" plain :disabled="syncing" class="ml-2"> <el-button @click="syncLocal" type="primary" plain :disabled="syncing" class="ml-2">
{{ $t('app.syncLocalApp') }} {{ $t('app.syncLocalApp') }}
@ -74,11 +74,9 @@ import { searchApp, syncApp, syncCutomAppStore, syncLocalApp, getCurrentNodeCust
import Install from '../detail/install/index.vue'; import Install from '../detail/install/index.vue';
import router from '@/routers'; import router from '@/routers';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store';
import { newUUID } from '@/utils/util'; import { newUUID } from '@/utils/util';
import Detail from '../detail/index.vue'; import Detail from '../detail/index.vue';
import TaskLog from '@/components/log/task/index.vue'; import TaskLog from '@/components/log/task/index.vue';
import { storeToRefs } from 'pinia';
import bus from '@/global/bus'; import bus from '@/global/bus';
import Tags from '@/views/app-store/components/tag.vue'; import Tags from '@/views/app-store/components/tag.vue';
import DockerStatus from '@/views/container/docker-status/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue';
@ -86,9 +84,8 @@ import NoApp from '@/views/app-store/apps/no-app/index.vue';
import AppCard from '@/views/app-store/apps/app/index.vue'; import AppCard from '@/views/app-store/apps/app/index.vue';
import MainDiv from '@/components/main-div/index.vue'; import MainDiv from '@/components/main-div/index.vue';
import { jumpToInstall } from '@/utils/app'; import { jumpToInstall } from '@/utils/app';
import { useGlobalStore } from '@/composables/useGlobalStore';
const globalStore = GlobalStore(); const { globalStore, isProductPro, isOffLine } = useGlobalStore();
const { isProductPro } = storeToRefs(globalStore);
const mobile = computed(() => { const mobile = computed(() => {
return globalStore.isMobile(); return globalStore.isMobile();
@ -183,7 +180,7 @@ const sync = async () => {
}; };
try { try {
let res; let res;
if (isProductPro.value && syncCustomAppstore.value) { if (isOffLine.value || (isProductPro.value && syncCustomAppstore.value)) {
res = await syncCutomAppStore(syncReq); res = await syncCutomAppStore(syncReq);
} else { } else {
res = await syncApp(syncReq); res = await syncApp(syncReq);
@ -249,6 +246,9 @@ onMounted(async () => {
syncCustomAppstore.value = res.data.status === 'Enable'; syncCustomAppstore.value = res.data.status === 'Enable';
} }
} }
if (isOffLine.value) {
syncCustomAppstore.value = true;
}
mainHeight.value = window.innerHeight - 380; mainHeight.value = window.innerHeight - 380;
window.onresize = () => { window.onresize = () => {
return (() => { return (() => {

View file

@ -146,6 +146,8 @@ import { Container } from '@/api/interface/container';
import CodemirrorPro from '@/components/codemirror-pro/index.vue'; import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import { computeSizeFromMB } from '@/utils/util'; import { computeSizeFromMB } from '@/utils/util';
import { loadResourceLimit } from '@/api/modules/container'; import { loadResourceLimit } from '@/api/modules/container';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isOffLine } = useGlobalStore();
interface ClusterProps { interface ClusterProps {
key: string; key: string;
@ -282,6 +284,9 @@ const initForm = async (appKey: string) => {
formData.value.version = defaultVersion; formData.value.version = defaultVersion;
getVersionDetail(defaultVersion); getVersionDetail(defaultVersion);
} }
if (isOffLine.value) {
formData.value.pullImage = false;
}
}; };
const getMasterAppInstall = async (appInstallID: number, masterNode: string) => { const getMasterAppInstall = async (appInstallID: number, masterNode: string) => {

View file

@ -39,7 +39,7 @@
@change="updateConfig('UpgradeBackup', config.upgradeBackup)" @change="updateConfig('UpgradeBackup', config.upgradeBackup)"
/> />
</el-form-item> </el-form-item>
<CustomSetting v-if="globalStore.isProductPro" /> <CustomSetting v-if="isProductPro" />
<span class="input-help logText" v-else> <span class="input-help logText" v-else>
{{ $t('xpack.customApp.licenseHelper') }} {{ $t('xpack.customApp.licenseHelper') }}
<el-link class="link" @click="toUpload" type="primary"> <el-link class="link" @click="toUpload" type="primary">
@ -58,10 +58,11 @@
import { getCurrentNodeCustomAppConfig } from '@/api/modules/app'; import { getCurrentNodeCustomAppConfig } from '@/api/modules/app';
import { getAppStoreConfig, updateAppStoreConfig } from '@/api/modules/setting'; import { getAppStoreConfig, updateAppStoreConfig } from '@/api/modules/setting';
import { FormRules } from 'element-plus'; import { FormRules } from 'element-plus';
import { GlobalStore } from '@/store';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang'; import i18n from '@/lang';
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isProductPro, isMasterProductPro } = useGlobalStore();
const CustomSetting = defineAsyncComponent(async () => { const CustomSetting = defineAsyncComponent(async () => {
const modules = import.meta.glob('@/xpack/views/appstore/index.vue'); const modules = import.meta.glob('@/xpack/views/appstore/index.vue');
@ -72,7 +73,6 @@ const CustomSetting = defineAsyncComponent(async () => {
return { template: '<div></div>' }; return { template: '<div></div>' };
}); });
const globalStore = GlobalStore();
const rules = ref<FormRules>({}); const rules = ref<FormRules>({});
const config = ref({ const config = ref({
uninstallDeleteImage: '', uninstallDeleteImage: '',
@ -107,7 +107,7 @@ const toUpload = () => {
}; };
const getNodeConfig = async () => { const getNodeConfig = async () => {
if (globalStore.isMasterProductPro) { if (isMasterProductPro.value) {
return; return;
} }
const res = await getCurrentNodeCustomAppConfig(); const res = await getCurrentNodeCustomAppConfig();

View file

@ -422,6 +422,7 @@ const getSetting = async () => {
handleCommand(loginForm.language); handleCommand(loginForm.language);
isIntl.value = res.data.isIntl; isIntl.value = res.data.isIntl;
globalStore.isIntl = isIntl.value; globalStore.isIntl = isIntl.value;
globalStore.isOffLine = res.data.isOffLine;
document.title = res.data.panelName; document.title = res.data.panelName;
i18n.locale.value = res.data.language; i18n.locale.value = res.data.language;

View file

@ -9,6 +9,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import i18n from '@/lang'; import i18n from '@/lang';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isOffLine } = useGlobalStore();
const buttons = [ const buttons = [
{ {
@ -40,4 +42,10 @@ const buttons = [
path: '/settings/about', path: '/settings/about',
}, },
]; ];
onMounted(() => {
if (isOffLine.value) {
buttons.splice(5, 1);
}
});
</script> </script>