feat: supports to display plugin CHANGELOG.md (#4337)

* feat: optimize plugin update changelog feature, refactor to reuse ReadmeDialog and support independent view entry

* fix: distinguish error state from empty state in ReadmeDialog
This commit is contained in:
clown145
2026-01-06 12:53:14 +08:00
committed by GitHub
parent 399062f14d
commit 9322218880
8 changed files with 1656 additions and 672 deletions

View File

@@ -55,6 +55,7 @@ class PluginRoute(Route):
"/plugin/on": ("POST", self.on_plugin),
"/plugin/reload": ("POST", self.reload_plugins),
"/plugin/readme": ("GET", self.get_plugin_readme),
"/plugin/changelog": ("GET", self.get_plugin_changelog),
"/plugin/source/get": ("GET", self.get_custom_source),
"/plugin/source/save": ("POST", self.save_custom_source),
}
@@ -615,6 +616,55 @@ class PluginRoute(Route):
logger.error(f"/api/plugin/readme: {traceback.format_exc()}")
return Response().error(f"读取README文件失败: {e!s}").__dict__
async def get_plugin_changelog(self):
"""获取插件更新日志
读取插件目录下的 CHANGELOG.md 文件内容。
"""
plugin_name = request.args.get("name")
logger.debug(f"正在获取插件 {plugin_name} 的更新日志")
if not plugin_name:
return Response().error("插件名称不能为空").__dict__
# 查找插件
plugin_obj = None
for plugin in self.plugin_manager.context.get_all_stars():
if plugin.name == plugin_name:
plugin_obj = plugin
break
if not plugin_obj:
return Response().error(f"插件 {plugin_name} 不存在").__dict__
if not plugin_obj.root_dir_name:
return Response().error(f"插件 {plugin_name} 目录不存在").__dict__
plugin_dir = os.path.join(
self.plugin_manager.plugin_store_path,
plugin_obj.root_dir_name,
)
# 尝试多种可能的文件名
changelog_names = ["CHANGELOG.md", "changelog.md", "CHANGELOG", "changelog"]
for name in changelog_names:
changelog_path = os.path.join(plugin_dir, name)
if os.path.isfile(changelog_path):
try:
with open(changelog_path, encoding="utf-8") as f:
changelog_content = f.read()
return (
Response()
.ok({"content": changelog_content}, "成功获取更新日志")
.__dict__
)
except Exception as e:
logger.error(f"/api/plugin/changelog: {traceback.format_exc()}")
return Response().error(f"读取更新日志失败: {e!s}").__dict__
# 没有找到 changelog 文件,返回 ok 但 content 为 null
return Response().ok({"content": None}, "该插件没有更新日志文件").__dict__
async def get_custom_source(self):
"""获取自定义插件源"""
sources = await sp.global_get("custom_plugin_sources", [])

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { ref, computed, inject } from 'vue';
import { ref, computed, inject } from "vue";
import { useCustomizerStore } from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
import UninstallConfirmDialog from './UninstallConfirmDialog.vue';
import { useModuleI18n } from "@/i18n/composables";
import UninstallConfirmDialog from "./UninstallConfirmDialog.vue";
const props = defineProps({
extension: {
@@ -21,80 +21,114 @@ const props = defineProps({
// 定义要发送到父组件的事件
const emit = defineEmits([
'configure',
'update',
'reload',
'install',
'uninstall',
'toggle-activation',
'view-handlers',
'view-readme'
"configure",
"update",
"reload",
"install",
"uninstall",
"toggle-activation",
"view-handlers",
"view-readme",
"view-changelog",
]);
const reveal = ref(false);
const showUninstallDialog = ref(false);
// 国际化
const { tm } = useModuleI18n('features/extension');
const { tm } = useModuleI18n("features/extension");
// 操作函数
const configure = () => {
emit('configure', props.extension);
emit("configure", props.extension);
};
const updateExtension = () => {
emit('update', props.extension);
emit("update", props.extension);
};
const reloadExtension = () => {
emit('reload', props.extension);
emit("reload", props.extension);
};
const $confirm = inject("$confirm");
const installExtension = async () => {
emit('install', props.extension);
emit("install", props.extension);
};
const uninstallExtension = async () => {
showUninstallDialog.value = true;
};
const handleUninstallConfirm = (options: { deleteConfig: boolean; deleteData: boolean }) => {
const handleUninstallConfirm = (options: {
deleteConfig: boolean;
deleteData: boolean;
}) => {
emit("uninstall", props.extension, options);
};
const toggleActivation = () => {
emit('toggle-activation', props.extension);
emit("toggle-activation", props.extension);
};
const viewHandlers = () => {
emit('view-handlers', props.extension);
emit("view-handlers", props.extension);
};
const viewReadme = () => {
emit('view-readme', props.extension);
emit("view-readme", props.extension);
};
const viewChangelog = () => {
emit("view-changelog", props.extension);
};
</script>
<template>
<v-card class="mx-auto d-flex flex-column" elevation="0" :style="{
position: 'relative',
backgroundColor: useCustomizerStore().uiTheme === 'PurpleTheme' ? marketMode ? '#f8f0dd' : '#ffffff' : '#282833',
color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff'
}">
<v-card-text style="padding: 16px; padding-bottom: 0px; display: flex; gap: 16px; width: 100%;">
<v-card
class="mx-auto d-flex flex-column"
elevation="0"
:style="{
position: 'relative',
backgroundColor:
useCustomizerStore().uiTheme === 'PurpleTheme'
? marketMode
? '#f8f0dd'
: '#ffffff'
: '#282833',
color:
useCustomizerStore().uiTheme === 'PurpleTheme'
? '#000000dd'
: '#ffffff',
}"
>
<v-card-text
style="
padding: 16px;
padding-bottom: 0px;
display: flex;
gap: 16px;
width: 100%;
"
>
<div v-if="extension?.logo">
<img :src="extension.logo" :alt="extension.name" cover width="100"/>
<img :src="extension.logo" :alt="extension.name" cover width="100" />
</div>
<div style="overflow-x: auto;">
<div style="overflow-x: auto">
<!-- Top-right three-dot menu -->
<div style="position: absolute; right: 8px; top: 8px; z-index: 5;">
<div style="position: absolute; right: 8px; top: 8px; z-index: 5">
<v-menu offset-y>
<template v-slot:activator="{ props: menuProps }">
<v-btn icon variant="text" aria-label="more" v-if="extension?.repo" :href="extension?.repo"
target="_blank">
<v-btn
icon
variant="text"
aria-label="more"
v-if="extension?.repo"
:href="extension?.repo"
target="_blank"
>
<v-icon icon="mdi-github"></v-icon>
</v-btn>
<v-btn v-bind="menuProps" icon variant="text" aria-label="more">
@@ -104,16 +138,30 @@ const viewReadme = () => {
<v-list>
<v-list-item @click="viewReadme">
<v-list-item-title>📄 {{ tm('buttons.viewDocs') }}</v-list-item-title>
<v-list-item-title
>📄 {{ tm("buttons.viewDocs") }}</v-list-item-title
>
</v-list-item>
<v-list-item v-if="marketMode && !extension?.installed" @click="installExtension">
<v-list-item v-if="!marketMode" @click="viewChangelog">
<v-list-item-title
>📝 {{ tm("pluginChangelog.menuTitle") }}</v-list-item-title
>
</v-list-item>
<v-list-item
v-if="marketMode && !extension?.installed"
@click="installExtension"
>
<v-list-item-title>
{{ tm('buttons.install') }}</v-list-item-title>
{{ tm("buttons.install") }}</v-list-item-title
>
</v-list-item>
<v-list-item v-if="marketMode && extension?.installed">
<v-list-item-title class="text--disabled">{{ tm('status.installed') }}</v-list-item-title>
<v-list-item-title class="text--disabled">{{
tm("status.installed")
}}</v-list-item-title>
</v-list-item>
<!-- Divider between market actions and plugin actions -->
@@ -122,34 +170,49 @@ const viewReadme = () => {
<template v-if="!marketMode">
<v-list-item @click="configure">
<v-list-item-title>
{{ tm('card.actions.pluginConfig') }}</v-list-item-title>
{{ tm("card.actions.pluginConfig") }}</v-list-item-title
>
</v-list-item>
<v-list-item @click="uninstallExtension">
<v-list-item-title class="text-error">{{ tm('card.actions.uninstallPlugin') }}</v-list-item-title>
<v-list-item-title class="text-error">{{
tm("card.actions.uninstallPlugin")
}}</v-list-item-title>
</v-list-item>
<v-list-item @click="reloadExtension">
<v-list-item-title>{{ tm('card.actions.reloadPlugin') }}</v-list-item-title>
<v-list-item-title>{{
tm("card.actions.reloadPlugin")
}}</v-list-item-title>
</v-list-item>
<v-list-item @click="toggleActivation">
<v-list-item-title>
{{ extension.activated ? tm('buttons.disable') : tm('buttons.enable') }}{{
tm('card.actions.togglePlugin') }}
{{
extension.activated
? tm("buttons.disable")
: tm("buttons.enable")
}}{{ tm("card.actions.togglePlugin") }}
</v-list-item-title>
</v-list-item>
<v-list-item @click="viewHandlers">
<v-list-item-title>{{ tm('card.actions.viewHandlers') }} ({{ extension.handlers.length
}})</v-list-item-title>
<v-list-item-title
>{{ tm("card.actions.viewHandlers") }} ({{
extension.handlers.length
}})</v-list-item-title
>
</v-list-item>
<v-list-item @click="updateExtension">
<v-list-item-title>
{{ extension.has_update
? tm('card.actions.updateTo') + ' ' + extension.online_version
: tm('card.actions.reinstall') }}
{{
extension.has_update
? tm("card.actions.updateTo") +
" " +
extension.online_version
: tm("card.actions.reinstall")
}}
</v-list-item-title>
</v-list-item>
</template>
@@ -157,23 +220,59 @@ const viewReadme = () => {
</v-menu>
</div>
<div style="width: 100%; margin-bottom: 24px;">
<div style="width: 100%; margin-bottom: 24px">
<!-- 最多一行 -->
<div class="text-caption"
style="color: gray; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 84px;">
<div
class="text-caption"
style="
color: gray;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 84px;
"
>
{{ extension.author }} / {{ extension.name }}
</div>
<p class="text-h3 font-weight-black extension-title" :class="{ 'text-h4': $vuetify.display.xs }">
<span class="extension-title__text">{{ extension.display_name?.length ? extension.display_name : extension.name }}</span>
<v-tooltip location="top" v-if="extension?.has_update && !marketMode">
<p
class="text-h3 font-weight-black extension-title"
:class="{ 'text-h4': $vuetify.display.xs }"
>
<span class="extension-title__text">{{
extension.display_name?.length
? extension.display_name
: extension.name
}}</span>
<v-tooltip
location="top"
v-if="extension?.has_update && !marketMode"
>
<template v-slot:activator="{ props: tooltipProps }">
<v-icon v-bind="tooltipProps" color="warning" class="ml-2" icon="mdi-update" size="small"></v-icon>
<v-icon
v-bind="tooltipProps"
color="warning"
class="ml-2"
icon="mdi-update"
size="small"
></v-icon>
</template>
<span>{{ tm("card.status.hasUpdate") }}: {{ extension.online_version }}</span>
<span
>{{ tm("card.status.hasUpdate") }}:
{{ extension.online_version }}</span
>
</v-tooltip>
<v-tooltip location="top" v-if="!extension.activated && !marketMode">
<v-tooltip
location="top"
v-if="!extension.activated && !marketMode"
>
<template v-slot:activator="{ props: tooltipProps }">
<v-icon v-bind="tooltipProps" color="error" class="ml-2" icon="mdi-cancel" size="small"></v-icon>
<v-icon
v-bind="tooltipProps"
color="error"
class="ml-2"
icon="mdi-cancel"
size="small"
></v-icon>
</template>
<span>{{ tm("card.status.disabled") }}</span>
</v-tooltip>
@@ -184,34 +283,58 @@ const viewReadme = () => {
<v-icon icon="mdi-source-branch" start></v-icon>
{{ extension.version }}
</v-chip>
<v-chip v-if="extension?.has_update" color="warning" label size="small" class="ml-2">
<v-chip
v-if="extension?.has_update"
color="warning"
label
size="small"
class="ml-2"
>
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
{{ extension.online_version }}
</v-chip>
<v-chip color="primary" label size="small" class="ml-2" v-if="extension.handlers?.length" @click="viewHandlers" style="cursor: pointer;">
<v-chip
color="primary"
label
size="small"
class="ml-2"
v-if="extension.handlers?.length"
@click="viewHandlers"
style="cursor: pointer"
>
<v-icon icon="mdi-cogs" start></v-icon>
{{ extension.handlers?.length }}{{ tm("card.status.handlersCount") }}
{{ extension.handlers?.length
}}{{ tm("card.status.handlersCount") }}
</v-chip>
<v-chip v-for="tag in extension.tags" :key="tag" :color="tag === 'danger' ? 'error' : 'primary'" label
size="small" class="ml-2">
{{ tag === 'danger' ? tm('tags.danger') : tag }}
<v-chip
v-for="tag in extension.tags"
:key="tag"
:color="tag === 'danger' ? 'error' : 'primary'"
label
size="small"
class="ml-2"
>
{{ tag === "danger" ? tm("tags.danger") : tag }}
</v-chip>
</div>
<div class="mt-2" :class="{ 'text-caption': $vuetify.display.xs }" style="overflow-y: auto; height: 70px; font-size: 90%;">
<div
class="mt-2"
:class="{ 'text-caption': $vuetify.display.xs }"
style="overflow-y: auto; height: 70px; font-size: 90%"
>
{{ extension.desc }}
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="extension-actions">
<v-btn color="primary" size="small" @click="viewReadme">
{{ tm('buttons.viewDocs') }}
{{ tm("buttons.viewDocs") }}
</v-btn>
<v-btn v-if="!marketMode" color="primary" size="small" @click="configure">
{{ tm('card.actions.pluginConfig') }}
<v-btn v-if="!marketMode" color="primary" size="small" @click="configure">
{{ tm("card.actions.pluginConfig") }}
</v-btn>
</v-card-actions>
</v-card>
@@ -221,7 +344,6 @@ const viewReadme = () => {
v-model="showUninstallDialog"
@confirm="handleUninstallConfirm"
/>
</template>
<style scoped>
@@ -241,7 +363,7 @@ const viewReadme = () => {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-top: 6px
padding-top: 6px;
}
@media (max-width: 600px) {

View File

@@ -1,11 +1,11 @@
<script setup>
import { ref, watch, onMounted, computed } from 'vue';
import axios from 'axios';
import { MarkdownRender, enableKatex, enableMermaid } from 'markstream-vue';
import 'markstream-vue/index.css';
import 'katex/dist/katex.min.css';
import 'highlight.js/styles/github.css';
import { useI18n } from '@/i18n/composables';
import { ref, watch, onMounted, computed } from "vue";
import axios from "axios";
import { MarkdownRender, enableKatex, enableMermaid } from "markstream-vue";
import "markstream-vue/index.css";
import "katex/dist/katex.min.css";
import "highlight.js/styles/github.css";
import { useI18n } from "@/i18n/composables";
enableKatex();
enableMermaid();
@@ -13,19 +13,25 @@ enableMermaid();
const props = defineProps({
show: {
type: Boolean,
default: false
default: false,
},
pluginName: {
type: String,
default: ''
default: "",
},
repoUrl: {
type: String,
default: null
}
default: null,
},
// 模式: 'readme' 或 'changelog'
mode: {
type: String,
default: "readme",
validator: (value) => ["readme", "changelog"].includes(value),
},
});
const emit = defineEmits(['update:show']);
const emit = defineEmits(["update:show"]);
// 国际化
const { t } = useI18n();
@@ -33,39 +39,83 @@ const { t } = useI18n();
const content = ref(null);
const error = ref(null);
const loading = ref(false);
const isEmpty = ref(false); // 请求成功但无内容
// 根据模式返回不同的配置
const modeConfig = computed(() => {
if (props.mode === "changelog") {
return {
title: t("core.common.changelog.title"),
loading: t("core.common.changelog.loading"),
emptyTitle: t("core.common.changelog.empty.title"),
emptySubtitle: t("core.common.changelog.empty.subtitle"),
apiPath: "/api/plugin/changelog",
};
}
return {
title: t("core.common.readme.title"),
loading: t("core.common.readme.loading"),
emptyTitle: t("core.common.readme.empty.title"),
emptySubtitle: t("core.common.readme.empty.subtitle"),
apiPath: "/api/plugin/readme",
};
});
// 监听show的变化当显示对话框时加载内容
watch(() => props.show, (newVal) => {
if (newVal && props.pluginName) {
fetchReadme();
}
});
watch(
() => props.show,
(newVal) => {
if (newVal && props.pluginName) {
fetchContent();
}
},
);
// 监听pluginName的变化
watch(() => props.pluginName, (newVal) => {
if (props.show && newVal) {
fetchReadme();
}
});
watch(
() => props.pluginName,
(newVal) => {
if (props.show && newVal) {
fetchContent();
}
},
);
// 获取README内容
async function fetchReadme() {
// 监听mode的变化
watch(
() => props.mode,
() => {
if (props.show && props.pluginName) {
fetchContent();
}
},
);
// 获取内容
async function fetchContent() {
if (!props.pluginName) return;
loading.value = true;
content.value = null;
error.value = null;
isEmpty.value = false;
try {
// 从本地文件获取README
const res = await axios.get(`/api/plugin/readme?name=${props.pluginName}`);
if (res.data.status === 'ok') {
content.value = res.data.data.content;
const res = await axios.get(
`${modeConfig.value.apiPath}?name=${props.pluginName}`,
);
if (res.data.status === "ok") {
if (res.data.data.content) {
content.value = res.data.data.content;
} else {
// 请求成功但无内容
isEmpty.value = true;
}
} else {
error.value = res.data.message || t('core.common.readme.errors.fetchFailed');
error.value = res.data.message;
}
} catch (err) {
error.value = err.message || t('core.common.readme.errors.fetchError');
error.value = err.message;
} finally {
loading.value = false;
}
@@ -74,14 +124,13 @@ async function fetchReadme() {
// 打开GitHub中的仓库
function openRepoInNewTab() {
if (props.repoUrl) {
window.open(props.repoUrl, '_blank');
window.open(props.repoUrl, "_blank");
}
}
// 刷新README内容
function refreshReadme() {
fetchReadme();
// 刷新内容
function refreshContent() {
fetchContent();
}
// 计算属性处理双向绑定
@@ -90,8 +139,8 @@ const _show = computed({
return props.show;
},
set(value) {
emit('update:show', value);
}
emit("update:show", value);
},
});
</script>
@@ -99,59 +148,98 @@ const _show = computed({
<v-dialog v-model="_show" width="800">
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<span class="text-h5">{{ t('core.common.readme.title') }}</span>
<span class="text-h5">{{ modeConfig.title }}</span>
<v-btn icon @click="$emit('update:show', false)" variant="text">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text style="height: 70vh; overflow-y: auto;">
<v-card-text style="height: 70vh; overflow-y: auto">
<div class="d-flex justify-space-between mb-4">
<v-btn
<v-btn
v-if="repoUrl"
color="primary"
color="primary"
prepend-icon="mdi-github"
@click="openRepoInNewTab()"
@click="openRepoInNewTab()"
>
{{ t('core.common.readme.buttons.viewOnGithub') }}
{{ t("core.common.readme.buttons.viewOnGithub") }}
</v-btn>
<v-btn
color="secondary"
prepend-icon="mdi-refresh"
@click="refreshReadme()"
@click="refreshContent()"
>
{{ t('core.common.readme.buttons.refresh') }}
{{ t("core.common.readme.buttons.refresh") }}
</v-btn>
</div>
<!-- 加载中 -->
<div v-if="loading" class="d-flex flex-column align-center justify-center" style="height: 100%;">
<v-progress-circular indeterminate color="primary" size="64" class="mb-4"></v-progress-circular>
<p class="text-body-1 text-center">{{ t('core.common.readme.loading') }}</p>
<div
v-if="loading"
class="d-flex flex-column align-center justify-center"
style="height: 100%"
>
<v-progress-circular
indeterminate
color="primary"
size="64"
class="mb-4"
></v-progress-circular>
<p class="text-body-1 text-center">{{ modeConfig.loading }}</p>
</div>
<!-- 内容显示 -->
<div v-else-if="content" class="markdown-body">
<MarkdownRender :content="content" :typewriter="false" class="markdown-content" />
<MarkdownRender
:content="content"
:typewriter="false"
class="markdown-content"
/>
</div>
<!-- 错误提示 -->
<div v-else-if="error" class="d-flex flex-column align-center justify-center" style="height: 100%;">
<v-icon size="64" color="error" class="mb-4">mdi-alert-circle-outline</v-icon>
<p class="text-body-1 text-center mb-4">{{ error }}</p>
<div
v-else-if="error"
class="d-flex flex-column align-center justify-center"
style="height: 100%"
>
<v-icon size="64" color="error" class="mb-4"
>mdi-alert-circle-outline</v-icon
>
<p class="text-body-1 text-center mb-2">
{{ t("core.common.error") }}
</p>
<p class="text-body-2 text-center text-medium-emphasis">
{{ error }}
</p>
</div>
<!-- 无内容提示 -->
<div v-else class="d-flex flex-column align-center justify-center" style="height: 100%;">
<v-icon size="64" color="warning" class="mb-4">mdi-file-question-outline</v-icon>
<p class="text-body-1 text-center mb-4">{{ t('core.common.readme.empty.title') }}<br>{{ t('core.common.readme.empty.subtitle') }}</p>
<div
v-else-if="isEmpty"
class="d-flex flex-column align-center justify-center"
style="height: 100%"
>
<v-icon size="64" color="warning" class="mb-4"
>mdi-file-question-outline</v-icon
>
<p class="text-body-1 text-center mb-2">
{{ modeConfig.emptyTitle }}
</p>
<p class="text-body-2 text-center text-medium-emphasis">
{{ modeConfig.emptySubtitle }}
</p>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="tonal" @click="$emit('update:show', false)">
{{ t('core.common.close') }}
<v-btn
color="primary"
variant="tonal"
@click="$emit('update:show', false)"
>
{{ t("core.common.close") }}
</v-btn>
</v-card-actions>
</v-card>
@@ -160,10 +248,11 @@ const _show = computed({
<style>
.markdown-body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
line-height: 1.6;
padding: 8px 0;
color: var(--v-theme-secondaryText);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
sans-serif;
line-height: 1.6;
padding: 8px 0;
color: var(--v-theme-secondaryText);
}
.markdown-body h1,
@@ -172,120 +261,120 @@ const _show = computed({
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h1 {
font-size: 2em;
border-bottom: 1px solid var(--v-theme-border);
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid var(--v-theme-border);
padding-bottom: 0.3em;
}
.markdown-body h2 {
font-size: 1.5em;
border-bottom: 1px solid var(--v-theme-border);
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid var(--v-theme-border);
padding-bottom: 0.3em;
}
.markdown-body p {
margin-top: 0;
margin-bottom: 16px;
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body code {
padding: 0.2em 0.4em;
margin: 0;
background-color: var(--v-theme-codeBg);
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 85%;
padding: 0.2em 0.4em;
margin: 0;
background-color: var(--v-theme-codeBg);
border-radius: 3px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 85%;
}
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--v-theme-containerBg);
border-radius: 3px;
margin-bottom: 16px;
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--v-theme-containerBg);
border-radius: 3px;
margin-bottom: 16px;
}
.markdown-body pre code {
background-color: transparent;
padding: 0;
background-color: transparent;
padding: 0;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
margin-bottom: 16px;
padding-left: 2em;
margin-bottom: 16px;
}
.markdown-body img {
max-width: 100%;
margin: 8px 0;
box-sizing: border-box;
background-color: var(--v-theme-background);
border-radius: 3px;
max-width: 100%;
margin: 8px 0;
box-sizing: border-box;
background-color: var(--v-theme-background);
border-radius: 3px;
}
.markdown-body blockquote {
padding: 0 1em;
color: var(--v-theme-secondaryText);
border-left: 0.25em solid var(--v-theme-border);
margin-bottom: 16px;
padding: 0 1em;
color: var(--v-theme-secondaryText);
border-left: 0.25em solid var(--v-theme-border);
margin-bottom: 16px;
}
.markdown-body a {
color: var(--v-theme-primary);
text-decoration: none;
color: var(--v-theme-primary);
text-decoration: none;
}
.markdown-body a:hover {
text-decoration: underline;
text-decoration: underline;
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
width: 100%;
overflow: auto;
margin-bottom: 16px;
border-spacing: 0;
border-collapse: collapse;
width: 100%;
overflow: auto;
margin-bottom: 16px;
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid var(--v-theme-background);
padding: 6px 13px;
border: 1px solid var(--v-theme-background);
}
.markdown-body table tr {
background-color: var(--v-theme-surface);
border-top: 1px solid var(--v-theme-border);
background-color: var(--v-theme-surface);
border-top: 1px solid var(--v-theme-border);
}
.markdown-body table tr:nth-child(2n) {
background-color: var(--v-theme-background);
background-color: var(--v-theme-background);
}
.markdown-body hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: var(--v-theme-containerBg);
border: 0;
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: var(--v-theme-containerBg);
border: 0;
}
</style>
<script>
export default {
name: 'ReadmeDialog',
name: "ReadmeDialog",
components: {
MarkdownRender
MarkdownRender,
},
computed: {
_show: {
@@ -293,9 +382,9 @@ export default {
return this.show;
},
set(value) {
this.$emit('update:show', value);
}
}
}
}
</script>
this.$emit("update:show", value);
},
},
},
};
</script>

View File

@@ -62,6 +62,14 @@
"subtitle": "Please check the extension marketplace or contact the extension author for more information."
}
},
"changelog": {
"title": "Changelog",
"loading": "Loading changelog...",
"empty": {
"title": "No changelog available for this plugin",
"subtitle": "Developers can add a CHANGELOG.md file in the plugin directory to provide changelog"
}
},
"editor": {
"fullscreen": "Fullscreen Edit",
"editingTitle": "Editing Content"

View File

@@ -212,5 +212,8 @@
"pairs": "command conflicts",
"goToManage": "Go to Manage",
"later": "Later"
},
"pluginChangelog": {
"menuTitle": "View Changelog"
}
}
}

View File

@@ -62,6 +62,14 @@
"subtitle": "请查看插件市场或联系插件作者获取更多信息。"
}
},
"changelog": {
"title": "更新日志",
"loading": "正在加载更新日志...",
"empty": {
"title": "该插件未提供更新日志",
"subtitle": "开发者可在插件目录下添加 CHANGELOG.md 文件来提供更新日志"
}
},
"editor": {
"fullscreen": "全屏编辑",
"editingTitle": "编辑内容"

View File

@@ -212,5 +212,8 @@
"pairs": "对指令冲突",
"goToManage": "前往处理",
"later": "稍后处理"
},
"pluginChangelog": {
"menuTitle": "查看更新日志"
}
}

File diff suppressed because it is too large Load Diff