add
This commit is contained in:
parent
867e86bd7b
commit
ce407dcf94
@ -1,5 +1,6 @@
|
|||||||
package io.modules.item.dto;
|
package io.modules.item.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import io.modules.item.entity.FrontUserEntity;
|
import io.modules.item.entity.FrontUserEntity;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -24,7 +25,9 @@ public class CertificatesDTO implements Serializable {
|
|||||||
private String name;
|
private String name;
|
||||||
private String company;
|
private String company;
|
||||||
private String department;
|
private String department;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date issueDate;
|
private Date issueDate;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date expireDate;
|
private Date expireDate;
|
||||||
private Integer status;
|
private Integer status;
|
||||||
private String img;
|
private String img;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package io.modules.item.entity;
|
package io.modules.item.entity;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
/**
|
/**
|
||||||
@ -13,7 +14,9 @@ public class CertificatesEntity {
|
|||||||
private String name;
|
private String name;
|
||||||
private String company;
|
private String company;
|
||||||
private String department;
|
private String department;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date issueDate;
|
private Date issueDate;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date expireDate;
|
private Date expireDate;
|
||||||
private Integer status;
|
private Integer status;
|
||||||
private String img;
|
private String img;
|
||||||
|
@ -25,10 +25,12 @@ public class CertificatesServiceImpl extends CrudServiceImpl<CertificatesDao, Ce
|
|||||||
String id = (String)params.get("id");
|
String id = (String)params.get("id");
|
||||||
String status = (String)params.get("status");
|
String status = (String)params.get("status");
|
||||||
String auditStatus = (String)params.get("auditStatus");
|
String auditStatus = (String)params.get("auditStatus");
|
||||||
|
String expireDate = (String)params.get("expireDate");
|
||||||
|
|
||||||
QueryWrapper<CertificatesEntity> wrapper = new QueryWrapper<>();
|
QueryWrapper<CertificatesEntity> wrapper = new QueryWrapper<>();
|
||||||
wrapper.eq(StrUtil.isNotBlank(id), "id", id);
|
wrapper.eq(StrUtil.isNotBlank(id), "id", id);
|
||||||
wrapper.eq(StrUtil.isNotBlank(status), "status", status);
|
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);
|
wrapper.eq(StrUtil.isNotBlank(auditStatus) && !auditStatus.equals("all"), "audit_status", auditStatus);
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
1292
db/certify.sql
1292
db/certify.sql
File diff suppressed because one or more lines are too long
@ -45,8 +45,7 @@ public class CertificatesController {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private FrontUserService userService;
|
private FrontUserService userService;
|
||||||
@Value("${upload.url}")
|
|
||||||
private String uploadUrl;
|
|
||||||
|
|
||||||
@Login
|
@Login
|
||||||
@GetMapping("page")
|
@GetMapping("page")
|
||||||
@ -61,8 +60,7 @@ public class CertificatesController {
|
|||||||
params.put("userId",userId);
|
params.put("userId",userId);
|
||||||
PageData<CertificatesDTO> page = certificatesService.page(params);
|
PageData<CertificatesDTO> page = certificatesService.page(params);
|
||||||
List<CertificatesDTO> collect = page.getList().stream().map(e -> {
|
List<CertificatesDTO> collect = page.getList().stream().map(e -> {
|
||||||
// 拼接域名
|
|
||||||
e.setImg(uploadUrl + e.getImg());
|
|
||||||
|
|
||||||
UserDTO userDTO = userService.get( e.getUserId());
|
UserDTO userDTO = userService.get( e.getUserId());
|
||||||
e.setUser(userDTO);
|
e.setUser(userDTO);
|
||||||
@ -81,7 +79,6 @@ public class CertificatesController {
|
|||||||
if (list == null){
|
if (list == null){
|
||||||
return new Result<CertificatesEntity>().error("没有查询到");
|
return new Result<CertificatesEntity>().error("没有查询到");
|
||||||
}
|
}
|
||||||
list.setImg(uploadUrl + list.getImg());
|
|
||||||
return new Result<CertificatesEntity>().ok(list);
|
return new Result<CertificatesEntity>().ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,14 +86,10 @@ public class CertificatesController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "保存")
|
@Operation(summary = "保存")
|
||||||
public Result save(@RequestBody CertificatesDTO dto,@Parameter(hidden = true) @RequestAttribute("userId") Long userId){
|
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);
|
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||||
dto.setImg(dto.getImg().replace(uploadUrl,""));
|
|
||||||
dto.setUserId(userId);
|
dto.setUserId(userId);
|
||||||
certificatesService.save(dto);
|
certificatesService.save(dto);
|
||||||
return new Result();
|
return new Result();
|
||||||
@ -107,7 +100,7 @@ public class CertificatesController {
|
|||||||
public Result update(@RequestBody CertificatesDTO dto){
|
public Result update(@RequestBody CertificatesDTO dto){
|
||||||
//效验数据
|
//效验数据
|
||||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||||
dto.setImg(dto.getImg().replace(uploadUrl,""));
|
|
||||||
certificatesService.update(dto);
|
certificatesService.update(dto);
|
||||||
return new Result();
|
return new Result();
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ spring:
|
|||||||
multi-statement-allow: true
|
multi-statement-allow: true
|
||||||
web:
|
web:
|
||||||
resources:
|
resources:
|
||||||
static-locations: "file:D:/2025/blockchain-certify/upload/"
|
static-locations: "file:D:/20250519/block-identity-auth/upload/"
|
||||||
upload:
|
upload:
|
||||||
path: D:\2025\blockchain-certify\upload
|
path: D:\20250519\block-identity-auth\upload
|
||||||
url: http://localhost:18081/
|
url: http://localhost:18081/
|
BIN
ui/public/508d288b-cd6e-4814-a99f-53a3999c4261.jpg
Normal file
BIN
ui/public/508d288b-cd6e-4814-a99f-53a3999c4261.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 515 KiB |
280
ui/src/pages/about.vue
Normal file
280
ui/src/pages/about.vue
Normal 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>
|
271
ui/src/pages/admin/about.vue
Normal file
271
ui/src/pages/admin/about.vue
Normal 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>
|
@ -1,20 +1,95 @@
|
|||||||
<template>
|
<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 :data="state.certificateList" v-loading="loading" class="el-table">
|
||||||
<el-table-column prop="id" label="编号" />
|
<el-table-column prop="id" label="ID" />
|
||||||
<el-table-column prop="user.nickName" label="用户" />
|
<el-table-column prop="hex" label="身份信息哈希值" width="180" show-overflow-tooltip align="center" />
|
||||||
<el-table-column prop="issueDate" label="签发日期" />
|
<el-table-column prop="user.password" label="用户公钥" show-overflow-tooltip align="center" />
|
||||||
<el-table-column prop="expireDate" label="过期日期" />
|
<el-table-column
|
||||||
<el-table-column prop="status" label="状态" :formatter="statusText" />
|
prop="issueDate"
|
||||||
<el-table-column prop="blockchainTxId" label="交易哈希">
|
label="证书生效时间"
|
||||||
<template #default="scope">
|
width="180"
|
||||||
<span v-if="scope.row.blockchainTxId">{{ scope.row.blockchainTxId }}</span>
|
align="center"
|
||||||
<span v-else>未上链</span>
|
>
|
||||||
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="创建时间" />
|
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 分页控件 -->
|
<!-- 分页控件 -->
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
@ -27,103 +102,132 @@
|
|||||||
@current-change="handlePageChange"
|
@current-change="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<template #footer>
|
|
||||||
<el-button type="primary" @click="state.chainDialogVisible = false">我已知晓</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 loading = ref(true)
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dialogVisible: false,
|
|
||||||
chainDialogVisible: false, // 控制上链须知弹窗
|
|
||||||
dialogTitle: "",
|
|
||||||
certificateList: [] as any[],
|
certificateList: [] as any[],
|
||||||
form: {
|
currentCertificate: {} as any,
|
||||||
id: null,
|
form: {},
|
||||||
certificateNumber: '',
|
query: {
|
||||||
userId: '',
|
|
||||||
issueDate: '',
|
|
||||||
expireDate: '',
|
|
||||||
certificateData: '',
|
|
||||||
blockchainTxId: '',
|
|
||||||
img:'',
|
|
||||||
status: 1
|
|
||||||
},
|
|
||||||
query:{
|
|
||||||
page: 1,
|
page: 1,
|
||||||
limit:10,
|
limit: 10,
|
||||||
total:1,
|
total: 1,
|
||||||
isBlock:1
|
auditStatus: null
|
||||||
},
|
},
|
||||||
|
userList: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const openForm = (isAdd: boolean, data?: any) => {
|
// 下载证书方法
|
||||||
state.dialogVisible = true
|
const downloadCertificate = (row: any) => {
|
||||||
state.dialogTitle = isAdd ? '添加证书' : '编辑证书'
|
if (!row.img) {
|
||||||
// 使用 nextTick 保证弹窗DOM更新
|
ElMessage.warning('没有可下载的证书图片')
|
||||||
nextTick(() => {
|
return
|
||||||
state.form = isAdd
|
|
||||||
? {
|
|
||||||
id: null,
|
|
||||||
certificateNumber: '',
|
|
||||||
userId: '',
|
|
||||||
issueDate: '',
|
|
||||||
expireDate: '',
|
|
||||||
certificateData: '',
|
|
||||||
blockchainTxId: '',
|
|
||||||
img:'',
|
|
||||||
status: 1
|
|
||||||
}
|
}
|
||||||
: { ...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) => {
|
const handlePageChange = (page: number) => {
|
||||||
state.query.page = page
|
state.query.page = page
|
||||||
loadCertificates()
|
loadCertificates()
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusText = (row: any) => {
|
|
||||||
const map = { 0: '撤销', 1: '有效', 2: '过期' }
|
|
||||||
return map[row.status] ?? '未知'
|
|
||||||
}
|
|
||||||
const loadCertificates = async () => {
|
const loadCertificates = async () => {
|
||||||
loading.value = true
|
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.certificateList = res.data.list || []
|
||||||
state.query.total = res.data.total
|
state.query.total = res.data.total
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('数据加载失败')
|
||||||
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const openChainInstructions = () => {
|
|
||||||
state.chainDialogVisible = true
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCertificates()
|
loadCertificates()
|
||||||
|
adminRequest.get(`sys/user-front/page`, { params: { limit: 9999 } })
|
||||||
|
.then((res: any) => {
|
||||||
|
state.userList = res.data.list
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.certificate-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@ -134,48 +238,19 @@ onMounted(() => {
|
|||||||
.el-table {
|
.el-table {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
.el-button {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
.certificate-image-preview h4 {
|
||||||
.text-right {
|
margin-bottom: 10px;
|
||||||
text-align: right;
|
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 {
|
.tip-dialog h4 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
color: #f0ad4e;
|
color: #f0ad4e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-dialog p {
|
.tip-dialog p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
67
ui/src/pages/admin/user.vue
Normal file
67
ui/src/pages/admin/user.vue
Normal 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>
|
@ -3,8 +3,8 @@
|
|||||||
<!-- 区块链宣传横幅 -->
|
<!-- 区块链宣传横幅 -->
|
||||||
<div class="blockchain-banner">
|
<div class="blockchain-banner">
|
||||||
<div class="chain-effect"></div>
|
<div class="chain-effect"></div>
|
||||||
<h2>区块链存证证书</h2>
|
<h2>基于区块链的网络身份认证系统设计</h2>
|
||||||
<p>所有证书信息已通过区块链技术永久存证,保障数据不可篡改</p>
|
<p>所有信息已通过区块链技术永久存证,保障数据不可篡改</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 证书列表 -->
|
<!-- 证书列表 -->
|
||||||
@ -41,19 +41,16 @@
|
|||||||
<label>过期日期:</label>
|
<label>过期日期:</label>
|
||||||
<span>{{ item.expireDate || '--' }}</span>
|
<span>{{ item.expireDate || '--' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
|
||||||
<label>申请查看证书:</label>
|
|
||||||
<el-button @click="audit(item)">申请</el-button>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<label>区块ID:</label>
|
<label>区块ID:</label>
|
||||||
<div
|
<div
|
||||||
class="blockchain-id"
|
class="blockchain-id"
|
||||||
:class="{ disabled: !item.blockchainTxId }"
|
:class="{ disabled: !item.hex }"
|
||||||
@click="item.blockchainTxId && copyBlockchainId(item.blockchainTxId)"
|
@click="item.hex && copyBlockchainId(item.hex)"
|
||||||
>
|
>
|
||||||
{{ shortenTxId(item.blockchainTxId) }}
|
{{ shortenTxId(item.hex) }}
|
||||||
<template v-if="item.blockchainTxId">
|
<template v-if="item.hex">
|
||||||
<el-icon class="copy-icon"><DocumentCopy /></el-icon>
|
<el-icon class="copy-icon"><DocumentCopy /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -61,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 区块链验证提示 -->
|
<!-- 区块链验证提示 -->
|
||||||
<div class="blockchain-tip" v-if="item.blockchainTxId">
|
<div class="blockchain-tip" v-if="item.hex">
|
||||||
<el-icon color="#409EFF"><Warning /></el-icon>
|
<el-icon color="#409EFF"><Warning /></el-icon>
|
||||||
<span>此证书已上链存证,可通过区块ID在区块链浏览器验证</span>
|
<span>此证书已上链存证,可通过区块ID在区块链浏览器验证</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,6 +60,8 @@
|
|||||||
:action="state.path"
|
:action="state.path"
|
||||||
multiple
|
multiple
|
||||||
limit=1
|
limit=1
|
||||||
|
:on-success="handleSuccess"
|
||||||
|
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
@ -105,7 +107,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox, UploadProps } from 'element-plus'
|
||||||
import type { FormInstance } from 'element-plus'
|
import type { FormInstance } from 'element-plus'
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const form = reactive(<any>{})
|
const form = reactive(<any>{})
|
||||||
@ -171,6 +173,18 @@ const resetForm = () => {
|
|||||||
formRef.value.resetFields()
|
formRef.value.resetFields()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 上传成功后的回调
|
||||||
|
const handleSuccess: UploadProps['onSuccess'] = (
|
||||||
|
response,
|
||||||
|
uploadFile
|
||||||
|
) => {
|
||||||
|
form.img = response.data.path
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
@ -5,58 +5,229 @@
|
|||||||
<el-table-column prop="name" label="颁发者" show-overflow-tooltip align="center" />
|
<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="issueDate" label="证书生效时间" width="120" align="center" />
|
||||||
<el-table-column prop="expireDate" 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">
|
<el-table-column label="操作项" min-width="180" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click.prevent="deleteRow(scope.$index)"
|
@click.prevent="info(scope.row)"
|
||||||
>
|
>
|
||||||
生成签名信息
|
生成密钥信息
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click.prevent="deleteRow(scope.$index)"
|
@click="downloadCertificate(scope.row)"
|
||||||
>
|
>
|
||||||
下载证书
|
下载证书
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
tableData:[
|
tableData:[],
|
||||||
{
|
query:{
|
||||||
id:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
|
total:0,
|
||||||
hex:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
|
|
||||||
name:"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
|
},
|
||||||
issueDate:"2023-10-25",
|
// 详情对话框相关状态
|
||||||
expireDate:"2025-10-25",
|
detailDialogVisible: false,
|
||||||
status:"已通过",
|
currentCertificate: {} as any
|
||||||
}, {
|
|
||||||
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:"已通过",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@ -58,7 +58,7 @@ const register = reactive({
|
|||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
})
|
})
|
||||||
const showKeyDialog = ref(true)
|
const showKeyDialog = ref(false)
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
getUserInfo:{
|
getUserInfo:{
|
||||||
"hex": "1893836584118175698",
|
"hex": "1893836584118175698",
|
||||||
|
93
ui/src/pages/user.vue
Normal file
93
ui/src/pages/user.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user