This commit is contained in:
18796357645 2025-05-20 08:42:00 +08:00
parent 867e86bd7b
commit ce407dcf94
16 changed files with 1192 additions and 1443 deletions

View File

@ -1,5 +1,6 @@
package io.modules.item.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.modules.item.entity.FrontUserEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -24,7 +25,9 @@ public class CertificatesDTO implements Serializable {
private String name;
private String company;
private String department;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date issueDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date expireDate;
private Integer status;
private String img;

View File

@ -1,5 +1,6 @@
package io.modules.item.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
@ -13,7 +14,9 @@ public class CertificatesEntity {
private String name;
private String company;
private String department;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date issueDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date expireDate;
private Integer status;
private String img;

View File

@ -25,10 +25,12 @@ public class CertificatesServiceImpl extends CrudServiceImpl<CertificatesDao, Ce
String id = (String)params.get("id");
String status = (String)params.get("status");
String auditStatus = (String)params.get("auditStatus");
String expireDate = (String)params.get("expireDate");
QueryWrapper<CertificatesEntity> wrapper = new QueryWrapper<>();
wrapper.eq(StrUtil.isNotBlank(id), "id", id);
wrapper.eq(StrUtil.isNotBlank(status), "status", status);
wrapper.eq(StrUtil.isNotBlank(expireDate), "expire_date", expireDate);
wrapper.eq(StrUtil.isNotBlank(auditStatus) && !auditStatus.equals("all"), "audit_status", auditStatus);
return wrapper;
}

File diff suppressed because one or more lines are too long

View File

@ -45,8 +45,7 @@ public class CertificatesController {
@Autowired
private FrontUserService userService;
@Value("${upload.url}")
private String uploadUrl;
@Login
@GetMapping("page")
@ -61,8 +60,7 @@ public class CertificatesController {
params.put("userId",userId);
PageData<CertificatesDTO> page = certificatesService.page(params);
List<CertificatesDTO> collect = page.getList().stream().map(e -> {
// 拼接域名
e.setImg(uploadUrl + e.getImg());
UserDTO userDTO = userService.get( e.getUserId());
e.setUser(userDTO);
@ -81,7 +79,6 @@ public class CertificatesController {
if (list == null){
return new Result<CertificatesEntity>().error("没有查询到");
}
list.setImg(uploadUrl + list.getImg());
return new Result<CertificatesEntity>().ok(list);
}
@ -89,14 +86,10 @@ public class CertificatesController {
@PostMapping
@Operation(summary = "保存")
public Result save(@RequestBody CertificatesDTO dto,@Parameter(hidden = true) @RequestAttribute("userId") Long userId){
LambdaQueryWrapper<CertificatesEntity> lwq = new LambdaQueryWrapper<>();
List<CertificatesEntity> list = certificatesDao.selectList(lwq);
if (!list.isEmpty()){
return new Result().error("证书编号有重复");
}
//效验数据
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
dto.setImg(dto.getImg().replace(uploadUrl,""));
dto.setUserId(userId);
certificatesService.save(dto);
return new Result();
@ -107,7 +100,7 @@ public class CertificatesController {
public Result update(@RequestBody CertificatesDTO dto){
//效验数据
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
dto.setImg(dto.getImg().replace(uploadUrl,""));
certificatesService.update(dto);
return new Result();
}

View File

@ -33,7 +33,7 @@ spring:
multi-statement-allow: true
web:
resources:
static-locations: "file:D:/2025/blockchain-certify/upload/"
static-locations: "file:D:/20250519/block-identity-auth/upload/"
upload:
path: D:\2025\blockchain-certify\upload
path: D:\20250519\block-identity-auth\upload
url: http://localhost:18081/

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

280
ui/src/pages/about.vue Normal file
View File

@ -0,0 +1,280 @@
<template>
<div class="about-container">
<div class="header">
<h1>关于区块链身份认证系统</h1>
<p>基于区块链技术的去中心化数字身份认证解决方案</p>
</div>
<div class="content">
<div class="feature-section">
<h2><el-icon><Star /></el-icon> </h2>
<el-row :gutter="30">
<el-col :md="8" :sm="12" :xs="24" v-for="(feature, index) in features" :key="index">
<div class="feature-card">
<div class="icon-wrapper">
<component :is="feature.icon" />
</div>
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
</el-col>
</el-row>
</div>
<div class="team-section">
<h2><el-icon><User /></el-icon> </h2>
<el-row :gutter="30">
<el-col :md="6" :sm="8" :xs="12" v-for="(member, index) in teamMembers" :key="index">
<div class="member-card">
<el-avatar :size="100" :src="member.avatar" />
<h3>{{ member.name }}</h3>
<p class="position">{{ member.position }}</p>
<p class="bio">{{ member.bio }}</p>
</div>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Opportunity, Lock, Star, User, Link, Key, Connection } from '@element-plus/icons-vue'
const features = [
{
icon: 'Shield',
title: '安全可靠',
description: '利用区块链不可篡改特性,确保身份数据真实可信'
},
{
icon: 'Key',
title: '自主控制',
description: '用户完全掌控自己的身份数据,决定数据分享范围'
},
{
icon: 'Connection',
title: '跨平台认证',
description: '一次认证,多平台通用,无需重复注册'
},
{
icon: 'Link',
title: '去中心化',
description: '无中心服务器,避免单点故障和数据泄露风险'
},
{
icon: 'Lock',
title: '隐私保护',
description: '采用零知识证明技术,最小化披露身份信息'
},
{
icon: 'Opportunity',
title: '透明可审计',
description: '所有认证记录上链,可追溯不可抵赖'
}
]
const teamMembers = [
{
name: '张区块链',
position: '首席架构师',
bio: '10年区块链开发经验专注于分布式身份解决方案',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
},
{
name: '李密码学',
position: '密码学专家',
bio: '专注于零知识证明和同态加密技术研究',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
},
{
name: '王前端',
position: '前端工程师',
bio: '负责用户界面设计和交互体验优化',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
},
{
name: '赵合约',
position: '智能合约工程师',
bio: '设计并实现身份认证智能合约逻辑',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
}
]
</script>
<style scoped lang="scss">
.about-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
color: #333;
.header {
text-align: center;
margin-bottom: 50px;
h1 {
font-size: 2.5rem;
color: #2c3e50;
margin-bottom: 15px;
}
p {
font-size: 1.2rem;
color: #7f8c8d;
}
}
.card {
background: #fff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
transition: transform 0.3s;
&:hover {
transform: translateY(-5px);
}
h2 {
color: #2c3e50;
margin-bottom: 20px;
display: flex;
align-items: center;
font-size: 1.5rem;
.el-icon {
margin-right: 10px;
color: #409eff;
}
}
p, li {
color: #666;
line-height: 1.8;
}
ul {
padding-left: 20px;
}
}
.feature-section, .team-section {
margin: 60px 0;
h2 {
text-align: center;
margin-bottom: 40px;
font-size: 2rem;
color: #2c3e50;
display: flex;
justify-content: center;
align-items: center;
.el-icon {
margin-right: 10px;
color: #409eff;
}
}
}
.feature-card {
background: #fff;
border-radius: 8px;
padding: 30px;
text-align: center;
margin-bottom: 30px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s;
height: 100%;
&:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.icon-wrapper {
width: 60px;
height: 60px;
margin: 0 auto 20px;
background: #f0f7ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.el-icon {
font-size: 24px;
color: #409eff;
}
}
h3 {
color: #2c3e50;
margin-bottom: 15px;
}
p {
color: #666;
line-height: 1.6;
}
}
.member-card {
text-align: center;
margin-bottom: 30px;
.el-avatar {
margin-bottom: 15px;
border: 3px solid #f0f7ff;
}
h3 {
color: #2c3e50;
margin-bottom: 5px;
}
.position {
color: #409eff;
font-weight: bold;
margin-bottom: 10px;
}
.bio {
color: #666;
font-size: 0.9rem;
line-height: 1.6;
}
}
}
@media (max-width: 768px) {
.about-container {
padding: 20px 15px;
.header {
h1 {
font-size: 1.8rem;
}
p {
font-size: 1rem;
}
}
.card, .feature-card {
padding: 20px;
}
}
}
</style>
<route lang="json">
{
"meta": {
"layout": "front",
"title": "关于我们 - 区块链身份认证系统"
}
}
</route>

View File

@ -0,0 +1,271 @@
<template>
<div class="about-container">
<div class="header">
<h1>关于区块链身份认证系统</h1>
<p>基于区块链技术的去中心化数字身份认证解决方案</p>
</div>
<div class="content">
<div class="feature-section">
<h2><el-icon><Star /></el-icon> </h2>
<el-row :gutter="30">
<el-col :md="8" :sm="12" :xs="24" v-for="(feature, index) in features" :key="index">
<div class="feature-card">
<div class="icon-wrapper">
<component :is="feature.icon" />
</div>
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
</el-col>
</el-row>
</div>
<div class="team-section">
<h2><el-icon><User /></el-icon> </h2>
<el-row :gutter="30">
<el-col :md="6" :sm="8" :xs="12" v-for="(member, index) in teamMembers" :key="index">
<div class="member-card">
<el-avatar :size="100" :src="member.avatar" />
<h3>{{ member.name }}</h3>
<p class="position">{{ member.position }}</p>
<p class="bio">{{ member.bio }}</p>
</div>
</el-col>
</el-row>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Opportunity, Lock, Star, User, Link, Key, Connection } from '@element-plus/icons-vue'
const features = [
{
icon: 'Shield',
title: '安全可靠',
description: '利用区块链不可篡改特性,确保身份数据真实可信'
},
{
icon: 'Key',
title: '自主控制',
description: '用户完全掌控自己的身份数据,决定数据分享范围'
},
{
icon: 'Connection',
title: '跨平台认证',
description: '一次认证,多平台通用,无需重复注册'
},
{
icon: 'Link',
title: '去中心化',
description: '无中心服务器,避免单点故障和数据泄露风险'
},
{
icon: 'Lock',
title: '隐私保护',
description: '采用零知识证明技术,最小化披露身份信息'
},
{
icon: 'Opportunity',
title: '透明可审计',
description: '所有认证记录上链,可追溯不可抵赖'
}
]
const teamMembers = [
{
name: '张区块链',
position: '首席架构师',
bio: '10年区块链开发经验专注于分布式身份解决方案',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
},
{
name: '李密码学',
position: '密码学专家',
bio: '专注于零知识证明和同态加密技术研究',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
},
{
name: '王前端',
position: '前端工程师',
bio: '负责用户界面设计和交互体验优化',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
},
{
name: '赵合约',
position: '智能合约工程师',
bio: '设计并实现身份认证智能合约逻辑',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
}
]
</script>
<style scoped lang="scss">
.about-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
color: #333;
.header {
text-align: center;
margin-bottom: 50px;
h1 {
font-size: 2.5rem;
color: #2c3e50;
margin-bottom: 15px;
}
p {
font-size: 1.2rem;
color: #7f8c8d;
}
}
.card {
background: #fff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
transition: transform 0.3s;
&:hover {
transform: translateY(-5px);
}
h2 {
color: #2c3e50;
margin-bottom: 20px;
display: flex;
align-items: center;
font-size: 1.5rem;
.el-icon {
margin-right: 10px;
color: #409eff;
}
}
p, li {
color: #666;
line-height: 1.8;
}
ul {
padding-left: 20px;
}
}
.feature-section, .team-section {
margin: 60px 0;
h2 {
text-align: center;
margin-bottom: 40px;
font-size: 2rem;
color: #2c3e50;
display: flex;
justify-content: center;
align-items: center;
.el-icon {
margin-right: 10px;
color: #409eff;
}
}
}
.feature-card {
background: #fff;
border-radius: 8px;
padding: 30px;
text-align: center;
margin-bottom: 30px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s;
height: 100%;
&:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.icon-wrapper {
width: 60px;
height: 60px;
margin: 0 auto 20px;
background: #f0f7ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.el-icon {
font-size: 24px;
color: #409eff;
}
}
h3 {
color: #2c3e50;
margin-bottom: 15px;
}
p {
color: #666;
line-height: 1.6;
}
}
.member-card {
text-align: center;
margin-bottom: 30px;
.el-avatar {
margin-bottom: 15px;
border: 3px solid #f0f7ff;
}
h3 {
color: #2c3e50;
margin-bottom: 5px;
}
.position {
color: #409eff;
font-weight: bold;
margin-bottom: 10px;
}
.bio {
color: #666;
font-size: 0.9rem;
line-height: 1.6;
}
}
}
@media (max-width: 768px) {
.about-container {
padding: 20px 15px;
.header {
h1 {
font-size: 1.8rem;
}
p {
font-size: 1rem;
}
}
.card, .feature-card {
padding: 20px;
}
}
}
</style>

View File

@ -1,20 +1,95 @@
<template>
<div class="certificate-container">
<!-- 查询表单 -->
<el-row>
<el-col :span="24">
<el-form :inline="true" :model="state.query" class="demo-form-inline">
<el-form-item label="证书编号">
<el-input v-model="state.query.id" placeholder="请输入证书编号" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select
style="width: 120px"
v-model="state.query.auditStatus"
placeholder="请选择状态"
clearable
>
<el-option label="待审核" value="0" />
<el-option label="已通过" value="1" />
<el-option label="已拒绝" value="2" />
</el-select>
</el-form-item>
<el-form-item label="过期时间">
<el-date-picker
v-model="state.query.expireDate"
type="date"
placeholder="请选择日期"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadCertificates()">查询</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
<!-- 证书列表 -->
<el-table :data="state.certificateList" v-loading="loading" class="el-table">
<el-table-column prop="id" label="编号" />
<el-table-column prop="user.nickName" label="用户" />
<el-table-column prop="issueDate" label="签发日期" />
<el-table-column prop="expireDate" label="过期日期" />
<el-table-column prop="status" label="状态" :formatter="statusText" />
<el-table-column prop="blockchainTxId" label="交易哈希">
<template #default="scope">
<span v-if="scope.row.blockchainTxId">{{ scope.row.blockchainTxId }}</span>
<span v-else>未上链</span>
<el-table-column prop="id" label="ID" />
<el-table-column prop="hex" label="身份信息哈希值" width="180" show-overflow-tooltip align="center" />
<el-table-column prop="user.password" label="用户公钥" show-overflow-tooltip align="center" />
<el-table-column
prop="issueDate"
label="证书生效时间"
width="180"
align="center"
>
<template #default="{ row }">
{{ formatDateSimple(row.issueDate) }}
</template>
</el-table-column>
<el-table-column
prop="expireDate"
label="证书过期时间"
width="180"
align="center"
>
<template #default="{ row }">
{{ formatDateSimple(row.expireDate) }}
</template>
</el-table-column>
<el-table-column prop="status" label="证书状态">
<template #default="{row}">
<el-tag
:type="{
'0': 'warning',
'1': 'success',
'2': 'primary'
}[row.auditStatus]"
effect="light"
>
{{
{
'0': '待验证',
'1': '已通过',
'2': '已更新'
}[row.auditStatus]
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作项" align="center" width="230">
<template #default="scope">
<el-button link type="primary" @click="update(scope.row)">生成签名信息</el-button>
<el-button link type="primary" @click="downloadCertificate(scope.row)">下载证书</el-button>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" />
</el-table>
<!-- 分页控件 -->
<div class="pagination-container">
<el-pagination
@ -27,103 +102,132 @@
@current-change="handlePageChange"
/>
</div>
<!-- 上链须知弹窗 -->
<el-dialog
v-model="state.chainDialogVisible"
title="上链须知与注意事项"
width="500px"
:append-to-body="false"
>
<div class="tip-dialog">
<h4>上链须知</h4>
<p>1. 一旦证书信息上链后将不可修改</p>
<p>2. 请务必确保所填写的证书信息准确无误</p>
<p>3. 上链信息具有公正性和透明性请谨慎操作</p>
<p>4. 如果发现错误请联系管理员进行后续处理</p>
</div>
<template #footer>
<el-button type="primary" @click="state.chainDialogVisible = false">我已知晓</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
const loading = ref(true)
const state = reactive({
dialogVisible: false,
chainDialogVisible: false, //
dialogTitle: "",
certificateList: [] as any[],
form: {
id: null,
certificateNumber: '',
userId: '',
issueDate: '',
expireDate: '',
certificateData: '',
blockchainTxId: '',
img:'',
status: 1
},
query:{
currentCertificate: {} as any,
form: {},
query: {
page: 1,
limit:10,
total:1,
isBlock:1
limit: 10,
total: 1,
auditStatus: null
},
userList: []
})
const openForm = (isAdd: boolean, data?: any) => {
state.dialogVisible = true
state.dialogTitle = isAdd ? '添加证书' : '编辑证书'
// 使 nextTick DOM
nextTick(() => {
state.form = isAdd
? {
id: null,
certificateNumber: '',
userId: '',
issueDate: '',
expireDate: '',
certificateData: '',
blockchainTxId: '',
img:'',
status: 1
//
const downloadCertificate = (row: any) => {
if (!row.img) {
ElMessage.warning('没有可下载的证书图片')
return
}
: { ...data }
// <a>
const link = document.createElement('a')
link.href = row.img
link.target = '_blank'
// URL使
const fileName = row.img.split('/').pop() || `certificate_${row.id}`
link.download = fileName
//
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 使fetch API
if (isCrossOrigin(row.img)) {
downloadCrossOriginImage(row.img, fileName)
}
}
//
const isCrossOrigin = (url: string) => {
try {
return new URL(url).origin !== window.location.origin
} catch {
return false
}
}
//
const downloadCrossOriginImage = async (url: string, filename: string) => {
try {
const response = await fetch(url, {
mode: 'cors',
credentials: 'same-origin' //
})
if (!response.ok) throw new Error('下载失败')
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
//
window.URL.revokeObjectURL(blobUrl)
} catch (error) {
console.error('下载错误:', error)
}
}
function update(data: any, status: number) {
ElMessage.success("操作完成")
}
function formatDateSimple(val: string) {
if (!val) return ''
const d = new Date(val)
const yyyy = d.getFullYear()
const mm = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${yyyy}-${mm}-${dd}`
}
//
const handlePageChange = (page: number) => {
state.query.page = page
loadCertificates()
}
const statusText = (row: any) => {
const map = { 0: '撤销', 1: '有效', 2: '过期' }
return map[row.status] ?? '未知'
}
const loadCertificates = async () => {
loading.value = true
const res = await adminRequest.get('/sys/certificate/page',{params:state.query})
try {
const res = await adminRequest.get('/sys/certificate/page', {
params: {
...state.query,
auditStatus: state.query.auditStatus === null ? undefined : state.query.auditStatus
}
})
state.certificateList = res.data.list || []
state.query.total = res.data.total
} catch (e) {
ElMessage.error('数据加载失败')
} finally {
loading.value = false
}
}
const openChainInstructions = () => {
state.chainDialogVisible = true
}
onMounted(() => {
loadCertificates()
adminRequest.get(`sys/user-front/page`, { params: { limit: 9999 } })
.then((res: any) => {
state.userList = res.data.list
})
})
</script>
<style scoped>
.certificate-container {
padding: 20px;
}
.pagination-container {
margin-top: 20px;
@ -134,48 +238,19 @@ onMounted(() => {
.el-table {
margin-top: 20px;
}
.el-button {
margin-bottom: 20px;
}
.text-right {
text-align: right;
.certificate-image-preview h4 {
margin-bottom: 10px;
color: #606266;
font-weight: normal;
}
/* 弹出框样式 */
.el-dialog__header {
background-color: #409EFF;
color: #fff;
text-align: center;
}
.el-dialog {
border-radius: 8px;
}
.el-dialog__body {
padding: 20px;
}
/* 表单样式 */
.el-form-item {
margin-bottom: 15px;
}
.el-input,
.el-date-picker,
.el-select {
width: 100%;
}
/* 上链须知弹框样式 */
.tip-dialog {
border: 1px solid #f0ad4e;
background-color: #fcf8e3;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
.tip-dialog h4 {
margin: 0 0 10px 0;
color: #f0ad4e;
}
.tip-dialog p {
margin: 0;
font-size: 14px;

View File

@ -0,0 +1,67 @@
<template>
<el-form
ref="formRef"
:model="state.userInfo"
label-position="top"
>
<el-form-item
prop="password"
label="原始密码"
:rules="[
{
required: true,
message: '原始密码不能为空',
trigger: 'blur',
},
]"
>
<el-input v-model="state.userInfo.password" />
</el-form-item>
<el-form-item
prop="newPassword"
label="新密码"
:rules="[
{
required: true,
message: '新密码不能为空',
trigger: 'blur',
},
]"
>
<el-input v-model="state.userInfo.newPassword" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(formRef)">确定修改</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
const formRef = ref<FormInstance>()
const state = reactive(<any>{
userInfo:{}
})
/**
* 修改密码
* @param formEl
*/
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid) => {
if (valid) {
updatePasswordAdmin(state.userInfo).then(result => {
ElMessage.success("修改成功")
})
}
})
}
</script>
<style scoped>
</style>

View File

@ -3,8 +3,8 @@
<!-- 区块链宣传横幅 -->
<div class="blockchain-banner">
<div class="chain-effect"></div>
<h2>区块链存证证书</h2>
<p>所有证书信息已通过区块链技术永久存证保障数据不可篡改</p>
<h2>基于区块链的网络身份认证系统设计</h2>
<p>所有信息已通过区块链技术永久存证保障数据不可篡改</p>
</div>
<!-- 证书列表 -->
@ -41,19 +41,16 @@
<label>过期日期</label>
<span>{{ item.expireDate || '--' }}</span>
</div>
<div class="detail-item">
<label>申请查看证书</label>
<el-button @click="audit(item)">申请</el-button>
</div>
<div class="detail-item">
<label>区块ID</label>
<div
class="blockchain-id"
:class="{ disabled: !item.blockchainTxId }"
@click="item.blockchainTxId && copyBlockchainId(item.blockchainTxId)"
:class="{ disabled: !item.hex }"
@click="item.hex && copyBlockchainId(item.hex)"
>
{{ shortenTxId(item.blockchainTxId) }}
<template v-if="item.blockchainTxId">
{{ shortenTxId(item.hex) }}
<template v-if="item.hex">
<el-icon class="copy-icon"><DocumentCopy /></el-icon>
</template>
</div>
@ -61,7 +58,7 @@
</div>
<!-- 区块链验证提示 -->
<div class="blockchain-tip" v-if="item.blockchainTxId">
<div class="blockchain-tip" v-if="item.hex">
<el-icon color="#409EFF"><Warning /></el-icon>
<span>此证书已上链存证可通过区块ID在区块链浏览器验证</span>
</div>

View File

@ -60,6 +60,8 @@
:action="state.path"
multiple
limit=1
:on-success="handleSuccess"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
</el-upload>
@ -105,7 +107,7 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox, UploadProps } from 'element-plus'
import type { FormInstance } from 'element-plus'
const formRef = ref<FormInstance>()
const form = reactive(<any>{})
@ -171,6 +173,18 @@ const resetForm = () => {
formRef.value.resetFields()
}
}
//
const handleSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) => {
form.img = response.data.path
ElMessage.success('上传成功')
}
</script>
<style scoped lang="scss">

View File

@ -5,58 +5,229 @@
<el-table-column prop="name" label="颁发者" show-overflow-tooltip align="center" />
<el-table-column prop="issueDate" label="证书生效时间" width="120" align="center" />
<el-table-column prop="expireDate" label="证书过期时间" width="120" align="center"/>
<el-table-column prop="status" label="证书状态" />
<el-table-column prop="status" label="证书状态">
<template #default="{row}">
<el-tag
:type="{
'0': 'warning',
'1': 'success',
'2': 'primary'
}[row.auditStatus]"
effect="light"
>
{{
{
'0': '待验证',
'1': '已通过',
'2': '已更新'
}[row.auditStatus]
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作项" min-width="180" align="center">
<template #default="scope">
<el-button
link
type="primary"
size="small"
@click.prevent="deleteRow(scope.$index)"
@click.prevent="info(scope.row)"
>
生成签名信息
生成密钥信息
</el-button>
<el-button
link
type="primary"
size="small"
@click.prevent="deleteRow(scope.$index)"
@click="downloadCertificate(scope.row)"
>
下载证书
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 详情对话框 -->
<el-dialog
v-model="state.detailDialogVisible"
title="生成密钥信息"
width="700px"
>
<el-descriptions :column="2" border>
<el-descriptions-item label="证书ID">{{ state.currentCertificate.id }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag
:type="{
'0': 'warning',
'1': 'success',
'2': 'primary'
}[state.currentCertificate.auditStatus]"
effect="light"
>
{{
{
'0': '待验证',
'1': '已通过',
'2': '已更新'
}[state.currentCertificate.auditStatus]
}}
</el-tag>
</el-descriptions-item>
<!-- <el-descriptions-item label="身份信息哈希值">{{ state.currentCertificate.hex }}</el-descriptions-item>-->
<el-descriptions-item label="颁发者">{{ state.currentCertificate.name }}</el-descriptions-item>
<el-descriptions-item label="证书生效时间">{{ formatDate(state.currentCertificate.issueDate) }}</el-descriptions-item>
<el-descriptions-item label="证书过期时间">{{ formatDate(state.currentCertificate.expireDate) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDateTime(state.currentCertificate.updateTime) }}</el-descriptions-item>
<el-descriptions-item label="区块链交易哈希" :span="2" v-if="state.currentCertificate.txHash">
{{ state.currentCertificate.txHash }}
<el-button
type="primary"
link
v-if="state.currentCertificate.txHash"
>
查看交易
</el-button>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2" v-if="state.currentCertificate.remarks">
{{ state.currentCertificate.remarks }}
</el-descriptions-item>
</el-descriptions>
<!-- 证书图片预览 -->
<div class="certificate-preview" v-if="state.currentCertificate.img">
<h4>证书预览</h4>
<el-image
:src="state.currentCertificate.img"
:preview-src-list="[state.currentCertificate.img]"
fit="contain"
style="max-height: 300px;"
/>
</div>
<template #footer>
<el-button @click="state.detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { onMounted } from 'vue'
const state = reactive({
tableData:[
{
id:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
hex:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
name:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
issueDate:"2023-10-25",
expireDate:"2025-10-25",
status:"已通过",
}, {
id:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
hex:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
name:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
issueDate:"2023-10-25",
expireDate:"2025-10-25",
status:"已通过",
}, {
id:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
hex:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
name:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
issueDate:"2023-10-25",
expireDate:"2025-10-25",
status:"已通过",
}
]
tableData:[],
query:{
total:0,
},
//
detailDialogVisible: false,
currentCertificate: {} as any
})
const loadCertificates = async () => {
try {
const res = await frontRequest.get('/api/certificate/page', {
params: {
...state.query
}
})
state.tableData = res.data.list || []
state.query.total = res.data.total
} catch (e) {
ElMessage.error('数据加载失败')
}
}
//
const info = (row: any) => {
state.currentCertificate = { ...row }
state.detailDialogVisible = true
}
// (yyyy-MM-dd)
const formatDate = (dateString: string) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleDateString()
}
// (yyyy-MM-dd HH:mm:ss)
const formatDateTime = (dateString: string) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleString()
}
onMounted(()=>{
loadCertificates()
})
//
const downloadCertificate = (row: any) => {
if (!row.img) {
ElMessage.warning('没有可下载的证书图片')
return
}
// <a>
const link = document.createElement('a')
link.href = row.img
link.target = '_blank'
// URL使
const fileName = row.img.split('/').pop() || `certificate_${row.id}`
link.download = fileName
//
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 使fetch API
if (isCrossOrigin(row.img)) {
downloadCrossOriginImage(row.img, fileName)
}
}
//
const isCrossOrigin = (url: string) => {
try {
return new URL(url).origin !== window.location.origin
} catch {
return false
}
}
//
const downloadCrossOriginImage = async (url: string, filename: string) => {
try {
const response = await fetch(url, {
mode: 'cors',
credentials: 'same-origin' //
})
if (!response.ok) throw new Error('下载失败')
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
//
window.URL.revokeObjectURL(blobUrl)
} catch (error) {
}
}
</script>
<style scoped lang="scss">

View File

@ -58,7 +58,7 @@ const register = reactive({
password: '',
confirmPassword: '',
})
const showKeyDialog = ref(true)
const showKeyDialog = ref(false)
const state = reactive({
getUserInfo:{
"hex": "1893836584118175698",

93
ui/src/pages/user.vue Normal file
View File

@ -0,0 +1,93 @@
<!--用户详情-->
<template>
<el-form
ref="formRef"
style="max-width: 600px"
:model="state.userInfo"
label-width="auto"
label-position="top"
>
<el-form-item
prop="username"
label="用户名"
:rules="[
{
required: true,
message: '用户名不能为空',
trigger: 'blur',
},
]"
>
<el-input v-model="state.userInfo.username" />
</el-form-item>
<el-form-item
prop="nickName"
label="用户昵称"
:rules="[
{
required: true,
message: '用户昵称不能为空',
trigger: 'blur',
}
]"
>
<el-input v-model="state.userInfo.nickName" />
</el-form-item>
<el-form-item
prop="password"
label="密码"
>
<el-input v-model="state.userInfo.password" type="password"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(formRef)">提交</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import type { FormInstance } from 'element-plus'
import { userInfoFront, userUpdateFront } from '~/api/user/frontUserApi'
const formRef = ref<FormInstance>()
const state = reactive({
userInfo: {}
})
function init() {
userInfoFront().then(res =>{
state.userInfo = res.data
})
}
onMounted(()=>{
init()
})
/**
* 提交
* @param formEl
*/
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid) => {
if (valid) {
userUpdateFront(state.userInfo).then(res =>{
toast.success("修改成功~")
init()
}).catch(err=>{
})
}
})
}
</script>
<style scoped>
</style>
<route lang="json">
{
"meta": {
"layout": "front"
}
}
</route>