feat: face image
This commit is contained in:
parent
22014a7b0e
commit
65b3a7074b
@ -36,6 +36,7 @@ def import_models():
|
||||
"""导入我们所有的 models,使 alembic 可以自动对比 db scheme 创建 migration revision"""
|
||||
for pkg in scan_models():
|
||||
try:
|
||||
print(f"导入 {pkg}")
|
||||
import_module(pkg) # 导入 models
|
||||
except Exception as e: # pylint: disable=W0703
|
||||
print(
|
||||
|
63
alembic/versions/a41d2f3443c8_face_image.py
Normal file
63
alembic/versions/a41d2f3443c8_face_image.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""face_image
|
||||
|
||||
Revision ID: a41d2f3443c8
|
||||
Revises: 1d50ca81be81
|
||||
Create Date: 2024-11-08 16:20:23.106957
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a41d2f3443c8"
|
||||
down_revision = "1d50ca81be81"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"face_image",
|
||||
sa.Column("delete_time", sa.DateTime(), nullable=True),
|
||||
sa.Column(
|
||||
"update_time",
|
||||
sa.DateTime(),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("create_time", sa.DateTime(), nullable=False),
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column(
|
||||
"image",
|
||||
sqlmodel.sql.sqltypes.AutoString(length=255),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("is_approved", sa.Boolean(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_face_image_create_time"),
|
||||
"face_image",
|
||||
["create_time"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_face_image_update_time"),
|
||||
"face_image",
|
||||
["update_time"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_face_image_update_time"), table_name="face_image")
|
||||
op.drop_index(op.f("ix_face_image_create_time"), table_name="face_image")
|
||||
op.drop_table("face_image")
|
||||
# ### end Alembic commands ###
|
@ -16,6 +16,13 @@
|
||||
<span v-else>{{not}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 自定义普通td -->
|
||||
<el-table-column v-else-if="type == 'custom-text'" :label="name" :width="width" :min-width="minWidth">
|
||||
<template slot-scope="s">
|
||||
<span v-if="s.row[prop]">{{jv[1]}}</span>
|
||||
<span v-else>{{jv[2]}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- num 数字 -->
|
||||
<el-table-column v-else-if="type == 'num'" :label="name" :width="width" :min-width="minWidth" class-name="tc-num">
|
||||
<template slot-scope="s">
|
||||
|
@ -28,6 +28,16 @@ var menuList = [
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '人脸信息',
|
||||
icon: 'el-icon-search',
|
||||
info: '人脸信息',
|
||||
childList: [
|
||||
{id: '2-1-1', name: '人脸图片列表', icon: 'el-icon-collection-tag', url: 'sa-view/face/swiper-list.html'},
|
||||
{id: '2-2-1', name: '人脸图片添加', icon: 'el-icon-plus', url: 'sa-view/face/swiper-add.html'},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
name: '各种示例',
|
||||
icon: 'el-icon-document-remove',
|
||||
info: '增删改查各种常用组件示例',
|
||||
@ -39,7 +49,7 @@ var menuList = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
id: '4',
|
||||
name: '首页设置',
|
||||
icon: 'el-icon-search',
|
||||
info: '首页的一些设置',
|
||||
@ -134,58 +144,4 @@ var menuList = [
|
||||
isBlank: true,
|
||||
isShow: false// 隐藏
|
||||
},
|
||||
// ========= jq22搜集 ================
|
||||
{
|
||||
id: '111',
|
||||
name: 'jq22搜集',
|
||||
icon: 'el-icon-link',
|
||||
info: '示例:外部链接',
|
||||
childList: [
|
||||
{
|
||||
id: '110',
|
||||
name: '大屏展示',
|
||||
icon: 'el-icon-link',
|
||||
info: '大屏展示页',
|
||||
childList: [
|
||||
{id: '110-1', name: '大屏1', url: 'http://www.jq22.com/demo/estszjcmoban202008030007/'}, // 原作者:http://www.jq22.com/jquery-info23260
|
||||
{id: '110-2', name: '大屏2', url: 'http://www.jq22.com/demo/estjkdsj202007301414/'}, // 原作者:http://www.jq22.com/jquery-info23247
|
||||
{id: '110-3', name: '大屏3', url: 'http://www.jq22.com/demo/jquerygndsjmoban202007212350/'}, // 原作者:http://www.jq22.com/jquery-info23239
|
||||
{id: '110-4', name: '大屏4', url: 'http://www.jq22.com/demo/jqueryEchartsny202006151033/'}, // 原作者:http://www.jq22.com/jquery-info23114
|
||||
{id: '110-5', name: '大屏5', url: 'http://www.jq22.com/demo/echartsdindanmoban202007302202/'}, // 原作者:http://www.jq22.com/jquery-info23202
|
||||
{id: '110-6', name: '大屏6', url: 'http://www.jq22.com/demo/echartssjmoban202005210009/'}, // 原作者:http://www.jq22.com/jquery-info23047
|
||||
{id: '110-7', name: '大屏7', url: 'http://www.jq22.com/demo/echartsdsj202002251026/'}, // 原作者:http://www.jq22.com/jquery-info22826
|
||||
{id: '110-8', name: '大屏8', url: 'http://www.jq22.com/demo/echartswldsj201912112223/'}, // 原作者:http://www.jq22.com/jquery-info22636
|
||||
],
|
||||
},
|
||||
{id: '111-1', name: '图片切换', url: 'http://www.jq22.com/demo/jQueryTpqh201804012309/'}, // 原作者:https://www.jq22.com/jquery-info18534
|
||||
{id: '111-2', name: '3D旋转特效', url: 'http://www.jq22.com/demo/jQueryCss3D201710241004/'}, // 原作者:https://www.jq22.com/jquery-info16495
|
||||
{id: '111-3', name: 'canvas炫酷星空', url: 'http://www.jq22.com/demo/warpDrive201712211120/index.html'}, // 原作者:https://www.jq22.com/jquery-info17456
|
||||
{id: '111-4', name: 'H5碰撞小球', url: 'http://www.jq22.com/demo/html5Pzxq201712242209/'}, // 原作者:https://www.jq22.com/jquery-info17482
|
||||
{id: '111-5', name: '网页画板', url: 'http://www.jq22.com/demo/Mapping201802252341/'}, // 原作者:https://www.jq22.com/jquery-info18172
|
||||
{id: '111-6', name: '简约富文本编辑器', url: 'http://www.jq22.com/demo/jquery-notebook-master/'}, // 原作者:https://www.jq22.com/jquery-info345
|
||||
{id: '111-7', name: '水滴特效', url: 'http://www.jq22.com/demo/jquery-shuidi20151123/'}, // 原作者:https://www.jq22.com/jquery-info4835
|
||||
{id: '111-8', name: '图片放大', url: 'http://www.jq22.com/demo/jQueryJpg201708110048/'}, // 原作者:http://www.jq22.com/jquery-info15264
|
||||
{id: '111-9', name: '3D云', url: 'http://www.jq22.com/demo/jquery-cloud-141217202931/'}, // 原作者:http://www.jq22.com/jquery-info1325
|
||||
{id: '111-10', name: '3D选择图片', url: 'http://www.jq22.com/demo/jquery-3d20150831/'}, // 原作者:http://www.jq22.com/jquery-info4000
|
||||
{id: '111-11', name: '蜘蛛纸牌', url: 'http://www.jq22.com/demo/jqueryspider201809140137/'}, // 原作者:http://www.jq22.com/jquery-info20047
|
||||
{id: '111-12', name: '大转盘', url: 'http://www.jq22.com/demo/jquerylocal201912122316/'}, // 原作者:http://www.jq22.com/jquery-info22646
|
||||
{id: '111-13', name: '旋转地球', url: 'http://www.jq22.com/demo/earth201810300101/'}, // 原作者:http://www.jq22.com/jquery-info20328
|
||||
{id: '111-14', name: '下雨动画', url: 'http://www.jq22.com/demo/html5-canvas-rain201710252014/'}, // 原作者:http://www.jq22.com/jquery-info16518
|
||||
{id: '111-15', name: '绚丽星空', url: 'http://www.jq22.com/demo/jQuery3dxk201710142249/'}, // 原作者:http://www.jq22.com/jquery-info16294
|
||||
{id: '111-16', name: '3d波浪墙', url: 'http://www.jq22.com/demo/voxels-liquid201704112355/'}, // 原作者:http://www.jq22.com/jquery-info13400
|
||||
{id: '111-17', name: '元素周期表', url: 'http://www.jq22.com/demo/jquery-3D20151113/'}, // 原作者:http://www.jq22.com/jquery-info4710
|
||||
|
||||
{id: '111-18', name: '旋转相册', url: 'http://www.jq22.com/demo/tikm202006072243/'}, // 原作者:http://www.jq22.com/jquery-info23116
|
||||
{id: '111-19', name: '装逼专用', url: 'http://www.jq22.com/demo/canvaslxy202003192234/'}, // 原作者:http://www.jq22.com/jquery-info22793
|
||||
{id: '111-20', name: '3D粒子文字', url: 'http://www.jq22.com/demo/3dwz201912102124/'}, // 原作者:http://www.jq22.com/jquery-info22631
|
||||
{id: '111-21', name: '多面立方体', url: 'http://www.jq22.com/demo/threelft201905080117/'}, // 原作者:http://www.jq22.com/demo/threelft201905080117/
|
||||
|
||||
{id: '111-22', name: '常见配色', url: 'http://www.jq22.com/demo/jQueryColour202008050020/'}, // 原作者:http://www.jq22.com/jquery-info23262
|
||||
{id: '111-23', name: '音量调节', url: 'http://www.jq22.com/demo/AdjustVolume202005122241/'}, // 原作者:http://www.jq22.com/jquery-info23045
|
||||
{id: '111-24', name: '重力下落', url: 'http://www.jq22.com/demo/gamecaisse202005220040/'}, // 原作者:http://www.jq22.com/jquery-info23074
|
||||
{id: '111-25', name: '表情匹配', url: 'http://www.jq22.com/demo/emojimatchgame201907170050/dist/'}, // 原作者:http://www.jq22.com/jquery-info21952
|
||||
|
||||
|
||||
]
|
||||
},
|
||||
]
|
||||
|
138
src/frontend/sa-view/face/swiper-add.html
Normal file
138
src/frontend/sa-view/face/swiper-add.html
Normal file
@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>人脸图片 - 添加/修改</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<!-- 所有的 css js 资源 -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
|
||||
<link rel="stylesheet" href="../../static/sa.css">
|
||||
<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
|
||||
<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
|
||||
<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script src="../../static/sa.js"></script>
|
||||
<script src="../../static/kj/upload-util.js"></script>
|
||||
<style type="text/css">
|
||||
.td-img{width: 200px; height: 200px; cursor: pointer; vertical-align: middle;}
|
||||
.c-panel .el-form .c-label{width: 5em !important;}
|
||||
.c-panel .el-form .el-input{width: 400px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="vue-box" :class="{sbot: id}" style="display: none;" :style="'display: block;'">
|
||||
<!-- ------- 内容部分 ------- -->
|
||||
<div class="s-body">
|
||||
<div class="c-panel">
|
||||
<div class="c-title">人脸图片添加</div>
|
||||
<el-form>
|
||||
<sa-item name="图片" br>
|
||||
<img :src="imageUrl" class="td-img" @click="sa.showImage(imageUrl)" >
|
||||
<el-link type="primary" @click="sa.uploadInput(beforeAvatarUpload)">上传</el-link>
|
||||
</sa-item>
|
||||
<sa-item name="" class="s-ok" br>
|
||||
<el-button type="primary" icon="el-icon-plus" @click="ok()">保存</el-button>
|
||||
</sa-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ------- 底部按钮 ------- -->
|
||||
<div class="s-foot">
|
||||
<el-button type="primary" @click="ok()">确定</el-button>
|
||||
<el-button @click="sa.closeCurrIframe()">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
var app = new Vue({
|
||||
components: {
|
||||
"sa-item": httpVueLoader('../../sa-frame/com/sa-item.vue'),
|
||||
"sa-info": httpVueLoader('../../sa-frame/com/sa-info.vue'),
|
||||
},
|
||||
el: '.vue-box',
|
||||
data: {
|
||||
id: sa.p('id', 0), // 获取超链接中的id参数(0=添加,非0=修改)
|
||||
imagePath: '',
|
||||
imageUrl: '',
|
||||
},
|
||||
methods: {
|
||||
handleAvatarSuccess(res, file) {
|
||||
this.imagePath = res.data
|
||||
this.imageUrl = this.getFaceUrl(res.data);
|
||||
},
|
||||
beforeAvatarUpload(file) {
|
||||
const isJPG = file.type === 'image/jpeg';
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isJPG) {
|
||||
sa.alert('上传头像图片只能是 JPG 格式!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
sa.alert('上传头像图片大小不能超过 2MB!');
|
||||
return false;
|
||||
}
|
||||
return this.handleAvatarUpload(file);
|
||||
},
|
||||
getFaceUrl(path) {
|
||||
return sa.cfg.api_url + path;
|
||||
},
|
||||
handleAvatarUpload(file) {
|
||||
var formData = new FormData();
|
||||
formData.append('file', file);
|
||||
sa.ajax("/face/image/upload", formData, function (res) {
|
||||
this.handleAvatarSuccess(res, file);
|
||||
}.bind(this), {processData: false, contentType: false})
|
||||
},
|
||||
// 提交数据
|
||||
ok: function(){
|
||||
// 验证
|
||||
if (this.imagePath === '' || this.imageUrl === '') {
|
||||
return sa.alert('请上传图片');
|
||||
}
|
||||
// 开始增加或修改
|
||||
if(this.id <= 0) { // 添加
|
||||
sa.ajax('/face/image/create', {image: this.imagePath}, function(res){
|
||||
sa.alert('增加成功', function() {
|
||||
if(parent.app) {
|
||||
parent.app.dataList.push(res.data);
|
||||
parent.app.dataList[parent.app.dataList.length - 1].nid = parent.app.dataList.length;
|
||||
parent.sa.f5TableHeight(); // 刷新表格高度
|
||||
sa.closeCurrIframe(); // 关闭本页
|
||||
} else {
|
||||
this.clean()
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this), {});
|
||||
} else { // 修改
|
||||
sa.ajax('/face/image/update', {fid: this.id, image: this.imagePath}, function(res){
|
||||
sa.alert('修改成功', this.clean);
|
||||
}.bind(this), {});
|
||||
}
|
||||
},
|
||||
// 添加/修改 完成后的动作
|
||||
clean: function() {
|
||||
if(this.id == 0) {
|
||||
this.imagePath = '';
|
||||
this.imageUrl = '';
|
||||
} else {
|
||||
parent.app.f5(); // 刷新父页面列表
|
||||
sa.closeCurrIframe(); // 关闭本页
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function(){
|
||||
// 初始化数据
|
||||
if(this.id <= 0) {
|
||||
|
||||
} else {
|
||||
sa.ajax('/face/image/get_by_id', {fid: this.id}, function(res) {
|
||||
this.imageUrl = this.getFaceUrl(res.data.image);
|
||||
}.bind(this), {});
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
127
src/frontend/sa-view/face/swiper-list.html
Normal file
127
src/frontend/sa-view/face/swiper-list.html
Normal file
@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>人脸图片列表</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<!-- 所有的 css & js 资源 -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
|
||||
<link rel="stylesheet" href="../../static/sa.css">
|
||||
<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
|
||||
<script src="https://unpkg.com/http-vue-loader@1.4.2/src/httpVueLoader.js"></script>
|
||||
<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script src="../../static/sa.js"></script>
|
||||
<style>
|
||||
.td-img{width: 90px; height: 90px; cursor: pointer; vertical-align: middle;}
|
||||
.c-panel-add .td-img{width: 200px;}
|
||||
.c-panel-add .el-form .el-input{width: 200px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="vue-box" style="display: none;" :style="'display: block;'">
|
||||
<div class="c-panel">
|
||||
<!-- ------------- 检索参数 ------------- -->
|
||||
<!-- <div class="c-title">检索参数</div>-->
|
||||
<!-- <el-form @submit.native.prevent >-->
|
||||
<!-- <sa-item name="标题搜索" v-model="p.title"></sa-item>-->
|
||||
<!-- <el-button type="primary" icon="el-icon-search" @click="p.pageNo = 1; f5()">查询</el-button>-->
|
||||
<!-- </el-form>-->
|
||||
<!-- ------------- 快捷curd按钮 ------------- -->
|
||||
<sa-item type="fast-btn" show="add,delete,export,reset"></sa-item>
|
||||
<!-- ------------- 数据列表 ------------- -->
|
||||
<el-table class="data-table" ref="data-table" :data="dataList" size="small">
|
||||
<sa-td type="selection"></sa-td>
|
||||
<sa-td name="编号" prop="nid" width="70px"></sa-td>
|
||||
<el-table-column label="图片" width="400px">
|
||||
<template slot-scope="s">
|
||||
<img :src="getFaceUrl(s.row.image)" class="td-img" title="点击预览" @click="sa.showImage(getFaceUrl(s.row.image), '50%')">
|
||||
<span style="color: #666; padding-left: 0.5em;"> 点击预览</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<sa-td name="状态" prop="is_approved" type="custom-text" :jv="{1: '已通过审核', 2: '审核中'}"></sa-td>
|
||||
<sa-td name="创建时间" prop="create_time" type="datetime"></sa-td>
|
||||
<el-table-column label="操作" width="180px">
|
||||
<template slot-scope="s">
|
||||
<el-button class="c-btn" type="primary" icon="el-icon-edit" @click="update(s.row)">修改</el-button>
|
||||
<el-button class="c-btn" type="danger" icon="el-icon-delete" @click="del(s.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- ------------- 分页 ------------- -->
|
||||
<!-- <sa-item type="page" :curr.sync="p.pageNo" :size.sync="p.pageSize" :total="dataCount" @change="f5()"></sa-item>-->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模拟数据 -->
|
||||
<!-- <script src="mock-data-list.js"></script>-->
|
||||
<script>
|
||||
var app = new Vue({
|
||||
components: {
|
||||
"sa-item": httpVueLoader('../../sa-frame/com/sa-item.vue'),
|
||||
"sa-td": httpVueLoader('../../sa-frame/com/sa-td.vue'),
|
||||
},
|
||||
el: '.vue-box',
|
||||
data: {
|
||||
sa: sa, // 超级对象
|
||||
dataList: [], // 数据集合
|
||||
},
|
||||
methods: {
|
||||
getFaceUrl(path) {
|
||||
return sa.cfg.api_url + "/face/view/" + path;
|
||||
},
|
||||
// 刷新
|
||||
f5: function(){
|
||||
sa.ajax('/face/image/get_me', undefined, function(res){
|
||||
this.dataList = res.data; // 数据
|
||||
for (let i = 0; i < this.dataList.length; i++) {
|
||||
this.dataList[i].nid = i + 1;
|
||||
}
|
||||
sa.f5TableHeight(); // 刷新表格高度
|
||||
}.bind(this), {type: 'get'});
|
||||
},
|
||||
// 保存
|
||||
add: function(){
|
||||
sa.showIframe('新增数据', 'swiper-add.html?id=-1', '580px', '450px');
|
||||
},
|
||||
// 修改
|
||||
update: function(data){
|
||||
sa.showIframe('修改数据', 'swiper-add.html?id=' + data.id, '580px', '450px');
|
||||
},
|
||||
// 删除
|
||||
del: function(data){
|
||||
sa.confirm('是否删除,此操作不可撤销', function(){
|
||||
sa.ajax('/face/image/delete', {fid: data.id}, function(res){
|
||||
sa.ok('删除成功');
|
||||
sa.arrayDelete(this.dataList, data);
|
||||
sa.f5TableHeight(); // 刷新表格高度
|
||||
}.bind(this), {})
|
||||
}.bind(this))
|
||||
},
|
||||
// 批量删除
|
||||
deleteByIds: function() {
|
||||
// 获取选中元素的id列表
|
||||
let selection = this.$refs['data-table'].selection;
|
||||
let ids = sa.getArrayField(selection, 'id');
|
||||
if(selection.length == 0) {
|
||||
return sa.msg('请至少选择一条数据')
|
||||
}
|
||||
// 提交删除
|
||||
sa.confirm('是否批量删除选中数据?此操作不可撤销', function() {
|
||||
sa.ajax('/face/image/delete_by_ids', {fids: ids}, function(res) {
|
||||
sa.arrayDelete(this.dataList, selection);
|
||||
sa.ok('删除成功');
|
||||
sa.f5TableHeight(); // 刷新表格高度
|
||||
}.bind(this), {})
|
||||
}.bind(this));
|
||||
},
|
||||
},
|
||||
created: function(){
|
||||
this.f5();
|
||||
sa.onInputEnter(); // 监听输入框的回车事件,执行查询
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -36,6 +36,18 @@ sa.uploadApk = function(successCB) {
|
||||
sa.uploadFile = function(successCB) {
|
||||
sa.uploadFn(upload_cfg.upload_file_url, successCB);
|
||||
}
|
||||
sa.uploadInput = function (successCB) {
|
||||
var fileInput = document.createElement("input"); //创建input
|
||||
fileInput.type = "file"; //设置类型为file
|
||||
fileInput.id = 'uploadfile-' + randomString(12);
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.onchange = function(evt) {
|
||||
successCB(evt.target.files[0]);
|
||||
}
|
||||
// 添加到body,并触发其点击事件
|
||||
document.body.appendChild(fileInput);
|
||||
document.querySelector('#' + fileInput.id).click();
|
||||
}
|
||||
// 上传的内部函数 (要上传到的地址,成功的回调)
|
||||
sa.uploadFn = function(url, successCB) {
|
||||
// 创建input
|
||||
|
169
src/route/face_image.py
Normal file
169
src/route/face_image.py
Normal file
@ -0,0 +1,169 @@
|
||||
from fastapi import UploadFile, File
|
||||
from fastapi_amis_admin.crud import BaseApiOut
|
||||
from starlette import status
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
from src.plugin import handler
|
||||
from src.plugin.plugin import Plugin
|
||||
from src.services.face_image.schemas import (
|
||||
FaceImageCreate,
|
||||
FaceImageDelete,
|
||||
FaceImageUpdate,
|
||||
FaceImageDeleteIds,
|
||||
)
|
||||
from src.services.face_image.services import FaceImageServices
|
||||
from src.services.users.services import UserRoleServices
|
||||
from src.utils.upload_file import save_face_image, get_face_image, check_face_image
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.services.users.models import UserModel as User
|
||||
|
||||
|
||||
class FaceImageRoutes(Plugin):
|
||||
_prefix = "/face"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
face_image_services: FaceImageServices,
|
||||
user_role_services: UserRoleServices,
|
||||
):
|
||||
self.face_image_services = face_image_services
|
||||
self.user_role_services = user_role_services
|
||||
self.avatar_path = "/face/view/"
|
||||
|
||||
@handler.get("/image/get_me", student=True, out=True)
|
||||
async def get_me_face_images(self, request: Request):
|
||||
user: "User" = request.user
|
||||
try:
|
||||
req = await self.face_image_services.get_by_user_id(user_id=user.id)
|
||||
return BaseApiOut(code=0, msg="查询成功", data=req)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error Execute SQL:{e}",
|
||||
) from e
|
||||
|
||||
@handler.get("/view/{uid}/{file_path}", student=True, out=True)
|
||||
async def get_face_image(self, request: Request, uid: int, file_path: str):
|
||||
can_see = False
|
||||
if await self.user_role_services.is_admin(request.user.username):
|
||||
can_see = True
|
||||
if request.user.id == uid:
|
||||
can_see = True
|
||||
if not can_see:
|
||||
return BaseApiOut(status=500, msg="无权查看他人人脸")
|
||||
path = await get_face_image(uid, file_path)
|
||||
if not path:
|
||||
return BaseApiOut(status=500, msg="文件不存在")
|
||||
return FileResponse(path)
|
||||
|
||||
@handler.post("/image/upload", student=True, out=True)
|
||||
async def upload_face_image(self, request: Request, file: UploadFile = File(...)):
|
||||
user: "User" = request.user
|
||||
path = await save_face_image(user.id, file)
|
||||
real_path = self.avatar_path + str(user.id) + "/" + path
|
||||
return BaseApiOut(code=0, msg="上传成功", data=real_path)
|
||||
|
||||
@handler.post("/image/get_by_id", student=True, out=True)
|
||||
async def get_face_image_by_id(self, request: Request, data: FaceImageDelete):
|
||||
user: "User" = request.user
|
||||
can_query = await self.user_role_services.is_admin(request.user.username)
|
||||
try:
|
||||
data = await self.face_image_services.get_by_fid(data.fid)
|
||||
if not data:
|
||||
return BaseApiOut(status=500, msg="图片不存在")
|
||||
if data.user_id != user.id and not can_query:
|
||||
return BaseApiOut(status=500, msg="人脸地址错误")
|
||||
data.image = self.avatar_path + data.image
|
||||
return BaseApiOut(code=0, msg="查询成功", data=data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error Execute SQL:{e}",
|
||||
) from e
|
||||
|
||||
@handler.post("/image/create", student=True, out=True)
|
||||
async def create_face_image(self, request: Request, data: FaceImageCreate):
|
||||
user: "User" = request.user
|
||||
image = data.image
|
||||
if not image.startswith(self.avatar_path):
|
||||
return BaseApiOut(status=500, msg="人脸地址错误")
|
||||
path = image[len(self.avatar_path) :]
|
||||
if not await check_face_image(user.id, path):
|
||||
return BaseApiOut(status=500, msg="人脸不存在")
|
||||
try:
|
||||
data = await self.face_image_services.upload_by_user_id(user.id, path)
|
||||
return BaseApiOut(code=0, msg="新增成功", data=data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error Execute SQL:{e}",
|
||||
) from e
|
||||
|
||||
@handler.post("/image/delete", student=True, out=True)
|
||||
async def delete_face_image(self, request: Request, data: FaceImageDelete):
|
||||
user: "User" = request.user
|
||||
fid = data.fid
|
||||
can_delete = await self.user_role_services.is_admin(request.user.username)
|
||||
try:
|
||||
data = await self.face_image_services.get_by_fid(fid)
|
||||
if not data:
|
||||
return BaseApiOut(status=500, msg="图片不存在")
|
||||
if data.user_id != user.id and not can_delete:
|
||||
return BaseApiOut(status=500, msg="人脸地址错误")
|
||||
new = await self.face_image_services.delete_image(data)
|
||||
return BaseApiOut(code=0, msg="删除成功", data=new)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error Execute SQL:{e}",
|
||||
) from e
|
||||
|
||||
@handler.post("/image/delete_by_ids", student=True, out=True)
|
||||
async def delete_face_image_by_ids(
|
||||
self, request: Request, data: FaceImageDeleteIds
|
||||
):
|
||||
user: "User" = request.user
|
||||
fids = data.fids
|
||||
can_delete = await self.user_role_services.is_admin(request.user.username)
|
||||
try:
|
||||
datas = await self.face_image_services.get_by_ids(fids)
|
||||
if not datas:
|
||||
return BaseApiOut(status=500, msg="图片不存在")
|
||||
for data in datas:
|
||||
if data.user_id != user.id and not can_delete:
|
||||
return BaseApiOut(status=500, msg="人脸地址错误")
|
||||
await self.face_image_services.delete_images(datas)
|
||||
return BaseApiOut(code=0, msg="删除成功")
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error Execute SQL:{e}",
|
||||
) from e
|
||||
|
||||
@handler.post("/image/update", student=True, out=True)
|
||||
async def update_face_image(self, request: Request, data: FaceImageUpdate):
|
||||
user: "User" = request.user
|
||||
image = data.image
|
||||
fid = data.fid
|
||||
path = image[len(self.avatar_path) :]
|
||||
if not await check_face_image(user.id, path):
|
||||
return BaseApiOut(status=500, msg="人脸不存在")
|
||||
can_edit = await self.user_role_services.is_admin(request.user.username)
|
||||
try:
|
||||
data = await self.face_image_services.get_by_fid(fid)
|
||||
if not data:
|
||||
return BaseApiOut(status=500, msg="图片不存在")
|
||||
if data.user_id != user.id and not can_edit:
|
||||
return BaseApiOut(status=500, msg="人脸地址错误")
|
||||
new = await self.face_image_services.edit_by_user_id(fid, path)
|
||||
return BaseApiOut(code=0, msg="更新成功", data=new)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error Execute SQL:{e}",
|
||||
) from e
|
0
src/services/face_image/__init__.py
Normal file
0
src/services/face_image/__init__.py
Normal file
14
src/services/face_image/models.py
Normal file
14
src/services/face_image/models.py
Normal file
@ -0,0 +1,14 @@
|
||||
from fastapi_user_auth.mixins.models import PkMixin, CUDTimeMixin
|
||||
from sqlmodel import Field
|
||||
|
||||
|
||||
class FaceImageModel(PkMixin, CUDTimeMixin, table=True):
|
||||
__tablename__ = "face_image"
|
||||
|
||||
image: str = Field(title="Image", max_length=255, nullable=False)
|
||||
is_approved: bool = Field(default=False, title="Approved")
|
||||
user_id: int = Field(title="ID", nullable=False)
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return not self.delete_time and self.is_approved
|
54
src/services/face_image/repositories.py
Normal file
54
src/services/face_image/repositories.py
Normal file
@ -0,0 +1,54 @@
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from persica.factory.component import AsyncInitializingComponent
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel import select, col
|
||||
|
||||
from src.core.database import Database
|
||||
from src.services.face_image.models import FaceImageModel
|
||||
|
||||
|
||||
class FaceImageRepo(AsyncInitializingComponent):
|
||||
__order__ = 1
|
||||
|
||||
def __init__(self, database: Database):
|
||||
self.engine = database.engine
|
||||
|
||||
async def get_by(self, fid: Optional[int]) -> Optional[FaceImageModel]:
|
||||
async with AsyncSession(self.engine) as session:
|
||||
query = select(FaceImageModel)
|
||||
if fid is not None:
|
||||
query = query.where(FaceImageModel.id == fid)
|
||||
r = await session.exec(query)
|
||||
return r.first()
|
||||
|
||||
async def get_by_ids(self, fids: Sequence[int]) -> Sequence[FaceImageModel]:
|
||||
async with AsyncSession(self.engine) as session:
|
||||
query = select(FaceImageModel)
|
||||
query = query.where(col(FaceImageModel.id).in_(fids))
|
||||
r = await session.exec(query)
|
||||
return r.all()
|
||||
|
||||
async def get_all(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
is_approved: Optional[bool] = None,
|
||||
ignore_deleted: bool = True,
|
||||
) -> Sequence[FaceImageModel]:
|
||||
async with AsyncSession(self.engine) as session:
|
||||
query = select(FaceImageModel)
|
||||
if user_id is not None:
|
||||
query = query.where(FaceImageModel.user_id == user_id)
|
||||
if is_approved is not None:
|
||||
query = query.where(FaceImageModel.is_approved == is_approved)
|
||||
if ignore_deleted:
|
||||
query = query.where(col(FaceImageModel.delete_time).is_(None))
|
||||
r = await session.exec(query)
|
||||
return r.all()
|
||||
|
||||
async def add_face_image(self, image: "FaceImageModel") -> "FaceImageModel":
|
||||
async with AsyncSession(self.engine) as session:
|
||||
session.add(image)
|
||||
await session.commit()
|
||||
await session.refresh(image)
|
||||
return image
|
19
src/services/face_image/schemas.py
Normal file
19
src/services/face_image/schemas.py
Normal file
@ -0,0 +1,19 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class FaceImageCreate(BaseModel):
|
||||
image: str
|
||||
|
||||
|
||||
class FaceImageDelete(BaseModel):
|
||||
fid: int
|
||||
|
||||
|
||||
class FaceImageDeleteIds(BaseModel):
|
||||
fids: List[int]
|
||||
|
||||
|
||||
class FaceImageUpdate(FaceImageCreate, FaceImageDelete):
|
||||
pass
|
55
src/services/face_image/services.py
Normal file
55
src/services/face_image/services.py
Normal file
@ -0,0 +1,55 @@
|
||||
from datetime import datetime
|
||||
|
||||
from persica.factory.component import AsyncInitializingComponent
|
||||
from typing_extensions import Optional
|
||||
|
||||
from .models import FaceImageModel
|
||||
from .repositories import FaceImageRepo
|
||||
|
||||
|
||||
class FaceImageServices(AsyncInitializingComponent):
|
||||
__order__ = 1
|
||||
|
||||
def __init__(self, repo: FaceImageRepo):
|
||||
self.repo = repo
|
||||
|
||||
async def get_by_fid(self, fid: int):
|
||||
return await self.repo.get_by(fid)
|
||||
|
||||
async def get_by_ids(self, fids: list[int]):
|
||||
return await self.repo.get_by_ids(fids)
|
||||
|
||||
async def get_by_user_id(self, user_id: int, ignore_deleted: bool = True):
|
||||
return await self.repo.get_all(user_id=user_id, ignore_deleted=ignore_deleted)
|
||||
|
||||
async def get_all(
|
||||
self, approved: Optional[bool] = None, ignore_deleted: bool = True
|
||||
):
|
||||
return await self.repo.get_all(
|
||||
is_approved=approved, ignore_deleted=ignore_deleted
|
||||
)
|
||||
|
||||
async def upload_by_user_id(self, user_id: int, image: str) -> FaceImageModel:
|
||||
new = FaceImageModel(image=image, user_id=user_id)
|
||||
return await self.repo.add_face_image(new)
|
||||
|
||||
async def delete_image(self, image: FaceImageModel):
|
||||
image.delete_time = datetime.now()
|
||||
return await self.repo.add_face_image(image)
|
||||
|
||||
async def delete_images(self, images: list[FaceImageModel]):
|
||||
now = datetime.now()
|
||||
for image in images:
|
||||
image.delete_time = now
|
||||
await self.repo.add_face_image(image)
|
||||
|
||||
async def edit_by_user_id(self, rid: int, image: str) -> FaceImageModel:
|
||||
old = await self.repo.get_by(rid)
|
||||
old.image = image
|
||||
old.is_approved = False
|
||||
return await self.repo.add_face_image(old)
|
||||
|
||||
async def approve_image(self, rid: int) -> FaceImageModel:
|
||||
image = await self.repo.get_by(rid)
|
||||
image.is_approved = True
|
||||
return await self.repo.add_face_image(image)
|
@ -4,6 +4,15 @@ from fastapi_amis_admin.models.fields import Field
|
||||
from fastapi_user_auth.auth.models import BaseUser, Role as RoleModel
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
__all__ = [
|
||||
"UserModel",
|
||||
"RoleModel",
|
||||
"StudentIdMixin",
|
||||
"PhoneMixin",
|
||||
"RealNameMixin",
|
||||
"SexMixin",
|
||||
]
|
||||
|
||||
|
||||
class StudentIdMixin(SQLModel):
|
||||
student_id: Optional[str] = Field("", title="学号", max_length=15)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Optional, List, Sequence
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from fastapi_user_auth.auth import Auth
|
||||
from fastapi_user_auth.auth.backends.redis import RedisTokenStore
|
||||
|
@ -9,3 +9,6 @@ DATA_PATH = PROJECT_ROOT / "data"
|
||||
DATA_PATH.mkdir(exist_ok=True)
|
||||
AVATAR_DATA_PATH = DATA_PATH / "avatar"
|
||||
AVATAR_DATA_PATH.mkdir(exist_ok=True)
|
||||
|
||||
FACE_IMAGE_DATA_PATH = DATA_PATH / "face_image"
|
||||
FACE_IMAGE_DATA_PATH.mkdir(exist_ok=True)
|
||||
|
@ -5,7 +5,7 @@ from typing import Optional
|
||||
import aiofiles
|
||||
from fastapi import UploadFile
|
||||
|
||||
from ._path import AVATAR_DATA_PATH
|
||||
from ._path import AVATAR_DATA_PATH, FACE_IMAGE_DATA_PATH
|
||||
from ..errors import ProjectBaseError
|
||||
|
||||
AVATAR_MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
|
||||
@ -51,3 +51,45 @@ async def save_avatar(uid: int, file: UploadFile) -> str:
|
||||
async with aiofiles.open(file_path, "wb") as f:
|
||||
await f.write(file_data)
|
||||
return name
|
||||
|
||||
|
||||
async def check_face_image(uid: int, uri_path: str) -> bool:
|
||||
try:
|
||||
real_uid, file_path = uri_path.split("/")
|
||||
if int(real_uid) != uid:
|
||||
return False
|
||||
path = FACE_IMAGE_DATA_PATH / f"{uid}" / f"{file_path}"
|
||||
return path.exists()
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
async def get_face_image(uid: int, file_path: str) -> Optional[Path]:
|
||||
path = FACE_IMAGE_DATA_PATH / f"{uid}" / f"{file_path}"
|
||||
if not path.exists():
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
async def save_face_image(uid: int, file: UploadFile) -> str:
|
||||
filename = file.filename.lower() if file.filename else ""
|
||||
if not filename or not filename.endswith(".jpg"):
|
||||
raise ProjectBaseError("请上传 jpg 格式的文件")
|
||||
|
||||
path = FACE_IMAGE_DATA_PATH / f"{uid}"
|
||||
path.mkdir(exist_ok=True)
|
||||
|
||||
file_data = await file.read()
|
||||
if len(file_data) > AVATAR_MAX_FILE_SIZE:
|
||||
raise ProjectBaseError("文件过大,请上传小于5MB的文件")
|
||||
|
||||
name = (
|
||||
hashlib.md5(file_data).hexdigest() + ".jpg"
|
||||
) # 使用md5作为文件名,以免同一个文件多次写入
|
||||
file_path = path / name
|
||||
if file_path.exists():
|
||||
return name
|
||||
|
||||
async with aiofiles.open(file_path, "wb") as f:
|
||||
await f.write(file_data)
|
||||
return name
|
||||
|
Loading…
Reference in New Issue
Block a user