feat: feat: face admin get list

This commit is contained in:
xtaodada 2024-11-16 17:06:55 +08:00
parent 2cda4a2ecd
commit eeccb88ebe
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
14 changed files with 443 additions and 12 deletions

View File

@ -84,7 +84,9 @@
location.href = "register.html"; location.href = "register.html";
}else{ }else{
sa.closeCurrIframe(); sa.closeCurrIframe();
parent.sa.$page.openReg("register.html", false); var shadeClose = parseInt(sa.p('canClose', 0)) === 1;
var shadeCloseInt = shadeClose ? 1 : 0;
parent.sa.$page.openReg("register.html?canClose=" + shadeCloseInt, shadeClose);
} }
} }
// 点击登录按钮 // 点击登录按钮

View File

@ -122,7 +122,9 @@
location.href = "login.html"; location.href = "login.html";
}else{ }else{
sa.closeCurrIframe(); sa.closeCurrIframe();
parent.sa.$page.openLogin("login.html", false); var shadeClose = parseInt(sa.p('canClose', 0)) === 1;
var shadeCloseInt = shadeClose ? 1 : 0;
parent.sa.$page.openLogin("login.html?canClose=" + shadeCloseInt, shadeClose);
} }
} }
// 选择注册类型 // 选择注册类型

View File

@ -175,4 +175,13 @@ var adminMenuList = [
}, },
] ]
}, },
{
id: '6',
name: '人脸信息管理',
icon: 'el-icon-search',
info: '人脸信息管理',
childList: [
{id: '6-1', name: '人脸信息列表', icon: 'el-icon-document-remove', url: 'sa-view/face/face-list.html'},
]
},
] ]

View File

@ -63,7 +63,7 @@ sa_admin.dropList = [ // 头像点击处可操作的选项
{ {
name: '切换账号', name: '切换账号',
click: function() { click: function() {
sa.$page.openLogin('login.html', true); sa.$page.openLogin('login.html?canClose=1', true);
// layer.open({ // layer.open({
// type: 2, // type: 2,
// title: false, // title: false,

View File

@ -0,0 +1,237 @@
<!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="../../static/kj/layer/layer.js"></script>
<script src="../../static/sa.js"></script>
</head>
<body>
<div class="vue-box" style="display: none;" :style="'display: block;'">
<div class="c-panel">
<!-- ------------- 检索参数 ------------- -->
<div class="c-title">人脸列表</div>
<el-form>
<div class="c-item">
<label class="c-label">用户 ID</label>
<el-input v-model="p.user_id" placeholder=""></el-input>
</div>
<div class="c-item">
<label class="c-label">删除状态:</label>
<el-select v-model="p.deleted">
<el-option label="所有" :value="0"></el-option>
<el-option label="已被删除" :value="1"></el-option>
<el-option label="未被删除" :value="2"></el-option>
</el-select>
</div>
<div class="c-item">
<label class="c-label">批准状态:</label>
<el-select v-model="p.approved">
<el-option label="所有" :value="0"></el-option>
<el-option label="已被批准" :value="1"></el-option>
<el-option label="未被批准" :value="2"></el-option>
</el-select>
</div>
<br />
<div class="c-item">
<label class="c-label">创建日期:</label>
<el-date-picker v-model="p.start_time" type="date" value-format="yyyy-MM-dd" placeholder="开始日期"></el-date-picker> -
<el-date-picker v-model="p.end_time" type="date" value-format="yyyy-MM-dd" placeholder="结束日期"></el-date-picker>
</div>
<div class="c-item" style="min-width: 0px;">
<el-button type="primary" icon="el-icon-search" @click="p.pageNo = 1; f5()">查询</el-button>
</div>
<br />
<div class="c-item s-radio-text">
<label class="c-label">综合排序:</label>
<el-radio-group v-model="p.sortType">
<el-radio :label="1">创建时间</el-radio>
<el-radio :label="2">用户 ID</el-radio>
<el-radio :label="3">是否批准</el-radio>
</el-radio-group>
</div>
</el-form>
<!-- <div class="c-title">数据列表</div> -->
<!-- ------------- 快捷按钮 ------------- -->
<div class="fast-btn">
<!-- <el-button type="primary" icon="el-icon-plus" @click="add()">新增</el-button>-->
<el-button type="success" icon="el-icon-view" @click="getBySelect()">查看</el-button>
<el-button type="success" icon="el-icon-check" @click="deleteByIds()">批准</el-button>
<el-button type="danger" icon="el-icon-delete" @click="deleteByIds()">禁用</el-button>
<el-button type="warning" icon="el-icon-download" @click="sa.exportExcel()">导出</el-button>
<el-button type="info" icon="el-icon-refresh" @click="sa.f5()">重置</el-button>
</div>
<!-- ------------- 数据列表 ------------- -->
<el-table class="data-table" ref="data-table" :data="dataList" size="small">
<el-table-column type="selection" width="45px"></el-table-column>
<el-table-column label="编号" prop="id" width="80px" > </el-table-column>
<el-table-column label="人脸" prop="username" width="220px">
<template slot-scope="s">
<img :src="sa.cfg.api_url + s.row.image" @click="sa.showImage(sa.cfg.api_url + s.row.image, '400px', '400px')"
style="width: 3em; height: 3em; float: left; margin-right: 1em; border-radius: 50%; cursor: pointer;" >
<div style="float: left; width: 130px; line-height: 40px;">
<b>UID: {{s.row.user_id}}</b>
</div>
</template>
</el-table-column>
<el-table-column label="创建于" prop="create_time"></el-table-column>
<el-table-column label="删除状态">
<template slot-scope="s">
<el-switch v-model="s.row.status_delete" :active-value="1" :inactive-value="0" inactive-color="#ff4949"></el-switch>
<b style="color: green;" v-if="s.row.status_delete === 1">已被删除</b>
<b style="color: red;" v-if="s.row.status_delete === 0">未被删除</b>
</template>
</el-table-column>
<el-table-column label="状态">
<template slot-scope="s">
<el-switch v-model="s.row.status" :active-value="1" :inactive-value="0" inactive-color="#ff4949"></el-switch>
<b style="color: green;" v-if="s.row.status === 1">已批准</b>
<b style="color: red;" v-if="s.row.status === 0">未批准</b>
</template>
</el-table-column>
<el-table-column prop="address" label="操作">
<template slot-scope="s">
<el-button class="c-btn" type="success" icon="el-icon-view" @click="get(s.row)">详情</el-button>
<el-button v-if="s.row.status === 1" class="c-btn" type="danger" icon="el-icon-delete" @click="del(s.row)">禁用</el-button>
<el-button v-if="s.row.status === 0" class="c-btn" type="success" icon="el-icon-check" @click="del(s.row)">批准</el-button>
<el-button v-if="s.row.status_delete === 0" class="c-btn" type="danger" icon="el-icon-delete" @click="del(s.row)">删除</el-button>
<el-button v-if="s.row.status_delete === 1" class="c-btn" type="success" icon="el-icon-check" @click="del(s.row)">撤销</el-button>
</template>
</el-table-column>
</el-table>
<!-- ------------- 分页 ------------- -->
<div class="page-box">
<el-pagination background
layout="total, prev, pager, next, sizes, jumper"
:current-page.sync="p.pageNo"
:page-size.sync="p.pageSize"
:total="dataCount"
:page-sizes="[1, 10, 20, 30, 40, 50, 100]"
@current-change="f5(true)"
@size-change="f5(true)">
</el-pagination>
</div>
</div>
<!-- 给layer打一波广告 -->
<!-- <div class="c-panel" style="background-color: rgba(0,0,0,0);">
layer<el-link type="primary" href="http://layer.layui.com/" target="_blank">
一个可以让你想到即可做到的JavaScript弹窗解决方案
</el-link>
</div> -->
</div>
<script src="data-list.js"></script>
<script type="text/javascript">
sa.checkAuth('admin');
var app = new Vue({
el: '.vue-box',
data: {
p: { // 查询参数
user_id: sa.p("user_id", '0'),
deleted: 0,
approved: 0,
sortType: 1,
start_time: '',
end_time: '',
pageNo: 1,
pageSize: 10,
},
dataCount: 0,
dataList: []
},
methods: {
// 数据刷新
f5: function() {
sa.ajax('/face/admin/image/list', this.p, function(res){
this.dataList = res.data.data; // 数据
for (let i = 0; i < this.dataList.length; i++) {
this.dataList[i].image = '/face/view/' + this.dataList[i].image;
this.dataList[i].status_delete = this.dataList[i].delete_time !== null ? 1 : 0;
this.dataList[i].status = this.dataList[i].is_approved ? 1 : 0;
}
this.dataCount = res.data.count; // 分页
sa.f5TableHeight(); // 刷新表格高度
}.bind(this), {});
},
// 查看
get: function(data) {
var str = '<div>';
str += '<p>编号:' + data.id + '</p>';
str += '<p>用户 ID' + data.user_id + '</p>';
str += '<p>当前状态:<b>' + (data.status == 1 ? '已批准' : '未批准') + '</b></p>';
str += '<p>创建时间:' + data.create_time + '</p>';
str += '<p>更新时间:' + data.update_time + '</p>';
str += '<p>删除时间:' + data.delete_time + '</p>';
str += '</div>';
sa.alert(str);
},
// 查看 - 根据选中的
getBySelect: function(data) {
var selection = this.$refs['data-table'].selection;
if(selection.length == 0) {
return sa.msg('请选择一条数据')
}
this.get(selection[0]);
},
// 删除
del: function(data) {
sa.confirm('是否修改,此操作可撤销', function() {
sa.ajax('/user/admin/change_status', {user_id: data.id, is_active: !data.is_active}, function(res) {
// sa.arrayDelete(this.dataList, data);
if (data.is_active) {
sa.ok('禁用成功');
} else {
sa.ok('启用成功');
}
this.f5();
}.bind(this), {})
}.bind(this));
},
// 重置密码
reset_pwd: function (data) {
sa.confirm('是否重置密码,此操作不可撤销', function() {
sa.ajax('/user/admin/reset_password', {user_id: data.id}, function(res) {
sa.ok('重置密码成功');
}.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.ajax2('/user/admin/change_status', {ids: 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>

View File

@ -113,8 +113,10 @@
<el-table-column prop="address" label="操作"> <el-table-column prop="address" label="操作">
<template slot-scope="s"> <template slot-scope="s">
<el-button class="c-btn" type="success" icon="el-icon-view" @click="get(s.row)">详情</el-button> <el-button class="c-btn" type="success" icon="el-icon-view" @click="get(s.row)">详情</el-button>
<el-button class="c-btn" type="success" icon="el-icon-view" @click="getFace(s.row)">人脸信息</el-button>
<el-button class="c-btn" type="danger" icon="el-icon-delete" @click="reset_pwd(s.row)">重置密码</el-button> <el-button class="c-btn" type="danger" icon="el-icon-delete" @click="reset_pwd(s.row)">重置密码</el-button>
<el-button class="c-btn" type="danger" icon="el-icon-delete" @click="del(s.row)">禁/启用</el-button> <el-button v-if="s.row.status === 1" class="c-btn" type="danger" icon="el-icon-delete" @click="del(s.row)">禁用</el-button>
<el-button v-if="s.row.status === 0" class="c-btn" type="success" icon="el-icon-check" @click="del(s.row)">启用</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -190,6 +192,10 @@
str += '</div>'; str += '</div>';
sa.alert(str); sa.alert(str);
}, },
// 查看人脸信息
getFace: function(data) {
sa.showIframe('人脸信息', '../face/face-list.html?user_id=' + data.id, '900px', '600px');
},
// 查看 - 根据选中的 // 查看 - 根据选中的
getBySelect: function(data) { getBySelect: function(data) {
var selection = this.$refs['data-table'].selection; var selection = this.$refs['data-table'].selection;

View File

@ -1,10 +1,10 @@
from typing import TYPE_CHECKING
from fastapi import UploadFile, File from fastapi import UploadFile, File
from fastapi_amis_admin.crud import BaseApiOut from fastapi_amis_admin.crud import BaseApiOut
from starlette import status from starlette import status
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from typing import TYPE_CHECKING
from starlette.responses import FileResponse from starlette.responses import FileResponse
from src.plugin import handler from src.plugin import handler

View File

@ -0,0 +1,52 @@
from fastapi_amis_admin.crud import BaseApiOut
from starlette import status
from starlette.exceptions import HTTPException
from src.plugin import handler
from src.plugin.plugin import Plugin
from src.services.face_image.schemas import FaceList
from src.services.face_image.services import FaceImageServices
from src.services.users.services import UserRoleServices
class FaceImageAdminRoutes(Plugin):
_prefix = "/face/admin"
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.post("/image/list", student=True)
async def get_me_face_images(self, data: FaceList):
user_id = data.user_id
deleted, approved, sort_type = data.deleted, data.approved, data.sortType
start_time, end_time = data.start, data.end
page_no, page_size = data.pageNo, data.pageSize
if page_no < 1:
page_no = 1
if page_size < 0 or page_size > 100:
page_size = 10
try:
data, count = await self.face_image_services.get_image_list(
user_id,
deleted,
approved,
sort_type,
start_time,
end_time,
page_no,
page_size,
)
return BaseApiOut(
code=0, msg="请求成功", data={"data": data, "count": count}
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error Execute SQL{e}",
) from e

View File

@ -1,9 +1,8 @@
import datetime import datetime
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Union
from fastapi_amis_admin.crud import BaseApiOut
from fastapi import HTTPException from fastapi import HTTPException
from fastapi_amis_admin.crud import BaseApiOut
from starlette import status from starlette import status
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import Response from starlette.responses import Response

View File

@ -9,7 +9,6 @@ from src.services.users.schemas import (
UserList, UserList,
DisableOrEnableUser, DisableOrEnableUser,
UserAdminAdd, UserAdminAdd,
UserRoleEnum,
CreateTypeEnum, CreateTypeEnum,
) )
from src.services.users.services import UserServices from src.services.users.services import UserServices

View File

@ -2,7 +2,6 @@ from fastapi import File, UploadFile
from fastapi_amis_admin.crud import BaseApiOut from fastapi_amis_admin.crud import BaseApiOut
from starlette import status from starlette import status
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import FileResponse from starlette.responses import FileResponse

View File

@ -1,11 +1,14 @@
from typing import Optional, Sequence from datetime import datetime
from typing import Optional, Sequence, Tuple
from persica.factory.component import AsyncInitializingComponent from persica.factory.component import AsyncInitializingComponent
from sqlalchemy import func
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel import select, col from sqlmodel import select, col
from src.core.database import Database from src.core.database import Database
from src.services.face_image.models import FaceImageModel from src.services.face_image.models import FaceImageModel
from src.services.face_image.schemas import TypeEnum, SortTypeEnum
class FaceImageRepo(AsyncInitializingComponent): class FaceImageRepo(AsyncInitializingComponent):
@ -52,3 +55,61 @@ class FaceImageRepo(AsyncInitializingComponent):
await session.commit() await session.commit()
await session.refresh(image) await session.refresh(image)
return image return image
@staticmethod
async def get_count(session, q) -> int:
count_q = (
q.with_only_columns(func.count())
.order_by(None)
.select_from(q.get_final_froms()[0])
)
iterator = await session.exec(count_q)
for count in iterator:
return count
return 0
async def get_image_list(
self,
user_id: Optional[int],
deleted: Optional[TypeEnum],
approved: Optional[TypeEnum],
sort_type: Optional[SortTypeEnum],
start_time: Optional[datetime],
end_time: Optional[datetime],
page_no: int,
page_size: int,
) -> Tuple[Sequence[FaceImageModel], int]:
async with AsyncSession(self.engine) as session:
statement = select(FaceImageModel)
if user_id:
statement = statement.where(FaceImageModel.user_id == user_id)
if deleted:
if deleted is TypeEnum.DO:
statement = statement.where(
col(FaceImageModel.delete_time).isnot(None)
)
elif deleted is TypeEnum.UNDO:
statement = statement.where(
col(FaceImageModel.delete_time).is_(None)
)
if approved:
if approved is TypeEnum.DO:
statement = statement.where(FaceImageModel.is_approved == True)
elif approved is TypeEnum.UNDO:
statement = statement.where(FaceImageModel.is_approved == False)
if start_time:
statement = statement.where(FaceImageModel.create_time >= start_time)
if end_time:
statement = statement.where(FaceImageModel.create_time <= end_time)
all_count = await self.get_count(session, statement)
if sort_type:
if sort_type.CREATE_TIME:
statement = statement.order_by(FaceImageModel.create_time)
elif sort_type.USER_ID:
statement = statement.order_by(FaceImageModel.user_id)
elif sort_type.APPROVED:
statement = statement.order_by(FaceImageModel.is_approved)
offset = (page_no - 1) * page_size
statement = statement.offset(offset).limit(page_size)
r = await session.exec(statement)
return r.all(), all_count

View File

@ -1,4 +1,6 @@
from typing import List from enum import Enum
from datetime import date, datetime
from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
@ -17,3 +19,43 @@ class FaceImageDeleteIds(BaseModel):
class FaceImageUpdate(FaceImageCreate, FaceImageDelete): class FaceImageUpdate(FaceImageCreate, FaceImageDelete):
pass pass
class TypeEnum(int, Enum):
ALL = 0
DO = 1
UNDO = 2
class SortTypeEnum(int, Enum):
CREATE_TIME = 1
USER_ID = 2
APPROVED = 3
class FaceList(BaseModel):
user_id: Optional[int] = None
deleted: TypeEnum = TypeEnum.ALL
approved: TypeEnum = TypeEnum.ALL
sortType: SortTypeEnum = SortTypeEnum.CREATE_TIME
start_time: Optional[str] = ""
end_time: Optional[str] = ""
pageNo: int = 1
pageSize: int = 10
@property
def start(self):
if not self.start_time:
return None
y, m, d = self.start_time.split("-")
start_time = date(int(y), int(m), int(d))
return datetime.combine(start_time, datetime.min.time())
@property
def end(self):
if not self.end_time:
return None
y, m, d = self.end_time.split("-")
end_time = date(int(y), int(m), int(d))
return datetime.combine(end_time, datetime.max.time())

View File

@ -5,6 +5,7 @@ from typing_extensions import Optional
from .models import FaceImageModel from .models import FaceImageModel
from .repositories import FaceImageRepo from .repositories import FaceImageRepo
from .schemas import SortTypeEnum, TypeEnum
class FaceImageServices(AsyncInitializingComponent): class FaceImageServices(AsyncInitializingComponent):
@ -53,3 +54,25 @@ class FaceImageServices(AsyncInitializingComponent):
image = await self.repo.get_by(rid) image = await self.repo.get_by(rid)
image.is_approved = True image.is_approved = True
return await self.repo.add_face_image(image) return await self.repo.add_face_image(image)
async def get_image_list(
self,
user_id: Optional[int],
deleted: Optional[TypeEnum],
approved: Optional[TypeEnum],
sort_type: Optional[SortTypeEnum],
start_time: Optional[datetime],
end_time: Optional[datetime],
page_no: int,
page_size: int,
):
return await self.repo.get_image_list(
user_id=user_id,
deleted=deleted,
approved=approved,
sort_type=sort_type,
start_time=start_time,
end_time=end_time,
page_no=page_no,
page_size=page_size,
)