mirror of https://github.com/certd/certd
feat: 首页全新改版
parent
e5e468a463
commit
63ec5b5519
|
@ -30,7 +30,7 @@ export class CertConverter {
|
|||
// 转der
|
||||
derPath = await this.convertDer(ctx);
|
||||
|
||||
//jksPath = await this.convertJks(ctx, pfxPath, opts.pfxPassword);
|
||||
//jksPath = await this.convertJks(ctx, opts.pfxPassword);
|
||||
};
|
||||
|
||||
await certReader.readCertFile({ logger: this.logger, handle });
|
||||
|
@ -64,7 +64,7 @@ export class CertConverter {
|
|||
if (pfxPassword) {
|
||||
passwordArg = `-password pass:${pfxPassword}`;
|
||||
}
|
||||
// 兼容server 2016
|
||||
// 兼容server 2016,旧版本不能用sha256
|
||||
const oldPfxCmd = `openssl pkcs12 -macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`;
|
||||
// const newPfx = `openssl pkcs12 -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`;
|
||||
await this.exec(oldPfxCmd);
|
||||
|
@ -98,18 +98,18 @@ export class CertConverter {
|
|||
// this.saveFile(filename, fileBuffer);
|
||||
}
|
||||
|
||||
async convertJks(opts: CertReaderHandleContext, pfxPath: string, pfxPassword = "") {
|
||||
async convertJks(opts: CertReaderHandleContext, pfxPassword = "") {
|
||||
const jksPassword = pfxPassword || "123456";
|
||||
try {
|
||||
const randomStr = Math.floor(Math.random() * 1000000) + "";
|
||||
|
||||
// const p12Path = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.p12`);
|
||||
// const { tmpCrtPath, tmpKeyPath } = opts;
|
||||
// let passwordArg = "-passout pass:";
|
||||
// if (pfxPassword) {
|
||||
// passwordArg = `-password pass:${pfxPassword}`;
|
||||
// }
|
||||
// await this.exec(`openssl pkcs12 -export -in ${tmpCrtPath} -inkey ${tmpKeyPath} -out ${p12Path} -name certd ${passwordArg}`);
|
||||
const p12Path = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.p12`);
|
||||
const { tmpCrtPath, tmpKeyPath } = opts;
|
||||
let passwordArg = "-passout pass:";
|
||||
if (pfxPassword) {
|
||||
passwordArg = `-password pass:${pfxPassword}`;
|
||||
}
|
||||
await this.exec(`openssl pkcs12 -export -in ${tmpCrtPath} -inkey ${tmpKeyPath} -out ${p12Path} -name certd ${passwordArg}`);
|
||||
|
||||
const jksPath = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.jks`);
|
||||
const dir = path.dirname(jksPath);
|
||||
|
@ -117,8 +117,9 @@ export class CertConverter {
|
|||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
await this.exec(
|
||||
`keytool -importkeystore -srckeystore ${pfxPath} -srcstoretype PKCS12 -srcstorepass "${pfxPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `
|
||||
`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${pfxPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `
|
||||
);
|
||||
fs.unlinkSync(p12Path);
|
||||
return jksPath;
|
||||
} catch (e) {
|
||||
this.logger.error("转换jks失败", e);
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"cron-parser": "^4.9.0",
|
||||
"cropperjs": "^1.6.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.5.1",
|
||||
"highlight.js": "^11.9.0",
|
||||
"humanize-duration": "^3.27.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
@ -58,6 +59,7 @@
|
|||
"sortablejs": "^1.15.2",
|
||||
"vue": "^3.4.21",
|
||||
"vue-cropperjs": "^5.0.0",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-i18n": "^9.10.2",
|
||||
"vue-router": "^4.3.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
|
|
|
@ -98,8 +98,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
|||
vModel: "modelValue",
|
||||
placeholder: "0 0 4 * * *"
|
||||
},
|
||||
helper:
|
||||
"点击上面的按钮,选择每天几点几分定时执行,后面的分秒都要选择0。\n例如:0 0 4 * * *,每天凌晨4点0分0秒触发\n建议设置为每天触发一次,证书未到期之前任务会跳过,不会重复执行",
|
||||
helper: "点击上面的按钮,选择每天几点几分定时执行,后面的分秒都要选择0。\n建议设置为每天触发一次,证书未到期之前任务会跳过,不会重复执行",
|
||||
order: 100
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-model:open="triggerDrawerVisible"
|
||||
placement="right"
|
||||
:closable="true"
|
||||
width="600px"
|
||||
width="650px"
|
||||
class="pi-trigger-form"
|
||||
@after-open-change="triggerDrawerOnAfterVisibleChange"
|
||||
>
|
||||
|
@ -58,8 +58,7 @@
|
|||
name: 'cron-editor',
|
||||
vModel: 'modelValue'
|
||||
},
|
||||
helper:
|
||||
'点击上面的按钮,选择每天几点几分定时执行,后面的分秒都要选择0。\n例如:0 0 4 * * *,每天凌晨4点0分0秒触发\n建议设置为每天触发一次,证书未到期之前任务会跳过,不会重复执行',
|
||||
helper: '点击上面的按钮,选择每天几点几分定时执行,后面的分秒都要选择0。\n建议设置为每天触发一次,证书未到期之前任务会跳过,不会重复执行',
|
||||
rules: [{ required: true, message: '此项必填' }]
|
||||
}"
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { request } from "/@/api/service";
|
||||
|
||||
export async function GetStatisticCount() {
|
||||
return await request({
|
||||
url: "/statistic/count",
|
||||
method: "POST"
|
||||
});
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export type ChartItem = {
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<v-chart class="chart" :option="option" autoresize />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { use } from "echarts/core";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import { PieChart, LineChart } from "echarts/charts";
|
||||
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from "echarts/components";
|
||||
import VChart, { THEME_KEY } from "vue-echarts";
|
||||
import { ref, provide, defineProps } from "vue";
|
||||
import { ChartItem } from "/@/views/framework/home/dashboard/charts/d";
|
||||
|
||||
use([CanvasRenderer, PieChart, LineChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent]);
|
||||
|
||||
provide(THEME_KEY, "");
|
||||
|
||||
const props = defineProps<{
|
||||
data: ChartItem[];
|
||||
}>();
|
||||
|
||||
const dates = props.data.map((item) => {
|
||||
return item.name;
|
||||
});
|
||||
const counts = props.data.map((item) => {
|
||||
return item.value;
|
||||
});
|
||||
|
||||
var noDataOption = {
|
||||
// 使用系统提供的 noData 图形
|
||||
graphic: {
|
||||
type: "text",
|
||||
left: "center",
|
||||
top: "center",
|
||||
style: {
|
||||
text: "无数据",
|
||||
textAlign: "center",
|
||||
fill: "#ccc"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const option = ref({
|
||||
noDataSchema: noDataOption,
|
||||
color: ["#91cc75", "#73c0de", "#ee6666", "#fac858", "#3ba272", "#fc8452", "#9a60b4", "#ea7ccc", "#5470c6"],
|
||||
// title: {
|
||||
// text: "",
|
||||
// left: "center"
|
||||
// },
|
||||
tooltip: {
|
||||
trigger: "item"
|
||||
},
|
||||
// tooltip: {
|
||||
// trigger: "axis",
|
||||
// axisPointer: {
|
||||
// type: "cross",
|
||||
// label: {
|
||||
// backgroundColor: "#6a7985"
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// legend: {
|
||||
// data: ["Email",]
|
||||
// },
|
||||
grid: {
|
||||
top: "20px",
|
||||
left: "20px",
|
||||
right: "20px",
|
||||
bottom: "10px",
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: dates
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: "value"
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "运行次数",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
label: {
|
||||
show: true,
|
||||
position: "top"
|
||||
},
|
||||
smooth: true,
|
||||
areaStyle: {},
|
||||
emphasis: {
|
||||
focus: "series"
|
||||
},
|
||||
data: counts
|
||||
}
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div v-if="data.length !== 0" class="expiring-pipeline-list">
|
||||
<div v-for="item of data" class="pipeline-row">
|
||||
<div class="title" :title="item.title">
|
||||
<pi-status-show :status="item.status"></pi-status-show> <a @click="goDetail(item)">{{ item.title }}</a>
|
||||
</div>
|
||||
<div class="time">
|
||||
<FsTimeHumanize v-model="item.lastVars.certExpiresTime" :use-format-greater="1000000000000" :options="{ units: ['d'] }"></FsTimeHumanize>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex-center flex-1 flex-col">
|
||||
<a-empty> </a-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import PiStatusShow from "/@/views/certd/pipeline/pipeline/component/status-show.vue";
|
||||
const props = defineProps<{
|
||||
data: any[];
|
||||
}>();
|
||||
const router = useRouter();
|
||||
function goDetail(item) {
|
||||
router.push({ path: "/certd/pipeline/detail", query: { id: item.id } });
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.expiring-pipeline-list {
|
||||
padding-top: 5px;
|
||||
.pipeline-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
//多余隐藏
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.time {
|
||||
margin-left: 10px;
|
||||
width: 70px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<v-chart class="chart" :option="option" autoresize />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { use } from "echarts/core";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import { PieChart } from "echarts/charts";
|
||||
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from "echarts/components";
|
||||
import VChart, { THEME_KEY } from "vue-echarts";
|
||||
import { ref, provide, defineProps } from "vue";
|
||||
import { ChartItem } from "./d";
|
||||
|
||||
use([CanvasRenderer, PieChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent]);
|
||||
|
||||
provide(THEME_KEY, "");
|
||||
|
||||
const props = defineProps<{
|
||||
data: ChartItem[];
|
||||
}>();
|
||||
|
||||
const option = ref({
|
||||
color: ["#91cc75", "#73c0de", "#ee6666", "#fac858", "#5470c6", "#3ba272", "#fc8452", "#9a60b4", "#ea7ccc", "#5470c6"],
|
||||
tooltip: {
|
||||
trigger: "item"
|
||||
},
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
bottom: "5%",
|
||||
left: "left"
|
||||
},
|
||||
grid: {
|
||||
top: "20px",
|
||||
left: "20px",
|
||||
right: "20px",
|
||||
bottom: "10px",
|
||||
containLabel: true
|
||||
},
|
||||
series: [
|
||||
{
|
||||
center: ["60%", "50%"],
|
||||
name: "状态",
|
||||
type: "pie",
|
||||
radius: "80%",
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 0,
|
||||
borderColor: "#fff",
|
||||
borderWidth: 1
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: "center"
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 18,
|
||||
fontWeight: "bold"
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: props.data
|
||||
}
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.chart {
|
||||
}
|
||||
</style>
|
|
@ -2,15 +2,19 @@
|
|||
<div class="dashboard-user">
|
||||
<div class="header-profile">
|
||||
<div class="avatar">
|
||||
<a-avatar size="large" :src="userStore?.userInfo?.avatar"></a-avatar>
|
||||
<a-avatar v-if="userInfo.avatar" size="large" :src="'/api/basic/file/download?&key=' + userInfo.avatar" style="background-color: #eee"> </a-avatar>
|
||||
<a-avatar v-else size="large" style="background-color: #00b4f5">
|
||||
{{ userInfo.username }}
|
||||
</a-avatar>
|
||||
</div>
|
||||
<div class="text">
|
||||
<div class="left">
|
||||
<div>
|
||||
<span>您好,{{ userStore?.userInfo?.nickName || userStore?.userInfo?.username }}。 欢迎使用 {{ siteInfo.title }} </span>
|
||||
<span>您好,{{ userInfo.nickName || userInfo.username }}。 欢迎使用 {{ siteInfo.title }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<a-tag color="green" class="flex-inline"> <fs-icon icon="ion:time-outline" class="mr-5"></fs-icon> {{ now }}</a-tag>
|
||||
<a-tag color="blue" class="flex-inline"> <fs-icon icon="ion:rocket-outline" class="mr-5"></fs-icon> v{{ version }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,7 +22,7 @@
|
|||
<div>
|
||||
<tutorial-button class="flex-center">
|
||||
<a-tag color="blue" class="flex-center">
|
||||
仅需3步,让你的证书永不过期 <fs-icon class="font-size-16 ml-5" icon="mingcute:question-line"></fs-icon
|
||||
仅需3步,全自动申请部署证书<fs-icon class="font-size-16 ml-5" icon="mingcute:question-line"></fs-icon
|
||||
></a-tag>
|
||||
</tutorial-button>
|
||||
<simple-steps></simple-steps>
|
||||
|
@ -28,7 +32,7 @@
|
|||
<div v-if="!settingStore.isComm" class="warning">
|
||||
<a-alert type="warning" show-icon>
|
||||
<template #message>
|
||||
证书和授权为敏感信息,不要使用来历不明的在线Certd服务和镜像,请务必私有化部署使用,保护数据安全,认准官方版本发布渠道:
|
||||
证书和授权为敏感信息,不要使用来历不明的在线Certd服务和镜像,以免泄露;请务必私有化部署使用,认准官方版本发布渠道:
|
||||
<a class="ml-5 flex-inline" href="https://gitee.com/certd/certd" target="_blank">gitee</a>、
|
||||
<a class="ml-5 flex-inline" href="https://github.com/certd/certd" target="_blank">github</a>、
|
||||
<a class="ml-5 flex-inline" href="https://certd.docmirror.cn" target="_blank">帮助文档</a>
|
||||
|
@ -39,22 +43,33 @@
|
|||
<div class="statistic-data m-20">
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="6">
|
||||
<statistic-card title="提醒"> </statistic-card>
|
||||
<statistic-card title="证书流水线数量" :count="count.pipelineCount">
|
||||
<template v-if="count.pipelineCount === 0" #default>
|
||||
<div class="flex-center flex-1 flex-col">
|
||||
<div style="font-size: 20px; font-weight: 700">您还没有证书流水线</div>
|
||||
<fs-button class="mt-10" icon="ion:add-circle-outline" type="primary" @click="goPipeline">立即添加</fs-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<router-link to="/certd/pipeline" class="flex"><fs-icon icon="ion:add-circle-outline" class="mr-5 fs-16" /> 管理流水线</router-link>
|
||||
</template>
|
||||
</statistic-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<statistic-card title="流水线数量"></statistic-card>
|
||||
<statistic-card title="流水线状态" :footer="false">
|
||||
<pie-count v-if="count.pipelineStatusCount" :data="count.pipelineStatusCount"></pie-count>
|
||||
</statistic-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<statistic-card title="最近运行"></statistic-card>
|
||||
<statistic-card title="最近运行统计" :footer="false">
|
||||
<day-count v-if="count.runningCount" :data="count.runningCount"></day-count>
|
||||
</statistic-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<statistic-card title="最近到期证书"></statistic-card>
|
||||
<statistic-card title="最快到期证书">
|
||||
<expiring-list v-if="count.expiringList" :data="count.expiringList"></expiring-list>
|
||||
</statistic-card>
|
||||
</a-col>
|
||||
<!-- <a-col :span="12">-->
|
||||
<!-- <statistic-card title="3步自动部署">-->
|
||||
<!-- <simple-steps></simple-steps>-->
|
||||
<!-- </statistic-card>-->
|
||||
<!-- </a-col>-->
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
|
@ -66,14 +81,16 @@
|
|||
<a-row :gutter="10">
|
||||
<a-col v-for="item of pluginGroups.groups.all.plugins" class="plugin-item-col" :span="4">
|
||||
<a-card>
|
||||
<div class="plugin-item">
|
||||
<div class="icon">
|
||||
<fs-icon :icon="item.icon" class="font-size-16 color-blue" />
|
||||
<a-tooltip :title="item.desc">
|
||||
<div class="plugin-item pointer">
|
||||
<div class="icon">
|
||||
<fs-icon :icon="item.icon" class="font-size-16 color-blue" />
|
||||
</div>
|
||||
<div class="text">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
@ -85,18 +102,26 @@
|
|||
<script lang="ts" setup>
|
||||
import { FsIcon } from "@fast-crud/fast-crud";
|
||||
import SimpleSteps from "./simple-steps.vue";
|
||||
defineOptions({
|
||||
name: "DashboardUser"
|
||||
});
|
||||
import { useUserStore } from "/@/store/modules/user";
|
||||
import { computed, onMounted, Ref, ref } from "vue";
|
||||
import { computed, ComputedRef, onMounted, Ref, ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import StatisticCard from "/@/views/framework/home/dashboard/statistic-card.vue";
|
||||
import * as pluginApi from "/@/views/certd/pipeline/api.plugin";
|
||||
import { PluginGroups } from "/@/views/certd/pipeline/pipeline/type";
|
||||
import TutorialButton from "/@/components/tutorial/index.vue";
|
||||
import DayCount from "./charts/day-count.vue";
|
||||
import PieCount from "./charts/pie-count.vue";
|
||||
import ExpiringList from "./charts/expiring-list.vue";
|
||||
|
||||
import { useSettingStore } from "/@/store/modules/settings";
|
||||
import { SiteInfo } from "/@/api/modules/api.basic";
|
||||
import { UserInfoRes } from "/@/api/modules/api.user";
|
||||
import { GetStatisticCount } from "/@/views/framework/home/dashboard/api";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
defineOptions({
|
||||
name: "DashboardUser"
|
||||
});
|
||||
|
||||
const version = ref(import.meta.env.VITE_APP_VERSION);
|
||||
const settingStore = useSettingStore();
|
||||
|
@ -105,18 +130,52 @@ const siteInfo: Ref<SiteInfo> = computed(() => {
|
|||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userInfo: ComputedRef<UserInfoRes> = computed(() => {
|
||||
return userStore.getUserInfo;
|
||||
});
|
||||
const now = computed(() => {
|
||||
return dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||
});
|
||||
const router = useRouter();
|
||||
function goPipeline() {
|
||||
router.push({ path: "/certd/pipeline" });
|
||||
}
|
||||
|
||||
async function getPluginGroups() {
|
||||
const count: any = ref({});
|
||||
function transformStatusCount() {
|
||||
const data = count.value.pipelineStatusCount;
|
||||
const sorted = [
|
||||
{ name: "success", label: "成功" },
|
||||
{ name: "start", label: "运行中" },
|
||||
{ name: "error", label: "失败" },
|
||||
{ name: "canceled", label: "已取消" },
|
||||
{ name: null, label: "未执行" }
|
||||
];
|
||||
const result = [];
|
||||
for (const item of sorted) {
|
||||
const find = data.find((v: any) => v.name === item.name);
|
||||
if (find) {
|
||||
result.push({ name: item.label, value: find.value });
|
||||
} else {
|
||||
result.push({ name: item.label, value: 0 });
|
||||
}
|
||||
}
|
||||
count.value.pipelineStatusCount = result;
|
||||
}
|
||||
async function loadCount() {
|
||||
count.value = await GetStatisticCount();
|
||||
transformStatusCount();
|
||||
}
|
||||
|
||||
async function loadPluginGroups() {
|
||||
const groups = await pluginApi.GetGroups({});
|
||||
return new PluginGroups(groups);
|
||||
pluginGroups.value = new PluginGroups(groups);
|
||||
}
|
||||
|
||||
const pluginGroups = ref();
|
||||
onMounted(async () => {
|
||||
pluginGroups.value = await getPluginGroups();
|
||||
await loadCount();
|
||||
await loadPluginGroups();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
</div>
|
||||
<div class="content">
|
||||
<div v-if="!slots.default" class="statistic">
|
||||
<div class="value">80</div>
|
||||
<div class="value">{{ count }}</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="icon-text"><fs-icon icon="ion:settings-outline" />管理流水线</div>
|
||||
<div v-if="slots.footer" class="footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
@ -23,6 +23,7 @@
|
|||
import { FsIcon } from "@fast-crud/fast-crud";
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
count?: number;
|
||||
}>();
|
||||
const slots = defineSlots();
|
||||
</script>
|
||||
|
@ -41,7 +42,7 @@ const slots = defineSlots();
|
|||
.data-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 180px;
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -51,7 +52,7 @@ const slots = defineSlots();
|
|||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100px;
|
||||
flex: 1;
|
||||
.statistic {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
@ -64,6 +65,9 @@ const slots = defineSlots();
|
|||
color: #2c254e;
|
||||
}
|
||||
}
|
||||
|
||||
x-vue-echarts {
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
color: #8077a4;
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||
import { BaseController, Constants } from '@certd/lib-server';
|
||||
import { UserService } from '../../modules/sys/authority/service/user-service.js';
|
||||
import { RoleService } from '../../modules/sys/authority/service/role-service.js';
|
||||
import { PipelineService } from '../../modules/pipeline/service/pipeline-service.js';
|
||||
import { HistoryService } from '../../modules/pipeline/service/history-service.js';
|
||||
|
||||
export type ChartItem = {
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
export type UserStatisticCount = {
|
||||
pipelineCount?: number;
|
||||
pipelineStatusCount?: ChartItem[];
|
||||
runningCount: ChartItem[];
|
||||
expiringList: any[];
|
||||
};
|
||||
/**
|
||||
*/
|
||||
@Provide()
|
||||
@Controller('/api/statistic/')
|
||||
export class StatisticController extends BaseController {
|
||||
@Inject()
|
||||
userService: UserService;
|
||||
@Inject()
|
||||
roleService: RoleService;
|
||||
|
||||
@Inject()
|
||||
pipelineService: PipelineService;
|
||||
@Inject()
|
||||
historyService: HistoryService;
|
||||
|
||||
@Post('/count', { summary: Constants.per.authOnly })
|
||||
public async count() {
|
||||
const pipelineCount = await this.pipelineService.count({ userId: this.getUserId() });
|
||||
let pipelineStatusCount = await this.pipelineService.statusCount({ userId: this.getUserId() });
|
||||
pipelineStatusCount = pipelineStatusCount.map(item => {
|
||||
return {
|
||||
name: item.status,
|
||||
value: item.count,
|
||||
};
|
||||
});
|
||||
|
||||
const historyCount = await this.historyService.dayCount({ userId: this.getUserId(), days: 7 });
|
||||
const runningCount = historyCount.map(item => {
|
||||
return {
|
||||
name: item.date,
|
||||
value: item.count,
|
||||
};
|
||||
});
|
||||
|
||||
const expiringList = await this.pipelineService.latestExpiringList({ userId: this.getUserId(), count: 5 });
|
||||
const count: UserStatisticCount = {
|
||||
pipelineCount,
|
||||
pipelineStatusCount,
|
||||
runningCount,
|
||||
expiringList,
|
||||
};
|
||||
return this.ok(count);
|
||||
}
|
||||
|
||||
@Post('/changePassword', { summary: Constants.per.authOnly })
|
||||
public async changePassword(@Body(ALL) body: any) {
|
||||
const userId = this.getUserId();
|
||||
await this.userService.changePassword(userId, body);
|
||||
return this.ok({});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { In, MoreThan, Repository } from 'typeorm';
|
||||
import { BaseService, PageReq } from '@certd/lib-server';
|
||||
import { HistoryEntity } from '../entity/history.js';
|
||||
import { PipelineEntity } from '../entity/pipeline.js';
|
||||
|
@ -9,7 +9,7 @@ import { HistoryLogService } from './history-log-service.js';
|
|||
import { FileItem, Pipeline, RunnableCollection } from '@certd/pipeline';
|
||||
import { FileStore } from '@certd/pipeline';
|
||||
import { logger } from '@certd/pipeline';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
/**
|
||||
* 证书申请
|
||||
*/
|
||||
|
@ -174,4 +174,21 @@ export class HistoryService extends BaseService<HistoryEntity> {
|
|||
logger.error('删除文件失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
async dayCount(param: { days: number; userId: any }) {
|
||||
const todayEnd = dayjs().endOf('day');
|
||||
const result = await this.getRepository()
|
||||
.createQueryBuilder('main')
|
||||
.select('date(main.createTime) AS date') // 将UNIX时间戳转换为日期
|
||||
.addSelect('COUNT(*) AS count')
|
||||
.where({
|
||||
// 0点
|
||||
userId: param.userId,
|
||||
createTime: MoreThan(todayEnd.add(-param.days, 'day').toDate()),
|
||||
})
|
||||
.groupBy('date')
|
||||
.getRawMany();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,11 +71,18 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||
|
||||
async page(pageReq: PageReq<PipelineEntity>) {
|
||||
const result = await super.page(pageReq);
|
||||
await this.fillLastVars(result.records);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async fillLastVars(records: PipelineEntity[]) {
|
||||
const pipelineIds: number[] = [];
|
||||
const recordMap = {};
|
||||
for (const record of result.records) {
|
||||
for (const record of records) {
|
||||
pipelineIds.push(record.id);
|
||||
recordMap[record.id] = record;
|
||||
record.title = record.title + '';
|
||||
}
|
||||
if (pipelineIds?.length > 0) {
|
||||
const vars = await this.storageService.findPipelineVars(pipelineIds);
|
||||
|
@ -87,8 +94,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async registerTriggerById(pipelineId) {
|
||||
|
@ -467,4 +472,48 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||
logEntity.logs = JSON.stringify(history.logs);
|
||||
await this.historyLogService.addOrUpdate(logEntity);
|
||||
}
|
||||
|
||||
async count(param: { userId: any }) {
|
||||
const count = await this.repository.count({
|
||||
where: {
|
||||
userId: param.userId,
|
||||
},
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
async statusCount(param: { userId: any }) {
|
||||
const statusCount = await this.repository
|
||||
.createQueryBuilder()
|
||||
.select('status')
|
||||
.addSelect('count(1)', 'count')
|
||||
.where({
|
||||
userId: param.userId,
|
||||
})
|
||||
.groupBy('status')
|
||||
.getRawMany();
|
||||
return statusCount;
|
||||
}
|
||||
|
||||
async latestExpiringList({ userId }: any) {
|
||||
let list = await this.repository.find({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
status: true,
|
||||
},
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
await this.fillLastVars(list);
|
||||
list = list.filter(item => {
|
||||
return item.lastVars?.certExpiresTime != null;
|
||||
});
|
||||
list = list.sort((a, b) => {
|
||||
return a.lastVars.certExpiresTime - b.lastVars.certExpiresTime;
|
||||
});
|
||||
|
||||
return list.slice(0, 5);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue