v1.0.2发布,支持视频分析结果查看
This commit is contained in:
@@ -10,7 +10,7 @@ val postgres_version: String by project
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "2.1.0"
|
kotlin("jvm") version "2.1.0"
|
||||||
id("io.ktor.plugin") version "3.1.0"
|
id("io.ktor.plugin") version "3.1.3"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +80,11 @@ dependencies {
|
|||||||
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||||
implementation("org.jetbrains.exposed:exposed-json:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-json:$exposed_version")
|
||||||
|
implementation ("org.jetbrains.exposed:exposed-kotlin-datetime:${exposed_version}")
|
||||||
// MySQL JDBC
|
// MySQL JDBC
|
||||||
// implementation("mysql:mysql-connector-java:8.0.33")
|
// implementation("mysql:mysql-connector-java:8.0.33")
|
||||||
// PostgreSQL JDBC
|
// PostgreSQL JDBC
|
||||||
implementation("org.postgresql:postgresql:$postgres_version")
|
implementation("org.postgresql:postgresql:$postgres_version")
|
||||||
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposed_version")
|
|
||||||
|
|
||||||
implementation("ch.qos.logback:logback-classic:$logback_version")
|
implementation("ch.qos.logback:logback-classic:$logback_version")
|
||||||
// Radis
|
// Radis
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ ktor_version=3.0.2
|
|||||||
kotlin_version=2.1.0
|
kotlin_version=2.1.0
|
||||||
logback_version=1.4.14
|
logback_version=1.4.14
|
||||||
kotlinx_html_version=0.10.1
|
kotlinx_html_version=0.10.1
|
||||||
exposed_version=0.57.0
|
exposed_version=1.0.0-beta-2
|
||||||
h2_version=2.1.214
|
h2_version=2.1.214
|
||||||
postgres_version=42.7.4
|
postgres_version=42.7.4
|
||||||
@@ -2,13 +2,13 @@ package ink.snowflake.server
|
|||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import ink.snowflake.server.plugins.*
|
import ink.snowflake.server.plugins.*
|
||||||
import ink.snowflake.server.route.func.User
|
import ink.snowflake.server.route.User
|
||||||
import ink.snowflake.server.route.func.chat
|
import ink.snowflake.server.route.chat
|
||||||
import ink.snowflake.server.route.configureSockets
|
import ink.snowflake.server.plugins.configureSockets
|
||||||
import ink.snowflake.server.route.func.ImageAnalytics
|
import ink.snowflake.server.route.ImageAnalytics
|
||||||
import ink.snowflake.server.route.func.RemoteDebug
|
import ink.snowflake.server.route.RemoteDebug
|
||||||
import ink.snowflake.server.route.func.VideoAnalytics
|
import ink.snowflake.server.route.VideoAnalytics
|
||||||
import ink.snowflake.server.route.func.VideoAnalyticsJetson
|
import ink.snowflake.server.route.VideoAnalyticsJetson
|
||||||
import ink.snowflake.server.route.mainFunc
|
import ink.snowflake.server.route.mainFunc
|
||||||
import ink.snowflake.server.utils.AppConfig
|
import ink.snowflake.server.utils.AppConfig
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
@@ -19,9 +19,14 @@ const val VIDEO_INPUT_PATH = "/tmp/"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务器地址
|
* 服务器地址
|
||||||
|
* ADB 秦朗FRP地址
|
||||||
*/
|
*/
|
||||||
const val SERVER_PATH = "171.212.101.201"
|
const val SERVER_PATH_FRP = "171.212.101.201"
|
||||||
//const val SERVER_PATH = "localhost"
|
/**
|
||||||
|
* 服务器地址
|
||||||
|
* OSS 对象存储服务器地址
|
||||||
|
*/
|
||||||
|
const val SERVER_PATH_OSS = "171.212.101.199"
|
||||||
|
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ink.snowflake.server.database
|
||||||
|
|
||||||
|
import ink.snowflake.server.database.table.AIProfile
|
||||||
|
import ink.snowflake.server.database.table.AIProfilesTable
|
||||||
|
import ink.snowflake.server.database.table.toAIProfile
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
|
||||||
|
object AIDao {
|
||||||
|
/**
|
||||||
|
* 获取所有AI
|
||||||
|
*/
|
||||||
|
fun getAllAIProfiles(): List<AIProfile> {
|
||||||
|
return transaction {
|
||||||
|
AIProfilesTable.selectAll().map { it.toAIProfile() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+9
-7
@@ -1,12 +1,14 @@
|
|||||||
package ink.snowflake.server.utils.database
|
package ink.snowflake.server.database
|
||||||
|
|
||||||
import ink.snowflake.server.model.database.*
|
import ink.snowflake.server.database.table.ChatRecord
|
||||||
|
import ink.snowflake.server.database.table.ChatRecordsTable
|
||||||
|
import ink.snowflake.server.database.table.toChatRecord
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.v1.datetime.timestampLiteral
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestampLiteral
|
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
|
||||||
object ChatRecordsDao {
|
object ChatRecordsDao {
|
||||||
/**
|
/**
|
||||||
+9
-10
@@ -1,15 +1,14 @@
|
|||||||
package ink.snowflake.server.utils.database
|
package ink.snowflake.server.database
|
||||||
|
|
||||||
import ink.snowflake.server.SERVER_PATH
|
import ink.snowflake.server.SERVER_PATH_OSS
|
||||||
import ink.snowflake.server.model.database.ImageAnalyticsRequest
|
import ink.snowflake.server.model.request.ImageAnalyticsRequest
|
||||||
import ink.snowflake.server.model.database.ImageTable
|
import ink.snowflake.server.database.table.ImageTable
|
||||||
import ink.snowflake.server.model.response.VideoListResponse
|
|
||||||
import ink.snowflake.server.utils.formatLocalDateTimeToString
|
import ink.snowflake.server.utils.formatLocalDateTimeToString
|
||||||
import kotlinx.datetime.toKotlinLocalDateTime
|
import kotlinx.datetime.toKotlinLocalDateTime
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import java.sql.Timestamp
|
import java.sql.Timestamp
|
||||||
|
|
||||||
object ImageDao {
|
object ImageDao {
|
||||||
@@ -41,7 +40,7 @@ object ImageDao {
|
|||||||
.orderBy(ImageTable.upload_datetime, SortOrder.DESC)
|
.orderBy(ImageTable.upload_datetime, SortOrder.DESC)
|
||||||
.map {
|
.map {
|
||||||
ImageAnalyticsRequest(
|
ImageAnalyticsRequest(
|
||||||
object_name = "http://${SERVER_PATH}:9000/image/" + it[ImageTable.object_name],
|
object_name = "http://${SERVER_PATH_OSS}:9000/image/" + it[ImageTable.object_name],
|
||||||
upload_datetime = formatLocalDateTimeToString(it[ImageTable.upload_datetime]),
|
upload_datetime = formatLocalDateTimeToString(it[ImageTable.upload_datetime]),
|
||||||
file_name = it[ImageTable.file_name],
|
file_name = it[ImageTable.file_name],
|
||||||
resolution = it[ImageTable.resolution],
|
resolution = it[ImageTable.resolution],
|
||||||
+10
-13
@@ -1,18 +1,14 @@
|
|||||||
package ink.snowflake.server.utils.database
|
package ink.snowflake.server.database
|
||||||
|
|
||||||
import ink.snowflake.server.model.database.ImageTable
|
import ink.snowflake.server.database.table.UserTable
|
||||||
import ink.snowflake.server.model.database.UserTable
|
|
||||||
import ink.snowflake.server.model.response.UserInfo
|
import ink.snowflake.server.model.response.UserInfo
|
||||||
import ink.snowflake.server.utils.formatLocalDateTimeToString
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import org.jetbrains.exposed.sql.Op
|
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder
|
import org.jetbrains.exposed.v1.datetime.timestampLiteral
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestampLiteral
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
object UserDAO {
|
object UserDAO {
|
||||||
/**
|
/**
|
||||||
@@ -82,7 +78,8 @@ object UserDAO {
|
|||||||
UserInfo(
|
UserInfo(
|
||||||
username = it[UserTable.username],
|
username = it[UserTable.username],
|
||||||
email = it[UserTable.email],
|
email = it[UserTable.email],
|
||||||
phone = it[UserTable.phone]
|
phone = it[UserTable.phone],
|
||||||
|
roles = it[UserTable.roles]?.removePrefix("{")!!.removeSuffix("}").split(","),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
+22
-17
@@ -1,20 +1,20 @@
|
|||||||
package ink.snowflake.server.utils.database
|
package ink.snowflake.server.database
|
||||||
|
|
||||||
import ink.snowflake.server.model.database.VideoAnalyticsDetailTable
|
import ink.snowflake.server.database.table.VideoAnalyticsDetailTable
|
||||||
import ink.snowflake.server.model.database.VideoTable
|
import ink.snowflake.server.database.table.VideoTable
|
||||||
import ink.snowflake.server.model.database.VideoTable.vATime
|
import ink.snowflake.server.database.table.VideoTable.vATime
|
||||||
import ink.snowflake.server.model.database.VideoTable.vName
|
import ink.snowflake.server.database.table.VideoTable.vName
|
||||||
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
||||||
import ink.snowflake.server.model.request.VideoDetail
|
import ink.snowflake.server.model.request.VideoDetail
|
||||||
import ink.snowflake.server.model.response.VideoListResponse
|
import ink.snowflake.server.model.response.VideoListResponse
|
||||||
import ink.snowflake.server.utils.formatLocalDateTimeToString
|
import ink.snowflake.server.utils.formatLocalDateTimeToString
|
||||||
import kotlinx.datetime.toKotlinLocalDateTime
|
import kotlinx.datetime.toKotlinLocalDateTime
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import java.sql.Timestamp
|
import java.sql.Timestamp
|
||||||
|
|
||||||
object VideoDao {
|
object VideoDao {
|
||||||
@@ -54,27 +54,32 @@ object VideoDao {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVideoList():List<VideoListResponse> {
|
fun getVideoList(name: String?): List<VideoListResponse> {
|
||||||
return transaction {
|
return transaction {
|
||||||
VideoTable.selectAll()
|
val query = VideoTable.selectAll()
|
||||||
.orderBy(vATime, SortOrder.DESC)
|
// 动态加 where 条件
|
||||||
|
if (!name.isNullOrBlank()) {
|
||||||
|
query.where { VideoTable.vName like "%${name}%" }
|
||||||
|
}
|
||||||
|
query.orderBy(VideoTable.vATime, SortOrder.DESC)
|
||||||
.map {
|
.map {
|
||||||
VideoListResponse(
|
VideoListResponse(
|
||||||
v_id = it[VideoTable.id].value,
|
v_id = it[VideoTable.id].value,
|
||||||
v_name = it[vName],
|
v_name = it[VideoTable.vName],
|
||||||
v_a_time = formatLocalDateTimeToString(it[vATime])
|
v_a_time = formatLocalDateTimeToString(it[VideoTable.vATime])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? {
|
fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? {
|
||||||
return transaction {
|
return transaction {
|
||||||
VideoTable.selectAll().where { VideoTable.id eq vId }.singleOrNull()
|
VideoTable.selectAll().where { VideoTable.id eq vId }.singleOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectVideoDetailByVid(vId:Int): List<VideoDetail>{
|
fun selectVideoDetailByVid(vId: Int): List<VideoDetail> {
|
||||||
return transaction {
|
return transaction {
|
||||||
VideoAnalyticsDetailTable
|
VideoAnalyticsDetailTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
package ink.snowflake.server.model.database
|
package ink.snowflake.server.database.table
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
|
||||||
import org.jetbrains.exposed.sql.*
|
|
||||||
import org.jetbrains.exposed.sql.json.json
|
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import ink.snowflake.server.gson
|
import ink.snowflake.server.gson
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
|
import org.jetbrains.exposed.v1.json.json
|
||||||
|
|
||||||
// 定义 AI 表
|
// 定义 AI 表
|
||||||
object AIProfilesTable : IntIdTable("ai_profiles") {
|
object AIProfilesTable : IntIdTable("ai_profiles") {
|
||||||
+5
-6
@@ -1,10 +1,9 @@
|
|||||||
package ink.snowflake.server.model.database
|
package ink.snowflake.server.database.table
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
import org.jetbrains.exposed.v1.core.Column
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
import org.jetbrains.exposed.sql.Column
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
|
||||||
|
|
||||||
object ChatRecordsTable : IntIdTable("chat_records") {
|
object ChatRecordsTable : IntIdTable("chat_records") {
|
||||||
// 用户 ID(外键)
|
// 用户 ID(外键)
|
||||||
+4
-5
@@ -1,11 +1,10 @@
|
|||||||
package ink.snowflake.server.model.database
|
package ink.snowflake.server.database.table
|
||||||
|
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import ink.snowflake.server.gson
|
import ink.snowflake.server.gson
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.datetime.datetime
|
||||||
import org.jetbrains.exposed.sql.json.json
|
import org.jetbrains.exposed.v1.json.json
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
|
||||||
|
|
||||||
object ImageTable : IntIdTable("image") {
|
object ImageTable : IntIdTable("image") {
|
||||||
val object_name = varchar("object_name", 255)
|
val object_name = varchar("object_name", 255)
|
||||||
+6
-5
@@ -1,9 +1,7 @@
|
|||||||
package ink.snowflake.server.model.database
|
package ink.snowflake.server.database.table
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
|
||||||
import org.jetbrains.exposed.sql.Column
|
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
|
|
||||||
object UserTable : IntIdTable("users") {
|
object UserTable : IntIdTable("users") {
|
||||||
// 用户名
|
// 用户名
|
||||||
@@ -26,4 +24,7 @@ object UserTable : IntIdTable("users") {
|
|||||||
|
|
||||||
// 更新时间
|
// 更新时间
|
||||||
val updatedAt = timestamp("updated_at").nullable()
|
val updatedAt = timestamp("updated_at").nullable()
|
||||||
|
|
||||||
|
// 角色
|
||||||
|
val roles = text("roles").nullable()
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package ink.snowflake.server.model.database
|
package ink.snowflake.server.database.table
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
|
|
||||||
|
|
||||||
object VideoAnalyticsDetailTable : IntIdTable("video_analytics_detail") {
|
object VideoAnalyticsDetailTable : IntIdTable("video_analytics_detail") {
|
||||||
+3
-4
@@ -1,8 +1,7 @@
|
|||||||
package ink.snowflake.server.model.database
|
package ink.snowflake.server.database.table
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.datetime.datetime
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
|
||||||
|
|
||||||
|
|
||||||
object VideoTable : IntIdTable("video") {
|
object VideoTable : IntIdTable("video") {
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ink.snowflake.server.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.*
|
||||||
|
import kotlinx.serialization.encoding.*
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object UUIDSerializer : KSerializer<UUID> {
|
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: UUID) {
|
||||||
|
encoder.encodeString(value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): UUID {
|
||||||
|
return UUID.fromString(decoder.decodeString())
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-3
@@ -1,4 +1,4 @@
|
|||||||
package ink.snowflake.server.model.ai
|
package ink.snowflake.server.model.request
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
@@ -17,5 +17,3 @@ data class ChatRequest(
|
|||||||
val role: String = "user"
|
val role: String = "user"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ink.snowflake.server.model.database
|
package ink.snowflake.server.model.request
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-3
@@ -1,4 +1,4 @@
|
|||||||
package ink.snowflake.server.model.ai
|
package ink.snowflake.server.model.response
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
@@ -33,5 +33,3 @@ data class ChatResponse(
|
|||||||
val role: String = ""
|
val role: String = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class VideoAnalyticsDetail(
|
data class VideoAnalyticsDetail(
|
||||||
|
val v_id: Int,
|
||||||
val v_name: String,
|
val v_name: String,
|
||||||
val v_video_play_path: String,
|
val v_video_play_path: String,
|
||||||
val v_file_name: String,
|
val v_file_name: String,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package ink.snowflake.server.plugins
|
package ink.snowflake.server.plugins
|
||||||
|
|
||||||
import ink.snowflake.server.utils.AppConfig
|
import ink.snowflake.server.utils.AppConfig
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.v1.jdbc.Database
|
||||||
|
|
||||||
fun configureDatabases(config: AppConfig) {
|
fun configureDatabases(config: AppConfig) {
|
||||||
Database.connect(
|
Database.connect(
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import io.ktor.server.auth.*
|
|||||||
import io.ktor.server.http.content.*
|
import io.ktor.server.http.content.*
|
||||||
import io.ktor.server.plugins.autohead.AutoHeadResponse
|
import io.ktor.server.plugins.autohead.AutoHeadResponse
|
||||||
import io.ktor.server.plugins.partialcontent.PartialContent
|
import io.ktor.server.plugins.partialcontent.PartialContent
|
||||||
import io.ktor.server.routing.*
|
|
||||||
import org.jetbrains.exposed.sql.*
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun Application.configureStaticPath() {
|
fun Application.configureStaticPath() {
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ fun Application.configureStatusPages() {
|
|||||||
status(HttpStatusCode.InternalServerError) { call, status ->
|
status(HttpStatusCode.InternalServerError) { call, status ->
|
||||||
val response = BaseResponse<Nothing>(
|
val response = BaseResponse<Nothing>(
|
||||||
status = false,
|
status = false,
|
||||||
message = "${status.value} Internal Server Error," + status.description
|
// message = "${status.value} Internal Server Error" + status.description
|
||||||
|
message = "${status.value} Internal Server Error"
|
||||||
)
|
)
|
||||||
call.respond(status, response)
|
call.respond(status, response)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ink.snowflake.server.route
|
package ink.snowflake.server.plugins
|
||||||
|
|
||||||
import io.ktor.serialization.gson.*
|
import io.ktor.serialization.gson.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package ink.snowflake.server.repository
|
|
||||||
|
|
||||||
import ink.snowflake.server.model.database.ImageAnalyticsRequest
|
|
||||||
import ink.snowflake.server.model.database.VideoTable
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vAAverageMaskedRatio
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vACountPeople
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vAMaxAction
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vAMaxStayTime
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vATotalPeople
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vStartDateTime
|
|
||||||
import ink.snowflake.server.model.response.Area
|
|
||||||
import ink.snowflake.server.model.response.DetailItem
|
|
||||||
import ink.snowflake.server.model.response.ItemStyle
|
|
||||||
import ink.snowflake.server.model.response.VideoAnalyticsData
|
|
||||||
import ink.snowflake.server.model.response.VideoAnalyticsDetail
|
|
||||||
import ink.snowflake.server.model.response.VideoListResponse
|
|
||||||
import ink.snowflake.server.utils.database.ImageDao
|
|
||||||
import ink.snowflake.server.utils.database.VideoDao
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
|
||||||
import kotlinx.datetime.toJavaLocalDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
class ImageDataBase {
|
|
||||||
|
|
||||||
fun saveImageAnalyticsData(request: ImageAnalyticsRequest) {
|
|
||||||
return ImageDao.insertImageAnalyticsData(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getImageList(): List<ImageAnalyticsRequest> {
|
|
||||||
return ImageDao.getVideoList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package ink.snowflake.server.repository
|
|
||||||
|
|
||||||
import ink.snowflake.server.utils.database.UserDAO
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import kotlin.text.Charsets.UTF_8
|
|
||||||
|
|
||||||
class UserDataBase {
|
|
||||||
|
|
||||||
fun login(email: String, password: String): Int {
|
|
||||||
// 查找用户
|
|
||||||
val userId = UserDAO.getUserIdByEmail(email)
|
|
||||||
return if(userId == null){
|
|
||||||
// 账号不存在
|
|
||||||
-1
|
|
||||||
}else{
|
|
||||||
val userPassword = UserDAO.getPasswordById(userId)
|
|
||||||
// 验证密码
|
|
||||||
if (password == userPassword) {
|
|
||||||
// 登录成功
|
|
||||||
userId
|
|
||||||
} else {
|
|
||||||
// 账号密码不匹配
|
|
||||||
-2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 正数:userId 负数:错误码
|
|
||||||
*/
|
|
||||||
fun registerByEmail(email: String, password: String): Int {
|
|
||||||
// 查找用户
|
|
||||||
val userId = UserDAO.getUserIdByEmail(email)
|
|
||||||
return if(userId != null){
|
|
||||||
// 如果用户已存在,返回 0
|
|
||||||
0
|
|
||||||
}else{
|
|
||||||
// 用户不存在,插入新用户
|
|
||||||
UserDAO.registerByEmailAndGetId(email, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun hashPassword(password: String): String {
|
|
||||||
val bytes = MessageDigest.getInstance("SHA-256").digest(password.toByteArray(UTF_8))
|
|
||||||
return bytes.joinToString("") { "%02x".format(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
package ink.snowflake.server.repository
|
|
||||||
|
|
||||||
import ink.snowflake.server.SERVER_PATH
|
|
||||||
import ink.snowflake.server.model.database.VideoTable
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vAAverageMaskedRatio
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vACountPeople
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vAMaxAction
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vAMaxStayTime
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vATotalPeople
|
|
||||||
import ink.snowflake.server.model.database.VideoTable.vStartDateTime
|
|
||||||
import ink.snowflake.server.model.response.Area
|
|
||||||
import ink.snowflake.server.model.response.DetailItem
|
|
||||||
import ink.snowflake.server.model.response.ItemStyle
|
|
||||||
import ink.snowflake.server.model.response.VideoAnalyticsData
|
|
||||||
import ink.snowflake.server.model.response.VideoAnalyticsDetail
|
|
||||||
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
|
||||||
import ink.snowflake.server.model.response.VideoListResponse
|
|
||||||
import ink.snowflake.server.utils.database.VideoDao
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
|
||||||
import kotlinx.datetime.toJavaLocalDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
class VideoDataBase {
|
|
||||||
|
|
||||||
fun saveVideoAnalyticsData(request: VideoAnalyticsRequest) {
|
|
||||||
return VideoDao.insertVideoAnalyticsData(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getVideoList(): List<VideoListResponse> {
|
|
||||||
return VideoDao.getVideoList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAnalyticsDetailByVideoId(vId: Int): VideoAnalyticsDetail? {
|
|
||||||
// 查询视频信息
|
|
||||||
val video = VideoDao.getAnalyticsDetailByVideoId(vId)
|
|
||||||
if (video == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
// 查询相关的分析详情
|
|
||||||
val details = VideoDao.selectVideoDetailByVid(vId)
|
|
||||||
|
|
||||||
// 用于返回的数据
|
|
||||||
val yTotalData = mutableListOf<Pair<String, Int>>() // (时间, 总人数)
|
|
||||||
val yMaskedData = mutableListOf<Pair<String, Int>>() // (时间, 佩戴口罩人数)
|
|
||||||
val areaData = mutableListOf<List<Area>>()
|
|
||||||
val detailList = mutableListOf<DetailItem>()
|
|
||||||
// 颜色映射
|
|
||||||
val colors = mapOf(
|
|
||||||
"feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色
|
|
||||||
"disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色
|
|
||||||
"other" to "rgba(255, 0, 0, 0.4)" // 淡红色
|
|
||||||
)
|
|
||||||
// 生成 yTotalData 和 yMaskedData,同时计算 areaData
|
|
||||||
var lastAction: String? = null
|
|
||||||
var areaStartTime: String? = null
|
|
||||||
var currentColor: String? = null
|
|
||||||
|
|
||||||
for (detail in details) {
|
|
||||||
// 获取视频开始时间
|
|
||||||
val vStartDatetime: LocalDateTime = video[vStartDateTime]!!
|
|
||||||
val timeStr = vStartDatetime.toJavaLocalDateTime()
|
|
||||||
.plusSeconds(detail.a_time_stamp.toLong())
|
|
||||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
|
||||||
|
|
||||||
// 添加总人数和口罩佩戴人数
|
|
||||||
yTotalData.add(timeStr to detail.a_total_people)
|
|
||||||
yMaskedData.add(timeStr to detail.a_total_people_masked)
|
|
||||||
|
|
||||||
// 处理 areaData,根据 a_action 判断是否需要创建新的区域
|
|
||||||
if (detail.a_action != "--") { // 只处理非 "--" 动作
|
|
||||||
if (lastAction == null || lastAction != detail.a_action) {
|
|
||||||
// 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域
|
|
||||||
if (areaStartTime != null) {
|
|
||||||
areaData.add(
|
|
||||||
listOf(
|
|
||||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
|
||||||
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 添加到 detailList,记录动作开始的时刻
|
|
||||||
detailList.add(
|
|
||||||
DetailItem(getFriendlyActionName(detail.a_action), time = timeStr)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 更新为新的动作
|
|
||||||
lastAction = detail.a_action
|
|
||||||
areaStartTime = timeStr
|
|
||||||
currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果当前动作为 "--",则结束当前区域,并重置状态
|
|
||||||
if (areaStartTime != null) {
|
|
||||||
// 添加到 detailList,记录动作开始的时刻
|
|
||||||
detailList.add(
|
|
||||||
DetailItem(getFriendlyActionName(detail.a_action), timeStr)
|
|
||||||
)
|
|
||||||
// 结束当前区域
|
|
||||||
areaData.add(
|
|
||||||
listOf(
|
|
||||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
|
||||||
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// 重置状态
|
|
||||||
lastAction = null
|
|
||||||
areaStartTime = null
|
|
||||||
currentColor = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 处理最后一个区域的结束时间
|
|
||||||
if (areaStartTime != null && lastAction != null) {
|
|
||||||
areaData.add(
|
|
||||||
listOf(
|
|
||||||
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
|
||||||
Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val analyticsData = VideoAnalyticsData(
|
|
||||||
yTotalData = yTotalData,
|
|
||||||
yMaskedData = yMaskedData,
|
|
||||||
areaData = areaData
|
|
||||||
)
|
|
||||||
// 返回数据
|
|
||||||
return VideoAnalyticsDetail(
|
|
||||||
v_name = video[VideoTable.vName],
|
|
||||||
v_video_play_path = "http://${SERVER_PATH}:9000/video/" + video[VideoTable.vObjectName],
|
|
||||||
v_file_name = video[VideoTable.vFileName],
|
|
||||||
v_duration = video[VideoTable.vDuration],
|
|
||||||
v_size = video[VideoTable.vSize],
|
|
||||||
v_start_datetime = video[VideoTable.vStartDateTime]?.toString() ?: "",
|
|
||||||
v_video_codec = video[VideoTable.vVideoCodec],
|
|
||||||
v_audio_codec = video[VideoTable.vAudioCodec],
|
|
||||||
v_overall_bit_rate = video[VideoTable.vOverallBitRate],
|
|
||||||
v_resolution = video[VideoTable.vResolution],
|
|
||||||
v_a_time = video[VideoTable.vATime].toString(),
|
|
||||||
v_a_total_people = video[vATotalPeople], // 总人数
|
|
||||||
v_a_count_people = video[vACountPeople], // 佩戴口罩人数
|
|
||||||
v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间
|
|
||||||
v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作
|
|
||||||
v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例
|
|
||||||
v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData
|
|
||||||
v_details_list = detailList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFriendlyActionName(name: String): String {
|
|
||||||
return if (name == "feed") {
|
|
||||||
"喂桑"
|
|
||||||
} else if (name == "disinfection") {
|
|
||||||
"消毒"
|
|
||||||
} else {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
package ink.snowflake.server.route.func
|
package ink.snowflake.server.route
|
||||||
|
|
||||||
import com.cyberecho.mdoel.database.WSChatRecords
|
import com.cyberecho.mdoel.database.WSChatRecords
|
||||||
import ink.snowflake.server.gson
|
import ink.snowflake.server.gson
|
||||||
import ink.snowflake.server.model.ai.ChatRequest
|
import ink.snowflake.server.model.request.ChatRequest
|
||||||
import ink.snowflake.server.model.ai.ChatResponse
|
import ink.snowflake.server.model.response.ChatResponse
|
||||||
import ink.snowflake.server.model.response.BaseResponse
|
import ink.snowflake.server.model.response.BaseResponse
|
||||||
import ink.snowflake.server.utils.WebSocketManager
|
import ink.snowflake.server.utils.WebSocketManager
|
||||||
import ink.snowflake.server.utils.database.ChatRecordsDao
|
import ink.snowflake.server.database.ChatRecordsDao
|
||||||
import ink.snowflake.server.utils.getUserIdByToken
|
import ink.snowflake.server.utils.getUserIdByToken
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
+5
-8
@@ -1,24 +1,21 @@
|
|||||||
package ink.snowflake.server.route.func
|
package ink.snowflake.server.route
|
||||||
|
|
||||||
import ink.snowflake.server.model.database.ImageAnalyticsRequest
|
import ink.snowflake.server.model.request.ImageAnalyticsRequest
|
||||||
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
|
||||||
import ink.snowflake.server.model.response.*
|
import ink.snowflake.server.model.response.*
|
||||||
import ink.snowflake.server.repository.ImageDataBase
|
import ink.snowflake.server.database.ImageDao
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.websocket.*
|
|
||||||
|
|
||||||
fun Application.ImageAnalytics() {
|
fun Application.ImageAnalytics() {
|
||||||
val repository = ImageDataBase()
|
|
||||||
routing {
|
routing {
|
||||||
route("/api") {
|
route("/api") {
|
||||||
// 上传分析结果
|
// 上传分析结果
|
||||||
post("/saveImageAnalyticsData") {
|
post("/saveImageAnalyticsData") {
|
||||||
val request = call.receive<ImageAnalyticsRequest>()
|
val request = call.receive<ImageAnalyticsRequest>()
|
||||||
call.respond(BaseResponse(data = repository.saveImageAnalyticsData(request)))
|
call.respond(BaseResponse(data = ImageDao.insertImageAnalyticsData(request)))
|
||||||
}
|
}
|
||||||
authenticate {
|
authenticate {
|
||||||
// 拍照保存为图片 并且调用Python程序进行分析
|
// 拍照保存为图片 并且调用Python程序进行分析
|
||||||
@@ -33,7 +30,7 @@ fun Application.ImageAnalytics() {
|
|||||||
}
|
}
|
||||||
// 获取已分析视频列表
|
// 获取已分析视频列表
|
||||||
get("/getImageList") {
|
get("/getImageList") {
|
||||||
val res = repository.getImageList()
|
val res = ImageDao.getVideoList()
|
||||||
call.respond(BaseResponse(data = res))
|
call.respond(BaseResponse(data = res))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-6
@@ -1,7 +1,7 @@
|
|||||||
package ink.snowflake.server.route.func
|
package ink.snowflake.server.route
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import ink.snowflake.server.SERVER_PATH
|
import ink.snowflake.server.SERVER_PATH_FRP
|
||||||
import ink.snowflake.server.model.request.DevicesInfo
|
import ink.snowflake.server.model.request.DevicesInfo
|
||||||
import ink.snowflake.server.model.response.*
|
import ink.snowflake.server.model.response.*
|
||||||
import ink.snowflake.server.utils.runCommand
|
import ink.snowflake.server.utils.runCommand
|
||||||
@@ -13,14 +13,11 @@ import io.ktor.client.plugins.auth.providers.basic
|
|||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.content.*
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.authenticate
|
import io.ktor.server.auth.authenticate
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import io.ktor.server.request.*
|
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.websocket.*
|
import io.ktor.server.websocket.*
|
||||||
import io.ktor.utils.io.*
|
|
||||||
import io.ktor.websocket.*
|
import io.ktor.websocket.*
|
||||||
import io.ktor.websocket.send
|
import io.ktor.websocket.send
|
||||||
import kotlinx.coroutines.channels.consumeEach
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
@@ -59,7 +56,7 @@ fun Application.RemoteDebug() {
|
|||||||
get("/connectLocalDevice") {
|
get("/connectLocalDevice") {
|
||||||
val port = call.parameters["port"]
|
val port = call.parameters["port"]
|
||||||
if (port != null) {
|
if (port != null) {
|
||||||
val result = runAdbCommand("connect ${SERVER_PATH}:$port")
|
val result = runAdbCommand("connect ${SERVER_PATH_FRP}:$port")
|
||||||
call.respond(BaseResponse(data = result))
|
call.respond(BaseResponse(data = result))
|
||||||
} else {
|
} else {
|
||||||
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
|
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
|
||||||
+45
-19
@@ -1,16 +1,14 @@
|
|||||||
package ink.snowflake.server.route.func
|
package ink.snowflake.server.route
|
||||||
|
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
import ink.snowflake.server.utils.database.AIDao
|
|
||||||
import ink.snowflake.server.model.request.CommonRequest
|
import ink.snowflake.server.model.request.CommonRequest
|
||||||
import ink.snowflake.server.model.request.LoginRequest
|
import ink.snowflake.server.model.request.LoginRequest
|
||||||
import ink.snowflake.server.model.request.RefreshTokenRequest
|
import ink.snowflake.server.model.request.RefreshTokenRequest
|
||||||
import ink.snowflake.server.model.request.RegisterRequest
|
import ink.snowflake.server.model.request.RegisterRequest
|
||||||
import ink.snowflake.server.model.response.*
|
import ink.snowflake.server.model.response.*
|
||||||
import ink.snowflake.server.repository.UserDataBase
|
|
||||||
import ink.snowflake.server.utils.AppConfig
|
import ink.snowflake.server.utils.AppConfig
|
||||||
import ink.snowflake.server.utils.database.UserDAO
|
import ink.snowflake.server.database.UserDAO
|
||||||
import ink.snowflake.server.utils.getUserIdByToken
|
import ink.snowflake.server.utils.getUserIdByToken
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
@@ -22,12 +20,15 @@ import org.redisson.Redisson
|
|||||||
import org.redisson.api.RBucket
|
import org.redisson.api.RBucket
|
||||||
import org.redisson.api.RedissonClient
|
import org.redisson.api.RedissonClient
|
||||||
import org.redisson.config.Config
|
import org.redisson.config.Config
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.mail.*
|
import javax.mail.*
|
||||||
import javax.mail.internet.InternetAddress
|
import javax.mail.internet.InternetAddress
|
||||||
import javax.mail.internet.MimeMessage
|
import javax.mail.internet.MimeMessage
|
||||||
|
import kotlin.collections.joinToString
|
||||||
|
import kotlin.text.Charsets.UTF_8
|
||||||
|
|
||||||
// 配置和初始化 Redis 客户端
|
// 配置和初始化 Redis 客户端
|
||||||
fun setupRedis(): RedissonClient {
|
fun setupRedis(): RedissonClient {
|
||||||
@@ -37,7 +38,6 @@ fun setupRedis(): RedissonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Application.User(config: AppConfig) {
|
fun Application.User(config: AppConfig) {
|
||||||
val repository = UserDataBase()
|
|
||||||
// 初始化 Redis 连接
|
// 初始化 Redis 连接
|
||||||
val redisClient: RedissonClient = setupRedis()
|
val redisClient: RedissonClient = setupRedis()
|
||||||
routing {
|
routing {
|
||||||
@@ -47,21 +47,33 @@ fun Application.User(config: AppConfig) {
|
|||||||
val loginRequest = call.receive<LoginRequest>()
|
val loginRequest = call.receive<LoginRequest>()
|
||||||
val email = loginRequest.account
|
val email = loginRequest.account
|
||||||
val password = loginRequest.password
|
val password = loginRequest.password
|
||||||
val userId = repository.login(email, password)
|
// 查找用户
|
||||||
call.respond(
|
val userId = UserDAO.getUserIdByEmail(email)
|
||||||
if (userId == -1) {
|
val res = if(userId == null){
|
||||||
BaseResponse(status = false, message = "尚未注册", data = null)
|
// 账号不存在
|
||||||
} else if (userId == -2) {
|
-1
|
||||||
BaseResponse(status = false, message = "账号密码不匹配,请重新登录", data = null)
|
}else{
|
||||||
|
val userPassword = UserDAO.getPasswordById(userId)
|
||||||
|
// 验证密码
|
||||||
|
if (password == userPassword) {
|
||||||
|
// 登录成功
|
||||||
|
userId
|
||||||
} else {
|
} else {
|
||||||
BaseResponse(
|
// 账号密码不匹配
|
||||||
|
-2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call.respond(
|
||||||
|
when (res) {
|
||||||
|
-1 -> BaseResponse(status = false, message = "尚未注册", data = null)
|
||||||
|
-2 -> BaseResponse(status = false, message = "账号密码不匹配,请重新登录", data = null)
|
||||||
|
else -> BaseResponse(
|
||||||
status = true, data = LoginResponse(
|
status = true, data = LoginResponse(
|
||||||
userId.toString(),
|
userId.toString(),
|
||||||
generateAccessToken(config, userId),
|
generateAccessToken(config, res),
|
||||||
generateRefreshToken(config, userId)
|
generateRefreshToken(config, res)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// match == -1
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -91,16 +103,24 @@ fun Application.User(config: AppConfig) {
|
|||||||
} else if (storedCode != register.code) {
|
} else if (storedCode != register.code) {
|
||||||
call.respond(BaseResponse(status = false, message = "验证码错误", data = null))
|
call.respond(BaseResponse(status = false, message = "验证码错误", data = null))
|
||||||
} else {
|
} else {
|
||||||
val userId = repository.registerByEmail(register.account, register.password)
|
// 查找用户
|
||||||
if (userId > 0) {
|
val userId = UserDAO.getUserIdByEmail(register.account)
|
||||||
|
val res = if(userId != null){
|
||||||
|
// 如果用户已存在,返回 0
|
||||||
|
0
|
||||||
|
}else{
|
||||||
|
// 用户不存在,插入新用户
|
||||||
|
UserDAO.registerByEmailAndGetId(register.account,register.password)
|
||||||
|
}
|
||||||
|
if (res > 0) {
|
||||||
call.respond(
|
call.respond(
|
||||||
BaseResponse(
|
BaseResponse(
|
||||||
status = true, message = "注册成功", data =
|
status = true, message = "注册成功", data =
|
||||||
BaseResponse(
|
BaseResponse(
|
||||||
status = true, data = LoginResponse(
|
status = true, data = LoginResponse(
|
||||||
account,
|
account,
|
||||||
generateAccessToken(config, userId),
|
generateAccessToken(config, res),
|
||||||
generateRefreshToken(config, userId)
|
generateRefreshToken(config, res)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -254,3 +274,9 @@ fun sendVerificationEmail(config: AppConfig, recipientEmail: String, verificatio
|
|||||||
println("Failed to send verification email.")
|
println("Failed to send verification email.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun hashPassword(password: String): String {
|
||||||
|
val bytes = MessageDigest.getInstance("SHA-256").digest(password.toByteArray(UTF_8))
|
||||||
|
return bytes.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
package ink.snowflake.server.route
|
||||||
|
|
||||||
|
import ink.snowflake.server.SERVER_PATH_OSS
|
||||||
|
import ink.snowflake.server.VIDEO_INPUT_PATH
|
||||||
|
import ink.snowflake.server.database.table.VideoTable
|
||||||
|
import ink.snowflake.server.database.table.VideoTable.vAAverageMaskedRatio
|
||||||
|
import ink.snowflake.server.database.table.VideoTable.vACountPeople
|
||||||
|
import ink.snowflake.server.database.table.VideoTable.vAMaxAction
|
||||||
|
import ink.snowflake.server.database.table.VideoTable.vAMaxStayTime
|
||||||
|
import ink.snowflake.server.database.table.VideoTable.vATotalPeople
|
||||||
|
import ink.snowflake.server.database.table.VideoTable.vStartDateTime
|
||||||
|
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
||||||
|
import ink.snowflake.server.model.response.*
|
||||||
|
import ink.snowflake.server.utils.WebSocketManager.broadcastMessage
|
||||||
|
import ink.snowflake.server.database.VideoDao
|
||||||
|
import ink.snowflake.server.utils.runCommand
|
||||||
|
import io.ktor.http.content.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
|
import io.ktor.websocket.*
|
||||||
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.datetime.toJavaLocalDateTime
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
val clients = Collections.synchronizedList<WebSocketServerSession>(ArrayList()) // 线程安全的客户端列表
|
||||||
|
var aiState = "等待分析任务中"
|
||||||
|
|
||||||
|
fun Application.VideoAnalytics() {
|
||||||
|
routing {
|
||||||
|
// 实时发送AI状态
|
||||||
|
webSocket("/handleState") { // WebSocket 路由
|
||||||
|
clients.add(this) // 添加当前连接的客户端
|
||||||
|
send(aiState) // 向客户端发送连接成功消息
|
||||||
|
try {
|
||||||
|
incoming.consumeEach { frame -> // 持续接收消息
|
||||||
|
when (frame) {
|
||||||
|
is Frame.Text -> {
|
||||||
|
aiState = frame.readText() // 更新状态
|
||||||
|
broadcastMessage(aiState) // 使用封装的方法广播消息
|
||||||
|
}
|
||||||
|
|
||||||
|
is Frame.Close -> {
|
||||||
|
println("Closed")
|
||||||
|
clients.remove(this)
|
||||||
|
close() // 确保关闭 WebSocket 连接
|
||||||
|
return@consumeEach
|
||||||
|
}
|
||||||
|
// 其他消息类型的处理
|
||||||
|
is Frame.Binary -> TODO() // 处理二进制消息
|
||||||
|
is Frame.Ping -> TODO() // 处理 Ping 消息
|
||||||
|
is Frame.Pong -> TODO() // 处理 Pong 消息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 处理接收消息时的异常
|
||||||
|
close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected"))
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
clients.remove(this) // 确保在连接关闭时移除客户端
|
||||||
|
}
|
||||||
|
}
|
||||||
|
route("/api") {
|
||||||
|
route("/iva") {
|
||||||
|
// 上传分析结果
|
||||||
|
post("/saveVideoAnalyticsData") {
|
||||||
|
val request = call.receive<VideoAnalyticsRequest>()
|
||||||
|
// todo 上传这里未做测试
|
||||||
|
call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request)))
|
||||||
|
}
|
||||||
|
authenticate {
|
||||||
|
post("/upload") {
|
||||||
|
val multipart = call.receiveMultipart() //1G
|
||||||
|
// 确保 uploads 目录存在
|
||||||
|
val uploadDir = File(VIDEO_INPUT_PATH)
|
||||||
|
if (!uploadDir.exists()) {
|
||||||
|
if (!uploadDir.mkdirs()) {
|
||||||
|
println("无法创建目录")
|
||||||
|
throw IOException("Failed to create upload directory.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var fileName = ""
|
||||||
|
var name = ""
|
||||||
|
var datetime = ""
|
||||||
|
broadcastMessage("正在上传数据")
|
||||||
|
multipart.forEachPart { part ->
|
||||||
|
when (part) {
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
fileName = part.originalFileName ?: "unknown"
|
||||||
|
val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径
|
||||||
|
//ktor3
|
||||||
|
// file.outputStream().use { outputStream ->
|
||||||
|
// val writableChannel = Channels.newChannel(outputStream)
|
||||||
|
// part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel
|
||||||
|
// }
|
||||||
|
//ktor2
|
||||||
|
part.streamProvider().use { inputStream ->
|
||||||
|
file.outputStream().buffered().use { outputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PartData.FormItem -> {
|
||||||
|
when (part.name) {
|
||||||
|
"name" -> name = part.value
|
||||||
|
"datetime" -> datetime = part.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> part.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call.respond(BaseResponse(message = "上传成功", data = null))
|
||||||
|
broadcastMessage("上传完成,开始启动AI引擎")
|
||||||
|
val command = listOf(
|
||||||
|
"/usr/bin/python3",
|
||||||
|
"/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py",
|
||||||
|
"$VIDEO_INPUT_PATH$fileName",
|
||||||
|
datetime,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
println("-----------------" + command.joinToString(" "))
|
||||||
|
runCommand(command) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 获取已分析视频列表
|
||||||
|
get("/getVideoList") {
|
||||||
|
val name = call.parameters["name"]
|
||||||
|
val res = VideoDao.getVideoList(name)
|
||||||
|
call.respond(BaseResponse(data = res))
|
||||||
|
}
|
||||||
|
// 获取某视频分析详情
|
||||||
|
get("/getAnalyticsDetailByVideoId") {
|
||||||
|
// 获取 vId 参数
|
||||||
|
val vIdParam = call.parameters["vId"]
|
||||||
|
val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全
|
||||||
|
if (vId == null) {
|
||||||
|
call.respond(BaseResponse(status = true, message = "Invalid vId", data = null))
|
||||||
|
return@get
|
||||||
|
}
|
||||||
|
// 查询视频信息
|
||||||
|
val video = VideoDao.getAnalyticsDetailByVideoId(vId)
|
||||||
|
if (video == null) {
|
||||||
|
call.respond(BaseResponse(status = false, message = "不存在该视频", data = null))
|
||||||
|
} else {
|
||||||
|
// 查询相关的分析详情
|
||||||
|
val details = VideoDao.selectVideoDetailByVid(vId)
|
||||||
|
|
||||||
|
// 用于返回的数据
|
||||||
|
val yTotalData = mutableListOf<Pair<String, Int>>() // (时间, 总人数)
|
||||||
|
val yMaskedData = mutableListOf<Pair<String, Int>>() // (时间, 佩戴口罩人数)
|
||||||
|
val areaData = mutableListOf<List<Area>>()
|
||||||
|
val detailList = mutableListOf<DetailItem>()
|
||||||
|
// 颜色映射
|
||||||
|
val colors = mapOf(
|
||||||
|
"feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色
|
||||||
|
"disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色
|
||||||
|
"other" to "rgba(255, 0, 0, 0.4)" // 淡红色
|
||||||
|
)
|
||||||
|
// 生成 yTotalData 和 yMaskedData,同时计算 areaData
|
||||||
|
var lastAction: String? = null
|
||||||
|
var areaStartTime: String? = null
|
||||||
|
var currentColor: String? = null
|
||||||
|
|
||||||
|
for (detail in details) {
|
||||||
|
// 获取视频开始时间
|
||||||
|
val vStartDatetime: LocalDateTime = video[vStartDateTime]!!
|
||||||
|
val timeStr = vStartDatetime.toJavaLocalDateTime()
|
||||||
|
.plusSeconds(detail.a_time_stamp.toLong())
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
||||||
|
|
||||||
|
// 添加总人数和口罩佩戴人数
|
||||||
|
yTotalData.add(timeStr to detail.a_total_people)
|
||||||
|
yMaskedData.add(timeStr to detail.a_total_people_masked)
|
||||||
|
|
||||||
|
// 处理 areaData,根据 a_action 判断是否需要创建新的区域
|
||||||
|
if (detail.a_action != "--") { // 只处理非 "--" 动作
|
||||||
|
if (lastAction == null || lastAction != detail.a_action) {
|
||||||
|
// 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域
|
||||||
|
if (areaStartTime != null) {
|
||||||
|
areaData.add(
|
||||||
|
listOf(
|
||||||
|
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||||
|
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 添加到 detailList,记录动作开始的时刻
|
||||||
|
detailList.add(
|
||||||
|
DetailItem(getFriendlyActionName(detail.a_action), time = timeStr)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 更新为新的动作
|
||||||
|
lastAction = detail.a_action
|
||||||
|
areaStartTime = timeStr
|
||||||
|
currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果当前动作为 "--",则结束当前区域,并重置状态
|
||||||
|
if (areaStartTime != null) {
|
||||||
|
// 添加到 detailList,记录动作开始的时刻
|
||||||
|
detailList.add(
|
||||||
|
DetailItem(getFriendlyActionName(detail.a_action), timeStr)
|
||||||
|
)
|
||||||
|
// 结束当前区域
|
||||||
|
areaData.add(
|
||||||
|
listOf(
|
||||||
|
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||||
|
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// 重置状态
|
||||||
|
lastAction = null
|
||||||
|
areaStartTime = null
|
||||||
|
currentColor = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理最后一个区域的结束时间
|
||||||
|
if (areaStartTime != null && lastAction != null) {
|
||||||
|
areaData.add(
|
||||||
|
listOf(
|
||||||
|
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
|
||||||
|
Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val analyticsData = VideoAnalyticsData(
|
||||||
|
yTotalData = yTotalData,
|
||||||
|
yMaskedData = yMaskedData,
|
||||||
|
areaData = areaData
|
||||||
|
)
|
||||||
|
// 返回数据
|
||||||
|
call.respond(
|
||||||
|
BaseResponse(
|
||||||
|
data = VideoAnalyticsDetail(
|
||||||
|
v_id = vId,
|
||||||
|
v_name = video[VideoTable.vName],
|
||||||
|
v_video_play_path = "http://${SERVER_PATH_OSS}:9000/video/" + video[VideoTable.vObjectName],
|
||||||
|
v_file_name = video[VideoTable.vFileName],
|
||||||
|
v_duration = video[VideoTable.vDuration],
|
||||||
|
v_size = video[VideoTable.vSize],
|
||||||
|
v_start_datetime = video[vStartDateTime]?.toString() ?: "",
|
||||||
|
v_video_codec = video[VideoTable.vVideoCodec],
|
||||||
|
v_audio_codec = video[VideoTable.vAudioCodec],
|
||||||
|
v_overall_bit_rate = video[VideoTable.vOverallBitRate],
|
||||||
|
v_resolution = video[VideoTable.vResolution],
|
||||||
|
v_a_time = video[VideoTable.vATime].toString(),
|
||||||
|
v_a_total_people = video[vATotalPeople], // 总人数
|
||||||
|
v_a_count_people = video[vACountPeople], // 佩戴口罩人数
|
||||||
|
v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间
|
||||||
|
v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作
|
||||||
|
v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例
|
||||||
|
v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData
|
||||||
|
v_details_list = detailList
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun broadcastMessage(message: String) { // 封装的广播消息方法
|
||||||
|
println("发送消息:$message")
|
||||||
|
clients.forEach { client ->
|
||||||
|
try {
|
||||||
|
client.send(message) // 发送消息到每个客户端
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("发送消息给客户端时出错,移除客户端: ${e.message}")
|
||||||
|
clients.remove(client) // 移除已断开的客户端
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFriendlyActionName(name: String): String {
|
||||||
|
return if (name == "feed") {
|
||||||
|
"喂桑"
|
||||||
|
} else if (name == "disinfection") {
|
||||||
|
"消毒"
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ink.snowflake.server.route.func
|
package ink.snowflake.server.route
|
||||||
|
|
||||||
import ink.snowflake.server.model.response.*
|
import ink.snowflake.server.model.response.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
package ink.snowflake.server.route.func
|
|
||||||
|
|
||||||
import ink.snowflake.server.VIDEO_INPUT_PATH
|
|
||||||
import ink.snowflake.server.model.request.VideoAnalyticsRequest
|
|
||||||
import ink.snowflake.server.model.response.*
|
|
||||||
import ink.snowflake.server.repository.VideoDataBase
|
|
||||||
import ink.snowflake.server.utils.WebSocketManager.broadcastMessage
|
|
||||||
import ink.snowflake.server.utils.runCommand
|
|
||||||
import io.ktor.http.content.*
|
|
||||||
import io.ktor.server.application.*
|
|
||||||
import io.ktor.server.auth.*
|
|
||||||
import io.ktor.server.routing.*
|
|
||||||
import io.ktor.server.request.*
|
|
||||||
import io.ktor.server.response.*
|
|
||||||
import io.ktor.server.websocket.*
|
|
||||||
import io.ktor.websocket.*
|
|
||||||
import kotlinx.coroutines.channels.consumeEach
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
val clients = Collections.synchronizedList<WebSocketServerSession>(ArrayList()) // 线程安全的客户端列表
|
|
||||||
var aiState = "等待分析任务中"
|
|
||||||
|
|
||||||
fun Application.VideoAnalytics() {
|
|
||||||
val repository = VideoDataBase()
|
|
||||||
routing {
|
|
||||||
// 实时发送AI状态
|
|
||||||
webSocket("/handleState") { // WebSocket 路由
|
|
||||||
clients.add(this) // 添加当前连接的客户端
|
|
||||||
send(aiState) // 向客户端发送连接成功消息
|
|
||||||
try {
|
|
||||||
incoming.consumeEach { frame -> // 持续接收消息
|
|
||||||
when (frame) {
|
|
||||||
is Frame.Text -> {
|
|
||||||
aiState = frame.readText() // 更新状态
|
|
||||||
broadcastMessage(aiState) // 使用封装的方法广播消息
|
|
||||||
}
|
|
||||||
|
|
||||||
is Frame.Close -> {
|
|
||||||
println("Closed")
|
|
||||||
clients.remove(this)
|
|
||||||
close() // 确保关闭 WebSocket 连接
|
|
||||||
return@consumeEach
|
|
||||||
}
|
|
||||||
// 其他消息类型的处理
|
|
||||||
is Frame.Binary -> TODO() // 处理二进制消息
|
|
||||||
is Frame.Ping -> TODO() // 处理 Ping 消息
|
|
||||||
is Frame.Pong -> TODO() // 处理 Pong 消息
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// 处理接收消息时的异常
|
|
||||||
close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected"))
|
|
||||||
e.printStackTrace()
|
|
||||||
} finally {
|
|
||||||
clients.remove(this) // 确保在连接关闭时移除客户端
|
|
||||||
}
|
|
||||||
}
|
|
||||||
route("/api") {
|
|
||||||
// 上传分析结果
|
|
||||||
post("/saveVideoAnalyticsData") {
|
|
||||||
val request = call.receive<VideoAnalyticsRequest>()
|
|
||||||
// todo 上传这里未做测试
|
|
||||||
call.respond(BaseResponse(data = repository.saveVideoAnalyticsData(request)))
|
|
||||||
}
|
|
||||||
authenticate {
|
|
||||||
post("/upload") {
|
|
||||||
val multipart = call.receiveMultipart() //1G
|
|
||||||
// 确保 uploads 目录存在
|
|
||||||
val uploadDir = File(VIDEO_INPUT_PATH)
|
|
||||||
if (!uploadDir.exists()) {
|
|
||||||
if (!uploadDir.mkdirs()) {
|
|
||||||
println("无法创建目录")
|
|
||||||
throw IOException("Failed to create upload directory.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var fileName = ""
|
|
||||||
var name = ""
|
|
||||||
var datetime = ""
|
|
||||||
broadcastMessage("正在上传数据")
|
|
||||||
multipart.forEachPart { part ->
|
|
||||||
when (part) {
|
|
||||||
is PartData.FileItem -> {
|
|
||||||
fileName = part.originalFileName ?: "unknown"
|
|
||||||
val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径
|
|
||||||
//ktor3
|
|
||||||
// file.outputStream().use { outputStream ->
|
|
||||||
// val writableChannel = Channels.newChannel(outputStream)
|
|
||||||
// part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel
|
|
||||||
// }
|
|
||||||
//ktor2
|
|
||||||
part.streamProvider().use { inputStream ->
|
|
||||||
file.outputStream().buffered().use { outputStream ->
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is PartData.FormItem -> {
|
|
||||||
when (part.name) {
|
|
||||||
"name" -> name = part.value
|
|
||||||
"datetime" -> datetime = part.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> part.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
call.respond(BaseResponse(message = "上传成功", data = null))
|
|
||||||
broadcastMessage("上传完成,开始启动AI引擎")
|
|
||||||
val command = listOf(
|
|
||||||
"/usr/bin/python3",
|
|
||||||
"/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py",
|
|
||||||
"$VIDEO_INPUT_PATH$fileName",
|
|
||||||
datetime,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
println("-----------------" + command.joinToString(" "))
|
|
||||||
runCommand(command) {
|
|
||||||
println(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 获取已分析视频列表
|
|
||||||
get("/getVideoList") {
|
|
||||||
val res = repository.getVideoList()
|
|
||||||
call.respond(BaseResponse(data = res))
|
|
||||||
}
|
|
||||||
// 获取某视频分析详情
|
|
||||||
get("/getAnalyticsDetailByVideoId") {
|
|
||||||
// 获取 vId 参数
|
|
||||||
val vIdParam = call.parameters["vId"]
|
|
||||||
val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全
|
|
||||||
if (vId == null) {
|
|
||||||
call.respond(BaseResponse(status = true, message = "Invalid vId", data = null))
|
|
||||||
return@get
|
|
||||||
}
|
|
||||||
val result = repository.getAnalyticsDetailByVideoId(vId)
|
|
||||||
if (result != null) {
|
|
||||||
call.respond(BaseResponse(data = result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun broadcastMessage(message: String) { // 封装的广播消息方法
|
|
||||||
println("发送消息:$message")
|
|
||||||
clients.forEach { client ->
|
|
||||||
try {
|
|
||||||
client.send(message) // 发送消息到每个客户端
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("发送消息给客户端时出错,移除客户端: ${e.message}")
|
|
||||||
clients.remove(client) // 移除已断开的客户端
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,12 +10,14 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.format
|
import kotlinx.datetime.format
|
||||||
import kotlinx.datetime.toJavaLocalDateTime
|
import kotlinx.datetime.toJavaLocalDateTime
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.experimental.newSuspendedTransaction
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
fun getUserIdByToken(call: ApplicationCall) : Int?{
|
fun getUserIdByToken(call: ApplicationCall) : Int?{
|
||||||
// 通过token获取user_id
|
// 通过token获取user_id
|
||||||
return call.principal<JWTPrincipal>()?.payload?.getClaim("user_id")?.asInt()
|
return call.principal<JWTPrincipal>()?.payload?.getClaim("user_id")?.asInt()
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package ink.snowflake.server.utils.database
|
|
||||||
|
|
||||||
import ink.snowflake.server.model.database.AIProfile
|
|
||||||
import ink.snowflake.server.model.database.AIProfilesTable
|
|
||||||
import ink.snowflake.server.model.database.toAIProfile
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
object AIDao {
|
|
||||||
/**
|
|
||||||
* 获取所有AI
|
|
||||||
*/
|
|
||||||
fun getAllAIProfiles(): List<AIProfile> {
|
|
||||||
return transaction {
|
|
||||||
AIProfilesTable.selectAll().map { it.toAIProfile() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# EXAMPLE USAGE:
|
||||||
|
#
|
||||||
|
# Refer for explanation to following link:
|
||||||
|
# https://lefthook.dev/configuration/
|
||||||
|
#
|
||||||
|
# pre-push:
|
||||||
|
# jobs:
|
||||||
|
# - name: packages audit
|
||||||
|
# tags:
|
||||||
|
# - frontend
|
||||||
|
# - security
|
||||||
|
# run: yarn audit
|
||||||
|
#
|
||||||
|
# - name: gems audit
|
||||||
|
# tags:
|
||||||
|
# - backend
|
||||||
|
# - security
|
||||||
|
# run: bundle audit
|
||||||
|
#
|
||||||
|
# pre-commit:
|
||||||
|
# parallel: true
|
||||||
|
# jobs:
|
||||||
|
# - run: yarn eslint {staged_files}
|
||||||
|
# glob: "*.{js,ts,jsx,tsx}"
|
||||||
|
#
|
||||||
|
# - name: rubocop
|
||||||
|
# glob: "*.rb"
|
||||||
|
# exclude:
|
||||||
|
# - config/application.rb
|
||||||
|
# - config/routes.rb
|
||||||
|
# run: bundle exec rubocop --force-exclusion {all_files}
|
||||||
|
#
|
||||||
|
# - name: govet
|
||||||
|
# files: git ls-files -m
|
||||||
|
# glob: "*.go"
|
||||||
|
# run: go vet {files}
|
||||||
|
#
|
||||||
|
# - script: "hello.js"
|
||||||
|
# runner: node
|
||||||
|
#
|
||||||
|
# - script: "hello.go"
|
||||||
|
# runner: go run
|
||||||
@@ -28,13 +28,19 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. 将Image保存为文件
|
2. 将Image保存为文件
|
||||||
```
|
```shell
|
||||||
docker save -o vue.tar ce-vue
|
docker save -o vue.tar ce-vue
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 将保存好的Image`vue.tar`上传到服务器
|
3. 将保存好的Image`vue.tar`上传到服务器
|
||||||
|
|
||||||
4. 在服务器读取Image
|
4. 停掉之前的docker compose
|
||||||
|
5. 删除之前的Image
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker rmi ce-vue:latest
|
||||||
|
```
|
||||||
|
6. 在服务器读取Image
|
||||||
```shell
|
```shell
|
||||||
docker load -i vue.tar
|
docker load -i vue.tar
|
||||||
```
|
```
|
||||||
@@ -45,7 +51,7 @@
|
|||||||
|
|
||||||
1. 将项目目录下的`server/docker-compose.yml`复制到服务器
|
1. 将项目目录下的`server/docker-compose.yml`复制到服务器
|
||||||
|
|
||||||
2. 运行Docker Compose
|
2. 在`/opt/fianl/vue-image/文件夹下`运行Docker Compose
|
||||||
```shell
|
```shell
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
services:
|
||||||
|
# vue:
|
||||||
|
# image: node:18
|
||||||
|
# container_name: ce_vue_dev
|
||||||
|
# working_dir: /app
|
||||||
|
# volumes:
|
||||||
|
# - ./vue:/app
|
||||||
|
# ports:
|
||||||
|
# - "8090:8090"
|
||||||
|
# command: sh -c "npm install && npm run dev -- --host"
|
||||||
|
# networks:
|
||||||
|
# - ce_network
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: ce_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ktor
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: 123456
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- ce_network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7.2.3-alpine
|
||||||
|
container_name: ce_redis
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- ce_network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:RELEASE.2025-03-12T18-04-18Z
|
||||||
|
container_name: ce_minio
|
||||||
|
environment:
|
||||||
|
MINIO_ACCESS_KEY: minioadmin
|
||||||
|
MINIO_SECRET_KEY: minioadmin
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
networks:
|
||||||
|
- ce_network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ollama:
|
||||||
|
# image: ollama/ollama
|
||||||
|
# container_name: ce_ollama
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# reservations:
|
||||||
|
# devices:7
|
||||||
|
# - capabilities: [gpu] # 启用 GPU 支持
|
||||||
|
# volumes:
|
||||||
|
# - ollama_data:/root/.ollama # 持久化 ollama 数据
|
||||||
|
# ports:
|
||||||
|
# - "11434:11434" # 暴露 ollama 的端口
|
||||||
|
# networks:
|
||||||
|
# - ce_network
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
|
# 定义数据卷
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
minio_data:
|
||||||
|
ollama_data: # 定义 ollama 的数据卷
|
||||||
|
|
||||||
|
# 定义网络
|
||||||
|
networks:
|
||||||
|
ce_network:
|
||||||
|
driver: bridge
|
||||||
@@ -4,13 +4,13 @@ VITE_PORT=8090
|
|||||||
VITE_BASE=/
|
VITE_BASE=/
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_GLOB_API_URL=/api
|
VITE_GLOB_API_URL=http://localhost:8089/api
|
||||||
|
|
||||||
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||||
VITE_NITRO_MOCK=false
|
VITE_NITRO_MOCK=false
|
||||||
|
|
||||||
# 是否打开 devtools,true 为打开,false 为关闭
|
# 是否打开 devtools,true 为打开,false 为关闭
|
||||||
VITE_DEVTOOLS=false
|
VITE_DEVTOOLS=true
|
||||||
|
|
||||||
# 是否注入全局loading
|
# 是否注入全局loading
|
||||||
VITE_INJECT_APP_LOADING=true
|
VITE_INJECT_APP_LOADING=true
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"dayjs": "catalog:",
|
"dayjs": "catalog:",
|
||||||
"js-sha256": "^0.11.0",
|
"js-sha256": "^0.11.0",
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
|
"video.js": "^8.22.0",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-router": "catalog:"
|
"vue-router": "catalog:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||||
@@ -61,7 +63,13 @@ setupVbenVxeTable({
|
|||||||
},
|
},
|
||||||
useVbenForm,
|
useVbenForm,
|
||||||
});
|
});
|
||||||
|
|
||||||
export { useVbenVxeGrid };
|
export { useVbenVxeGrid };
|
||||||
|
export type OnActionClickParams<T = Recordable<any>> = {
|
||||||
|
code: string;
|
||||||
|
row: T;
|
||||||
|
};
|
||||||
|
export type OnActionClickFn<T = Recordable<any>> = (
|
||||||
|
params: OnActionClickParams<T>,
|
||||||
|
) => void;
|
||||||
|
|
||||||
export type * from '@vben/plugins/vxe-table';
|
export type * from '@vben/plugins/vxe-table';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './iva';
|
||||||
export * from './menu';
|
export * from './menu';
|
||||||
export * from './remote';
|
export * from './remote';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已分析的视频列表
|
||||||
|
*/
|
||||||
|
export async function refreshVideoList(name = '') {
|
||||||
|
return requestClient.get('/iva/getVideoList', { params: { name } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已分析的视频列表
|
||||||
|
*/
|
||||||
|
export async function refreshVideoDetail(vId = '') {
|
||||||
|
return requestClient.get('/iva/getAnalyticsDetailByVideoId', {
|
||||||
|
params: { vId },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,5 +6,5 @@ import { requestClient } from '#/api/request';
|
|||||||
* 获取用户所有菜单
|
* 获取用户所有菜单
|
||||||
*/
|
*/
|
||||||
export async function getAllMenusApi() {
|
export async function getAllMenusApi() {
|
||||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
return requestClient.get<RouteRecordStringComponent[]>('/user/menus');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { requestClient } from '#/api/request';
|
|||||||
/**
|
/**
|
||||||
* 获取在线设备列表数据
|
* 获取在线设备列表数据
|
||||||
*/
|
*/
|
||||||
export async function refreshDeviceList() {
|
export async function refreshDeviceList(name = '') {
|
||||||
return requestClient.get('/remote/refreshDeviceList');
|
return requestClient.get('/remote/refreshDeviceList', { params: { name } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace SystemDeptApi {
|
||||||
|
export interface SystemDept {
|
||||||
|
[key: string]: any;
|
||||||
|
children?: SystemDept[];
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
remark?: string;
|
||||||
|
status: 0 | 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部门列表数据
|
||||||
|
*/
|
||||||
|
async function getDeptList() {
|
||||||
|
return requestClient.get<Array<SystemDeptApi.SystemDept>>(
|
||||||
|
'/system/dept/list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建部门
|
||||||
|
* @param data 部门数据
|
||||||
|
*/
|
||||||
|
async function createDept(
|
||||||
|
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
|
||||||
|
) {
|
||||||
|
return requestClient.post('/system/dept/add', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新部门
|
||||||
|
*
|
||||||
|
* @param id 部门 ID
|
||||||
|
* @param data 部门数据
|
||||||
|
*/
|
||||||
|
async function updateDept(
|
||||||
|
id: string,
|
||||||
|
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
|
||||||
|
) {
|
||||||
|
return requestClient.put(`/system/dept/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除部门
|
||||||
|
* @param id 部门 ID
|
||||||
|
*/
|
||||||
|
async function deleteDept(id: string) {
|
||||||
|
return requestClient.delete(`/system/dept/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createDept, deleteDept, getDeptList, updateDept };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './dept';
|
||||||
|
export * from './menu';
|
||||||
|
export * from './role';
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace SystemMenuApi {
|
||||||
|
/** 徽标颜色集合 */
|
||||||
|
export const BadgeVariants = [
|
||||||
|
'default',
|
||||||
|
'destructive',
|
||||||
|
'primary',
|
||||||
|
'success',
|
||||||
|
'warning',
|
||||||
|
] as const;
|
||||||
|
/** 徽标类型集合 */
|
||||||
|
export const BadgeTypes = ['dot', 'normal'] as const;
|
||||||
|
/** 菜单类型集合 */
|
||||||
|
export const MenuTypes = [
|
||||||
|
'catalog',
|
||||||
|
'menu',
|
||||||
|
'embedded',
|
||||||
|
'link',
|
||||||
|
'button',
|
||||||
|
] as const;
|
||||||
|
/** 系统菜单 */
|
||||||
|
export interface SystemMenu {
|
||||||
|
[key: string]: any;
|
||||||
|
/** 后端权限标识 */
|
||||||
|
authCode: string;
|
||||||
|
/** 子级 */
|
||||||
|
children?: SystemMenu[];
|
||||||
|
/** 组件 */
|
||||||
|
component?: string;
|
||||||
|
/** 菜单ID */
|
||||||
|
id: string;
|
||||||
|
/** 菜单元数据 */
|
||||||
|
meta?: {
|
||||||
|
/** 激活时显示的图标 */
|
||||||
|
activeIcon?: string;
|
||||||
|
/** 作为路由时,需要激活的菜单的Path */
|
||||||
|
activePath?: string;
|
||||||
|
/** 固定在标签栏 */
|
||||||
|
affixTab?: boolean;
|
||||||
|
/** 在标签栏固定的顺序 */
|
||||||
|
affixTabOrder?: number;
|
||||||
|
/** 徽标内容(当徽标类型为normal时有效) */
|
||||||
|
badge?: string;
|
||||||
|
/** 徽标类型 */
|
||||||
|
badgeType?: (typeof BadgeTypes)[number];
|
||||||
|
/** 徽标颜色 */
|
||||||
|
badgeVariants?: (typeof BadgeVariants)[number];
|
||||||
|
/** 在菜单中隐藏下级 */
|
||||||
|
hideChildrenInMenu?: boolean;
|
||||||
|
/** 在面包屑中隐藏 */
|
||||||
|
hideInBreadcrumb?: boolean;
|
||||||
|
/** 在菜单中隐藏 */
|
||||||
|
hideInMenu?: boolean;
|
||||||
|
/** 在标签栏中隐藏 */
|
||||||
|
hideInTab?: boolean;
|
||||||
|
/** 菜单图标 */
|
||||||
|
icon?: string;
|
||||||
|
/** 内嵌Iframe的URL */
|
||||||
|
iframeSrc?: string;
|
||||||
|
/** 是否缓存页面 */
|
||||||
|
keepAlive?: boolean;
|
||||||
|
/** 外链页面的URL */
|
||||||
|
link?: string;
|
||||||
|
/** 同一个路由最大打开的标签数 */
|
||||||
|
maxNumOfOpenTab?: number;
|
||||||
|
/** 无需基础布局 */
|
||||||
|
noBasicLayout?: boolean;
|
||||||
|
/** 是否在新窗口打开 */
|
||||||
|
openInNewWindow?: boolean;
|
||||||
|
/** 菜单排序 */
|
||||||
|
order?: number;
|
||||||
|
/** 额外的路由参数 */
|
||||||
|
query?: Recordable<any>;
|
||||||
|
/** 菜单标题 */
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
/** 菜单名称 */
|
||||||
|
name: string;
|
||||||
|
/** 路由路径 */
|
||||||
|
path: string;
|
||||||
|
/** 父级ID */
|
||||||
|
pid: string;
|
||||||
|
/** 重定向 */
|
||||||
|
redirect?: string;
|
||||||
|
/** 菜单类型 */
|
||||||
|
type: (typeof MenuTypes)[number];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取菜单数据列表
|
||||||
|
*/
|
||||||
|
async function getMenuList() {
|
||||||
|
return requestClient.get<Array<SystemMenuApi.SystemMenu>>(
|
||||||
|
'/system/menu/list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isMenuNameExists(
|
||||||
|
name: string,
|
||||||
|
id?: SystemMenuApi.SystemMenu['id'],
|
||||||
|
) {
|
||||||
|
return requestClient.get<boolean>('/system/menu/name-exists', {
|
||||||
|
params: { id, name },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isMenuPathExists(
|
||||||
|
path: string,
|
||||||
|
id?: SystemMenuApi.SystemMenu['id'],
|
||||||
|
) {
|
||||||
|
return requestClient.get<boolean>('/system/menu/path-exists', {
|
||||||
|
params: { id, path },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建菜单
|
||||||
|
* @param data 菜单数据
|
||||||
|
*/
|
||||||
|
async function createMenu(
|
||||||
|
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
|
||||||
|
) {
|
||||||
|
return requestClient.post('/system/menu', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新菜单
|
||||||
|
*
|
||||||
|
* @param id 菜单 ID
|
||||||
|
* @param data 菜单数据
|
||||||
|
*/
|
||||||
|
async function updateMenu(
|
||||||
|
id: string,
|
||||||
|
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
|
||||||
|
) {
|
||||||
|
return requestClient.put(`/system/menu/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除菜单
|
||||||
|
* @param id 菜单 ID
|
||||||
|
*/
|
||||||
|
async function deleteMenu(id: string) {
|
||||||
|
return requestClient.delete(`/system/menu/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createMenu,
|
||||||
|
deleteMenu,
|
||||||
|
getMenuList,
|
||||||
|
isMenuNameExists,
|
||||||
|
isMenuPathExists,
|
||||||
|
updateMenu,
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace SystemRoleApi {
|
||||||
|
export interface SystemRole {
|
||||||
|
[key: string]: any;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
permissions: string[];
|
||||||
|
remark?: string;
|
||||||
|
status: 0 | 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色列表数据
|
||||||
|
*/
|
||||||
|
async function getRoleList(params: Recordable<any>) {
|
||||||
|
return requestClient.get<Array<SystemRoleApi.SystemRole>>(
|
||||||
|
'/system/role/list',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建角色
|
||||||
|
* @param data 角色数据
|
||||||
|
*/
|
||||||
|
async function createRole(data: Omit<SystemRoleApi.SystemRole, 'id'>) {
|
||||||
|
return requestClient.post('/system/role', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新角色
|
||||||
|
*
|
||||||
|
* @param id 角色 ID
|
||||||
|
* @param data 角色数据
|
||||||
|
*/
|
||||||
|
async function updateRole(
|
||||||
|
id: string,
|
||||||
|
data: Omit<SystemRoleApi.SystemRole, 'id'>,
|
||||||
|
) {
|
||||||
|
return requestClient.put(`/system/role/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除角色
|
||||||
|
* @param id 角色 ID
|
||||||
|
*/
|
||||||
|
async function deleteRole(id: string) {
|
||||||
|
return requestClient.delete(`/system/role/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createRole, deleteRole, getRoleList, updateRole };
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"forgetPassword": "忘记密码"
|
"forgetPassword": "忘记密码"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "概览",
|
"title": "常规",
|
||||||
"analytics": "分析页",
|
"analytics": "分析页",
|
||||||
"workspace": "工作台"
|
"workspace": "工作台"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ export const overridesPreferences = defineOverridesPreferences({
|
|||||||
// overrides
|
// overrides
|
||||||
app: {
|
app: {
|
||||||
name: import.meta.env.VITE_APP_TITLE,
|
name: import.meta.env.VITE_APP_TITLE,
|
||||||
layout: 'header-sidebar-nav',
|
layout: 'header-sidebar-nav', // 布局方式
|
||||||
defaultHomePath: '/workspace',
|
defaultHomePath: '/workspace', // 默认首页路径
|
||||||
enablePreferences: false,
|
enablePreferences: false, // 是否启用偏好设置
|
||||||
|
loginExpiredMode: 'modal', // 登录过期模式 弹窗登录
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
mode: 'light',
|
mode: 'light',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'IVA',
|
name: 'IVA',
|
||||||
path: '/ai/iva',
|
path: '/ai/iva',
|
||||||
meta: {
|
meta: {
|
||||||
|
authority: ['iva'],
|
||||||
icon: 'mdi:video',
|
icon: 'mdi:video',
|
||||||
title: $t('ai.intelligence_video_analysis'),
|
title: $t('ai.intelligence_video_analysis'),
|
||||||
},
|
},
|
||||||
@@ -28,7 +29,8 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'YSA',
|
name: 'YSA',
|
||||||
path: '/ai/ysa',
|
path: '/ai/ysa',
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:home',
|
authority: ['ysa'],
|
||||||
|
icon: 'mdi:account-key-outline',
|
||||||
title: $t('ai.young_silkworm_analysis'),
|
title: $t('ai.young_silkworm_analysis'),
|
||||||
},
|
},
|
||||||
component: () => import('#/views/ai/ysa/index.vue'),
|
component: () => import('#/views/ai/ysa/index.vue'),
|
||||||
|
|||||||
@@ -12,16 +12,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
children: [
|
children: [
|
||||||
// {
|
|
||||||
// name: 'Analytics',
|
|
||||||
// path: '/analytics',
|
|
||||||
// component: () => import('#/views/dashboard/analytics/index.vue'),
|
|
||||||
// meta: {
|
|
||||||
// affixTab: true,
|
|
||||||
// icon: 'lucide:area-chart',
|
|
||||||
// title: $t('page.dashboard.analytics'),
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
name: 'Workspace',
|
name: 'Workspace',
|
||||||
path: '/workspace',
|
path: '/workspace',
|
||||||
@@ -31,6 +21,16 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: $t('page.dashboard.workspace'),
|
title: $t('page.dashboard.workspace'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Remote',
|
||||||
|
path: '/remote',
|
||||||
|
component: () => import('#/views/remote/index.vue'),
|
||||||
|
meta: {
|
||||||
|
authority: ['remote'],
|
||||||
|
icon: 'mdi:home',
|
||||||
|
title: $t('remote.remote'),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
icon: 'ic:baseline-view-in-ar',
|
|
||||||
keepAlive: true,
|
|
||||||
order: 1,
|
|
||||||
title: $t('remote.tools'),
|
|
||||||
},
|
|
||||||
name: 'Room',
|
|
||||||
path: '/room',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'Remote',
|
|
||||||
path: '/remote',
|
|
||||||
component: () => import('#/views/remote/index.vue'),
|
|
||||||
meta: {
|
|
||||||
icon: 'mdi:home',
|
|
||||||
title: $t('remote.remote'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default routes;
|
|
||||||
@@ -1,5 +1,488 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type Player from 'video.js/dist/types/player';
|
||||||
|
|
||||||
|
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { AnalysisOverview } from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
SvgBellIcon,
|
||||||
|
SvgCakeIcon,
|
||||||
|
SvgCardIcon,
|
||||||
|
SvgDownloadIcon,
|
||||||
|
} from '@vben/icons';
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
|
import * as api from '#/api';
|
||||||
|
|
||||||
|
import 'video.js/dist/video-js.css';
|
||||||
|
|
||||||
|
const list = ref<any[]>([]);
|
||||||
|
const error = ref<null | string>(null);
|
||||||
|
const filterKeyword = ref('');
|
||||||
|
const activeTab = ref<'detail' | 'video'>('detail');
|
||||||
|
const selectedItem = ref<any>(null);
|
||||||
|
const detailList = ref<any[]>([]);
|
||||||
|
const videoEl = ref<HTMLVideoElement | null>(null);
|
||||||
|
const player = ref<null | Player>(null);
|
||||||
|
async function loadList() {
|
||||||
|
error.value = null;
|
||||||
|
const res = await api.refreshVideoList(filterKeyword.value);
|
||||||
|
list.value = res || [];
|
||||||
|
}
|
||||||
|
function refreshList() {
|
||||||
|
filterKeyword.value = '';
|
||||||
|
loadList();
|
||||||
|
message.success('视频列表加载完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTask = () => {};
|
||||||
|
|
||||||
|
async function selectItem(item: any) {
|
||||||
|
const res = await api.refreshVideoDetail(item.v_id);
|
||||||
|
selectedItem.value = res;
|
||||||
|
refreshLineChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'detail', label: '分析详情' },
|
||||||
|
{ key: 'video', label: '分析视频' },
|
||||||
|
];
|
||||||
|
let overviewItems: AnalysisOverviewItem[];
|
||||||
|
|
||||||
|
// 监听关键词变化,调用防抖接口
|
||||||
|
watch(filterKeyword, () => {
|
||||||
|
loadList();
|
||||||
|
});
|
||||||
|
watch(selectedItem, () => {
|
||||||
|
overviewItems = [
|
||||||
|
{
|
||||||
|
icon: SvgCardIcon,
|
||||||
|
title: '动作',
|
||||||
|
totalTitle: '出现最多的动作',
|
||||||
|
totalValue: 0,
|
||||||
|
value: selectedItem.value.v_a_max_action,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgCakeIcon,
|
||||||
|
title: '人数',
|
||||||
|
totalTitle: '最多同框人数',
|
||||||
|
totalValue: 0,
|
||||||
|
value: selectedItem.value.v_a_total_people,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgDownloadIcon,
|
||||||
|
title: '人次',
|
||||||
|
totalTitle: '视频出现人次',
|
||||||
|
totalValue: 0,
|
||||||
|
value: selectedItem.value.v_a_count_people,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgBellIcon,
|
||||||
|
title: '时间',
|
||||||
|
totalTitle: '最长停留时间',
|
||||||
|
totalValue: 0,
|
||||||
|
value: selectedItem.value.v_a_max_stay_time,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
watch([activeTab, selectedItem], async ([tab]) => {
|
||||||
|
if (tab === 'video' && selectedItem.value?.v_video_play_path) {
|
||||||
|
refreshVideoPlayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
loadList();
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
player.value?.dispose();
|
||||||
|
player.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 切换视频项时销毁并重建
|
||||||
|
function refreshVideoPlayer() {
|
||||||
|
nextTick(() => {
|
||||||
|
if (player.value) {
|
||||||
|
player.value.src([
|
||||||
|
{
|
||||||
|
src: selectedItem.value.v_video_play_path,
|
||||||
|
type: 'video/mp4',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
if (!videoEl.value) return;
|
||||||
|
player.value = videojs(videoEl.value, {
|
||||||
|
controls: true,
|
||||||
|
autoplay: false,
|
||||||
|
preload: 'auto',
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
src: selectedItem.value.v_video_play_path,
|
||||||
|
type: 'video/mp4',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
drawVideoProcess();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function drawVideoProcess() {
|
||||||
|
const data = selectedItem.value;
|
||||||
|
player.value?.one('loadedmetadata', () => {
|
||||||
|
// 计算起始时间戳
|
||||||
|
const vStartTime = new Date(data.v_start_datetime).getTime();
|
||||||
|
|
||||||
|
// 获取进度条 DOM
|
||||||
|
const progressControl = player.value?.controlBar?.progressControl?.el();
|
||||||
|
if (!progressControl) return;
|
||||||
|
|
||||||
|
// 清除旧的自定义进度段
|
||||||
|
progressControl
|
||||||
|
.querySelectorAll('.custom-range')
|
||||||
|
.forEach((el) => el.remove());
|
||||||
|
|
||||||
|
const duration = player.value?.duration() || 1;
|
||||||
|
|
||||||
|
const areaData = Array.isArray(data.v_a_details.areaData)
|
||||||
|
? data.v_a_details.areaData.map((actionGroup: any) => {
|
||||||
|
return actionGroup.map((action: any) => {
|
||||||
|
return { xAxis: action.xAxis, itemStyle: action.itemStyle };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
: []; // 默认值为空数组
|
||||||
|
// 遍历区域数据,生成每个时间段
|
||||||
|
for (const area of areaData) {
|
||||||
|
const startMs = new Date(area[0].xAxis).getTime();
|
||||||
|
const endMs = new Date(area[1].xAxis).getTime();
|
||||||
|
|
||||||
|
const startSec = (startMs - vStartTime) / 1000;
|
||||||
|
const endSec = (endMs - vStartTime) / 1000;
|
||||||
|
|
||||||
|
if (startSec < 0 || endSec < 0 || startSec >= endSec) continue;
|
||||||
|
|
||||||
|
const startPct = (startSec / duration) * 100;
|
||||||
|
const endPct = (endSec / duration) * 100;
|
||||||
|
|
||||||
|
const rangeDiv = document.createElement('div');
|
||||||
|
rangeDiv.className = 'custom-range';
|
||||||
|
rangeDiv.style.position = 'absolute';
|
||||||
|
rangeDiv.style.left = `${startPct}%`;
|
||||||
|
rangeDiv.style.width = `${endPct - startPct}%`;
|
||||||
|
rangeDiv.style.height = '100%';
|
||||||
|
rangeDiv.style.backgroundColor = area[0].itemStyle.color;
|
||||||
|
rangeDiv.style.pointerEvents = 'none'; // 避免阻挡鼠标交互
|
||||||
|
rangeDiv.style.zIndex = '2';
|
||||||
|
|
||||||
|
progressControl.append(rangeDiv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const showInfoStr = ref<Record<string, number | string>>({});
|
||||||
|
|
||||||
|
const chartRef1 = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts: renderEcharts1 } = useEcharts(chartRef1);
|
||||||
|
|
||||||
|
const chartRef2 = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts: renderEcharts2 } = useEcharts(chartRef2);
|
||||||
|
|
||||||
|
function refreshLineChart() {
|
||||||
|
const data = selectedItem.value;
|
||||||
|
showInfoStr.value = {
|
||||||
|
项目名: data.v_name,
|
||||||
|
视频开始时间: data.v_start_datetime,
|
||||||
|
文件名: data.v_file_name,
|
||||||
|
文件大小: `${data.v_size} MB`,
|
||||||
|
总时长: `${data.v_duration} 秒`,
|
||||||
|
分辨率: data.v_resolution,
|
||||||
|
视频编码格式: data.v_video_codec,
|
||||||
|
音频编码格式: data.v_audio_codec,
|
||||||
|
总体比特率: data.v_overall_bit_rate,
|
||||||
|
};
|
||||||
|
|
||||||
|
detailList.value = data.v_details_list || [];
|
||||||
|
|
||||||
|
const detail = selectedItem.value.v_a_details;
|
||||||
|
let yTotalData = Array.isArray(detail.yTotalData)
|
||||||
|
? detail.yTotalData.map((item: any) => [item.first, item.second])
|
||||||
|
: []; // 默认值为空数组
|
||||||
|
let yMaskedData = Array.isArray(detail.yMaskedData)
|
||||||
|
? detail.yMaskedData.map((item: any) => [item.first, item.second])
|
||||||
|
: []; // 默认值为空数组
|
||||||
|
const areaData = Array.isArray(detail.areaData)
|
||||||
|
? detail.areaData.map((actionGroup: any) => {
|
||||||
|
return actionGroup.map((action: any) => {
|
||||||
|
return { xAxis: action.xAxis, itemStyle: action.itemStyle };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
: []; // 默认值为空数组
|
||||||
|
yTotalData = yTotalData.map((item: any) => [
|
||||||
|
new Date(item[0]).getTime(),
|
||||||
|
item[1],
|
||||||
|
]);
|
||||||
|
yMaskedData = yMaskedData.map((item: any) => [
|
||||||
|
new Date(item[0]).getTime(),
|
||||||
|
item[1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
renderEcharts1({
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '总人数',
|
||||||
|
type: 'line',
|
||||||
|
step: 'end',
|
||||||
|
data: yTotalData,
|
||||||
|
markArea: {
|
||||||
|
itemStyle: { color: 'rgba(255, 173, 177, 0.4)' },
|
||||||
|
data: areaData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: '口罩佩戴人数', type: 'line', step: 'end', data: yMaskedData },
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
axisPointer: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#019680',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
xAxis: { type: 'time' },
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const maskedRatio = data.v_a_average_masked_ratio * 100;
|
||||||
|
const noMaskedRatio = 100 - maskedRatio;
|
||||||
|
renderEcharts2({
|
||||||
|
legend: { top: '5%', left: 'center' },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
animationDelay() {
|
||||||
|
return Math.random() * 100;
|
||||||
|
},
|
||||||
|
animationEasing: 'exponentialInOut',
|
||||||
|
animationType: 'scale',
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: maskedRatio,
|
||||||
|
name: '未佩戴口罩(%)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: noMaskedRatio,
|
||||||
|
name: '佩戴口罩(%)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
fontSize: '12',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 3,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
position: 'center',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
padAngle: 5,
|
||||||
|
name: '佩戴口罩人数占总人数的平均占比',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
type: 'pie',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeTab.value === 'video' && player.value) {
|
||||||
|
// 如果当前是视频标签页,刷新播放器
|
||||||
|
refreshVideoPlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onListItemClick(video: any) {
|
||||||
|
// 视频跳转到指定时间点
|
||||||
|
const vStartTime =
|
||||||
|
new Date(selectedItem.value.v_start_datetime).getTime() / 1000;
|
||||||
|
const xAxisTimeStart = new Date(video.time).getTime() / 1000;
|
||||||
|
const relativeTimeStart = xAxisTimeStart - vStartTime;
|
||||||
|
|
||||||
|
if (player.value) {
|
||||||
|
const duration = player.value.duration() || 1; // 获取视频总时长,避免除以0
|
||||||
|
if (relativeTimeStart >= 0 && relativeTimeStart <= duration) {
|
||||||
|
player.value.currentTime(relativeTimeStart);
|
||||||
|
} else {
|
||||||
|
message.warn(
|
||||||
|
`时间点超出视频范围,请选择 ${vStartTime} 秒到 秒之间的时间点`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.warn('请先选择左侧视频分析任务');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex h-full w-full bg-gray-50">
|
||||||
<h1>正在开发中,敬请期待</h1>
|
<!-- 左侧:筛选 + 列表 -->
|
||||||
|
<div class="flex w-64 flex-col border-r bg-white p-4">
|
||||||
|
<!-- 按钮组 -->
|
||||||
|
<div class="mb-4 flex justify-between space-x-2">
|
||||||
|
<Button type="primary" @click="createTask" class="flex-1">
|
||||||
|
新建任务
|
||||||
|
</Button>
|
||||||
|
<Button @click="refreshList" class="flex-1"> 刷新列表 </Button>
|
||||||
|
</div>
|
||||||
|
<!-- 筛选框 -->
|
||||||
|
<input
|
||||||
|
v-model="filterKeyword"
|
||||||
|
placeholder="筛选分析任务"
|
||||||
|
class="focus:ring-primary mb-4 rounded-md border px-3 py-2 focus:outline-none focus:ring-2"
|
||||||
|
/>
|
||||||
|
<!-- 列表 -->
|
||||||
|
<div class="flex-1 space-y-2 overflow-auto">
|
||||||
|
<div
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.v_id"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
|
||||||
|
:class="{ 'bg-gray-100': item.v_id === selectedItem?.v_id }"
|
||||||
|
>
|
||||||
|
<div class="text-base font-medium">{{ item.v_name }}</div>
|
||||||
|
<div class="text-sm text-gray-400">{{ item.v_a_time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:Tab 内容区 -->
|
||||||
|
<div class="flex flex-1 flex-col overflow-hidden p-6">
|
||||||
|
<!-- Tab 标题 -->
|
||||||
|
<div class="mb-4 flex shrink-0 space-x-4 border-b">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
@click="activeTab = tab.key as 'detail' | 'video'"
|
||||||
|
class="px-4 py-2"
|
||||||
|
:class="[
|
||||||
|
activeTab === tab.key
|
||||||
|
? 'border-primary text-primary border-b-2'
|
||||||
|
: 'hover:text-primary text-gray-500',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 内容(滚动区域) -->
|
||||||
|
<div class="flex-1 overflow-auto">
|
||||||
|
<div
|
||||||
|
v-if="!selectedItem"
|
||||||
|
class="flex h-full items-center justify-center text-gray-400"
|
||||||
|
>
|
||||||
|
请先选择左侧列表中的分析任务
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'detail'"
|
||||||
|
class="flex h-full flex-col gap-4"
|
||||||
|
>
|
||||||
|
<!-- 主内容区域:左右结构 -->
|
||||||
|
<div class="flex flex-1 gap-4">
|
||||||
|
<!-- 左侧 -->
|
||||||
|
<div class="flex w-72 flex-col gap-4">
|
||||||
|
<!-- 视频基础信息展示 -->
|
||||||
|
<div
|
||||||
|
class="mt-6 w-full rounded border bg-white p-4"
|
||||||
|
id="video_base_info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(value, key) in showInfoStr"
|
||||||
|
:key="key"
|
||||||
|
class="mb-2 flex text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
<div class="w-32 font-medium text-gray-900">
|
||||||
|
{{ key }}:
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 break-all text-gray-600">
|
||||||
|
{{ value || '—' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下:空白卡片 -->
|
||||||
|
<div class="h-[300px] flex-1 rounded border bg-white p-4">
|
||||||
|
<EchartsUI ref="chartRef2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧 -->
|
||||||
|
<div class="flex flex-1 flex-col gap-4">
|
||||||
|
<!-- 上:四个统计卡片 -->
|
||||||
|
<AnalysisOverview
|
||||||
|
:items="overviewItems"
|
||||||
|
class="grid grid-cols-4 gap-4"
|
||||||
|
/>
|
||||||
|
<!-- 下:折线图区域 -->
|
||||||
|
<div class="flex-1 rounded border bg-white p-4">
|
||||||
|
<EchartsUI ref="chartRef1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="activeTab === 'video'" class="flex h-full space-x-4">
|
||||||
|
<!-- 左侧视频区域 -->
|
||||||
|
<div class="flex-1 overflow-hidden rounded bg-black">
|
||||||
|
<video
|
||||||
|
ref="videoEl"
|
||||||
|
class="video-js vjs-default-skin h-full w-full"
|
||||||
|
preload="auto"
|
||||||
|
controls
|
||||||
|
></video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧时间点列表 -->
|
||||||
|
<div
|
||||||
|
class="flex w-1/4 flex-col overflow-auto rounded-md border bg-white"
|
||||||
|
>
|
||||||
|
<!-- 列表标题 -->
|
||||||
|
<div
|
||||||
|
class="flex justify-between border-b p-3 text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
<span>事件</span>
|
||||||
|
<span>时间点</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列表内容 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div
|
||||||
|
v-for="(video, index) in detailList"
|
||||||
|
:key="index"
|
||||||
|
@click="onListItemClick(video)"
|
||||||
|
class="flex cursor-pointer justify-between border-b p-3 text-sm hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<span>{{ video.action }}</span>
|
||||||
|
<span>{{ video.time }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
@@ -26,7 +27,6 @@ interface RowType {
|
|||||||
deviceName: string;
|
deviceName: string;
|
||||||
devicePort: number;
|
devicePort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gridOptions: VxeGridProps<RowType> = {
|
const gridOptions: VxeGridProps<RowType> = {
|
||||||
columns: [
|
columns: [
|
||||||
{ title: '序号', type: 'seq', width: 50 },
|
{ title: '序号', type: 'seq', width: 50 },
|
||||||
@@ -47,14 +47,14 @@ const gridOptions: VxeGridProps<RowType> = {
|
|||||||
gt: 0,
|
gt: 0,
|
||||||
},
|
},
|
||||||
// showOverflow: true, // 超出隐藏
|
// showOverflow: true, // 超出隐藏
|
||||||
height: '500px',
|
height: '750px',
|
||||||
// keepSource: true,
|
// keepSource: true,
|
||||||
stripe: true, // 条纹
|
stripe: true, // 条纹
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
ajax: {
|
ajax: {
|
||||||
query: async () => {
|
query: async ({ page }, formValues) => {
|
||||||
return {
|
return {
|
||||||
items: await refreshDeviceList(),
|
items: await refreshDeviceList(formValues.deviceName),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -68,33 +68,28 @@ const gridOptions: VxeGridProps<RowType> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 筛选表单
|
// 筛选表单
|
||||||
// const formOptions: VbenFormProps = {
|
const formOptions: VbenFormProps = {
|
||||||
// // 默认展开
|
// 默认展开
|
||||||
// collapsed: true,
|
collapsed: true,
|
||||||
// schema: [
|
schema: [
|
||||||
// {
|
{
|
||||||
// component: 'Input',
|
component: 'Input',
|
||||||
// componentProps: {
|
componentProps: {
|
||||||
// placeholder: '输入设备名',
|
placeholder: '设备名',
|
||||||
// },
|
},
|
||||||
// fieldName: 'deviceName',
|
fieldName: 'deviceName',
|
||||||
// label: '筛选',
|
label: '筛选',
|
||||||
// width: 300,
|
},
|
||||||
// },
|
],
|
||||||
// ],
|
// 控制表单是否显示折叠按钮
|
||||||
// // 控制表单是否显示折叠按钮
|
showCollapseButton: false,
|
||||||
// showCollapseButton: false,
|
showDefaultActions: false, // 是否显示默认操作按钮
|
||||||
// submitButtonOptions: {
|
submitOnChange: true, // 是否在字段值改变时提交表单
|
||||||
// content: '查询',
|
submitOnEnter: true, // 按下回车时是否提交表单
|
||||||
// },
|
};
|
||||||
|
|
||||||
// submitOnChange: true, // 是否在字段值改变时提交表单
|
|
||||||
|
|
||||||
// submitOnEnter: true, // 按下回车时是否提交表单
|
|
||||||
// };
|
|
||||||
|
|
||||||
const [Grid] = useVbenVxeGrid({
|
const [Grid] = useVbenVxeGrid({
|
||||||
// formOptions,
|
formOptions,
|
||||||
gridOptions,
|
gridOptions,
|
||||||
separator: false,
|
separator: false,
|
||||||
});
|
});
|
||||||
@@ -103,7 +98,7 @@ const [Grid] = useVbenVxeGrid({
|
|||||||
* @param devicePort 设备端口号
|
* @param devicePort 设备端口号
|
||||||
*/
|
*/
|
||||||
const handleConnect = async (devicePort: number) => {
|
const handleConnect = async (devicePort: number) => {
|
||||||
message.info(await connectDeviceByPort(devicePort));
|
message.info(await connectDeviceByPort(devicePort.toString()));
|
||||||
};
|
};
|
||||||
const disconnectAll = async () => {
|
const disconnectAll = async () => {
|
||||||
message.info(await disConnectAll());
|
message.info(await disConnectAll());
|
||||||
@@ -131,7 +126,10 @@ const disconnectAll = async () => {
|
|||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<Card class="ml-2 h-full">
|
<Card class="ml-2 h-full">
|
||||||
<iframe src="http://171.212.101.199:8088" class="w-full"></iframe>
|
<iframe
|
||||||
|
src="http://171.212.101.199:8088"
|
||||||
|
class="h-full w-full"
|
||||||
|
></iframe>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
</ColPage>
|
</ColPage>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -33,17 +33,22 @@ withDefaults(defineProps<Props>(), {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent class="flex items-center justify-between">
|
<CardContent class="flex items-center justify-between">
|
||||||
|
<div class="text-xl">
|
||||||
<VbenCountToAnimator
|
<VbenCountToAnimator
|
||||||
|
v-if="typeof item.value === 'number'"
|
||||||
:end-val="item.value"
|
:end-val="item.value"
|
||||||
:start-val="1"
|
:start-val="1"
|
||||||
class="text-xl"
|
|
||||||
prefix=""
|
prefix=""
|
||||||
/>
|
/>
|
||||||
|
<span v-else>{{ item.value }}</span>
|
||||||
|
</div>
|
||||||
<VbenIcon :icon="item.icon" class="size-8 flex-shrink-0" />
|
<VbenIcon :icon="item.icon" class="size-8 flex-shrink-0" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter class="justify-between">
|
<CardFooter class="justify-between">
|
||||||
<span>{{ item.totalTitle }}</span>
|
<span>{{ item.totalTitle }}</span>
|
||||||
<VbenCountToAnimator
|
<VbenCountToAnimator
|
||||||
|
v-if="item.totalValue !== 0"
|
||||||
:end-val="item.totalValue"
|
:end-val="item.totalValue"
|
||||||
:start-val="1"
|
:start-val="1"
|
||||||
prefix=""
|
prefix=""
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ interface AnalysisOverviewItem {
|
|||||||
title: string;
|
title: string;
|
||||||
totalTitle: string;
|
totalTitle: string;
|
||||||
totalValue: number;
|
totalValue: number;
|
||||||
value: number;
|
value: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkbenchProjectItem {
|
interface WorkbenchProjectItem {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
height: '300px',
|
height: '100%',
|
||||||
width: '100%',
|
width: '98%',
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
|
|||||||
if (!isDark.value) {
|
if (!isDark.value) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+3617
-3931
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user