2025年基于BALKANFamilyTreeJS插件的家谱可视化项目功能Demo

基于BALKANFamilyTreeJS插件的家谱可视化项目功能Demo0 演示视频 家谱可视化 demo 演示 演示 bilibili com 1 思路分析视频 5 哔哩哔哩 bilibili 演示 2 项目基本构成 后端 go 语言 gin gorm 框架 前端 vue element plus

大家好,我是讯享网,很高兴认识大家。

0、演示视频:

家谱可视化demo演示_演示 (bilibili.com)

1、思路分析视频:

5_哔哩哔哩bilibili_演示

2、项目基本构成:

        后端:go语言+gin+gorm框架

        前端:vue+element-plus

        数据库:postgresSQL

        所使用的插件官网:

Royal Family Tree - Family Tree JS (balkan.app)

3、数据表设计:

 sql语句:

 DROP TABLE IF EXISTS "public"."visualization"; CREATE TABLE "public"."visualization" ( "id" int primary key, "pids" int, "mid" int, "fid" int, "name" varchar(255), "gender" varchar(255), "birthday" varchar(255), "familytree" varchar(255), "img" varchar(255), "tel" varchar(255), "email" varchar(255), "resume" varchar(255), "is_del" bool NOT NULL DEFAULT false, "update_time" timestamptz(6) NOT NULL ); /* INSERT INTO "public"."visualization" (id,pids,mid,fid,name,gender,birthday,familytree,img,tel,email,resume,is_del,update_time) VALUES('1','0','0','0','蒋介石','男','1887年10月31日-1975年4月5日','蒋介石家族','1.jpg','','@tel.com','原名瑞元,学名志清、中正,字介石。', 'f', '2022-06-08 08:00:00+08'); */
讯享网

4、后端业务逻辑实现+接口

model层:数据表的结构体设计:

讯享网package models import "time" type Visualization struct { Id int `gorm:"column:id"` Pids int `gorm:"column:pids"` Mid int `gorm:"column:mid"` Fid int `gorm:"column:fid"` Name string `gorm:"column:name"` Gender string `gorm:"column:gender"` Birth string `gorm:"column:birthday"` Familytree string `gorm:"column:familytree"` Img string `gorm:"column:img"` Tel string `gorm:"column:tel"` Email string `gorm:"column:email"` Resume string `gorm:"column:resume"` Isdel bool `gorm:"column:is_del"` UpdateTime time.Time `gorm:"column:update_time"` } 

dao层:主要负责对数据的基本操作。

        familytree.go 获取显示内容:

package dao import ( "familytree/models" ) func GetFamilytree() (error, []models.Visualization, int64) { var familytreeData []models.Visualization var total int64 err := db.Table("visualization").Select("id,pids,mid,fid,name,gender,birthday,familytree,img,tel,email,resume,is_del,update_time").Where("is_del=?", false).Order("id ASC").Count(&total).Find(&familytreeData).Error return err, familytreeData, total } 

        visualization.go 对数据表进行增删改查: 

讯享网package dao import "familytree/models" func GetAllVisual(visual map[string]interface{}) (error, []models.Visualization, int64) { var visualData []models.Visualization page := visual["page"].(int) pageSize := visual["limit"].(int) searchName := visual["searchName"].(string) var total int64 err := db.Table("visualization").Select("*").Where("name like ? And is_del = false", searchName+"%").Order("id ASC").Count(&total).Offset((page - 1) * pageSize).Limit(pageSize).Find(&visualData).Error return err, visualData, total } func DelVisual(id string) error { err := db.Table("visualization").Where("id = ?", id).Update("is_del", true).Error return err } func UpdateVisual(visual models.Visualization) error { err := db.Table("visualization").Where("id = ?", visual.Id).Updates(&visual).Error return err } func Addvisual(visual models.Visualization) error { err := db.Table("visualization").Select("id", "pids", "mid", "fid", "name", "gender", "birthday", "familytree", "img", "tel", "email", "resume", "update_time", "is_del").Create(&visual).Error return err } 

service层:实现基本业务逻辑,调用dao层封装函数。

        visualization.go调用dao层函数:

package services import ( "familytree/dao" "familytree/models" ) func GetAllVisual(visual map[string]interface{}) (error, []models.Visualization, int64) { err, visualData, total := dao.GetAllVisual(visual) return err, visualData, total } func DelVisual(id string) error { err := dao.DelVisual(id) return err } func UpdateVisual(visual models.Visualization) error { err := dao.UpdateVisual(visual) return err } func AddVisual(visual models.Visualization) error { err := dao.Addvisual(visual) return err } 

       familytree.go数据可视化函数:

package services import ( "familytree/dao" "familytree/models" ) func GetFamilytree() (error, []models.Visualization, int64) { err, familytreeData, total := dao.GetFamilytree() return err, familytreeData, total } 

controller层:调用service层函数进行前后端数据操作:

familytree,go显示数据:

package controllers import ( "familytree/pkg/app" "familytree/pkg/e" "familytree/services" "github.com/gin-gonic/gin" ) func GetFamilytree(c *gin.Context) { err, info, total := services.GetFamilytree() if err != nil { app.Error(c, e.ERROR, err, err.Error()) return } var m = map[string]interface{}{"value": info, "total": total} app.OK(c, m, "查询成功") } 

visualization.go表操作

package controllers import ( "encoding/json" "familytree/models" "familytree/pkg/app" "familytree/pkg/e" "familytree/services" "time" "github.com/gin-gonic/gin" "github.com/unknwon/com" ) func GetVisual(c *gin.Context) { page := -1 if arg := c.Query("page"); arg != "" { page = com.StrTo(arg).MustInt() } limit := -1 if arg := c.Query("limit"); arg != "" { limit = com.StrTo(arg).MustInt() } searchName := "" if arg := c.Query("searchName"); arg != "" { searchName = arg } supplierParam := map[string]interface{}{ "page": page, "limit": limit, "searchName": searchName, } err, info, total := services.GetAllVisual(supplierParam) if err != nil { app.Error(c, e.ERROR, err, err.Error()) return } app.OK(c, map[string]interface{}{"value": info, "total": total}, "查询成功") } func DelVisual(c *gin.Context) { id := "" if arg := c.Query("id"); arg != "" { id = arg } if id == "" { app.INFO(c, 30001, "参数错误") return } err := services.DelVisual(id) if err != nil { app.Error(c, e.ERROR, err, err.Error()) return } app.OK(c, map[string]interface{}{}, "删除成功") } func UpdateVisual(c *gin.Context) { b, _ := c.GetRawData() var m map[string]string _ = json.Unmarshal(b, &m) if m["Id"] == "" { app.INFO(c, 30000, "参数非法") return } Id := com.StrTo(m["Id"]).MustInt() Pids := com.StrTo(m["Pids"]).MustInt() Mid := com.StrTo(m["Mid"]).MustInt() Fid := com.StrTo(m["Fid"]).MustInt() Name := m["Name"] Gender := m["Gender"] Birth := m["Birth"] Familytree := m["Familytree"] Img := m["Img"] Tel := m["Tel"] Email := m["Email"] Resume := m["Resume"] err := services.UpdateVisual(models.Visualization{Id: Id, Pids: Pids, Mid: Mid, Fid: Fid, Name: Name, Gender: Gender, Birth: Birth, Familytree: Familytree, Img: Img, Tel: Tel, Email: Email, Resume: Resume, Isdel: false, UpdateTime: time.Now()}) if err != nil { app.Error(c, e.ERROR, err, err.Error()) return } app.OK(c, map[string]interface{}{}, "更新成功") } func Addvisual(c *gin.Context) { b, _ := c.GetRawData() var m map[string]string _ = json.Unmarshal(b, &m) Id := com.StrTo(m["Id"]).MustInt() Pids := com.StrTo(m["Pids"]).MustInt() Mid := com.StrTo(m["Mid"]).MustInt() Fid := com.StrTo(m["Fid"]).MustInt() Name := m["Name"] Gender := m["Gender"] Birth := m["Birth"] Familytree := m["Familytree"] Img := m["Img"] Tel := m["Tel"] Email := m["Email"] Resume := m["Resume"] err := services.AddVisual(models.Visualization{Id: Id, Pids: Pids, Mid: Mid, Fid: Fid, Name: Name, Gender: Gender, Birth: Birth, Familytree: Familytree, Img: Img, Tel: Tel, Email: Email, Resume: Resume, Isdel: false, UpdateTime: time.Now()}) if err != nil { app.Error(c, e.ERROR, err, err.Error()) return } app.OK(c, map[string]interface{}{}, "添加成功") } 

router层:设置路由接口

数据可视化接口:

package routers import ( "familytree/controllers" "github.com/gin-gonic/gin" ) func FamilytreeRouter(r *gin.RouterGroup) { r.GET("/familytree", controllers.GetFamilytree) } 

操作表接口:

package routers import ( "familytree/controllers" "github.com/gin-gonic/gin" ) func VisualRouter(r *gin.RouterGroup) { r.GET("/visualization", controllers.GetVisual) r.DELETE("/visualization", controllers.DelVisual) r.PUT("/visualization", controllers.UpdateVisual) r.POST("/visualization", controllers.Addvisual) } 

        

5、前端代码设计:

表操作代码实现:

<template> <!-- 卡片视图区 --> <el-card> <el-row :gutter="25"> <el-col :span="7"> <!-- 搜索添加 --> <el-input placeholder="请输入搜索成员的名字" v-model.lazy="queryInfo.searchName" @change="getVisualList" > </el-input> </el-col> <el-col :span="4"> <el-button type="primary" >搜索</el-button > <el-button type="primary" @click="dialogAddVisible = true" >添加</el-button > </el-col> </el-row> <!-- 列表 --> <el-table :data="VisualList" border stripe v-loading="isLoading" element-loading-background="rgba(255, 255, 255, .5)" element-loading-text="加载中,请稍后..." element-loading-spinner="el-icon-loading" > <el-table-column label="序号" type="index" fixed="left"></el-table-column> <el-table-column label="成员ID" prop="Id"></el-table-column> <el-table-column label="伴侣ID" prop="Pids"></el-table-column> <el-table-column label="母亲ID" prop="Mid"></el-table-column> <el-table-column label="父亲ID" prop="Fid"></el-table-column> <el-table-column label="姓名" prop="Name"></el-table-column> <el-table-column label="性别" prop="Gender"></el-table-column> <el-table-column label="生日" prop="Birth"></el-table-column> <el-table-column label="家族" prop="Familytree"></el-table-column> <el-table-column label="头像" prop="Img"></el-table-column> <el-table-column label="电话" prop="Tel"></el-table-column> <el-table-column label="邮箱" prop="Email"></el-table-column> <el-table-column label="履历" prop="Resume"></el-table-column> <el-table-column label="更新时间" prop="UpdateTime"></el-table-column> <el-table-column label="操作" fixed="right"> <template #default="scope"> <!-- 修改 --> <el-button type="primary" icon="el-icon-edit" size="mini" @click=" dialogEditOpen( scope.row.Id, scope.row.Pids, scope.row.Mid, scope.row.Fid, scope.row.Name, scope.row.Gender, scope.row.Birth, scope.row.Familytree, scope.row.Img, scope.row.Tel, scope.row.Email, scope.row.Resume, ) " > </el-button> <!-- 删除 --> <el-button type="danger" icon="el-icon-delete" size="mini" @click="deleteVisual(scope.row.Id)" > </el-button> </template> </el-table-column> </el-table> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryInfo.page" :page-sizes="[1, 2, 10, 100]" :page-size="queryInfo.limit" layout="total, sizes, prev, pager, next, jumper" :total="total" ></el-pagination> </el-card> <el-dialog title="添加成员" v-model="dialogAddVisible" width="50%"> <el-form :model="addForm" :rules="addFormRules" ref="addFormRef" label-width="100px" > <el-form-item label="成员ID" prop="Id"> <el-input v-model="addForm.Id" @blur="test()"></el-input> </el-form-item> <el-form-item label="伴侣ID" prop="Pids"> <el-input v-model="addForm.Pids"></el-input> </el-form-item> <el-form-item label="母亲ID" prop="Mid"> <el-input v-model="addForm.Mid"></el-input> </el-form-item> <el-form-item label="父亲ID" prop="Fid"> <el-input v-model="addForm.Fid"></el-input> </el-form-item> <el-form-item label="姓名" prop="Name"> <el-input v-model="addForm.Name"></el-input> </el-form-item> <el-form-item label="性别" prop="Gender"> <el-input v-model="addForm.Gender"></el-input> </el-form-item> <el-form-item label="生日" prop="Birth"> <el-input v-model="addForm.Birth"></el-input> </el-form-item> <el-form-item label="家族" prop="Familytree"> <el-input v-model="addForm.Familytree"></el-input> </el-form-item> <el-form-item label="头像" prop="Img"> <el-input v-model="addForm.Img"></el-input> </el-form-item> <el-form-item label="电话" prop="Tel"> <el-input v-model="addForm.Tel"></el-input> </el-form-item> <el-form-item label="邮箱" prop="Email"> <el-input v-model="addForm.Email"></el-input> </el-form-item> <el-form-item label="履历" prop="Resume"> <el-input v-model="addForm.Resume"></el-input> </el-form-item> <el-form-item label="更新时间" prop="UpdateTime"> <el-input v-model="addForm.UpdateTime"></el-input> </el-form-item> </el-form> <el-button type="primary" @click="addVisual()">确 定</el-button> <el-button @click="dialogAddVisible = false">取 消</el-button> </el-dialog> <!-- 编辑对话框 --> <el-dialog title="编辑成员" v-model="dialogEditVisible" width="50%"> <el-form :model="editForm" :rules="editFormRules" ref="editFormRef" label-width="100px" > <el-form-item label="成员ID" prop="Id"> <el-input v-model="editForm.Id"></el-input> </el-form-item> <el-form-item label="伴侣ID" prop="Pids"> <el-input v-model="editForm.Pids"></el-input> </el-form-item> <el-form-item label="母亲ID" prop="Mid"> <el-input v-model="editForm.Mid"></el-input> </el-form-item> <el-form-item label="父亲ID" prop="Fid"> <el-input v-model="editForm.Fid"></el-input> </el-form-item> <el-form-item label="姓名" prop="Name"> <el-input v-model="editForm.Name"></el-input> </el-form-item> <el-form-item label="性别" prop="Gender"> <el-input v-model="editForm.Gender"></el-input> </el-form-item> <el-form-item label="生日" prop="Birth"> <el-input v-model="editForm.Birth"></el-input> </el-form-item> <el-form-item label="家族" prop="Familytree"> <el-input v-model="editForm.Familytree"></el-input> </el-form-item> <el-form-item label="头像" prop="Img"> <el-input v-model="editForm.Img"></el-input> </el-form-item> <el-form-item label="电话" prop="Tel"> <el-input v-model="editForm.Tel"></el-input> </el-form-item> <el-form-item label="邮箱" prop="Email"> <el-input v-model="editForm.Email"></el-input> </el-form-item> <el-form-item label="履历" prop="Resume"> <el-input v-model="editForm.Resume"></el-input> </el-form-item> </el-form> <el-button type="primary" @click="updateVisual">确 定</el-button> <el-button @click="dialogEditVisible = false">取 消</el-button> </el-dialog> </template> <script> export default { data() { return { isLoading: false, dialogAddVisible: false, dialogEditVisible: false, queryInfo: { searchName: "", page: 1, limit: 10, }, VisualList: [], // 成员列表 total: 0, // 最大数据记录 addForm: { Id: "", Pids: "", Mid: "", Fid: "", Name: "", Gender: "", Birth: "", Familytree: "", Img: "", Tel: "", Email: "", Resume: "", UpdateTime: "", }, editForm: { Id: "", Pids: "", Mid: "", Fid: "", Name: "", Gender: "", Birth: "", Familytree: "", Img: "", Tel: "", Email: "", Resume: "", UpdateTime: "", }, addFormRules: { Id: [ { required: true, message: "请输入成员ID", trigger: "blur", }, ], Pids: [ { required: true, message: "请输入伴侣ID", trigger: "blur", }, ], Mid: [ { required: true, message: "请输入母亲ID", trigger: "blur", }, ], Fid: [ { required: true, message: "请输入父亲ID", trigger: "blur", }, ], Name: [ { required: true, message: "请输入姓名姓名", trigger: "blur", }, ], Gender: [ { required: true, message: "请输入母亲姓名", trigger: "blur", }, ], Birth: [ { required: true, message: "请输入成员生日", trigger: "blur", }, ], Familytree: [ { required: true, message: "请输入成员家族", trigger: "blur", }, ], Img: [ { required: true, message: "请输入头像", trigger: "blur", }, ], Tel: [ { required: true, message: "请输入电话", trigger: "blur", }, ], Email: [ { required: true, message: "请输入成员邮箱", trigger: "blur", }, ], Resume:[ { required: true, message: "请输入成员履历", trigger: "blur", }, ], }, editFormRules: { Id: [ { required: false, message: "请输入成员ID", trigger: "blur", }, ], Pids: [ { required: false, message: "请输入伴侣ID", trigger: "blur", }, ], Mid: [ { required: false, message: "请输入母亲ID", trigger: "blur", }, ], Fid: [ { required: false, message: "请输入父亲ID", trigger: "blur", }, ], Name: [ { required: false, message: "请输入姓名", trigger: "blur", }, ], Gender: [ { required: false, message: "请输入性别", trigger: "blur", }, ], Birth: [ { required: false, message: "请输入成员生日", trigger: "blur", }, ], Familytree: [ { required: false, message: "请输入成员家族", trigger: "blur", }, ], Img: [ { required: false, message: "请输入头像", trigger: "blur", }, ], Tel: [ { required: false, message: "请输入电话", trigger: "blur", }, ], Email: [ { required: false, message: "请输入成员邮箱", trigger: "blur", }, ], Resume:[ { required: true, message: "请输入成员履历", trigger: "blur", }, ], }, }; }, created() { this.getVisualList(); }, methods: { async getVisualList() { this.isLoading = true; // 调用post请求 console.log(this.VisualList) const { data: res } = await this.$http.get("system/apis/visualization", { params: this.queryInfo, }); if (res.code != 20000) { this.$message.error("加载用户列表失败"); this.isLoading = false; } this.VisualList = res.data.value; // 将返回数据赋值 this.total = res.data.total; // 总个数 this.isLoading = false; }, // 监听pageSize改变的事件 handleSizeChange(newLimit) { this.queryInfo.limit = newLimit; this.getVisualList(); // 数据发生改变重新申请数据 }, // 监听pageNum改变的事件 handleCurrentChange(newPage) { this.queryInfo.page = newPage; this.getVisualList(); // 数据发生改变重新申请数据 }, test(){ console.log(this.addForm.Id); }, async deleteVisual(Id) { // 弹框 const confirmResult = await this.$confirm( "此操作将永久删除该成员, 是否继续?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", } ).catch((err) => err); // 成功删除为confirm 取消为 cancel if (confirmResult != "confirm") { return this.$message.info("已取消删除"); } const { data: res } = await this.$http.delete( "system/apis/visualization?id=" + Id ); if (res.code != 20000) { return this.$message.error("删除失败"); } this.$message.success("删除成功"); this.getVisualList(); }, // 添加成员 addVisual() { this.$refs.addFormRef.validate(async (valid) => { if (!valid) return; // 发起请求 const { data: res } = await this.$http.post( "/system/apis/visualization", this.addForm ); if (res.code == 20000) { this.getVisualList(); this.dialogAddVisible = false; return this.$message.success("添加成功"); } this.$message.error("添加失败"); }); }, // 编辑 updateVisual() { this.$refs.editFormRef.validate(async (valid) => { if (!valid) return; // 发起请求 console.log(this.Id) const { data: res } = await this.$http.put( "/system/apis/visualization", this.editForm ); if (res.code == 20000) { this.getVisualList(); this.dialogEditVisible = false; return this.$message.success("编辑成功"); } this.$message.error("编辑失败"); }); }, dialogEditOpen( Id, Pids, Mid, Fid, Name, Gender, Birth, Familytree, Img, Tel, Email, Resume ) { this.editForm.Id = String(Id); this.editForm.Pids = String(Pids); this.editForm.Mid = String(Mid); this.editForm.Fid = String(Fid); this.editForm.Name = String(Name); this.editForm.Gender = String(Gender); this.editForm.Birth = String(Birth); this.editForm.Familytree = String(Familytree); this.editForm.Img = String(Img); this.editForm.Tel = String(Tel); this.editForm.Email = String(Email); this.editForm.Resume = String(Resume); this.dialogEditVisible = true; }, }, }; </script> <style> </style>

可视化代码实现:

<template> <div>{ 
  
    
  {total}},{ 
  
    
  {v}},{ 
  
    
  {nodes}}</div> <div id="tree" ref="tree"></div> </template> <script> import FamilyTree from '@balkangraph/familytree.js' export default { name: 'tree', data() { return { nodes:[], temp:[], total:0, v:"", }}, created(){ this.getNodeList(); }, methods: { mytree: function(domEl, x) { this.family = new FamilyTree (domEl, { nodes: x, nodeBinding: { field_0: "name", img_0: "img" } }); }, async getNodeList(){ const { data: res } = await this.$http.get("system/apis/familytree"); if (res.code != 20000) { this.$message.error("加载家族族谱失败"); } this.total=res.data["total"]; this.v=res.data.value; var count=0; for(var i in res.data.value) //for(var i =0;i<this.total;i++) { count++; if(count==1) { var map={"id":0,"pids":[],"name":"","gender":"","birthday":"","familytree":"","img":"","tel":"","email":"","resume":"","is_del":false,"update_time":""}; map["id"]=res.data.value[i]["Id"]; map["pids"][0]=res.data.value[i]["Pids"]; //this.map["mid"]=res.data.value[i]["Mid"]; //this.map["fid"]=res.data.value[i]["Fid"]; map["name"]=res.data.value[i]["Name"]; map["gender"]=res.data.value[i]["Gender"]; map["birthday"]=res.data.value[i]["Birthday"]; map["familytree"]=res.data.value[i]["Familytree"]; map["img"]=res.data.value[i]["Img"]; map["tel"]=res.data.value[i]["Tel"]; map["email"]=res.data.value[i]["Email"]; map["resume"]=res.data.value[i]["Resume"]; map["is_del"]=res.data.value[i]["Isdel"]; map["update_time"]=res.data.value[i]["Updatetime"]; this.temp[i]=map; } else{ // eslint-disable-next-line no-redeclare var map={"id":0,"pids":[],"mid":[],"fid":[],"name":"","gender":"","birthday":"","familytree":"","img":"","tel":"","email":"","resume":"","is_del":false,"update_time":""}; map["id"]=res.data.value[i]["Id"]; map["pids"][0]=res.data.value[i]["Pids"]; map["mid"][0]=res.data.value[i]["Mid"]; map["fid"][0]=res.data.value[i]["Fid"]; map["name"]=res.data.value[i]["Name"]; map["gender"]=res.data.value[i]["Gender"]; map["birthday"]=res.data.value[i]["Birthday"]; map["familytree"]=res.data.value[i]["Familytree"]; map["img"]=res.data.value[i]["Img"]; map["tel"]=res.data.value[i]["Tel"]; map["email"]=res.data.value[i]["Email"]; map["resume"]=res.data.value[i]["Resume"]; map["is_del"]=res.data.value[i]["Isdel"]; map["update_time"]=res.data.value[i]["Updatetime"]; this.temp[i]=map; } } //this.nodes[0] = this.temp[0]; // 将返回数据赋值 //this.nodes[1] = this.temp[1]; // 将返回数据赋值 // eslint-disable-next-line no-redeclare for(var i =this.total-1;i>=0;i--) { //if(this.temp[i]["is_del"]==false) this.nodes[i] = this.temp[i]; // 将返回数据赋值 } } }, mounted(){ this.mytree(this.$refs.tree, this.nodes) } } </script> <style scoped> </style> 

6、在前端项目导入familytree插件方法:

cnpm install
 cnpm i @balkangraph/familytree.js

然后复制粘贴代码使用即可。

7、演示:

插入表:

 可视化:

示例2: 

<div>温馨提示:如果家谱树没有加载出来,请尝试刷新界面,点击页面空白处,调整缩放比例即可看到可视化家谱图。</div>

优点:完成了数据库连接的可视化界面

缺点:目前不支持多父母和多前任夫妻关系,可以将pids和mid、fid的值改为列表即可。

小讯
上一篇 2025-01-04 18:20
下一篇 2025-01-11 10:16

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/20502.html