mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-13 01:50:53 +08:00
【新增】私有证书
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
import { defineComponent, ref, computed, watch } from 'vue';
|
||||
import { NForm, NFormItem, NInput, NSelect, NSpace, NButton, FormRules, useMessage } from 'naive-ui';
|
||||
import { useStore } from '../useStore';
|
||||
import { useAddCaController } from '../useController';
|
||||
import { useModalClose } from '@baota/naive-ui/hooks';
|
||||
|
||||
/**
|
||||
* 添加CA模态框组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
emits: ['success'],
|
||||
setup(props, { emit }) {
|
||||
const { addForm, resetAddForm, createType, rootCaList } = useStore();
|
||||
const message = useMessage();
|
||||
const closeModal = useModalClose();
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref();
|
||||
|
||||
// 有效期单位选择
|
||||
const validityUnit = ref<'day' | 'year'>('day');
|
||||
|
||||
// 使用表单控制器
|
||||
const { handleSubmit } = useAddCaController();
|
||||
|
||||
// 表单验证规则
|
||||
const rules = computed((): FormRules => {
|
||||
const baseRules: any = {
|
||||
name: [
|
||||
{ required: true, message: '请输入CA名称', trigger: 'blur' }
|
||||
],
|
||||
cn: [
|
||||
{ required: true, message: '请输入通用名称', trigger: 'blur' }
|
||||
],
|
||||
o: [
|
||||
{ required: true, message: '请输入组织名称', trigger: 'blur' }
|
||||
],
|
||||
c: [
|
||||
{ required: true, message: '请选择国家', trigger: 'change' }
|
||||
],
|
||||
ou: [
|
||||
{ required: true, message: '请输入组织单位', trigger: 'blur' }
|
||||
],
|
||||
province: [
|
||||
{ required: true, message: '请输入省份', trigger: 'blur' }
|
||||
],
|
||||
locality: [
|
||||
{ required: true, message: '请输入城市', trigger: 'blur' }
|
||||
],
|
||||
key_length: [
|
||||
{ required: true, message: '请选择密钥长度', trigger: 'change' }
|
||||
],
|
||||
valid_days: [
|
||||
{ required: true, message: '请选择有效期', trigger: 'change' }
|
||||
]
|
||||
};
|
||||
|
||||
if (createType.value === 'root') {
|
||||
baseRules.algorithm = [
|
||||
{ required: true, message: '请选择加密算法', trigger: 'change' }
|
||||
];
|
||||
}
|
||||
|
||||
if (createType.value === 'intermediate') {
|
||||
baseRules.root_id = [
|
||||
{ required: true, message: '请选择父级CA', trigger: 'change' }
|
||||
];
|
||||
}
|
||||
|
||||
return baseRules;
|
||||
});
|
||||
|
||||
// 算法选项
|
||||
const algorithmOptions = [
|
||||
{ label: "ECDSA", value: "ecdsa" },
|
||||
{ label: "RSA", value: "rsa" },
|
||||
{ label: "SM2", value: "sm2" },
|
||||
];
|
||||
|
||||
const keyLengthOptions = computed(() => {
|
||||
switch (addForm.value.algorithm) {
|
||||
case 'ecdsa':
|
||||
return [
|
||||
{ label: "P-256 (256 bit)", value: "256" },
|
||||
{ label: "P-384 (384 bit)", value: "384" },
|
||||
{ label: "P-521 (521 bit)", value: "521" },
|
||||
];
|
||||
case 'rsa':
|
||||
return [
|
||||
{ label: "2048 bit", value: "2048" },
|
||||
{ label: "3072 bit", value: "3072" },
|
||||
{ label: "4096 bit", value: "4096" },
|
||||
];
|
||||
case 'sm2':
|
||||
return [
|
||||
{ label: "SM2 (256 bit)", value: "256" },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// 国家选项
|
||||
const countryOptions = [
|
||||
{ label: "中国", value: "CN" },
|
||||
{ label: "美国", value: "US" },
|
||||
{ label: "日本", value: "JP" },
|
||||
{ label: "德国", value: "DE" },
|
||||
{ label: "英国", value: "GB" },
|
||||
];
|
||||
|
||||
// 监听算法变化,重置密钥长度选择
|
||||
watch(() => addForm.value.algorithm, (newAlgorithm) => {
|
||||
addForm.value.key_length = '';
|
||||
if (newAlgorithm === 'ecdsa') {
|
||||
addForm.value.key_length = '256';
|
||||
} else if (newAlgorithm === 'sm2') {
|
||||
addForm.value.key_length = '256';
|
||||
} else if (newAlgorithm === 'rsa') {
|
||||
addForm.value.key_length = '2048';
|
||||
}
|
||||
});
|
||||
|
||||
// 监听父级CA选择,自动填充算法值
|
||||
watch(() => addForm.value.root_id, (newRootId) => {
|
||||
if (createType.value === 'intermediate' && newRootId) {
|
||||
const selectedRootCa = rootCaList.value.find(ca => ca.id.toString() === newRootId);
|
||||
if (selectedRootCa) {
|
||||
addForm.value.algorithm = selectedRootCa.algorithm;
|
||||
if (selectedRootCa.algorithm === 'ecdsa') {
|
||||
addForm.value.key_length = '256';
|
||||
} else if (selectedRootCa.algorithm === 'sm2') {
|
||||
addForm.value.key_length = '256';
|
||||
} else if (selectedRootCa.algorithm === 'rsa') {
|
||||
addForm.value.key_length = '2048';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 处理表单提交
|
||||
const handleFormSubmit = async () => {
|
||||
try {
|
||||
// 先验证表单
|
||||
await formRef.value?.validate();
|
||||
const formData = { ...addForm.value };
|
||||
if (validityUnit.value === 'year' && formData.valid_days) {
|
||||
const years = parseInt(formData.valid_days);
|
||||
if (!isNaN(years)) {
|
||||
formData.valid_days = (years * 365).toString();
|
||||
}
|
||||
}
|
||||
const success = await handleSubmit(formData);
|
||||
if (success) {
|
||||
resetAddForm();
|
||||
closeModal();
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 处理取消操作
|
||||
const handleCancel = () => {
|
||||
resetAddForm();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return () => (
|
||||
<NForm
|
||||
ref={formRef}
|
||||
model={addForm.value}
|
||||
rules={rules.value}
|
||||
labelPlacement="left"
|
||||
labelWidth="auto"
|
||||
requireMarkPlacement="right-hanging"
|
||||
>
|
||||
<NFormItem label="CA名称" path="name" required>
|
||||
<NInput
|
||||
v-model:value={addForm.value.name}
|
||||
placeholder="请输入CA名称"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="通用名称(CN)" path="cn" required>
|
||||
<NInput
|
||||
v-model:value={addForm.value.cn}
|
||||
placeholder="请输入通用名称"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="组织(O)" path="o" required>
|
||||
<NInput
|
||||
v-model:value={addForm.value.o}
|
||||
placeholder="请输入组织名称"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="国家(C)" path="c" required>
|
||||
<NSelect
|
||||
v-model:value={addForm.value.c}
|
||||
options={countryOptions}
|
||||
placeholder="请选择国家"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="组织单位(OU)" path="ou" required>
|
||||
<NInput
|
||||
v-model:value={addForm.value.ou}
|
||||
placeholder="请输入组织单位"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="省份" path="province" required>
|
||||
<NInput
|
||||
v-model:value={addForm.value.province}
|
||||
placeholder="请输入省份"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="城市" path="locality" required>
|
||||
<NInput
|
||||
v-model:value={addForm.value.locality}
|
||||
placeholder="请输入城市"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
{createType.value === "intermediate" && (
|
||||
<NFormItem label="父级CA" path="root_id" required>
|
||||
<NSelect
|
||||
v-model:value={addForm.value.root_id}
|
||||
options={rootCaList.value.map((ca) => ({
|
||||
label: ca.name,
|
||||
value: ca.id.toString(),
|
||||
algorithm: ca.algorithm,
|
||||
key_length: ca.key_length,
|
||||
not_after: ca.not_after,
|
||||
}))}
|
||||
placeholder="请选择父级CA"
|
||||
/>
|
||||
</NFormItem>
|
||||
)}
|
||||
|
||||
<NFormItem label="加密算法" path="algorithm" required>
|
||||
<NSelect
|
||||
v-model:value={addForm.value.algorithm}
|
||||
options={algorithmOptions}
|
||||
placeholder="请选择加密算法"
|
||||
disabled={createType.value === "intermediate"}
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="密钥长度" path="key_length" required>
|
||||
<NSelect
|
||||
v-model:value={addForm.value.key_length}
|
||||
options={keyLengthOptions.value}
|
||||
placeholder="请选择密钥长度"
|
||||
disabled={createType.value === "root" && !addForm.value.algorithm}
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="有效期" path="valid_days" required>
|
||||
<NSpace align="center">
|
||||
<NInput
|
||||
v-model:value={addForm.value.valid_days}
|
||||
placeholder="请输入数值"
|
||||
/>
|
||||
<NSelect
|
||||
v-model:value={validityUnit.value}
|
||||
options={[
|
||||
{ label: '天', value: 'day' },
|
||||
{ label: '年', value: 'year' }
|
||||
]}
|
||||
style={{ width: '80px' }}
|
||||
/>
|
||||
</NSpace>
|
||||
</NFormItem>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<NButton onClick={handleCancel}>取消</NButton>
|
||||
<NButton type="primary" onClick={handleFormSubmit}>
|
||||
确定
|
||||
</NButton>
|
||||
</div>
|
||||
</NForm>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/* 私有CA管理页面样式 */
|
||||
|
||||
/* 行状态样式 */
|
||||
.bg-red-500\/10 {
|
||||
background-color: rgba(239, 68, 68, 0.1) !important;
|
||||
}
|
||||
|
||||
.bg-red-500\/10:hover {
|
||||
background-color: rgba(239, 68, 68, 0.15) !important;
|
||||
}
|
||||
|
||||
.bg-orange-500\/10 {
|
||||
background-color: rgba(249, 115, 22, 0.1) !important;
|
||||
}
|
||||
|
||||
.bg-orange-500\/10:hover {
|
||||
background-color: rgba(249, 115, 22, 0.15) !important;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:global(.n-data-table .n-data-table-td) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:global(.n-data-table .n-data-table-th) {
|
||||
padding: 12px 16px;
|
||||
background-color: #f9fafb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
:global(.n-tag) {
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
:global(.n-button--tiny) {
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
:global(.n-icon) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
96
frontend/apps/allin-ssl/src/views/privateCaManage/index.tsx
Normal file
96
frontend/apps/allin-ssl/src/views/privateCaManage/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { NButton, NDropdown, NIcon } from "naive-ui";
|
||||
import { useThemeCssVar } from "@baota/naive-ui/theme";
|
||||
import { $t } from "@locales/index";
|
||||
import { AddOutline, ChevronDown } from "@vicons/ionicons5";
|
||||
|
||||
import { useController } from "./useController";
|
||||
import { useStore } from "./useStore";
|
||||
import BaseComponent from "@components/BaseLayout";
|
||||
import EmptyState from "@components/TableEmptyState";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
/**
|
||||
* 私有CA管理组件
|
||||
* @description 提供私有CA的管理界面,包括列表展示、搜索、添加、编辑等功能
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: "PrivateCa",
|
||||
setup() {
|
||||
const {
|
||||
TableComponent,
|
||||
PageComponent,
|
||||
SearchComponent,
|
||||
openAddModal,
|
||||
getRowClassName,
|
||||
} = useController();
|
||||
const { setCreateType } = useStore();
|
||||
const dropdownOptions = [
|
||||
{
|
||||
label: '创建根CA',
|
||||
key: 'root',
|
||||
onClick: () => {
|
||||
setCreateType('root');
|
||||
openAddModal();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '创建中间CA',
|
||||
key: 'intermediate',
|
||||
onClick: () => {
|
||||
setCreateType('intermediate');
|
||||
openAddModal();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const cssVar = useThemeCssVar(['contentPadding', 'borderColor', 'headerHeight', 'iconColorHover']);
|
||||
|
||||
return () => (
|
||||
<div class={`h-full flex flex-col ${styles.privateCa}`} style={cssVar.value}>
|
||||
<div class="mx-auto max-w-[1600px] w-full p-6">
|
||||
<BaseComponent
|
||||
v-slots={{
|
||||
headerLeft: () => (
|
||||
<NDropdown
|
||||
trigger="click"
|
||||
options={dropdownOptions}
|
||||
onSelect={(key) => {
|
||||
const option = dropdownOptions.find(opt => opt.key === key);
|
||||
option?.onClick();
|
||||
}}
|
||||
show-arrow={false}
|
||||
width={100}
|
||||
>
|
||||
<NButton type="primary" size="large" class="px-5">
|
||||
创建CA
|
||||
<NIcon size="20" class="ml-2">
|
||||
<ChevronDown />
|
||||
</NIcon>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
),
|
||||
headerRight: () => <SearchComponent placeholder="请输入名称搜索" />,
|
||||
content: () => (
|
||||
<div class="rounded-lg">
|
||||
<TableComponent
|
||||
size="medium"
|
||||
rowClassName={getRowClassName}
|
||||
v-slots={{
|
||||
empty: () => <EmptyState addButtonText="添加CA" onAddClick={openAddModal} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
footerRight: () => (
|
||||
<div class="mt-4 flex justify-end">
|
||||
<PageComponent />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
></BaseComponent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
75
frontend/apps/allin-ssl/src/views/privateCaManage/types.d.ts
vendored
Normal file
75
frontend/apps/allin-ssl/src/views/privateCaManage/types.d.ts
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 私有CA项类型
|
||||
*/
|
||||
export interface PrivateCaItem {
|
||||
/** CA ID */
|
||||
id: string;
|
||||
/** CA名称 */
|
||||
name: string;
|
||||
/** 可分辨名称 */
|
||||
distinguishedName: string;
|
||||
/** CA类型:root-根CA,intermediate-中间CA */
|
||||
type: 'root' | 'intermediate';
|
||||
/** 加密算法 */
|
||||
algorithm: string;
|
||||
/** 密钥长度 */
|
||||
keySize: string;
|
||||
/** 有效期开始时间 */
|
||||
validFrom: string;
|
||||
/** 有效期结束时间 */
|
||||
validTo: string;
|
||||
/** 剩余天数 */
|
||||
remainingDays: number;
|
||||
/** 状态:normal-正常,expired-已过期,revoked-已吊销 */
|
||||
status: 'normal' | 'expired' | 'revoked';
|
||||
/** 创建时间 */
|
||||
createdAt: string;
|
||||
/** 父CA ID(中间CA才有) */
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格查询参数类型
|
||||
*/
|
||||
export interface TableQueryParams {
|
||||
/** 页码 */
|
||||
page: number;
|
||||
/** 每页数量 */
|
||||
pageSize: number;
|
||||
/** 搜索关键词 */
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加私有CA参数
|
||||
*/
|
||||
export interface AddPrivateCaParams {
|
||||
/** CA名称 */
|
||||
name: string;
|
||||
/** 通用名称 */
|
||||
cn: string;
|
||||
/** 组织 */
|
||||
o: string;
|
||||
/** 国家 */
|
||||
c: string;
|
||||
/** 组织单位 */
|
||||
ou: string;
|
||||
/** 省份 */
|
||||
province: string;
|
||||
/** 城市 */
|
||||
locality: string;
|
||||
/** 加密算法 */
|
||||
algorithm: 'rsa' | 'ecdsa' | 'sm2';
|
||||
/** 密钥长度 */
|
||||
key_length: string;
|
||||
/** 有效期(年) */
|
||||
valid_days: string;
|
||||
}
|
||||
/**
|
||||
* 表格行类名函数参数
|
||||
*/
|
||||
export interface RowClassNameParams {
|
||||
row: PrivateCaItem;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
import { NButton, NFlex, NTag, type DataTableColumns } from 'naive-ui';
|
||||
import {
|
||||
useTable,
|
||||
useSearch,
|
||||
useMessage,
|
||||
useDialog,
|
||||
useModal,
|
||||
useLoadingMask,
|
||||
} from '@baota/naive-ui/hooks';
|
||||
import { useError } from "@baota/hooks/error";
|
||||
import { useStore } from './useStore';
|
||||
import type { PrivateCaItem } from './types';
|
||||
import { getCaList, downloadCaCert, deleteCa as deleteCaApi, createRootCa, createIntermediateCa } from '@/api/ca';
|
||||
import type { GetCaListParams } from '@/types/ca';
|
||||
import { onMounted } from 'vue';
|
||||
import AddCaModal from './components/AddCaModal';
|
||||
|
||||
const { handleError } = useError();
|
||||
|
||||
/**
|
||||
* useController
|
||||
* @description 私有CA管理业务逻辑控制器
|
||||
* @returns {object} 返回controller对象
|
||||
*/
|
||||
export const useController = () => {
|
||||
const {
|
||||
createType,
|
||||
rootCaList,
|
||||
addForm,
|
||||
resetAddForm
|
||||
} = useStore();
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
// 获取状态标签类型和文本
|
||||
const getStatusInfo = (validTo: string) => {
|
||||
const calculateRemainingDays = (expiryDate: string): number => {
|
||||
const expiry = new Date(expiryDate);
|
||||
const now = new Date();
|
||||
const diffTime = expiry.getTime() - now.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
};
|
||||
|
||||
const remainingDays = calculateRemainingDays(validTo);
|
||||
|
||||
if (remainingDays > 30) {
|
||||
return { type: 'success' as const, text: '正常' };
|
||||
} else if (remainingDays > 0) {
|
||||
return { type: 'warning' as const, text: '即将过期' };
|
||||
} else if (remainingDays === 0) {
|
||||
return { type: 'warning' as const, text: '今天到期' };
|
||||
} else {
|
||||
return { type: 'error' as const, text: '已过期' };
|
||||
}
|
||||
};
|
||||
|
||||
// 创建表格列
|
||||
const createColumns = (): DataTableColumns<PrivateCaItem> => [
|
||||
{
|
||||
title: "名称",
|
||||
key: "name",
|
||||
width: 250,
|
||||
render: (row: PrivateCaItem) => (
|
||||
<div class="flex flex-col">
|
||||
<div class="text-gray-900">{row.name}</div>
|
||||
<div class="text-xl text-gray-500">{row.distinguishedName}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
key: "type",
|
||||
width: 100,
|
||||
render: (row: PrivateCaItem) => {
|
||||
const typeText = row.type === 'root' ? '根CA' : '中间CA';
|
||||
return <NTag size="small">{typeText}</NTag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "算法",
|
||||
key: "algorithm",
|
||||
width: 120,
|
||||
render: (row: PrivateCaItem) => (
|
||||
<div class="flex flex-col">
|
||||
<div class="text-gray-900">{row.algorithm.toUpperCase()}</div>
|
||||
<div class="text-xl text-gray-500">{row.keySize} bit</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "有效期",
|
||||
key: "validTo",
|
||||
width: 200,
|
||||
render: (row: PrivateCaItem) => {
|
||||
const calculateRemainingDays = (expiryDate: string) => {
|
||||
const expiry = new Date(expiryDate);
|
||||
const now = new Date();
|
||||
const diffTime = expiry.getTime() - now.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
};
|
||||
|
||||
const remainingDays = calculateRemainingDays(row.validTo);
|
||||
let remainingText = "";
|
||||
let textColor = "";
|
||||
|
||||
if (remainingDays > 0) {
|
||||
if (remainingDays <= 30) {
|
||||
remainingText = `${remainingDays} 天后`;
|
||||
textColor = "text-orange-500";
|
||||
} else {
|
||||
remainingText = `${remainingDays} 天后`;
|
||||
textColor = "text-gray-500";
|
||||
}
|
||||
} else if (remainingDays === 0) {
|
||||
remainingText = "今天到期";
|
||||
textColor = "text-orange-500";
|
||||
} else {
|
||||
remainingText = `已过期 ${Math.abs(remainingDays)} 天`;
|
||||
textColor = "text-red-500";
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col">
|
||||
<div class="text-gray-900">{row.validTo}</div>
|
||||
<div class={`text-xl ${textColor}`}>{remainingText}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: 100,
|
||||
render: (row: PrivateCaItem) => {
|
||||
const statusInfo = getStatusInfo(row.validTo);
|
||||
return (
|
||||
<NTag type={statusInfo.type} size="small">
|
||||
{statusInfo.text}
|
||||
</NTag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
key: "createdAt",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
align: "right",
|
||||
width: 200,
|
||||
render: (row: PrivateCaItem) => (
|
||||
<NFlex justify="end">
|
||||
<NButton
|
||||
size="tiny"
|
||||
strong
|
||||
secondary
|
||||
type="primary"
|
||||
onClick={() => handleDownload(row)}
|
||||
>
|
||||
下载
|
||||
</NButton>
|
||||
<NButton
|
||||
size="tiny"
|
||||
strong
|
||||
secondary
|
||||
type="error"
|
||||
onClick={() => handleDelete(row)}
|
||||
>
|
||||
删除
|
||||
</NButton>
|
||||
</NFlex>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 表格实例
|
||||
const { TableComponent, PageComponent, loading, param, data, fetch } = useTable<PrivateCaItem, GetCaListParams>({
|
||||
config: createColumns(),
|
||||
request: async (params: GetCaListParams): Promise<any> => {
|
||||
const { fetch: getCaListFetch, data } = getCaList(params);
|
||||
await getCaListFetch();
|
||||
|
||||
if (data.value && data.value.status === true && data.value.data) {
|
||||
const transformedData = data.value.data.map((item: any) => {
|
||||
const remainingDays = Math.ceil((new Date(item.not_after).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));
|
||||
let status: 'normal' | 'expired' | 'revoked' = 'normal';
|
||||
|
||||
if (remainingDays <= 0) {
|
||||
status = 'expired';
|
||||
}
|
||||
const dnParts = [];
|
||||
if (item.cn) dnParts.push(`CN=${item.cn}`);
|
||||
if (item.ou) dnParts.push(`OU=${item.ou}`);
|
||||
if (item.o) dnParts.push(`O=${item.o}`);
|
||||
if (item.locality) dnParts.push(`L=${item.locality}`);
|
||||
if (item.province) dnParts.push(`ST=${item.province}`);
|
||||
if (item.c) dnParts.push(`C=${item.c}`);
|
||||
const distinguishedName = dnParts.join(', ');
|
||||
|
||||
return {
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
distinguishedName: distinguishedName || item.cn,
|
||||
type: item.root_id ? 'intermediate' : 'root' as const,
|
||||
algorithm: item.algorithm,
|
||||
keySize: item.key_length.toString(),
|
||||
validFrom: item.not_before,
|
||||
validTo: item.not_after,
|
||||
remainingDays,
|
||||
status,
|
||||
createdAt: item.create_time,
|
||||
parentId: item.root_id?.toString(),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
list: transformedData,
|
||||
total: data.value.count || transformedData.length,
|
||||
};
|
||||
}
|
||||
return {
|
||||
list: [],
|
||||
total: 0,
|
||||
};
|
||||
},
|
||||
defaultValue: { p: '1', limit: '20', search: '' },
|
||||
alias: { page: 'p', pageSize: 'limit' },
|
||||
watchValue: ['p', 'limit', 'search'],
|
||||
});
|
||||
|
||||
// 搜索组件
|
||||
const { SearchComponent } = useSearch({
|
||||
onSearch: async (keyword: string) => {
|
||||
param.value.search = keyword;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 获取根证书列表
|
||||
*/
|
||||
const fetchRootCaList = async () => {
|
||||
try {
|
||||
const { fetch: getCaListFetch, data } = getCaList({ p: '-1', limit: '-1', level: 'root' });
|
||||
await getCaListFetch();
|
||||
|
||||
if (data.value?.status === true) {
|
||||
rootCaList.value = data.value.data;
|
||||
if (createType.value === 'intermediate' && rootCaList.value.length > 0 && rootCaList.value[0]) {
|
||||
addForm.value.root_id = rootCaList.value[0].id.toString();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取根证书列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开添加模态框
|
||||
*/
|
||||
const openAddModal = async () => {
|
||||
// 先获取根CA列表,确保数据加载完成
|
||||
await fetchRootCaList();
|
||||
useModal({
|
||||
title: createType.value === 'root' ? '创建根CA' : '创建中间CA',
|
||||
area: 600,
|
||||
component: () => (
|
||||
<AddCaModal
|
||||
onSuccess={() => {
|
||||
fetch();
|
||||
resetAddForm();
|
||||
return false;
|
||||
}}
|
||||
/>
|
||||
),
|
||||
footer: false,
|
||||
onUpdateShow: (show: boolean) => {
|
||||
if (!show) {
|
||||
fetch(); // 刷新列表
|
||||
resetAddForm();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载CA证书
|
||||
*/
|
||||
const handleDownload = (row: PrivateCaItem) => {
|
||||
try {
|
||||
const downloadUrl = downloadCaCert({ id: row.id.toString(), type: 'ca' });
|
||||
console.log('download_url', downloadUrl);
|
||||
window.open(downloadUrl, '_blank');
|
||||
} catch (error: any) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除CA事件
|
||||
const handleDelete = async (row: PrivateCaItem) => {
|
||||
const { open: openLoad, close: close } = useLoadingMask({ text: '正在删除CA,请稍后...', zIndex: 3000 });
|
||||
useDialog({
|
||||
title: "删除CA",
|
||||
content: `确认要删除CA "${row.name}" 吗?此操作不可恢复。`,
|
||||
onPositiveClick: async () => {
|
||||
openLoad();
|
||||
try {
|
||||
const { fetch: deleteFetch, data } = deleteCaApi({ id: row.id });
|
||||
await deleteFetch();
|
||||
if (data.value && data.value.status === true) {
|
||||
message.success("删除成功");
|
||||
await fetch();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("删除CA失败:", err);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 获取表格行类名
|
||||
const getRowClassName = (row: PrivateCaItem): string => {
|
||||
const statusInfo = getStatusInfo(row.validTo);
|
||||
if (statusInfo.type === 'error') return 'bg-red-500/10';
|
||||
if (statusInfo.type === 'warning') return 'bg-orange-500/10';
|
||||
return '';
|
||||
};
|
||||
|
||||
onMounted(() => fetch());
|
||||
|
||||
return {
|
||||
// 表格组件
|
||||
TableComponent,
|
||||
PageComponent,
|
||||
SearchComponent,
|
||||
// 状态
|
||||
loading,
|
||||
data,
|
||||
param,
|
||||
// 方法
|
||||
openAddModal,
|
||||
getRowClassName,
|
||||
fetch,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 添加CA控制器
|
||||
*/
|
||||
export const useAddCaController = () => {
|
||||
const message = useMessage();
|
||||
const { open: openLoad, close: closeLoad } = useLoadingMask({
|
||||
text: '正在创建CA,请稍后...',
|
||||
zIndex: 3000,
|
||||
});
|
||||
|
||||
const {
|
||||
createType,
|
||||
resetAddForm
|
||||
} = useStore();
|
||||
|
||||
// 算法选项
|
||||
const algorithmOptions = [
|
||||
{ label: "ECDSA", value: "ecdsa" },
|
||||
{ label: "RSA", value: "rsa" },
|
||||
{ label: "SM2", value: "sm2" },
|
||||
];
|
||||
|
||||
const getKeyLengthOptions = (algorithm: string) => {
|
||||
switch (algorithm) {
|
||||
case 'ecdsa':
|
||||
return [
|
||||
{ label: "P-256 (256 bit)", value: "256" },
|
||||
{ label: "P-384 (384 bit)", value: "384" },
|
||||
{ label: "P-521 (521 bit)", value: "521" },
|
||||
];
|
||||
case 'rsa':
|
||||
return [
|
||||
{ label: "2048 bit", value: "2048" },
|
||||
{ label: "3072 bit", value: "3072" },
|
||||
{ label: "4096 bit", value: "4096" },
|
||||
];
|
||||
case 'sm2':
|
||||
return [
|
||||
{ label: "SM2 (256 bit)", value: "256" },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 有效期选项
|
||||
const getValidityOptions = (isRoot: boolean) => {
|
||||
if (isRoot) {
|
||||
return [
|
||||
{ label: "10年", value: "10" },
|
||||
{ label: "15年", value: "15" },
|
||||
{ label: "20年", value: "20" },
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{ label: "5天", value: "5" },
|
||||
{ label: "10天", value: "10" },
|
||||
{ label: "15天", value: "15" },
|
||||
{ label: "30天", value: "30" },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// 国家选项
|
||||
const countryOptions = [
|
||||
{ label: "中国", value: "CN" },
|
||||
{ label: "美国", value: "US" },
|
||||
{ label: "日本", value: "JP" },
|
||||
{ label: "德国", value: "DE" },
|
||||
{ label: "英国", value: "GB" },
|
||||
];
|
||||
|
||||
// 表单验证规则
|
||||
const getValidationRules = () => {
|
||||
const baseRules: any = {
|
||||
name: [{ required: true, message: '请输入CA名称', trigger: 'blur' }],
|
||||
cn: [{ required: true, message: '请输入通用名称', trigger: 'blur' }],
|
||||
o: [{ required: true, message: '请输入组织名称', trigger: 'blur' }],
|
||||
c: [{ required: true, message: '请选择国家', trigger: 'change' }],
|
||||
ou: [{ required: true, message: '请输入组织单位', trigger: 'blur' }],
|
||||
province: [{ required: true, message: '请输入省份', trigger: 'blur' }],
|
||||
locality: [{ required: true, message: '请输入城市', trigger: 'blur' }],
|
||||
key_length: [{ required: true, message: '请选择密钥长度', trigger: 'change' }],
|
||||
valid_days: [{ required: true, message: '请选择有效期', trigger: 'change' }],
|
||||
};
|
||||
|
||||
if (createType.value === 'root') {
|
||||
baseRules.algorithm = [{ required: true, message: '请选择加密算法', trigger: 'change' }];
|
||||
}
|
||||
|
||||
if (createType.value === 'intermediate') {
|
||||
baseRules.root_id = [{ required: true, message: '请选择父级CA', trigger: 'change' }];
|
||||
}
|
||||
|
||||
return baseRules;
|
||||
};
|
||||
|
||||
// 创建请求函数
|
||||
const createRequest = async (formData: any) => {
|
||||
if (createType.value === 'root') {
|
||||
const { root_id, ...rootFormData } = formData;
|
||||
const { fetch: createFetch, data } = createRootCa(rootFormData);
|
||||
await createFetch();
|
||||
return data.value;
|
||||
} else {
|
||||
const { algorithm, ...intermediateFormData } = formData;
|
||||
const { fetch: createFetch, data } = createIntermediateCa(intermediateFormData);
|
||||
await createFetch();
|
||||
return data.value;
|
||||
}
|
||||
};
|
||||
|
||||
// 表单提交处理函数
|
||||
const handleSubmit = async (formData: any) => {
|
||||
try {
|
||||
openLoad();
|
||||
// 验证必填字段
|
||||
let requiredFields: string[] = ['name', 'cn', 'o', 'c', 'ou', 'province', 'locality', 'key_length', 'valid_days'];
|
||||
if (createType.value === 'root') {
|
||||
requiredFields.push('algorithm');
|
||||
}
|
||||
if (createType.value === 'intermediate') {
|
||||
requiredFields.push('root_id');
|
||||
}
|
||||
|
||||
const missingFields = requiredFields.filter(field => !formData[field]);
|
||||
if (missingFields.length > 0) {
|
||||
message.error(`请填写必填字段: ${missingFields.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
const response = await createRequest(formData);
|
||||
if (response.status) {
|
||||
message.success(response?.message);
|
||||
resetAddForm();
|
||||
return true;
|
||||
} else {
|
||||
message.error(response?.message);
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '创建失败');
|
||||
return false;
|
||||
} finally {
|
||||
closeLoad();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
algorithmOptions,
|
||||
getKeyLengthOptions,
|
||||
getValidityOptions,
|
||||
countryOptions,
|
||||
getValidationRules,
|
||||
handleSubmit,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { defineStore, storeToRefs } from 'pinia';
|
||||
import type { AddPrivateCaParams } from './types';
|
||||
|
||||
// 根证书类型
|
||||
export interface RootCaItem {
|
||||
id: number;
|
||||
name: string;
|
||||
cn: string;
|
||||
o: string;
|
||||
c: string;
|
||||
algorithm: 'rsa' | 'ecdsa' | 'sm2';
|
||||
key_length: number;
|
||||
not_before: string;
|
||||
not_after: string;
|
||||
create_time: string;
|
||||
root_id: number | null;
|
||||
}
|
||||
|
||||
export const usePrivateCaStore = defineStore('private-ca-store', () => {
|
||||
const createType = ref<'root' | 'intermediate'>('root');
|
||||
|
||||
const rootCaList = ref<RootCaItem[]>([]);
|
||||
|
||||
const addForm = ref<AddPrivateCaParams & { root_id?: string }>({
|
||||
name: '',
|
||||
cn: '',
|
||||
o: '',
|
||||
c: 'CN',
|
||||
ou: '',
|
||||
province: '',
|
||||
locality: '',
|
||||
algorithm: 'ecdsa',
|
||||
key_length: '256',
|
||||
valid_days: '10',
|
||||
root_id: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 重置添加CA表单
|
||||
*/
|
||||
const resetAddForm = () => {
|
||||
addForm.value = {
|
||||
name: '',
|
||||
cn: '',
|
||||
o: '',
|
||||
c: 'CN',
|
||||
ou: '',
|
||||
province: '',
|
||||
locality: '',
|
||||
algorithm: 'ecdsa',
|
||||
key_length: '256',
|
||||
valid_days: '10',
|
||||
root_id: '',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 设置创建类型
|
||||
*/
|
||||
const setCreateType = (type: 'root' | 'intermediate') => {
|
||||
createType.value = type;
|
||||
resetAddForm();
|
||||
};
|
||||
|
||||
return {
|
||||
createType,
|
||||
rootCaList,
|
||||
addForm,
|
||||
resetAddForm,
|
||||
setCreateType,
|
||||
};
|
||||
});
|
||||
|
||||
export const useStore = () => {
|
||||
const store = usePrivateCaStore();
|
||||
return { ...store, ...storeToRefs(store) };
|
||||
};
|
||||
Reference in New Issue
Block a user