Merge branch 'main' into perf/username-validation

pull/868/head
John Niang 2023-02-21 14:10:16 +08:00 committed by GitHub
commit 8f79ece3a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 367 additions and 77 deletions

View File

@ -78,6 +78,7 @@
"transliteration": "^2.3.5",
"vue": "^3.2.45",
"vue-grid-layout": "3.0.0-beta1",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vuedraggable": "^4.1.0"
},

View File

@ -98,6 +98,7 @@ importers:
vitest: ^0.25.3
vue: ^3.2.45
vue-grid-layout: 3.0.0-beta1
vue-i18n: ^9.2.2
vue-router: ^4.1.6
vue-tsc: ^1.0.24
vuedraggable: ^4.1.0
@ -147,6 +148,7 @@ importers:
transliteration: 2.3.5
vue: 3.2.45
vue-grid-layout: 3.0.0-beta1_farzh4kmmmdsqeu7trbjloi3zi
vue-i18n: 9.2.2_vue@3.2.45
vue-router: 4.1.6_vue@3.2.45
vuedraggable: 4.1.0_vue@3.2.45
devDependencies:
@ -2632,6 +2634,44 @@ packages:
resolution: {integrity: sha512-sZAW08CkqgvqRjUIaLRjScjObcCzN9D75yekLA21EClYAZIhi4A+GEt2z/WqOCOksTaEPLYmQyhkpXcboc0LhQ==}
dev: false
/@intlify/core-base/9.2.2:
resolution: {integrity: sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==}
engines: {node: '>= 14'}
dependencies:
'@intlify/devtools-if': 9.2.2
'@intlify/message-compiler': 9.2.2
'@intlify/shared': 9.2.2
'@intlify/vue-devtools': 9.2.2
dev: false
/@intlify/devtools-if/9.2.2:
resolution: {integrity: sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==}
engines: {node: '>= 14'}
dependencies:
'@intlify/shared': 9.2.2
dev: false
/@intlify/message-compiler/9.2.2:
resolution: {integrity: sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==}
engines: {node: '>= 14'}
dependencies:
'@intlify/shared': 9.2.2
source-map: 0.6.1
dev: false
/@intlify/shared/9.2.2:
resolution: {integrity: sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==}
engines: {node: '>= 14'}
dev: false
/@intlify/vue-devtools/9.2.2:
resolution: {integrity: sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==}
engines: {node: '>= 14'}
dependencies:
'@intlify/core-base': 9.2.2
'@intlify/shared': 9.2.2
dev: false
/@istanbuljs/schema/0.1.3:
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
@ -11029,6 +11069,19 @@ packages:
- '@interactjs/utils'
dev: false
/vue-i18n/9.2.2_vue@3.2.45:
resolution: {integrity: sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==}
engines: {node: '>= 14'}
peerDependencies:
vue: ^3.0.0
dependencies:
'@intlify/core-base': 9.2.2
'@intlify/shared': 9.2.2
'@intlify/vue-devtools': 9.2.2
'@vue/devtools-api': 6.4.5
vue: 3.2.45
dev: false
/vue-resize/2.0.0-alpha.1_vue@3.2.45:
resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==}
peerDependencies:

View File

@ -36,6 +36,7 @@ const props = withDefaults(
const emit = defineEmits<{
(event: "uploaded", response: SuccessResponse): void;
(event: "error", file, response): void;
}>();
const uppy = computed(() => {
@ -72,6 +73,10 @@ uppy.value.on("upload-success", (_, response: SuccessResponse) => {
emit("uploaded", response);
});
uppy.value.on("upload-error", (file, _, response) => {
emit("error", file, response);
});
onUnmounted(() => {
uppy.value.close({ reason: "unmount" });
});

View File

@ -18,7 +18,7 @@ import {
useRouter,
type RouteRecordRaw,
} from "vue-router";
import { computed, onMounted, onUnmounted, ref } from "vue";
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import axios from "axios";
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
import LoginModal from "@/components/login/LoginModal.vue";
@ -28,6 +28,8 @@ import { useRoleStore } from "@/stores/role";
import { hasPermission } from "@/utils/permission";
import { useUserStore } from "@/stores/user";
import { rbacAnnotations } from "@/constants/annotations";
import { useScroll } from "@vueuse/core";
import { defineStore } from "pinia";
const route = useRoute();
const router = useRouter();
@ -189,6 +191,31 @@ const generateMenus = () => {
};
onMounted(generateMenus);
// store scroll position
const navbarScroller = ref();
const { y } = useScroll(navbarScroller);
const useNavbarScrollStore = defineStore("navbar", {
state: () => ({
y: 0,
}),
});
const navbarScrollStore = useNavbarScrollStore();
watch(
() => y.value,
() => {
navbarScrollStore.y = y.value;
}
);
onMounted(() => {
nextTick(() => {
y.value = navbarScrollStore.y;
});
});
</script>
<template>
@ -203,7 +230,7 @@ onMounted(generateMenus);
/>
</a>
</div>
<div class="flex-1 overflow-y-auto">
<div ref="navbarScroller" class="flex-1 overflow-y-auto">
<div class="px-3">
<div
class="flex cursor-pointer items-center rounded bg-gray-100 p-2 text-gray-400 transition-all hover:text-gray-900"

14
src/locales/index.ts Normal file
View File

@ -0,0 +1,14 @@
import { createI18n } from "vue-i18n";
import zh from "./lang/zh";
const messages = {
zh,
};
const i18n = createI18n({
legacy: false,
locale: "zh",
messages,
});
export default i18n;

73
src/locales/lang/zh.ts Normal file
View File

@ -0,0 +1,73 @@
const zh = {
rbac: {
"Attachments Management": "附件",
"Attachment Manage": "附件管理",
"Attachment View": "附件查看",
"role-template-view-attachments": "附件查看",
"Comments Management": "评论",
"Comment Manage": "评论管理",
"Comment View": "评论查看",
"role-template-view-comments": "评论查看",
"ConfigMaps Management": "配置",
"ConfigMap Manage": "配置管理",
"ConfigMap View": "配置查看",
"role-template-view-configmaps": "配置查看",
"Menus Management": "菜单",
"Menu Manage": "菜单管理",
"Menu View": "菜单查看",
"role-template-view-menus": "菜单查看",
"Permissions Management": "权限",
"Permissions Manage": "权限管理",
"Permissions View": "权限查看",
"role-template-view-permissions": "权限查看",
"role-template-manage-permissions": "权限管理",
"Plugins Management": "插件",
"Plugin Manage": "插件管理",
"Plugin View": "插件查看",
"role-template-view-plugins": "插件查看",
"Posts Management": "文章",
"Post Manage": "文章管理",
"Post View": "文章查看",
"role-template-view-posts": "文章查看",
"role-template-manage-snaphosts": "版本管理",
"role-template-view-snaphosts": "版本查看",
"role-template-manage-tags": "标签管理",
"role-template-view-tags": "标签查看",
"role-template-manage-categories": "分类管理",
"role-template-view-categories": "分类查看",
"Roles Management": "角色",
"Role Manage": "角色管理",
"Role View": "角色查看",
"role-template-view-roles": "角色查看",
"Settings Management": "设置表单",
"Setting Manage": "设置表单管理",
"Setting View": "设置表单查看",
"role-template-view-settings": "设置表单查看",
"SinglePages Management": "页面",
"SinglePage Manage": "页面管理",
"SinglePage View": "页面查看",
"role-template-view-singlepages": "页面查看",
"Themes Management": "主题",
"Theme Manage": "主题管理",
"Theme View": "主题查看",
"role-template-view-themes": "主题查看",
"Users Management": "用户",
"User manage": "用户管理",
"User View": "用户查看",
"role-template-view-users": "用户查看",
"role-template-change-password": "修改密码",
},
};
export default zh;

View File

@ -20,12 +20,14 @@ import { useThemeStore } from "./stores/theme";
import { useSystemStatesStore } from "./stores/system-states";
import { useUserStore } from "./stores/user";
import { useSystemConfigMapStore } from "./stores/system-configmap";
import i18n from "./locales";
const app = createApp(App);
setupComponents(app);
app.use(createPinia());
app.use(i18n);
function registerModule(pluginModule: PluginModule, core: boolean) {
if (pluginModule.components) {

View File

@ -347,37 +347,37 @@ function handleClearKeyword() {
</template>
<template #start>
<VEntityField :title="post.post.spec.title" width="27rem">
<template #extra>
<VSpace class="mt-1 sm:mt-0">
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
:tag="tag"
route
></PostTag>
</VSpace>
</template>
<template #description>
<VSpace class="flex-wrap !gap-y-1">
<p
v-if="post.categories.length"
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
>
分类<span
v-for="(category, categoryIndex) in post.categories"
:key="categoryIndex"
class="cursor-pointer hover:text-gray-900"
<div class="flex flex-col gap-1.5">
<VSpace class="flex-wrap !gap-y-1">
<p
v-if="post.categories.length"
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
>
{{ category.spec.displayName }}
分类<span
v-for="(category, categoryIndex) in post.categories"
:key="categoryIndex"
class="cursor-pointer hover:text-gray-900"
>
{{ category.spec.displayName }}
</span>
</p>
<span class="text-xs text-gray-500">
访问量 {{ post.stats.visit || 0 }}
</span>
</p>
<span class="text-xs text-gray-500">
访问量 {{ post.stats.visit || 0 }}
</span>
<span class="text-xs text-gray-500">
评论 {{ post.stats.totalComment || 0 }}
</span>
</VSpace>
<span class="text-xs text-gray-500">
评论 {{ post.stats.totalComment || 0 }}
</span>
</VSpace>
<VSpace v-if="post.tags.length" class="flex-wrap">
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
:tag="tag"
route
></PostTag>
</VSpace>
</div>
</template>
</VEntityField>
</template>

View File

@ -783,12 +783,6 @@ const hasFilters = computed(() => {
>
<VStatusDot state="success" animate />
</RouterLink>
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
:tag="tag"
route
></PostTag>
<a
v-if="post.post.status?.permalink"
target="_blank"
@ -801,35 +795,45 @@ const hasFilters = computed(() => {
</VSpace>
</template>
<template #description>
<VSpace class="flex-wrap !gap-y-1">
<p
v-if="post.categories.length"
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
>
分类<a
v-for="(category, categoryIndex) in post.categories"
:key="categoryIndex"
:href="category.status?.permalink"
:title="category.status?.permalink"
target="_blank"
class="cursor-pointer hover:text-gray-900"
<div class="flex flex-col gap-1.5">
<VSpace class="flex-wrap !gap-y-1">
<p
v-if="post.categories.length"
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
>
{{ category.spec.displayName }}
</a>
</p>
<span class="text-xs text-gray-500">
访问量 {{ post.stats.visit || 0 }}
</span>
<span class="text-xs text-gray-500">
评论 {{ post.stats.totalComment || 0 }}
</span>
<span
v-if="post.post.spec.pinned"
class="text-xs text-gray-500"
>
已置顶
</span>
</VSpace>
分类<a
v-for="(category, categoryIndex) in post.categories"
:key="categoryIndex"
:href="category.status?.permalink"
:title="category.status?.permalink"
target="_blank"
class="cursor-pointer hover:text-gray-900"
>
{{ category.spec.displayName }}
</a>
</p>
<span class="text-xs text-gray-500">
访问量 {{ post.stats.visit || 0 }}
</span>
<span class="text-xs text-gray-500">
评论 {{ post.stats.totalComment || 0 }}
</span>
<span
v-if="post.post.spec.pinned"
class="text-xs text-gray-500"
>
已置顶
</span>
</VSpace>
<VSpace v-if="post.tags.length" class="flex-wrap">
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
:tag="tag"
route
></PostTag>
</VSpace>
</div>
</template>
</VEntityField>
</template>

View File

@ -78,6 +78,7 @@ const handleCopy = () => {
- 构建时间${formatDatetime(info.value?.build?.time)}
- Git Commit${info.value?.git?.commit.id}
- Java${info.value?.java.runtime.name} / ${info.value?.java.runtime.version}
- 数据库${info.value?.database.name} / ${info.value?.database.version}
- 操作系统${info.value?.os.name} / ${info.value?.os.version}
`;
@ -85,6 +86,28 @@ const handleCopy = () => {
Toast.success("复制成功");
};
const handleDownloadLogfile = () => {
axios
.get(`${import.meta.env.VITE_API_URL}/actuator/logfile`)
.then((response) => {
const blob = new Blob([response.data]);
const downloadElement = document.createElement("a");
const href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = `halo-log-${formatDatetime(new Date())}.log`;
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
Toast.success("下载成功");
})
.catch((e) => {
Toast.error("下载失败");
console.log("Failed to download log file.", e);
});
};
</script>
<template>
@ -225,6 +248,14 @@ const handleCopy = () => {
{{ info.java.runtime.name }} / {{ info.java.runtime.version }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">数据库</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ [info.database.name, info.database.version].join(" / ") }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
@ -233,6 +264,16 @@ const handleCopy = () => {
{{ info.os.name }} {{ info.os.version }} / {{ info.os.arch }}
</dd>
</div>
<div
class="items-center bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">运行日志</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<VButton size="sm" @click="handleDownloadLogfile()">
下载
</VButton>
</dd>
</div>
</dl>
</div>
</div>

View File

@ -12,6 +12,12 @@ export interface Info {
build?: Build;
java: Java;
os: Os;
database: Database;
}
export interface Database {
name: string;
version: string;
}
export interface Commit {

View File

@ -1,10 +1,11 @@
<script lang="ts" setup>
import { VModal, Dialog } from "@halo-dev/components";
import { VModal, Dialog, Toast } from "@halo-dev/components";
import UppyUpload from "@/components/upload/UppyUpload.vue";
import { apiClient } from "@/utils/api-client";
import type { Plugin } from "@halo-dev/api-client";
import { computed, ref, watch } from "vue";
import type { SuccessResponse } from "@uppy/core";
import type { SuccessResponse, ErrorResponse } from "@uppy/core";
import type { UppyFile } from "@uppy/utils";
const props = withDefaults(
defineProps<{
@ -76,6 +77,40 @@ const onUploaded = async (response: SuccessResponse) => {
});
};
interface PluginInstallationErrorResponse {
detail: string;
instance: string;
pluginName: string;
requestId: string;
status: number;
timestamp: string;
title: string;
type: string;
}
const PLUGIN_ALREADY_EXISTS_TYPE =
"https://halo.run/probs/plugin-alreay-exists";
const onError = (file: UppyFile<unknown>, response: ErrorResponse) => {
const body = response.body as PluginInstallationErrorResponse;
if (body.type === PLUGIN_ALREADY_EXISTS_TYPE) {
Dialog.info({
title: "插件已存在",
description: "当前安装的插件已存在,是否升级?",
onConfirm: async () => {
await apiClient.plugin.upgradePlugin({
name: body.pluginName,
file: file.data as File,
});
Toast.success("升级成功");
window.location.reload();
},
});
}
};
watch(
() => props.visible,
(newValue) => {
@ -106,6 +141,7 @@ watch(
:endpoint="endpoint"
auto-proceed
@uploaded="onUploaded"
@error="onError"
/>
</VModal>
</template>

View File

@ -249,7 +249,7 @@ onMounted(() => {
>
<dt class="text-sm font-medium text-gray-900">
<div>
{{ group.module }}
{{ $t(`rbac.${group.module}`, group.module as string) }}
</div>
<div
v-if="
@ -300,9 +300,16 @@ onMounted(() => {
<div class="flex flex-1 flex-col gap-y-3">
<span class="font-medium text-gray-900">
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
]
$t(
`rbac.${
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
]
}`,
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] as string
)
}}
</span>
<span
@ -319,7 +326,11 @@ onMounted(() => {
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
).join(", ")
)
.map((item: string) =>
$t(`rbac.${item}`, item as string)
)
.join("")
}}
</span>
</div>

View File

@ -156,7 +156,9 @@ const handleResetForm = () => {
class="flex flex-col gap-3 bg-white py-5 first:pt-0"
>
<dt class="text-sm font-medium text-gray-900">
<div>{{ group.module }}</div>
<div>
{{ $t(`rbac.${group.module}`, group.module as string) }}
</div>
<div
v-if="
group.roles.length &&
@ -197,9 +199,16 @@ const handleResetForm = () => {
<div class="flex flex-1 flex-col gap-y-3">
<span class="font-medium text-gray-900">
{{
roleTemplate.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
]
$t(
`rbac.${
roleTemplate.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
]
}`,
roleTemplate.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] as string
)
}}
</span>
<span
@ -216,7 +225,11 @@ const handleResetForm = () => {
roleTemplate.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
).join(", ")
)
.map((item: string) =>
$t(`rbac.${item}`, item as string)
)
.join("")
}}
</span>
</div>

View File

@ -65,6 +65,7 @@ const userStore = useUserStore();
const roleStore = useRoleStore();
const ANONYMOUSUSER_NAME = "anonymousUser";
const DELETEDUSER_NAME = "ghost";
const handleFetchUsers = async (options?: {
mute?: boolean;
@ -85,7 +86,10 @@ const handleFetchUsers = async (options?: {
page: users.value.page,
size: users.value.size,
keyword: keyword.value,
fieldSelector: [`name!=${ANONYMOUSUSER_NAME}`],
fieldSelector: [
`name!=${ANONYMOUSUSER_NAME}`,
`name!=${DELETEDUSER_NAME}`,
],
sort: [selectedSortItem.value?.value].filter(
(item) => !!item
) as string[],