Feat: show config file

This commit is contained in:
Lazy 2023-10-29 22:33:32 +08:00
parent 8dfb6cfc44
commit ad31e054cf
10 changed files with 432 additions and 2 deletions

View File

@ -11,6 +11,7 @@ declare module 'vue' {
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col']
@ -37,6 +38,7 @@ declare module 'vue' {
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
AResult: typeof import('ant-design-vue/es')['Result']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
@ -62,6 +64,7 @@ declare module 'vue' {
CopyButton: typeof import('./src/components/CopyButton.vue')['default']
DataStatistic: typeof import('./src/components/DataStatistic.vue')['default']
Editor: typeof import('./src/components/Editor.vue')['default']
'Eula.txt': typeof import('./src/components/mc_process_config/eula.txt.vue')['default']
FadeUpAnimation: typeof import('./src/components/FadeUpAnimation.vue')['default']
IconBtn: typeof import('./src/components/IconBtn.vue')['default']
InnerCard: typeof import('./src/components/InnerCard.vue')['default']
@ -69,6 +72,7 @@ declare module 'vue' {
LayoutCard: typeof import('./src/components/LayoutCard.vue')['default']
LeftMenuBtn: typeof import('./src/components/LeftMenuBtn.vue')['default']
LeftMenusPanel: typeof import('./src/components/LeftMenusPanel.vue')['default']
LineOption: typeof import('./src/components/LineOption.vue')['default']
Loading: typeof import('./src/components/Loading.vue')['default']
MyselfInfoDialog: typeof import('./src/components/MyselfInfoDialog.vue')['default']
NewCardList: typeof import('./src/components/NewCardList/index.vue')['default']

View File

@ -0,0 +1,86 @@
<!-- eslint-disable vue/no-mutating-props -->
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { t } from "@/lang/i18n";
import { EditOutlined } from "@ant-design/icons-vue";
const props = defineProps<{
optionValue?: { [key: string]: any };
optionKey?: any;
custom?: boolean;
}>();
const value = ref();
const key = ref();
const type = ref(0);
const valueType = (value: string) => {
if (typeof value === "object") return -1;
if (typeof value === "boolean") return 1;
if (typeof value === "number") return 2;
return 0;
};
const forceType = () => {
if (type.value >= 2) return (type.value = 1);
type.value = type.value + 1;
};
onMounted(() => {
if (props.custom) return;
if (props.optionValue) value.value = props.optionValue;
if (props.optionKey) key.value = props.optionKey;
if (valueType(value.value[key.value]) == 1) {
type.value = 1;
}
if (valueType(value.value[key.value]) != 1) {
type.value = 2;
}
});
</script>
<template>
<div class="line-option-wrapper">
<a-card :body-style="{ padding: '10px' }">
<div v-if="!custom">
<a-row :gutter="20" align="middle" :wrap="true">
<a-col :sm="7">
<slot name="title"></slot>
</a-col>
<a-col :sm="11">
<slot name="info"></slot>
</a-col>
<a-col :sm="6">
<div v-if="$slots.optionInput">
<slot name="optionInput"></slot>
</div>
<div v-else class="flex">
<a-input v-if="type == 2" v-model:value="value[key]" />
<a-select
v-if="type == 1"
v-model:value="value[key]"
style="width: 100%"
:placeholder="t('未选择')"
>
<a-select-option :value="true">{{ t("是") }}</a-select-option>
<a-select-option :value="false">{{ t("否") }}</a-select-option>
</a-select>
<a-button v-if="type == 1" @click="forceType"><EditOutlined /></a-button>
</div>
</a-col>
</a-row>
</div>
<div v-else>
<slot></slot>
</div>
</a-card>
</div>
</template>
<style lang="scss" scoped>
.line-option-wrapper {
width: 100%;
margin-bottom: 4px;
}
</style>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { t } from "@/lang/i18n";
import LineOption from "@/components/LineOption.vue";
import { getDescriptionByTitle } from "@/tools/commom";
const props = defineProps<{
config: Object;
}>();
const description = {
eula: t("是否同意 Minecraft EULA协议如果您要启动 Minecraft 服务器,则此选项是必须开启")
};
</script>
<template>
<a-col :span="24">
<CardPanel style="height: 100%">
<template #body>
<a-typography>
<a-typography-title :level="5">{{ t("关于配置兼容与翻译") }}</a-typography-title>
<a-typography-paragraph>
{{
t(
"此界面由开源社区开发者开发与翻译,若翻译发现错误可前往开源社区进行反馈。配置文件部分设置因版本和服务端类型不同会有些许变化,某些配置文件子元素过于复杂,可能会导致配置项值无法正常显示,如遇到不正常的配置项值切勿进行修改。若对配置文件要进行更为详细的配置,建议前往文件在线管理功能进行文件编辑。"
)
}}
</a-typography-paragraph>
</a-typography>
<a-typography>
<a-typography-title :level="5">{{ t("关于配置文件") }}</a-typography-title>
<a-typography-paragraph>
{{
t(
"此配置文件为 Minecraft 服务端的 EULA最终用户许可协议您必须同意此协议才可以正常运行服务端软件如果您发现此设置并不是“是”状态那么请立即修改。"
)
}}
</a-typography-paragraph>
</a-typography>
<a-typography>
<a-typography-title :level="5">{{ t("什么是最终用户许可协议") }}</a-typography-title>
<a-typography-paragraph>
{{
t(
"最终用户许可协议是软件应用程序作者或者发布者与应用程序使用者之间的合法合同。最终用户许可协议EULA通常是指“软件许可”它与租赁协议类似用户同意支付软件的使用费用并且向软件作者或者发行者承诺遵守EULA中规定的所有约束条件。用户被告知当他们打开软件包的包装、打开CD盒的封条、将卡片寄回给软件发行者、安装应用程序、执行下载文件或者简单的使用应用程序的时候就意味着他们已经“接受”了EULA中的条款。"
)
}}
</a-typography-paragraph>
</a-typography>
</template>
</CardPanel>
</a-col>
<a-col :span="24">
<CardPanel style="height: 100%">
<template #body>
<!-- 根据一层 Map 对象遍历所有选项 -->
<div v-for="(item, index) in config" :key="index">
<!-- 选项标题与选项传值,组件会自动判断其值类型采用不同组件 -->
<LineOption :option-value="config" :option-key="index">
<!-- 选项标题 -->
<template #title>{{ index }}</template>
<!-- 选项中文解释 -->
<template #info>{{ getDescriptionByTitle(description, index) }}</template>
</LineOption>
</div>
</template>
</CardPanel>
</a-col>
</template>

View File

@ -18,6 +18,7 @@ import Terminal from "@/widgets/instance/Terminal.vue";
import InstanceManagerBtns from "@/widgets/instance/ManagerBtns.vue";
import InstanceBaseInfo from "@/widgets/instance/BaseInfo.vue";
import InstanceServerConfigOverview from "@/widgets/instance/ServerConfigOverview.vue";
import InstanceServerConfigFile from "@/widgets/instance/ServerConfigFile.vue";
import InstanceFileManager from "@/widgets/instance/FileManager.vue";
import UserAccessSettings from "@/widgets/user/AccessSettings.vue";
import ImageBox from "@/widgets/others/ImageBox.vue";
@ -52,6 +53,7 @@ export const LAYOUT_CARD_TYPES: { [key: string]: any } = {
InstanceManagerBtns,
InstanceBaseInfo,
InstanceServerConfigOverview,
InstanceServerConfigFile,
InstanceFileManager,
UserAccessSettings,
ImageBox,

View File

@ -223,6 +223,28 @@ export let ORIGIN_LAYOUT_CONFIG: PageLayoutConfig[] = [
}
]
},
{
page: "/instances/terminal/serverConfig/fileEdit",
items: [
{
id: getRandomId(),
meta: {},
type: "InstanceServerConfigFile",
title: t("编辑服务端配置文件"),
width: 12,
height: LayoutCardHeight.AUTO,
disableDelete: true
},
{
id: getRandomId(),
meta: {},
type: "EmptyCard",
title: "",
width: 12,
height: LayoutCardHeight.MINI
}
]
},
{
page: "/users",
items: [

View File

@ -90,7 +90,17 @@ let originRouterConfig: RouterConfig[] = [
component: LayoutContainer,
meta: {
permission: ROLE.USER
}
},
children: [
{
path: "/instances/terminal/serverConfig/fileEdit",
name: t("编辑配置文件"),
component: LayoutContainer,
meta: {
permission: ROLE.USER
}
}
]
}
]
}

View File

@ -250,3 +250,19 @@ export const getConfigFileList = useDefineApi<
method: "POST",
url: "/api/protected_instance/process_config/list"
});
// 获取配置文件内容
export const getConfigFile = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
fileName: string;
type: string;
};
},
any
>({
method: "GET",
url: "/api/protected_instance/process_config/file"
});

View File

@ -1,5 +1,6 @@
import { LoadingOutlined } from "@ant-design/icons-vue";
import { h } from "vue";
import type { JsonData } from "@/types";
export async function sleep(t: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, t));
@ -14,3 +15,67 @@ export async function loadingIconFc(fontSize = 24) {
});
return indicator;
}
interface Description {
[key: string]: any;
}
export function getDescriptionByTitle(description: Description, title: string = "") {
const arr: string[] = title.split("/");
function _exec(keys: string[], _description: Description) {
if (!_description) return null;
const currentTitle: string | undefined = keys.shift();
if (keys.length !== 0) {
return _exec(keys, _description[currentTitle!]);
} else {
return _description[currentTitle!];
}
}
return _exec(arr, description);
}
export function jsonToMap(json: JsonData, topTitle = "", map = {}) {
for (const key in json) {
let title = null;
if (topTitle) {
title = `${topTitle}/${key}`;
} else {
title = `${key}`;
}
const value = json[key];
if (value instanceof Array) {
if (typeof value[0] === "object") {
jsonToMap(value, title, map);
} else {
Object.defineProperty(map, title, {
enumerable: true,
configurable: true,
get() {
return json[key];
},
set(v) {
json[key] = String(v).split(",");
}
});
}
} else if (typeof value === "object") {
jsonToMap(value, title, map);
} else {
Object.defineProperty(map, title, {
enumerable: true,
configurable: true,
get() {
return json[key];
},
set(v) {
const preValue = json[key];
if (typeof preValue === "number" && !isNaN(Number(v))) return (json[key] = Number(v));
json[key] = v;
}
});
}
}
return map;
}

View File

@ -0,0 +1,141 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { t } from "@/lang/i18n";
import CardPanel from "@/components/CardPanel.vue";
import type { LayoutCard } from "@/types";
import { useScreen } from "@/hooks/useScreen";
import { getConfigFile } from "@/services/apis/instance";
import { message } from "ant-design-vue";
import { useLayoutCardTools } from "@/hooks/useCardTools";
import { getInstanceConfigByType, type InstanceConfigs } from "@/hooks/useInstance";
import { useAppRouters } from "@/hooks/useAppRouters";
import eulaTxt from "@/components/mc_process_config/eula.txt.vue";
const props = defineProps<{
card: LayoutCard;
}>();
const { isPhone } = useScreen();
const { getMetaOrRouteValue } = useLayoutCardTools(props.card);
const instanceId = getMetaOrRouteValue("instanceId");
const daemonId = getMetaOrRouteValue("daemonId");
const configName = getMetaOrRouteValue("configName");
const configPath = getMetaOrRouteValue("configPath");
const extName = getMetaOrRouteValue("extName");
const type = getMetaOrRouteValue("type");
const component: any = {
// "common/server.properties": serverProperties,
"common/eula.txt": eulaTxt
// "bukkit/spigot.yml": spigotYml,
// "bukkit/bukkit.yml": bukkitYml,
// "bungeecord/config.yml": configYml,
// "bds/server.properties": bdsServerProperties,
// "mohist/mohist.yml": mohistYml,
// "paper/paper.yml": paperYml,
// "paper/paper-global.yml": paperGlobalYml,
// "paper/paper-world-defaults.yml": paperWorldDefaultsYml,
// "geyser/config.yml": geyserYml,
// "mcdr/config.yml": mcdrConfigYml,
// "mcdr/permission.yml": permissionYml,
// "velocity/velocity.toml": velocityToml
};
const isFailure = ref(false);
const config = ref();
const { toPage } = useAppRouters();
const toConfigOverview = () => {
toPage({
path: "/instances/terminal/serverConfig",
query: {
type
}
});
};
const { execute, state, isLoading } = getConfigFile();
const render = async () => {
try {
await execute({
params: {
uuid: instanceId ?? "",
remote_uuid: daemonId ?? "",
fileName: configPath ?? "",
type: extName ?? ""
}
});
if (state.value) {
config.value = state.value;
}
} catch (err: any) {
console.error(err);
isFailure.value = true;
return message.error(err.message);
}
};
onMounted(async () => {
await render();
});
</script>
<template>
<div style="height: 100%" class="container">
<a-row :gutter="[24, 24]" style="height: 100%">
<a-col v-if="!isFailure" :span="24">
<BetweenMenus>
<template #left>
<a-button class="mr-8" @click="toConfigOverview">
{{ t("返回配置文件列表") }}
</a-button>
</template>
<template #right>
<a-button type="primary" class="mr-8">
{{ t("保存文件") }}
</a-button>
<a-button v-if="!isPhone" :loading="isLoading" class="mr-8" @click="render">
{{ t("重新加载") }}
</a-button>
<a-button v-if="!isPhone" type="dashed">
{{ t("编辑源文件") }}
</a-button>
<a-dropdown v-if="isPhone">
<template #overlay>
<a-menu>
<a-menu-item key="2">
{{ t("重新加载") }}
</a-menu-item>
<a-menu-item key="3">
{{ t("编辑源文件") }}
</a-menu-item>
</a-menu>
</template>
<a-button type="primary">
{{ t("更多操作") }}
<DownOutlined />
</a-button>
</a-dropdown>
</template>
</BetweenMenus>
</a-col>
<component :is="component[configName ?? '']" v-if="!isFailure" :config="config" />
<a-col v-else :span="24">
<a-result
status="error"
:title="t('错误')"
:sub-title="'文件不存在或权限不正确,无法查看此文件的具体配置,您也许可以尝试到 “文件管理”功能在线编辑此文件,或尝试重启实例刷新此文件。'"
>
<template #extra>
<a-button type="primary" @click="toConfigOverview">
{{ t("回到配置文件列表") }}
</a-button>
</template>
</a-result>
</a-col>
</a-row>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -64,6 +64,18 @@ const render = async () => {
}
};
const toEdit = (configName: string, configPath: string, extName: string) => {
toPage({
path: "/instances/terminal/serverConfig/fileEdit",
query: {
type: type,
configName,
configPath,
extName
}
});
};
onMounted(async () => {
await render();
});
@ -131,7 +143,9 @@ onMounted(async () => {
</template>
</a-list-item-meta>
<template #actions>
<a-button size="">{{ t("编辑") }}</a-button>
<a-button size="" @click="toEdit(item.redirect, item.path, item.type)">
{{ t("编辑") }}
</a-button>
</template>
</a-list-item>
</template>